@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.13

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 (516) hide show
  1. package/CHANGELOG.md +661 -0
  2. package/README.md +142 -6
  3. package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
  4. package/contracts/atelier-architecture.v1.schema.json +258 -0
  5. package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
  6. package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
  7. package/contracts/atelier-build.v1.schema.json +1076 -0
  8. package/contracts/atelier-capability.v1.schema.json +1252 -0
  9. package/contracts/atelier-coordination.v1.schema.json +1050 -0
  10. package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
  11. package/contracts/atelier-harness.v1.schema.json +234 -0
  12. package/contracts/atelier-ingestion.v1.schema.json +806 -0
  13. package/contracts/atelier-inquiry.v1.schema.json +1772 -0
  14. package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
  15. package/contracts/atelier-interaction.v1.schema.json +553 -0
  16. package/contracts/atelier-judgment.v1.schema.json +690 -0
  17. package/contracts/atelier-knowledge.v1.schema.json +1512 -0
  18. package/contracts/atelier-learning.v1.schema.json +1095 -0
  19. package/contracts/atelier-migration.v3.schema.json +64 -0
  20. package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
  21. package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
  22. package/contracts/atelier-template-adoption.v1.schema.json +223 -0
  23. package/contracts/atelier-template-profile.v1.schema.json +1168 -0
  24. package/contracts/atelier-trackable.v1.schema.json +781 -0
  25. package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
  26. package/docs/architecture.md +89 -0
  27. package/docs/assurance-controls.md +2 -2
  28. package/docs/blocks/capability-stewardship.md +17 -0
  29. package/docs/blocks/claims.md +16 -3
  30. package/docs/blocks/inquiry-harnesses.md +19 -0
  31. package/docs/blocks/learning-harnesses.md +17 -0
  32. package/docs/capability-stewardship.md +269 -0
  33. package/docs/coordination.md +51 -0
  34. package/docs/decision-practices.md +111 -0
  35. package/docs/decisions.md +29 -0
  36. package/docs/evidence-navigation.md +237 -0
  37. package/docs/exact-upgrades.md +3 -2
  38. package/docs/ingestion.md +181 -0
  39. package/docs/inquiry-harnesses.md +211 -0
  40. package/docs/install.md +80 -3
  41. package/docs/intake-and-guides.md +5 -0
  42. package/docs/integration-contract-decisions.md +24 -2
  43. package/docs/interaction-and-reflection.md +72 -0
  44. package/docs/knowledge-graph.md +20 -0
  45. package/docs/knowledge-health/api.md +30 -0
  46. package/docs/knowledge-health/assessment/api.md +96 -0
  47. package/docs/knowledge-health/assessment/capabilities.md +40 -0
  48. package/docs/knowledge-health/assessment/evaluation.md +27 -0
  49. package/docs/knowledge-health/assessment/facilitator.md +30 -0
  50. package/docs/knowledge-health/assessment/quickstart.md +61 -0
  51. package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
  52. package/docs/knowledge-health/devday/facilitator.md +16 -0
  53. package/docs/knowledge-health/devday/participant-response.md +22 -0
  54. package/docs/knowledge-health/devday/public-default.md +107 -0
  55. package/docs/knowledge-health/devday/quickstart.md +18 -0
  56. package/docs/knowledge-health/devday/support.md +22 -0
  57. package/docs/knowledge-health/exercise.md +17 -0
  58. package/docs/knowledge-health/facilitator.md +20 -0
  59. package/docs/knowledge-health/quickstart.md +94 -0
  60. package/docs/knowledge-health/readme.md +15 -0
  61. package/docs/knowledge-health/support.md +34 -0
  62. package/docs/knowledge-setup.md +248 -0
  63. package/docs/knowledge-workspace.md +264 -0
  64. package/docs/learning-harnesses.md +238 -0
  65. package/docs/learning.md +215 -0
  66. package/docs/local-services.md +181 -16
  67. package/docs/obsidian-contract.md +223 -15
  68. package/docs/obsidian-plugin.md +38 -4
  69. package/docs/obsidian.md +613 -51
  70. package/docs/optional-ingestion.md +83 -0
  71. package/docs/personal-workspace.md +257 -0
  72. package/docs/practical-judgment.md +92 -0
  73. package/docs/progressive-ingestion-acceptance.md +89 -0
  74. package/docs/progressive-ingestion.md +235 -0
  75. package/docs/project-command-inventory.md +1 -0
  76. package/docs/release-engineering.md +49 -0
  77. package/docs/repo-boundary-guard.md +94 -0
  78. package/docs/research-starter-quickstart.md +66 -0
  79. package/docs/research-starter.md +114 -0
  80. package/docs/semantic-ingestion.md +198 -0
  81. package/docs/semantic-operation.md +317 -0
  82. package/docs/skill-steward.md +6 -0
  83. package/docs/template-profile.md +164 -0
  84. package/docs/template-upgrades.md +114 -0
  85. package/docs/trackables.md +111 -0
  86. package/docs/upgrade.md +47 -2
  87. package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
  88. package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
  89. package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
  90. package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
  91. package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
  92. package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
  93. package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
  94. package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
  95. package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
  96. package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
  97. package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
  98. package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
  99. package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
  100. package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
  101. package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
  102. package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
  103. package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
  104. package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
  105. package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
  106. package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
  107. package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
  108. package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
  109. package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
  110. package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
  111. package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
  112. package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
  113. package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
  114. package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
  115. package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
  116. package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
  117. package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
  118. package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
  119. package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
  120. package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
  121. package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
  122. package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
  123. package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
  124. package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
  125. package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
  126. package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
  127. package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
  128. package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
  129. package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
  130. package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
  131. package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
  132. package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
  133. package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
  134. package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
  135. package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
  136. package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
  137. package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
  138. package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
  139. package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
  140. package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
  141. package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
  142. package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
  143. package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
  144. package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
  145. package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
  146. package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
  147. package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
  148. package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
  149. package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
  150. package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
  151. package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
  152. package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
  153. package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
  154. package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
  155. package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
  156. package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
  157. package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
  158. package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
  159. package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
  160. package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
  161. package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
  162. package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
  163. package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
  164. package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
  165. package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
  166. package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
  167. package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
  168. package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
  169. package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
  170. package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
  171. package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
  172. package/fixtures/atelier-interaction/act/valid/document.json +24 -0
  173. package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
  174. package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
  175. package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
  176. package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
  177. package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
  178. package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
  179. package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
  180. package/fixtures/atelier-judgment/case/valid/document.json +75 -0
  181. package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
  182. package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
  183. package/fixtures/atelier-judgment/native-decision.md +3 -0
  184. package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
  185. package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
  186. package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
  187. package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
  188. package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
  189. package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
  190. package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
  191. package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
  192. package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
  193. package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
  194. package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
  195. package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
  196. package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
  197. package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
  198. package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
  199. package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
  200. package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
  201. package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
  202. package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
  203. package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
  204. package/fixtures/atelier-learning/invalid/authority.json +14 -0
  205. package/fixtures/atelier-learning/valid/capture.json +13 -0
  206. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
  207. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
  208. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
  209. package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
  210. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
  211. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
  212. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
  213. package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
  214. package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
  215. package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
  216. package/fixtures/atelier-trackable/request/valid/document.json +22 -0
  217. package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
  218. package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
  219. package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
  220. package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
  221. package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
  222. package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
  223. package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
  224. package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
  225. package/fixtures/capability-packages/adoption.example.json +70 -0
  226. package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
  227. package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
  228. package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
  229. package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
  230. package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
  231. package/fixtures/capability-packages/research/capability-package.json +88 -0
  232. package/fixtures/capability-packages/research/capability-release.json +134 -0
  233. package/fixtures/capability-packages/research/evaluation.md +15 -0
  234. package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
  235. package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
  236. package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
  237. package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
  238. package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
  239. package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
  240. package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
  241. package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
  242. package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
  243. package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
  244. package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
  245. package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
  246. package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
  247. package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
  248. package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
  249. package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
  250. package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
  251. package/fixtures/evidence-navigation/valid/claim.json +36 -0
  252. package/fixtures/evidence-navigation/valid/host.json +16 -0
  253. package/fixtures/evidence-navigation/valid/profile.json +16 -0
  254. package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
  255. package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
  256. package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
  257. package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
  258. package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
  259. package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
  260. package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
  261. package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
  262. package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
  263. package/fixtures/harnesses/learning-cycle.json +780 -0
  264. package/fixtures/inquiry/workshop.json +675 -0
  265. package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
  266. package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
  267. package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
  268. package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
  269. package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
  270. package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
  271. package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
  272. package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
  273. package/fixtures/knowledge-health/calibration-suite.json +26 -0
  274. package/fixtures/knowledge-health/calibration-trial.json +37 -0
  275. package/fixtures/knowledge-health/corrected/checklist.md +18 -0
  276. package/fixtures/knowledge-health/devday/participant-record.json +19 -0
  277. package/fixtures/knowledge-health/expected-summary.json +27 -0
  278. package/fixtures/knowledge-health/workshop/README.md +20 -0
  279. package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
  280. package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
  281. package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
  282. package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
  283. package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
  284. package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
  285. package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
  286. package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
  287. package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
  288. package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
  289. package/fixtures/personal-workspace/overlay.json +24 -0
  290. package/fixtures/templates/local-library.v1.json +75 -0
  291. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
  292. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
  293. package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
  294. package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
  295. package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
  296. package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
  297. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
  298. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
  299. package/package.json +59 -5
  300. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  301. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  302. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  303. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  304. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  305. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  306. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  307. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  308. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  309. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  310. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  311. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  312. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  313. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  314. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  315. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  316. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  317. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  318. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  319. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  320. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  321. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  322. package/src/architecture/catalog.mjs +355 -0
  323. package/src/architecture/index.mjs +93 -0
  324. package/src/boundary/content-rules.mjs +4 -2
  325. package/src/boundary/delegation-command.mjs +158 -0
  326. package/src/boundary/delegation.mjs +396 -0
  327. package/src/boundary/policy.mjs +52 -10
  328. package/src/build/git.mjs +18 -0
  329. package/src/build/index.mjs +3 -0
  330. package/src/build/ledger.mjs +88 -0
  331. package/src/capabilities/adoption.mjs +381 -0
  332. package/src/capabilities/evidence.mjs +140 -0
  333. package/src/capabilities/files.mjs +124 -0
  334. package/src/capabilities/index.mjs +4 -0
  335. package/src/capabilities/instructions.mjs +250 -0
  336. package/src/capabilities/package.mjs +136 -0
  337. package/src/cli/command-failure.mjs +36 -0
  338. package/src/cli/execute-command.mjs +16 -2
  339. package/src/cli/run.mjs +65 -9
  340. package/src/coauthor/store.mjs +43 -2
  341. package/src/collaboration/proposals.mjs +189 -0
  342. package/src/commands/architecture.mjs +27 -0
  343. package/src/commands/boundary.mjs +3 -0
  344. package/src/commands/capability.mjs +73 -0
  345. package/src/commands/enroll.mjs +3 -0
  346. package/src/commands/harness.mjs +79 -0
  347. package/src/commands/ingest.mjs +39 -0
  348. package/src/commands/init.mjs +4 -1
  349. package/src/commands/inquiry.mjs +66 -0
  350. package/src/commands/knowledge-semantic.mjs +128 -0
  351. package/src/commands/knowledge.mjs +119 -0
  352. package/src/commands/learn.mjs +61 -0
  353. package/src/commands/obsidian.mjs +754 -63
  354. package/src/commands/practice.mjs +24 -0
  355. package/src/commands/setup.mjs +22 -2
  356. package/src/commands/trackable.mjs +19 -0
  357. package/src/contracts/corpus.mjs +30 -0
  358. package/src/coordination/index.mjs +110 -0
  359. package/src/evidence-navigation/contracts.mjs +60 -0
  360. package/src/evidence-navigation/evaluate.mjs +69 -0
  361. package/src/evidence-navigation/generation.mjs +22 -0
  362. package/src/evidence-navigation/index.mjs +2 -0
  363. package/src/evidence-navigation/local.mjs +167 -0
  364. package/src/evidence-navigation/schema.generated.mjs +604 -0
  365. package/src/evidence-navigation/types.mjs +12 -0
  366. package/src/export/atelier-export-contract.mjs +35 -8
  367. package/src/graph/enroll.mjs +248 -0
  368. package/src/graph/graph.mjs +17 -1
  369. package/src/graph/knowledge-graph.mjs +58 -8
  370. package/src/harnesses/contracts.mjs +41 -0
  371. package/src/harnesses/exchange.mjs +154 -0
  372. package/src/harnesses/feedback.mjs +16 -0
  373. package/src/harnesses/history.mjs +37 -0
  374. package/src/harnesses/index.mjs +4 -0
  375. package/src/harnesses/store.mjs +29 -0
  376. package/src/ingestion/contracts.mjs +41 -0
  377. package/src/ingestion/evaluation.mjs +119 -0
  378. package/src/ingestion/optional/composition/index.mjs +171 -0
  379. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  380. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  381. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  382. package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
  383. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  384. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  385. package/src/ingestion/optional/index.mjs +9 -0
  386. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  387. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  388. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  389. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  390. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  391. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  392. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  393. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  394. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  395. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  396. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  397. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  398. package/src/ingestion/processors.mjs +218 -0
  399. package/src/ingestion/semantic.mjs +203 -0
  400. package/src/ingestion/store.mjs +372 -0
  401. package/src/inquiry/beliefs.mjs +32 -0
  402. package/src/inquiry/contracts.mjs +23 -0
  403. package/src/inquiry/index.mjs +5 -0
  404. package/src/inquiry/ledger.mjs +135 -0
  405. package/src/inquiry/lenses.mjs +15 -0
  406. package/src/inquiry/projection.mjs +77 -0
  407. package/src/inquiry/store.mjs +35 -0
  408. package/src/intake/read-scope.mjs +12 -0
  409. package/src/intake/store.mjs +69 -6
  410. package/src/interaction/index.mjs +91 -0
  411. package/src/judgment/index.mjs +82 -0
  412. package/src/judgment/practice-evaluation.mjs +70 -0
  413. package/src/judgment/practice.mjs +144 -0
  414. package/src/knowledge/context.mjs +217 -0
  415. package/src/knowledge/index.mjs +5 -0
  416. package/src/knowledge/ingestion.mjs +79 -0
  417. package/src/knowledge/intake.mjs +33 -0
  418. package/src/knowledge/ledger.mjs +110 -0
  419. package/src/knowledge/participatory/guidance.mjs +36 -0
  420. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  421. package/src/knowledge/participatory/index.mjs +2 -0
  422. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  423. package/src/knowledge/plan.mjs +67 -0
  424. package/src/knowledge/projection.mjs +43 -0
  425. package/src/knowledge/semantic-operation.mjs +567 -0
  426. package/src/knowledge/sessions.mjs +467 -0
  427. package/src/knowledge/workspace.mjs +295 -0
  428. package/src/knowledge-health/assessment.mjs +165 -0
  429. package/src/knowledge-health/capabilities.mjs +350 -0
  430. package/src/knowledge-health/context.mjs +62 -0
  431. package/src/knowledge-health/example.mjs +47 -0
  432. package/src/knowledge-health/index.mjs +10 -0
  433. package/src/knowledge-health/owner-join.mjs +88 -0
  434. package/src/knowledge-health/proof/common.mjs +24 -0
  435. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  436. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  437. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  438. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  439. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  440. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  441. package/src/knowledge-health/rule-identity.mjs +44 -0
  442. package/src/knowledge-health/workshop-composition.mjs +19 -0
  443. package/src/knowledge-health/workshop-profile.mjs +176 -0
  444. package/src/knowledge-health/workshop.mjs +99 -0
  445. package/src/learning/adapters.mjs +68 -0
  446. package/src/learning/assistance.mjs +127 -0
  447. package/src/learning/contracts.mjs +65 -0
  448. package/src/learning/store.mjs +272 -0
  449. package/src/personal-workspace/index.mjs +391 -0
  450. package/src/personal-workspace/selection.mjs +385 -0
  451. package/src/project/config.mjs +8 -0
  452. package/src/project/durable-state.mjs +3 -1
  453. package/src/project/file-class.mjs +1 -1
  454. package/src/projection/obsidian/edits/apply.mjs +55 -16
  455. package/src/projection/obsidian/personal-workspace.mjs +436 -0
  456. package/src/projection/obsidian/proposals/adapter.mjs +47 -8
  457. package/src/projection/obsidian/proposals/observation.mjs +10 -2
  458. package/src/projection/obsidian/publication/publisher.mjs +142 -13
  459. package/src/projection/obsidian/publication/vault-list.mjs +4 -3
  460. package/src/projection/obsidian/recovery/index.mjs +4 -1
  461. package/src/projection/obsidian/recovery/store.mjs +160 -7
  462. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  463. package/src/readiness/readiness.mjs +10 -3
  464. package/src/reflection/index.mjs +56 -0
  465. package/src/runtime/obsidian/app-production-seams.mjs +48 -16
  466. package/src/runtime/obsidian/app-registration.mjs +175 -28
  467. package/src/runtime/obsidian/app-restart.mjs +154 -0
  468. package/src/runtime/obsidian/engine.mjs +163 -20
  469. package/src/runtime/obsidian/extension-points.mjs +1 -1
  470. package/src/runtime/obsidian/index.mjs +10 -7
  471. package/src/runtime/obsidian/launch-plan.mjs +7 -4
  472. package/src/runtime/obsidian/lifecycle.mjs +92 -12
  473. package/src/runtime/obsidian/login-item.mjs +332 -0
  474. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  475. package/src/runtime/obsidian/opening.mjs +171 -37
  476. package/src/runtime/obsidian/pipeline.mjs +71 -3
  477. package/src/runtime/obsidian/plugin-channel.mjs +11 -3
  478. package/src/runtime/obsidian/plugin-choice.mjs +16 -4
  479. package/src/runtime/obsidian/plugin-drift.mjs +59 -11
  480. package/src/runtime/obsidian/plugin-presence.mjs +14 -1
  481. package/src/runtime/obsidian/process-identity.mjs +20 -10
  482. package/src/runtime/obsidian/project-views.mjs +282 -0
  483. package/src/runtime/obsidian/questions.mjs +48 -0
  484. package/src/runtime/obsidian/release-watch.mjs +56 -0
  485. package/src/runtime/obsidian/service-main.mjs +99 -16
  486. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  487. package/src/runtime/obsidian/service-managers.mjs +275 -0
  488. package/src/runtime/obsidian/service-record.mjs +87 -23
  489. package/src/runtime/obsidian/service.mjs +36 -3
  490. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  491. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  492. package/src/runtime/obsidian/vault-location.mjs +300 -0
  493. package/src/runtime/obsidian/view-counts.mjs +34 -0
  494. package/src/server/local-sidecar.mjs +169 -2
  495. package/src/server/server.mjs +28 -2
  496. package/src/template-bindings/project.mjs +92 -0
  497. package/src/templates/profile.mjs +225 -0
  498. package/src/trackables/domain.mjs +175 -0
  499. package/src/trackables/store.mjs +114 -0
  500. package/src/ui/knowledge-page.mjs +1388 -0
  501. package/src/upgrade/explanation.mjs +1 -1
  502. package/src/upgrade/template-participant.mjs +171 -0
  503. package/src/upgrade/transaction.mjs +72 -25
  504. package/src/upgrade/upgrade.mjs +1 -0
  505. package/templates/external-project-workspace/pack-lifecycle.json +6 -1
  506. package/templates/knowledge-workspace/.gitattributes +2 -0
  507. package/templates/knowledge-workspace/AGENTS.md +27 -0
  508. package/templates/knowledge-workspace/README.md +44 -0
  509. package/templates/knowledge-workspace/atelier.project.json +22 -0
  510. package/templates/knowledge-workspace/gitignore +6 -0
  511. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  512. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  513. package/templates/knowledge-workspace/records/forecast.md +22 -0
  514. package/templates/knowledge-workspace/records/inspection.md +25 -0
  515. package/templates/knowledge-workspace/records/telescope.md +24 -0
  516. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -0,0 +1,396 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import crypto from 'node:crypto'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { canonicalize } from '../attestation/jcs.mjs'
6
+ import { verifyDocument } from '../attestation/sign.mjs'
7
+ import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
8
+
9
+ // Owner-signed delegation of the local boundary actor check (FD-BOUND-1).
10
+ // A delegation lets one declared operator pass the private-domain actor check
11
+ // for named repositories and operations. It never changes ownership, audiences,
12
+ // paths, content rules, promotion, push-content approval or publication.
13
+ //
14
+ // Guarantee: attribution integrity for a cooperating operator. The actor check
15
+ // itself stays attribution-grade: an operator can still declare another actor
16
+ // (--actor, MNSTRY_ATELIER_ACTOR), edit the policy or project configuration, or
17
+ // control the process that runs the check. A delegation adds that an owner's
18
+ // signed, current, host-anchored consent is recorded when someone else commits.
19
+ //
20
+ // Trust: the policy and the delegations document are operator-writable, so a
21
+ // delegation is authority only when its signature verifies against an owner key
22
+ // in the host file at a fixed path, owned by root in root-owned directories. That
23
+ // file is also the only source of revocation. Each delegation binds the whole
24
+ // policy (ext excluded), the managed repository set and each repository's root
25
+ // commit, so it cannot be loosened in place or replayed into an independently
26
+ // created repository. Forks sharing the root commit are not distinguished.
27
+ // `ext` is accepted and never read.
28
+
29
+ export const DELEGATION_SCHEMA = 'atelier-boundary-delegation@v1'
30
+ export const DELEGATIONS_DOCUMENT_SCHEMA = 'atelier-boundary-delegations@v1'
31
+ export const OWNER_KEYS_SCHEMA = 'atelier-boundary-owner-keys@v1'
32
+ export const DELEGATION_OPERATIONS = Object.freeze(['boundary-check', 'pre-commit'])
33
+ export const MAX_DELEGATION_SPAN_MS = 90 * 24 * 60 * 60 * 1000
34
+ export const HOST_OWNER_KEYS_PATH = '/etc/atelier/boundary-owner-keys.json'
35
+
36
+ const DELEGATION_KEYS = ['schema', 'id', 'owner', 'operator', 'repos', 'operations', 'notBefore', 'expiresAt', 'policyDigest', 'repoRoots', 'signature', 'ext']
37
+ const VERSION_RE = /^1\.[0-9]+\.[0-9]+$/
38
+ const ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/
39
+ const REPO_RE = /^[a-z0-9][a-z0-9._-]*$/
40
+ // Owner and operator ids are shown to the signing owner, so they are restricted
41
+ // to printable identifiers: no control characters, spaces or lookalikes.
42
+ export const DELEGATION_ACTOR_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
43
+ const UTC_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/
44
+ const DIGEST_RE = /^sha256:[0-9a-f]{64}$/
45
+ const COMMIT_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/
46
+ const JWK_X_RE = /^[A-Za-z0-9_-]{43}$/
47
+ const OWNER_KEYS_MAX_BYTES = 64 * 1024
48
+ const MAX_DELEGATIONS = 64
49
+ const MAX_REPOS = 64
50
+ const MAX_KEYS = 64
51
+ const MAX_REVOKED = 1024
52
+
53
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
54
+ const boundedString = (value, max = 128) => typeof value === 'string' && value.length > 0 && value.length <= max
55
+ // A time must be a real calendar instant that round-trips: 2026-02-31 is refused,
56
+ // never rolled over to March.
57
+ const timeOf = (value) => {
58
+ if (typeof value !== 'string' || !UTC_RE.test(value)) return Number.NaN
59
+ const ms = Date.parse(value)
60
+ if (Number.isNaN(ms)) return Number.NaN
61
+ const iso = new Date(ms).toISOString()
62
+ return iso === value || iso.replace(/\.000Z$/, 'Z') === value || iso.replace(/0*Z$/, 'Z').replace(/\.Z$/, 'Z') === value ? ms : Number.NaN
63
+ }
64
+
65
+ // `ext` is never read, so it is removed before hashing, but only at the
66
+ // extension positions the schema declares. A repository or actor that happens
67
+ // to be named `ext` is ordinary policy and stays bound.
68
+ const dropExt = (value) => {
69
+ if (!isObject(value)) return value
70
+ const { ext, ...rest } = value
71
+ return rest
72
+ }
73
+ function withoutExt(policy) {
74
+ if (!isObject(policy)) return policy ?? null
75
+ const out = dropExt(policy)
76
+ const each = (map) => (isObject(map) ? Object.fromEntries(Object.entries(map).map(([key, item]) => [key, dropExt(item)])) : map)
77
+ if (Object.hasOwn(out, 'repos')) out.repos = each(out.repos)
78
+ if (Object.hasOwn(out, 'actors')) out.actors = each(out.actors)
79
+ if (Object.hasOwn(out, 'promotion')) out.promotion = dropExt(out.promotion)
80
+ if (Array.isArray(out.contentRules)) out.contentRules = out.contentRules.map(dropExt)
81
+ if (Array.isArray(out.contentRuleExceptions)) out.contentRuleExceptions = out.contentRuleExceptions.map(dropExt)
82
+ return out
83
+ }
84
+
85
+ const managedRepoNames = (project) => (project?.repos ?? []).filter((repo) => !repo.external).map((repo) => repo.name).sort()
86
+
87
+ /**
88
+ * The digest a delegation binds: the whole boundary policy (ext excluded) and the
89
+ * project's managed repository names. Any later edit to them voids every
90
+ * delegation until the owner signs again. Other project configuration (repo
91
+ * access, graph settings, repository paths) is not bound.
92
+ */
93
+ export function policyDigest(policy, project) {
94
+ const bound = { policy: withoutExt(policy), managedRepos: managedRepoNames(project) }
95
+ return `sha256:${crypto.createHash('sha256').update(canonicalize(bound), 'utf8').digest('hex')}`
96
+ }
97
+
98
+ /**
99
+ * The repository's identity: the lowest-sorting root commit of HEAD, ignoring
100
+ * replace refs, or null when it has none. Forks and histories built on the same
101
+ * root share it; independently created repositories do not.
102
+ */
103
+ export function repoRootCommit(repoPath, { gitExecutable = 'git' } = {}) {
104
+ if (!repoPath) return null
105
+ const result = spawnSync(gitExecutable, ['--no-replace-objects', '-C', repoPath, 'rev-list', '--max-parents=0', 'HEAD', '--'], { encoding: 'utf8', env: sanitizedGitEnvironment() })
106
+ if (result.status !== 0) return null
107
+ const roots = result.stdout.split('\n').map((line) => line.trim()).filter((line) => COMMIT_RE.test(line)).sort()
108
+ return roots[0] ?? null
109
+ }
110
+
111
+ function repoPathFor(project, name) {
112
+ return (project?.repos ?? []).find((repo) => repo.name === name && !repo.external)?.path ?? null
113
+ }
114
+
115
+ function delegationErrors(item, at, { actors, repos }) {
116
+ if (!isObject(item)) return [`${at} must be an object`]
117
+ const errors = []
118
+ for (const key of Object.keys(item)) {
119
+ if (!DELEGATION_KEYS.includes(key)) errors.push(`${at} must not include additional property ${key}`)
120
+ }
121
+ if (item.ext != null && !isObject(item.ext)) errors.push(`${at}.ext must be an object`)
122
+ if (item.schema !== DELEGATION_SCHEMA) errors.push(`${at}.schema must be ${DELEGATION_SCHEMA}`)
123
+ if (typeof item.id !== 'string' || !ID_RE.test(item.id)) errors.push(`${at}.id is invalid`)
124
+ if (typeof item.owner !== 'string' || !DELEGATION_ACTOR_RE.test(item.owner) || !Object.hasOwn(actors, item.owner)) errors.push(`${at}.owner must be a declared actor with a plain identifier`)
125
+ if (typeof item.operator !== 'string' || !DELEGATION_ACTOR_RE.test(item.operator) || !Object.hasOwn(actors, item.operator)) errors.push(`${at}.operator must be a declared actor with a plain identifier`)
126
+ if (item.owner === item.operator) errors.push(`${at}.operator must differ from the owner`)
127
+ const repoNames = Array.isArray(item.repos) ? item.repos : []
128
+ if (!repoNames.length || repoNames.length > MAX_REPOS || new Set(repoNames).size !== repoNames.length) {
129
+ errors.push(`${at}.repos must be a non-empty list of at most ${MAX_REPOS} without duplicates`)
130
+ }
131
+ for (const name of repoNames) {
132
+ const repo = typeof name === 'string' && REPO_RE.test(name) && Object.hasOwn(repos, name) ? repos[name] : null
133
+ if (!repo || repo.kind !== 'private_domain') errors.push(`${at}.repos ${String(name)} must be a declared private_domain repo`)
134
+ else if (repo.ownerActor !== item.owner) errors.push(`${at}.repos ${name} is not owned by ${String(item.owner)}`)
135
+ }
136
+ const operations = Array.isArray(item.operations) ? item.operations : []
137
+ if (!operations.length || new Set(operations).size !== operations.length || operations.some((op) => !DELEGATION_OPERATIONS.includes(op))) {
138
+ errors.push(`${at}.operations must be a non-empty list of ${DELEGATION_OPERATIONS.join(', ')}`)
139
+ }
140
+ const notBefore = timeOf(item.notBefore)
141
+ const expiresAt = timeOf(item.expiresAt)
142
+ if (Number.isNaN(notBefore)) errors.push(`${at}.notBefore must be an RFC 3339 UTC time`)
143
+ if (Number.isNaN(expiresAt)) errors.push(`${at}.expiresAt must be an RFC 3339 UTC time`)
144
+ if (!Number.isNaN(notBefore) && !Number.isNaN(expiresAt) && !(expiresAt > notBefore && expiresAt - notBefore <= MAX_DELEGATION_SPAN_MS)) {
145
+ errors.push(`${at}.expiresAt must be after notBefore and at most 90 days later`)
146
+ }
147
+ if (typeof item.policyDigest !== 'string' || !DIGEST_RE.test(item.policyDigest)) errors.push(`${at}.policyDigest must be a sha256 digest`)
148
+ const roots = item.repoRoots
149
+ if (!isObject(roots) || Object.keys(roots).length !== repoNames.length ||
150
+ !repoNames.every((name) => typeof name === 'string' && Object.hasOwn(roots, name) && typeof roots[name] === 'string' && COMMIT_RE.test(roots[name]))) {
151
+ errors.push(`${at}.repoRoots must hold one root commit per listed repo`)
152
+ }
153
+ const signature = item.signature
154
+ if (!isObject(signature) || signature.algorithm !== 'ed25519' || !boundedString(signature.keyId) || !boundedString(signature.value, 512) ||
155
+ Object.keys(signature).some((key) => !['algorithm', 'keyId', 'value', 'ext'].includes(key))) {
156
+ errors.push(`${at}.signature must be an ed25519 signature; an unsigned delegation grants nothing`)
157
+ }
158
+ return errors
159
+ }
160
+
161
+ /** Errors for one delegation checked against a policy (used before signing). */
162
+ export function validateDelegation(item, policy) {
163
+ return delegationErrors(item, 'delegation', { actors: isObject(policy?.actors) ? policy.actors : {}, repos: isObject(policy?.repos) ? policy.repos : {} })
164
+ }
165
+
166
+ /**
167
+ * Errors for a delegations document, checked against the boundary policy it
168
+ * delegates under. Delegations live in their own document beside the policy,
169
+ * so the policy contract is not widened.
170
+ */
171
+ export function validateDelegationsDocument(doc, policy) {
172
+ if (!isObject(doc)) return ['delegations document must be a JSON object']
173
+ const errors = []
174
+ for (const key of Object.keys(doc)) {
175
+ if (!['schema', 'contractVersion', 'delegations', 'ext'].includes(key)) errors.push(`delegations document must not include additional property ${key}`)
176
+ }
177
+ if (doc.schema !== DELEGATIONS_DOCUMENT_SCHEMA) errors.push(`delegations document schema must be ${DELEGATIONS_DOCUMENT_SCHEMA}`)
178
+ if (doc.contractVersion != null && !(typeof doc.contractVersion === 'string' && VERSION_RE.test(doc.contractVersion))) errors.push('delegations document contractVersion must be a 1.x.y version')
179
+ if (doc.ext != null && !isObject(doc.ext)) errors.push('delegations document ext must be an object')
180
+ if (!Array.isArray(doc.delegations)) return [...errors, 'delegations document must list delegations']
181
+ if (doc.delegations.length > MAX_DELEGATIONS) errors.push(`delegations document lists more than ${MAX_DELEGATIONS} delegations`)
182
+ const context = { actors: isObject(policy?.actors) ? policy.actors : {}, repos: isObject(policy?.repos) ? policy.repos : {} }
183
+ const seen = new Set()
184
+ for (const [index, item] of doc.delegations.entries()) {
185
+ const at = `delegations[${index}]`
186
+ errors.push(...delegationErrors(item, at, context))
187
+ if (isObject(item) && typeof item.id === 'string') {
188
+ if (seen.has(item.id)) errors.push(`${at}.id ${item.id} is duplicated`)
189
+ seen.add(item.id)
190
+ }
191
+ }
192
+ return errors
193
+ }
194
+
195
+ function ownerKeysErrors(doc) {
196
+ if (!isObject(doc)) return ['owner keys must be a JSON object']
197
+ const errors = []
198
+ for (const key of Object.keys(doc)) {
199
+ if (!['schema', 'contractVersion', 'keys', 'revokedDelegations', 'ext'].includes(key)) errors.push(`owner keys must not include additional property ${key}`)
200
+ }
201
+ if (doc.schema !== OWNER_KEYS_SCHEMA) errors.push(`owner keys schema must be ${OWNER_KEYS_SCHEMA}`)
202
+ if (doc.contractVersion != null && !(typeof doc.contractVersion === 'string' && VERSION_RE.test(doc.contractVersion))) errors.push('owner keys contractVersion must be a 1.x.y version')
203
+ if (doc.ext != null && !isObject(doc.ext)) errors.push('owner keys ext must be an object')
204
+ if (!Array.isArray(doc.keys) || doc.keys.length > MAX_KEYS) errors.push(`owner keys must list at most ${MAX_KEYS} keys`)
205
+ const seen = new Set()
206
+ for (const [index, key] of (Array.isArray(doc.keys) ? doc.keys : []).entries()) {
207
+ if (!isObject(key) || Object.keys(key).some((name) => !['actorId', 'keyId', 'algorithm', 'publicKeyJwk', 'ext'].includes(name)) ||
208
+ (key.ext != null && !isObject(key.ext)) || !boundedString(key.actorId) || !boundedString(key.keyId) || key.algorithm !== 'ed25519' ||
209
+ !isObject(key.publicKeyJwk) || key.publicKeyJwk.kty !== 'OKP' || key.publicKeyJwk.crv !== 'Ed25519' ||
210
+ typeof key.publicKeyJwk.x !== 'string' || !JWK_X_RE.test(key.publicKeyJwk.x) || Object.hasOwn(key.publicKeyJwk, 'd')) {
211
+ errors.push(`owner keys keys[${index}] must be an ed25519 public key with actorId and keyId`)
212
+ continue
213
+ }
214
+ const identity = `${key.actorId}\u0000${key.keyId}`
215
+ if (seen.has(identity)) errors.push(`owner keys keys[${index}] duplicates ${key.actorId}/${key.keyId}`)
216
+ seen.add(identity)
217
+ }
218
+ const revoked = doc.revokedDelegations
219
+ if (revoked != null && (!Array.isArray(revoked) || revoked.length > MAX_REVOKED || new Set(revoked).size !== revoked.length ||
220
+ revoked.some((id) => typeof id !== 'string' || !ID_RE.test(id)))) {
221
+ errors.push('owner keys revokedDelegations must be a list of distinct delegation ids')
222
+ }
223
+ return errors
224
+ }
225
+
226
+ /** Normalize a host-supplied owner-keys document. The caller is the trusted host. */
227
+ export function ownerKeysFromDocument(doc) {
228
+ const errors = ownerKeysErrors(doc)
229
+ if (errors.length) return { ok: false, reason: 'delegation-owner-keys-invalid', errors }
230
+ return { ok: true, keys: doc.keys, revoked: new Set(doc.revokedDelegations ?? []) }
231
+ }
232
+
233
+ /** The normalized form of a host `ownerKeys` value, re-validated. */
234
+ function normalizedOwnerKeys(ownerKeys) {
235
+ if (!isObject(ownerKeys)) return null
236
+ if (ownerKeys.ok === false) return ownerKeys
237
+ if (ownerKeys.ok === true && Array.isArray(ownerKeys.keys) && ownerKeys.revoked instanceof Set) {
238
+ return ownerKeysFromDocument({ schema: OWNER_KEYS_SCHEMA, keys: ownerKeys.keys, revokedDelegations: [...ownerKeys.revoked] })
239
+ }
240
+ return ownerKeysFromDocument(ownerKeys)
241
+ }
242
+
243
+ /**
244
+ * Load the host owner-keys file. It is trusted only when it is a regular file
245
+ * owned by the trusted account (root), not group- or world-writable, opened
246
+ * without following links, and every directory above it is owned by that
247
+ * account (or root) and not group- or world-writable. The checks run on the open
248
+ * descriptor, which is also what is read. The current account must not be the
249
+ * trusted one. Windows is refused until an owner and DACL check exists.
250
+ */
251
+ export function loadOwnerKeysFile(file = HOST_OWNER_KEYS_PATH, { platform = process.platform, trustedUid = 0, currentUid = process.getuid?.() } = {}) {
252
+ const untrusted = (detail) => ({ ok: false, reason: 'delegation-owner-keys-untrusted', errors: [detail] })
253
+ if (platform === 'win32') return untrusted('owner keys are not supported on Windows')
254
+ if (typeof currentUid !== 'number') return untrusted('the current account cannot be determined')
255
+ if (currentUid === 0) return untrusted('owner keys cannot be trusted for the root account')
256
+ if (currentUid === trustedUid) return untrusted('the current account must not own the owner keys')
257
+ const resolved = path.resolve(file)
258
+ let fd
259
+ try {
260
+ fd = fs.openSync(resolved, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0))
261
+ } catch (error) {
262
+ if (error?.code === 'ENOENT') return { ok: false, reason: 'delegation-owner-key-missing', errors: ['owner keys file not found'] }
263
+ if (error?.code === 'ELOOP') return untrusted('owner keys file must not be a symlink')
264
+ return untrusted('owner keys file cannot be opened')
265
+ }
266
+ try {
267
+ const stat = fs.fstatSync(fd)
268
+ if (!stat.isFile()) return untrusted('owner keys file must be a regular file')
269
+ if (stat.size > OWNER_KEYS_MAX_BYTES) return untrusted('owner keys file is too large')
270
+ if (stat.uid !== trustedUid) return untrusted('owner keys file must be owned by the trusted account')
271
+ if ((stat.mode & 0o022) !== 0) return untrusted('owner keys file must not be group- or world-writable')
272
+ let real
273
+ try {
274
+ real = fs.realpathSync(resolved)
275
+ const named = fs.lstatSync(real)
276
+ if (named.dev !== stat.dev || named.ino !== stat.ino) return untrusted('owner keys file changed while it was checked')
277
+ } catch {
278
+ return untrusted('owner keys file changed while it was checked')
279
+ }
280
+ for (let dir = path.dirname(real); ; dir = path.dirname(dir)) {
281
+ let info
282
+ try {
283
+ info = fs.statSync(dir)
284
+ } catch {
285
+ return untrusted('owner keys file directories changed while they were checked')
286
+ }
287
+ if ((info.uid !== trustedUid && info.uid !== 0) || (info.mode & 0o022) !== 0) {
288
+ return untrusted('owner keys file must be in directories owned by the trusted account and not group- or world-writable')
289
+ }
290
+ if (path.dirname(dir) === dir) break
291
+ }
292
+ let doc
293
+ try {
294
+ doc = JSON.parse(fs.readFileSync(fd, 'utf8'))
295
+ } catch {
296
+ return { ok: false, reason: 'delegation-owner-keys-invalid', errors: ['owner keys file is not valid JSON'] }
297
+ }
298
+ return ownerKeysFromDocument(doc)
299
+ } finally {
300
+ fs.closeSync(fd)
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Owner keys for a CLI check: only the host file at its fixed path. The command
306
+ * line and environment cannot choose another file, so the host's keys and
307
+ * revocations always apply. Returns null when the host has none.
308
+ */
309
+ export function ownerKeysForCommand() {
310
+ return fs.existsSync(HOST_OWNER_KEYS_PATH) || isSymlink(HOST_OWNER_KEYS_PATH) ? loadOwnerKeysFile(HOST_OWNER_KEYS_PATH) : null
311
+ }
312
+
313
+ function isSymlink(file) {
314
+ try {
315
+ return fs.lstatSync(file).isSymbolicLink()
316
+ } catch {
317
+ return false
318
+ }
319
+ }
320
+
321
+ /** The delegations document beside a boundary policy file. */
322
+ export function delegationsPathFor(policyPath) {
323
+ return path.join(path.dirname(policyPath), 'boundary-delegations.v1.json')
324
+ }
325
+
326
+ /** Read the delegations document beside the policy, if one exists. */
327
+ export function loadDelegationsFile(file) {
328
+ if (!fs.existsSync(file)) return { present: false, document: null, errors: [] }
329
+ let document
330
+ try {
331
+ document = JSON.parse(fs.readFileSync(file, 'utf8'))
332
+ } catch {
333
+ return { present: true, document: null, errors: ['delegations document is not valid JSON'] }
334
+ }
335
+ if (!isObject(document)) return { present: true, document: null, errors: ['delegations document must be a JSON object'] }
336
+ return { present: true, document, errors: [] }
337
+ }
338
+
339
+ /**
340
+ * Decide whether a delegation lets `actorId` pass the actor check for one
341
+ * repository and operation. Returns `{ applies: true, delegation }` or
342
+ * `{ applies: false, reason }`; it never widens anything else.
343
+ */
344
+ export function resolveDelegation({ policy, project, delegations = [], repoName, repo, actorId, operation, ownerKeys = null, now = Date.now(), rootCommitOf = repoRootCommit }) {
345
+ const deny = (reason) => ({ applies: false, reason })
346
+ const all = Array.isArray(delegations) ? delegations.filter(isObject) : []
347
+ const forOperator = all.filter((item) => item.operator === actorId)
348
+ if (!forOperator.length) return deny('delegation-missing')
349
+ const forRepo = forOperator.filter((item) => Array.isArray(item.repos) && item.repos.includes(repoName))
350
+ const inScope = forRepo.filter((item) => Array.isArray(item.operations) && item.operations.includes(operation))
351
+ if (!inScope.length) return deny('delegation-scope')
352
+ if (inScope.length > 1) return deny('delegation-ambiguous')
353
+ const [delegation] = inScope
354
+ if (validateDelegation(delegation, policy).length || delegation.owner !== repo?.ownerActor) return deny('delegation-malformed')
355
+ const keys = normalizedOwnerKeys(ownerKeys)
356
+ if (!keys) return deny('delegation-owner-key-missing')
357
+ if (!keys.ok) return deny(keys.reason)
358
+ // The key must be registered for the delegation's owner, not merely present.
359
+ const key = keys.keys.find((item) => item.actorId === delegation.owner && item.keyId === delegation.signature.keyId)
360
+ if (!key) return deny('delegation-owner-key-missing')
361
+ if (!verifyDocument(delegation, { publicKey: key }).valid) return deny('delegation-signature-invalid')
362
+ if (keys.revoked.has(delegation.id)) return deny('delegation-revoked')
363
+ if (now < Date.parse(delegation.notBefore)) return deny('delegation-not-yet-valid')
364
+ if (now >= Date.parse(delegation.expiresAt)) return deny('delegation-expired')
365
+ if (delegation.policyDigest !== policyDigest(policy, project)) return deny('delegation-policy-changed')
366
+ for (const name of delegation.repos) {
367
+ if (rootCommitOf(repoPathFor(project, name)) !== delegation.repoRoots[name]) return deny('delegation-repository-changed')
368
+ }
369
+ return { applies: true, delegation }
370
+ }
371
+
372
+ /** The bound values a delegation must carry for this policy and project. */
373
+ export function delegationBindings({ policy, project, repos, rootCommitOf = repoRootCommit }) {
374
+ return {
375
+ policyDigest: policyDigest(policy, project),
376
+ repoRoots: Object.fromEntries((Array.isArray(repos) ? repos : []).map((name) => [name, rootCommitOf(repoPathFor(project, name))])),
377
+ }
378
+ }
379
+
380
+ /** An unsigned delegation document for the owner to read and sign. */
381
+ export function draftDelegation({ policy, project, id, owner, operator, repos, operations, notBefore, expiresAt, rootCommitOf = repoRootCommit }) {
382
+ const doc = {
383
+ schema: DELEGATION_SCHEMA,
384
+ id,
385
+ owner,
386
+ operator,
387
+ repos,
388
+ operations,
389
+ notBefore,
390
+ expiresAt,
391
+ ...delegationBindings({ policy, project, repos, rootCommitOf }),
392
+ signature: null,
393
+ }
394
+ const errors = validateDelegation({ ...doc, signature: { algorithm: 'ed25519', keyId: 'unsigned', value: 'unsigned' } }, policy)
395
+ return { ok: errors.length === 0, delegation: doc, errors }
396
+ }
@@ -15,6 +15,7 @@ import {
15
15
  validateContentRuleExceptions,
16
16
  validateContentRules,
17
17
  } from './content-rules.mjs'
18
+ import { delegationsPathFor, loadDelegationsFile, ownerKeysForCommand, resolveDelegation, validateDelegationsDocument } from './delegation.mjs'
18
19
 
19
20
  export const BOUNDARY_POLICY_SCHEMA = 'mnstry.atelier-boundary-policy@v1'
20
21
  export const PROMOTE_EVENT_SCHEMA = 'git-promote-event@v1'
@@ -115,6 +116,12 @@ function unknownKeys(value, label, allowed) {
115
116
  .map((key) => `${label} must not include additional property ${key}`)
116
117
  }
117
118
 
119
+ // The schema declares `ext` as a reserved extension container that consumers
120
+ // must ignore. It is accepted, never read, and never carries authority.
121
+ function extErrors(value, label) {
122
+ return value?.ext != null && !isObject(value.ext) ? [`${label}.ext must be an object`] : []
123
+ }
124
+
118
125
  function validAudienceList(value, label, required = false) {
119
126
  if (value == null && !required) return []
120
127
  if (!Array.isArray(value)) return [`${label} must be a list`]
@@ -128,8 +135,12 @@ function validAudienceList(value, label, required = false) {
128
135
  export function validateBoundaryPolicy(policy, project = null) {
129
136
  const errors = []
130
137
  if (!isObject(policy)) return ['boundary policy must be a JSON object']
131
- errors.push(...unknownKeys(policy, '/', new Set(['schema', 'mode', 'actors', 'repos', 'promotion', 'forbiddenPaths', 'contentRules', 'contentRuleExceptions', 'governanceLedgerPath'])))
138
+ errors.push(...unknownKeys(policy, '/', new Set(['schema', 'contractVersion', 'mode', 'actors', 'repos', 'promotion', 'forbiddenPaths', 'contentRules', 'contentRuleExceptions', 'governanceLedgerPath', 'ext'])))
132
139
  if (policy.schema !== BOUNDARY_POLICY_SCHEMA) errors.push(`schema must be ${BOUNDARY_POLICY_SCHEMA}`)
140
+ if (policy.contractVersion != null && !(typeof policy.contractVersion === 'string' && /^1\.[0-9]+\.[0-9]+$/.test(policy.contractVersion))) {
141
+ errors.push('contractVersion must be a 1.x.y version')
142
+ }
143
+ errors.push(...extErrors(policy, '/'))
133
144
  if (!VALID_BOUNDARY_MODES.has(policy.mode)) errors.push('mode must be strict or legacy-warning')
134
145
 
135
146
  const actors = policy.actors
@@ -138,7 +149,8 @@ export function validateBoundaryPolicy(policy, project = null) {
138
149
  if (!isObject(actors)) errors.push('actors must be an object')
139
150
  if (!isObject(repos)) errors.push('repos must be an object')
140
151
  if (policy.promotion != null && !isObject(policy.promotion)) errors.push('promotion must be an object')
141
- errors.push(...unknownKeys(promotion, 'promotion', new Set(['requiresGitPromote', 'recordsPath'])))
152
+ errors.push(...unknownKeys(promotion, 'promotion', new Set(['requiresGitPromote', 'recordsPath', 'ext'])))
153
+ errors.push(...extErrors(promotion, 'promotion'))
142
154
  if (policy.forbiddenPaths != null && !Array.isArray(policy.forbiddenPaths)) errors.push('forbiddenPaths must be a list')
143
155
  if (policy.contentRules != null) errors.push(...validateContentRules(policy.contentRules))
144
156
  if (policy.contentRuleExceptions != null) {
@@ -153,11 +165,15 @@ export function validateBoundaryPolicy(policy, project = null) {
153
165
  }
154
166
 
155
167
  for (const [actorId, actor] of Object.entries(isObject(actors) ? actors : {})) {
156
- errors.push(...unknownKeys(actor, `actors.${actorId}`, new Set(['githubLogin', 'gitEmails', 'privateDomainRepo'])))
168
+ errors.push(...unknownKeys(actor, `actors.${actorId}`, new Set(['githubLogin', 'gitEmails', 'privateDomainRepo', 'ext'])))
169
+ errors.push(...extErrors(actor, `actors.${actorId}`))
157
170
  if (!isObject(actor)) {
158
171
  errors.push(`actors.${actorId} must be an object`)
159
172
  continue
160
173
  }
174
+ // The schema types these fields; a non-string login would still match through String().
175
+ if (actor.githubLogin != null && typeof actor.githubLogin !== 'string') errors.push(`actors.${actorId}.githubLogin must be a string`)
176
+ if (actor.gitEmails != null && !Array.isArray(actor.gitEmails)) errors.push(`actors.${actorId}.gitEmails must be a list`)
161
177
  if (!firstString(actor.githubLogin) && !asArray(actor.gitEmails).length) {
162
178
  errors.push(`actors.${actorId} must declare githubLogin or gitEmails`)
163
179
  }
@@ -176,7 +192,8 @@ export function validateBoundaryPolicy(policy, project = null) {
176
192
  }
177
193
 
178
194
  for (const [repoName, repo] of Object.entries(isObject(repos) ? repos : {})) {
179
- errors.push(...unknownKeys(repo, `repos.${repoName}`, new Set(['kind', 'ownerActor', 'readBoundary', 'allowedAudiences', 'forbiddenAudiences', 'autoCommit'])))
195
+ errors.push(...unknownKeys(repo, `repos.${repoName}`, new Set(['kind', 'ownerActor', 'readBoundary', 'allowedAudiences', 'forbiddenAudiences', 'autoCommit', 'ext'])))
196
+ errors.push(...extErrors(repo, `repos.${repoName}`))
180
197
  if (!isObject(repo)) {
181
198
  errors.push(`repos.${repoName} must be an object`)
182
199
  continue
@@ -306,7 +323,7 @@ function nodePlacementFindings({ node, policy }) {
306
323
  return findings
307
324
  }
308
325
 
309
- function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors }) {
326
+ function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors, operation, delegations, ownerKeys, now }) {
310
327
  const findings = []
311
328
  const operatedRepos = new Set(managedRepos(project).map((repo) => repo.name))
312
329
  const ownedRepos = Object.entries(policy.repos ?? {}).filter(([name, repo]) =>
@@ -322,7 +339,20 @@ function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActo
322
339
  findings.push(finding({ severity, code: 'private-domain-actor-unverified', repo: repoName,
323
340
  message: `${repoName}: could not verify local actor for private domain repo owned by ${repo.ownerActor}${current.reason ? ` (${current.reason})` : ''}` }))
324
341
  } else if (current.actorId !== repo.ownerActor) {
325
- findings.push(finding({ severity, code: 'private-domain-actor-mismatch', repo: repoName, message: `${repoName}: private domain repo is owned by ${repo.ownerActor}, but current actor is ${current.actorId}` }))
342
+ // Only an owner-signed delegation, verified against a host-supplied key,
343
+ // changes this outcome, and only for its operator, repos and operation.
344
+ const delegated = resolveDelegation({ policy, project, delegations, repoName, repo, actorId: current.actorId, operation, ownerKeys, now })
345
+ if (delegated.applies) {
346
+ const { id, owner, expiresAt } = delegated.delegation
347
+ findings.push(finding({ severity: 'info', code: 'private-domain-delegated-operator', repo: repoName,
348
+ message: `${repoName}: ${current.actorId} operates under delegation ${id} from owner ${owner} until ${expiresAt}`,
349
+ details: { actorId: current.actorId, owner, delegationId: id, expiresAt, operation } }))
350
+ } else {
351
+ const note = delegated.reason === 'delegation-missing' ? '' : ` (${delegated.reason})`
352
+ findings.push(finding({ severity, code: 'private-domain-actor-mismatch', repo: repoName,
353
+ message: `${repoName}: private domain repo is owned by ${repo.ownerActor}, but current actor is ${current.actorId}${note}`,
354
+ details: { delegationReason: delegated.reason } }))
355
+ }
326
356
  }
327
357
  }
328
358
  return findings
@@ -635,7 +665,7 @@ function promotionFindings({ policy, project, graph }) {
635
665
  return findings
636
666
  }
637
667
 
638
- export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false, forceActorErrors = false } = {}) {
668
+ export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false, forceActorErrors = false, delegations = null, ownerKeys = null, now = Date.now() } = {}) {
639
669
  const validationErrors = validateBoundaryPolicy(policy, project)
640
670
  let graph = null
641
671
  const findings = validationErrors.map((message) => finding({ severity: 'error', code: 'boundary-policy-invalid', message }))
@@ -645,7 +675,14 @@ export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnl
645
675
  for (const node of graph.nodes ?? []) findings.push(...nodePlacementFindings({ node, policy }))
646
676
  findings.push(...promotionFindings({ policy, project, graph }))
647
677
  }
648
- findings.push(...actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors }))
678
+ // Delegations come from their own document, never from the policy. An invalid
679
+ // document is an error and grants nothing.
680
+ const delegationErrors = delegations == null ? [] : validateDelegationsDocument(delegations, policy)
681
+ findings.push(...delegationErrors.map((message) => finding({ severity: 'error', code: 'boundary-delegations-invalid', message })))
682
+ const delegationList = delegations != null && !delegationErrors.length ? delegations.delegations : []
683
+ // A staged check is the commit path (the installed pre-commit hook runs it).
684
+ const operation = staged ? 'pre-commit' : 'boundary-check'
685
+ findings.push(...actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors, operation, delegations: delegationList, ownerKeys, now }))
649
686
  if (staged) {
650
687
  const stagedPaths = stagedPathsForProject(project, { gitExecutable })
651
688
  findings.push(...forbiddenPathFindings({ policy, stagedPaths }))
@@ -673,9 +710,14 @@ export function runBoundaryCheckCommand(argv = process.argv.slice(2)) {
673
710
  const project = commandProject({ argv })
674
711
  const loaded = loadBoundaryPolicy(project)
675
712
  const missingFindings = loaded.errors?.map((message) => finding({ code: 'boundary-policy-missing', message })) ?? []
676
- const report = loaded.ok
677
- ? checkBoundaryPolicy({ project, policy: loaded.policy, staged, stagedOnly, actor: firstString(args.actor) })
713
+ const sidecar = loaded.ok ? loadDelegationsFile(delegationsPathFor(loaded.policyPath)) : { present: false, document: null, errors: [] }
714
+ const sidecarFindings = sidecar.errors.map((message) => finding({ code: 'boundary-delegations-invalid', message }))
715
+ // Owner keys are read only when a delegations document exists.
716
+ const ownerKeys = sidecar.present && sidecar.document ? ownerKeysForCommand() : null
717
+ let report = loaded.ok
718
+ ? checkBoundaryPolicy({ project, policy: loaded.policy, staged, stagedOnly, actor: firstString(args.actor), delegations: sidecar.document, ownerKeys })
678
719
  : { ok: false, mode: null, schema: BOUNDARY_POLICY_SCHEMA, graphCounts: null, findings: missingFindings, errors: missingFindings, warnings: [] }
720
+ if (sidecarFindings.length) report = { ...report, ok: false, findings: [...sidecarFindings, ...report.findings], errors: [...sidecarFindings, ...report.errors] }
679
721
 
680
722
  if (json) {
681
723
  console.log(JSON.stringify(report, null, 2))
@@ -0,0 +1,18 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { bytesAt, workspaceRoot, digest } from '../capabilities/files.mjs'
3
+ import { harnessRef } from '../harnesses/contracts.mjs'
4
+ import { inspectBuild } from './ledger.mjs'
5
+
6
+ // Read-only local adapter. A host still owns commands, writer custody and CI.
7
+ export function prepareGitCandidate({ workspaceRoot: input, records, artifact, writer }) {
8
+ const root = workspaceRoot(input), state = inspectBuild(records)
9
+ if (!state.establishment) throw new Error('build objective missing')
10
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')))
11
+ const git = args => execFileSync('git', ['-C', root, ...args], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
12
+ if (git(['status', '--porcelain', '--untracked-files=all'])) throw new Error('candidate requires clean repository sources')
13
+ const commit = git(['rev-parse', 'HEAD']), tree = git(['rev-parse', 'HEAD^{tree}'])
14
+ const artifactDigest = digest(bytesAt(root, artifact))
15
+ if (git(['rev-parse', 'HEAD']) !== commit || git(['status', '--porcelain', '--untracked-files=all']) || digest(bytesAt(root, artifact)) !== artifactDigest) throw new Error('candidate changed during capture')
16
+ return { data: { objective: harnessRef(state.establishment), repository: state.establishment.data.repository, commit, tree, artifactDigest, writer,
17
+ ...(state.candidate ? { supersedes: harnessRef(state.candidate) } : {}) }, artifact, assurance: 'local-bytes-and-git-observation; writer-is-caller-reported', authority: 'none' }
18
+ }
@@ -0,0 +1,3 @@
1
+ export { inspectBuild } from './ledger.mjs'
2
+ export { prepareGitCandidate } from './git.mjs'
3
+ export { buildReadiness, buildCoordinationProposal } from '../harnesses/exchange.mjs'