@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,154 @@
1
+ import path from 'node:path'
2
+ import { performance } from 'node:perf_hooks'
3
+
4
+ // Quitting a running Obsidian so that `open` can add a vault and turn its
5
+ // command line on, only when a person asked for it (`--restart-obsidian`).
6
+ //
7
+ // `open` restarts the app in two states only, where it runs but cannot be
8
+ // reached: its command line is turned off, or no vault is open (and the app
9
+ // does not list the view's vault). The settings file then belongs to the
10
+ // running app, which would write its own list back over any change, and the
11
+ // app cannot be asked to add a vault. Before anything is sent, `open` checks,
12
+ // read-only, that the write the restart is for can succeed (opening.mjs); right
13
+ // before the first signal, once the main process is proven, `beforeSignal` keeps
14
+ // a copy of the settings file, and a copy that cannot be kept sends nothing.
15
+ //
16
+ // SIGTERM, which Electron handles as a quit, is sent to one process: the app's
17
+ // main process, proven from the process table, and never to anything else. The
18
+ // main process is proven when, in a table of this machine's processes as
19
+ // `pid ppid uid comm` (`comm` is the process's argv[0] as macOS reports it, not
20
+ // a verified image: a process of this user can name itself anything, which is
21
+ // why two candidates prove nothing):
22
+ //
23
+ // - every process of the app (the names the process probe counts: `Obsidian`
24
+ // and the bundle's `Obsidian Helper…`) runs as this user;
25
+ // - exactly one of them names the app bundle's main executable by an absolute
26
+ // path (`…/Contents/MacOS/Obsidian`); a process shown by its short name
27
+ // only is not proven;
28
+ // - every other one is a child of that process (its helpers).
29
+ //
30
+ // Anything else (two instances, another user's app, a helper of another
31
+ // parent, a table that cannot be read) is `app-main-process-unproven`, and no
32
+ // signal is sent. Immediately before the signal the one process is read again
33
+ // (`ppid uid start-time comm`) and must be of this user, under the parent the
34
+ // table showed, with the same name, and read the same twice.
35
+ //
36
+ // What SIGTERM does (1.13.7, checked on an isolated instance): the app handles
37
+ // the first one as a quit. It closes every window, keeping the vaults that were
38
+ // open flagged open in its list, and its helper processes end; on macOS the main
39
+ // process can then stay, with no window, as a macOS app does. Electron handles
40
+ // only the first SIGTERM as a quit: a second one ends the process at once,
41
+ // without its quit handlers. It is sent only after that same process has shown
42
+ // no window and no helper (it is alone in the table, and reads the same) at
43
+ // every reading for at least `lingerMs`, measured on a monotonic clock; a
44
+ // reading that cannot be parsed does not count as alone. That is a margin, not
45
+ // a proof that nothing is left to save. There is never a third signal, and
46
+ // never another kind; an app that is still not gone is reported
47
+ // (`app-did-not-quit`) and left to the person. `signals` counts the signals
48
+ // that were delivered.
49
+ //
50
+ // Qualified on macOS only: on Linux a quit app is started again with a link,
51
+ // which drops the other vaults' reopen flags (launch-plan.mjs), so the promise
52
+ // that they reopen would not hold there.
53
+ //
54
+ // Nothing here reads the process table or sends a signal itself: every seam is
55
+ // injected, with no default. Only the production seams module of the command
56
+ // entry and the opt-in real-app suite, for its isolated app, construct one.
57
+
58
+ export const RESTART_PLATFORMS = Object.freeze(['darwin'])
59
+ const APP_PROCESS = /^obsidian(?: helper\b.*)?$/i
60
+ const MAIN_EXECUTABLE = /^\/.+\.app\/Contents\/MacOS\/Obsidian$/i
61
+ // `pid ppid uid comm`. macOS prints the uid of `nobody` as -2: a uid is signed.
62
+ export const PROCESS_ROW = /^\s*(\d+)\s+(\d+)\s+(-?\d+)\s+(.+?)\s*$/
63
+ // `ppid uid start-time comm`, one process read again.
64
+ const DESCRIBED = /^\s*(\d+)\s+(-?\d+)\s+(.+?)\s*$/
65
+
66
+ const unproven = (detail) => ({ state: 'unproven', reason: 'app-main-process-unproven', detail })
67
+ const rowsOf = (table) => table.split('\n').filter((line) => line.trim() !== '').map((line) => PROCESS_ROW.exec(line))
68
+
69
+ // Pure. The app's main process in a process table (`ps -A -o pid=,ppid=,uid=,comm=`), for the account `uid`:
70
+ // { state: 'proven', pid, ppid, executable } | { state: 'absent' } | { state: 'unproven', reason, detail }.
71
+ export function provenMainProcess({ table, uid } = {}) {
72
+ if (typeof table !== 'string' || table.trim() === '') return unproven('the process table could not be read')
73
+ if (!Number.isSafeInteger(uid) || uid < 0) return unproven('the account is not known')
74
+ const matches = rowsOf(table)
75
+ if (matches.some((match) => match === null)) return unproven('the process table has a line that is not a process')
76
+ const rows = matches.map((match) => ({ pid: Number(match[1]), ppid: Number(match[2]), uid: Number(match[3]), executable: match[4] }))
77
+ const app = rows.filter((row) => APP_PROCESS.test(path.posix.basename(row.executable)))
78
+ if (app.length === 0) return { state: 'absent' }
79
+ if (app.some((row) => row.uid !== uid)) return unproven('an Obsidian process runs as another user')
80
+ const main = app.filter((row) => /^obsidian$/i.test(path.posix.basename(row.executable)))
81
+ if (main.length !== 1) return unproven(main.length === 0 ? 'no Obsidian main process is in the process table' : 'more than one Obsidian main process runs')
82
+ const [one] = main
83
+ if (!MAIN_EXECUTABLE.test(one.executable)) return unproven('the main process does not name the app bundle\'s executable')
84
+ if (app.some((row) => row !== one && row.ppid !== one.pid)) return unproven('an Obsidian helper process is not a child of the main process')
85
+ return { state: 'proven', pid: one.pid, ppid: one.ppid, executable: one.executable }
86
+ }
87
+
88
+ // `readTable()` -> the process table as provenMainProcess reads it (throws or answers '' when it cannot)
89
+ // `readProcess(pid)` -> that process read again as `ppid uid start-time comm`, or null
90
+ // `signal(pid)` -> sends SIGTERM to exactly that pid; throws when it was not delivered
91
+ // `processProbe()` -> 'running' | 'absent' | 'unknown', the probe the settings write uses
92
+ //
93
+ // quit({ beforeSignal }) answers { quit: true, pid, signalled, signals, settingsCopy? } once the probe says `absent`,
94
+ // or { quit: false, reason, signalled, signals, pid?, detail?, settingsCopy? }: `restart-platform-unqualified`,
95
+ // `app-main-process-unproven` and the refusal of `beforeSignal` before any signal, `app-did-not-quit` after one, when
96
+ // the probe did not say `absent` within `waitMs`. `beforeSignal()`, when given, runs once the main process is proven
97
+ // and read again, immediately before the first signal: { ok: true, backupPath? } lets it be sent, anything else
98
+ // ({ ok: false, code }) sends nothing. `now` is a monotonic clock in milliseconds.
99
+ export function createAppQuitter({ platform, uid, readTable, readProcess, signal, processProbe, sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms) }), now = () => performance.now(), waitMs = 30_000, pollMs = 250, lingerMs = 5000 } = {}) {
100
+ for (const [name, seam] of Object.entries({ readTable, readProcess, signal, processProbe })) if (typeof seam !== 'function') throw new TypeError(`quitting Obsidian needs an injected ${name}`)
101
+ const probe = () => { try { return processProbe() } catch { return 'unknown' } }
102
+ const describe = (pid) => { try { const line = readProcess(pid); return typeof line === 'string' && line.trim() !== '' ? line.trim() : null } catch { return null } }
103
+ const table = () => { try { const text = readTable(); return typeof text === 'string' ? text : '' } catch { return '' } }
104
+ // The process read again is the one the table proved: this user's, under the parent the table showed, same name.
105
+ const isFound = (line, found) => {
106
+ const match = line === null ? null : DESCRIBED.exec(line)
107
+ return match !== null && Number(match[1]) === found.ppid && Number(match[2]) === uid && match[3].endsWith(found.executable)
108
+ }
109
+ // The proven main process alone in the table, still the process `seen` described: its windows closed, it did not quit.
110
+ const lingersAlone = (found, seen) => {
111
+ const matches = rowsOf(table())
112
+ if (matches.length === 0 || matches.some((match) => match === null)) return false
113
+ const rows = matches.filter((match) => APP_PROCESS.test(path.posix.basename(match[4])))
114
+ return rows.length === 1 && Number(rows[0][1]) === found.pid && describe(found.pid) === seen
115
+ }
116
+ return {
117
+ async quit({ beforeSignal = null } = {}) {
118
+ if (!RESTART_PLATFORMS.includes(platform)) return { quit: false, reason: 'restart-platform-unqualified', signalled: false, signals: 0 }
119
+ const found = provenMainProcess({ table: table(), uid })
120
+ if (found.state === 'absent') return probe() === 'absent' ? { quit: true, pid: null, signalled: false, signals: 0 } : { quit: false, reason: 'app-main-process-unproven', detail: 'the process table shows no Obsidian, and the process probe does not say it is gone', signalled: false, signals: 0 }
121
+ if (found.state !== 'proven') return { quit: false, reason: found.reason, detail: found.detail, signalled: false, signals: 0 }
122
+ const seen = describe(found.pid)
123
+ if (!isFound(seen, found)) return { quit: false, reason: 'app-main-process-unproven', detail: 'the main process read again is not the one the table showed', signalled: false, signals: 0 }
124
+ // Immediately before the signal: still the same process.
125
+ if (describe(found.pid) !== seen) return { quit: false, reason: 'app-main-process-unproven', detail: 'the main process changed while it was being proven', signalled: false, signals: 0 }
126
+ let copy = {}
127
+ if (typeof beforeSignal === 'function') {
128
+ let kept
129
+ try { kept = await beforeSignal() } catch { kept = null }
130
+ if (kept?.ok !== true) return { quit: false, reason: typeof kept?.code === 'string' ? kept.code : 'obsidian-settings-unwritable', signalled: false, signals: 0 }
131
+ if (typeof kept.backupPath === 'string') copy = { settingsCopy: kept.backupPath }
132
+ }
133
+ try { signal(found.pid) } catch { return { quit: false, reason: 'app-main-process-unproven', detail: 'the main process could not be signalled', signalled: false, signals: 0, ...copy } }
134
+ let signals = 1
135
+ let second = false
136
+ let aloneSince = null
137
+ const deadline = now() + waitMs
138
+ for (;;) {
139
+ if (probe() === 'absent') return { quit: true, pid: found.pid, signalled: true, signals, ...copy }
140
+ if (now() >= deadline) return { quit: false, reason: 'app-did-not-quit', pid: found.pid, signalled: true, signals, ...copy }
141
+ if (!second) {
142
+ if (!lingersAlone(found, seen)) aloneSince = null
143
+ else if (aloneSince === null) aloneSince = now()
144
+ else if (now() - aloneSince >= lingerMs) {
145
+ // Once, whether or not it is delivered.
146
+ second = true
147
+ try { signal(found.pid); signals = 2 } catch { /* not delivered: not counted */ }
148
+ }
149
+ }
150
+ await sleep(pollMs)
151
+ }
152
+ },
153
+ }
154
+ }
@@ -2,11 +2,12 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { AtelierDiagnosticError } from '../../project/config.mjs'
4
4
  import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, manifestLayoutVersion } from '../../projection/obsidian/contracts.mjs'
5
+ import { PERSONAL_VALIDATION_PENDING, personalWorkspaceBindingOf, personalWorkspaceInputs, validatePersonalWorkspace } from '../../projection/obsidian/personal-workspace.mjs'
5
6
  import { PROTOCOL_ID } from '../../projection/obsidian/publication/bridge-script.mjs'
6
- import { PublicationRefusal } from '../../projection/obsidian/recovery/store.mjs'
7
+ import { PublicationRefusal, allocatedFolderState, hasCommittedGeneration, readVaultAllocation } from '../../projection/obsidian/recovery/store.mjs'
7
8
  import { canonicalJson, compareText, isoTime } from './documents.mjs'
8
9
  import { readObsidianEnablement } from './enablement.mjs'
9
- import { ObsidianMaintenanceRefusal } from './errors.mjs'
10
+ import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
10
11
  import { createMaintenanceExtensions } from './extension-points.mjs'
11
12
  import {
12
13
  assertOutsideRepositories, authorizeAutomaticApply, defaultMachineSettings, ensureWorkspaceIdentity, protectedRoots, readLocalPointer,
@@ -14,10 +15,12 @@ import {
14
15
  } from './machine-settings.mjs'
15
16
  import { configKey, listConfigFiles, listSourceFiles, listVaultNotes, readFileFacts, reconcile, sha256Digest, sourceKey, vaultKey } from './observation.mjs'
16
17
  import { dispatchAutomaticApply, heldPaths, layoutHeldPaths, observeVaultEdits, preserveInRecoveryStore, trustedNoteBases } from './pending-edits.mjs'
17
- import { DEFAULT_ELIGIBILITY, createProductionSeams } from './pipeline.mjs'
18
+ import { createProductionSeams, eligibilityFor } from './pipeline.mjs'
19
+ import { PLUGIN_FILES_WAIT_FOR_APP } from './plugin-presence.mjs'
18
20
  import { ENGINE_LOCK_DIRECTORY, acquirePrivateGenerationLock, createAbandonmentProof } from './private-lock.mjs'
19
21
  import { probeHealth } from './service-client.mjs'
20
22
  import { FRESHNESS_SCHEMA, LATE_WRITERS_SCHEMA, createMaintenanceStateStore } from './state-store.mjs'
23
+ import { ensureVaultAllocation, projectDisplayName } from './vault-location.mjs'
21
24
  import { createNullWatcherFactory } from './watchers.mjs'
22
25
 
23
26
  // The maintenance engine. One explicit `tick()`; no timer, no process, no
@@ -126,7 +129,15 @@ export const ENGINE_PRIMITIVES = Object.freeze({
126
129
  })
127
130
 
128
131
  const isTypedRefusal = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof AtelierDiagnosticError || error instanceof ObsidianContractRefusal || error instanceof PublicationRefusal
132
+ // The code a view is refused with when its own vault cannot be placed: a typed refusal's, or `vault-location-unusable`
133
+ // for a file-system error there (a file in the way, no permission, a read-only volume). Null for anything else, which
134
+ // is a fault of the engine and stops the tick.
135
+ const viewRefusalCode = (error) => (isTypedRefusal(error) ? error.code : typeof error?.code === 'string' && /^E[A-Z]+$/.test(error.code) ? 'vault-location-unusable' : null)
129
136
  const digestOfJson = (value) => sha256Digest(Buffer.from(canonicalJson(value)))
137
+ // The configuration a tick observes. A project bound to a personal workspace adds the person's authored manifest and
138
+ // overlay and its generation's record: a change to any of them is a configuration change, so every view is prepared
139
+ // again, asks the composition, and is refused at once when the generation no longer holds.
140
+ const configFilesOf = (project) => [...listConfigFiles(project), ...personalWorkspaceInputs(project).map((absolute) => ({ key: configKey(absolute), changeClass: 'config', absolute }))]
130
141
 
131
142
  // An editor adapter that `build` makes, synchronously or not, the first time the publisher calls it; a refusal of
132
143
  // `build` is thrown from that call, and `refusal()` answers it afterwards.
@@ -142,6 +153,11 @@ function builtOnFirstUse(build) {
142
153
  publish: async (payload) => (await built()).publish(payload),
143
154
  }
144
155
  }
156
+ // The adapter a publication gets when the factory refused the app: it coordinates with nothing and never calls it.
157
+ function uncoordinatedAdapter(refusal) {
158
+ const never = async () => { throw refusal }
159
+ return { probe: async () => ({ state: 'uncoordinated', reason: `${refusal.code}: the installed Obsidian does not qualify, and nothing is published through it` }), inspect: never, collect: never, publish: never }
160
+ }
145
161
  // Views a tick may be asked to prepare again, at most, before it runs.
146
162
  const MAX_PREPARATION_REQUESTS = 64
147
163
 
@@ -166,9 +182,19 @@ function journalsToRecheck(store, { nowMs, full, windowMs }) {
166
182
  export function createMaintenanceEngineForOracleTests(options = {}, primitives = ENGINE_PRIMITIVES) {
167
183
  const {
168
184
  loadProject, dataRoot, adapterFactory, clock,
169
- watcherFactory = createNullWatcherFactory(), extensions = createMaintenanceExtensions(), eligibility = DEFAULT_ELIGIBILITY,
185
+ // Which notes may enter a view: what the machine settings decide at each tick (eligibilityFor), unless a test fixes it.
186
+ watcherFactory = createNullWatcherFactory(), extensions = createMaintenanceExtensions(), eligibility: fixedEligibility = null,
170
187
  fullReconciliationIntervalMs = DEFAULT_FULL_RECONCILIATION_INTERVAL_MS, retryIntervalMs = DEFAULT_RETRY_INTERVAL_MS, lateWriterWindowMs = DEFAULT_LATE_WRITER_WINDOW_MS,
171
188
  publicationRetryMs = DEFAULT_PUBLICATION_RETRY_MS, observeApp = null,
189
+ // Reads the app's own vault list without asking the app (readAppVaultListForAllocation): { ok: true, vaults } with
190
+ // the map id -> { path }, or { ok: false, code }. A folder for a new vault is never allocated inside a vault it
191
+ // lists, nor under a name a vault it lists already has, nor while the list cannot be read.
192
+ readAppVaultList = null,
193
+ // Reads the app's own vault list from its file, without asking the app (unheld-evidence.mjs): { unlisted: true } only
194
+ // when no list names a view's vault, a folder above it or one inside it. A view never published into its allocated
195
+ // folder is then published even while an app runs that cannot be coordinated with, creating files only (the
196
+ // publisher's `direct-unheld` path). An engine given no reader never takes that path.
197
+ readUnheldEvidence = null,
172
198
  quietPeriodMs, lstat = fs.lstatSync, randomBytes, env = process.env, platform = process.platform,
173
199
  // A service names where it answers health, so a lock it leaves behind can be proven abandoned.
174
200
  lockOwner = null, lockProbe = probeHealth,
@@ -215,6 +241,14 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
215
241
  const waitingForApp = new Map()
216
242
  const proveAbandoned = createAbandonmentProof({ probe: lockProbe })
217
243
 
244
+ // Whether a view kept from publication by an app that does not qualify may still be published on the publisher's
245
+ // `direct-unheld` path: never committed, its vault the folder allocated for it, and the app's list naming no folder
246
+ // on the way. The publisher checks every condition again, under its locks.
247
+ async function unheldMayApply(store, pointer) {
248
+ if (typeof readUnheldEvidence !== 'function' || pointer !== null || store.vaultOrigin !== 'allocated') return false
249
+ try { return (await readUnheldEvidence({ vaultRoot: store.vaultRoot }))?.unlisted === true } catch { return false }
250
+ }
251
+
218
252
  async function takeLock(workspaceRoot, workspaceId) {
219
253
  if (heldLock) return { acquired: true }
220
254
  const lock = await rules.acquireEngineLock({
@@ -249,9 +283,18 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
249
283
  }
250
284
 
251
285
  function storeFor(scope, workspaceRoot, workspaceId, repositoryRoots) {
252
- const signature = JSON.stringify([workspaceRoot, workspaceId, repositoryRoots])
286
+ // A view's vault can be allocated, made again or moved while the engine runs: the store follows its record. A
287
+ // store is used again only while its allocated folder is still the one the record names; otherwise it is dropped,
288
+ // and the store made in its place refuses, typed (vault-allocation-replaced, say), so a folder replaced while the
289
+ // service runs, or a link put where it was, is never published into.
290
+ const signature = JSON.stringify([workspaceRoot, workspaceId, repositoryRoots, readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })])
253
291
  const cached = stores.get(scope.scopeId)
254
- if (cached?.signature === signature) return cached.store
292
+ // A vault root that is gone (a folder under the data root the person removed, say) is looked for again too, so the
293
+ // store made in its place says why rather than failing on the folder that is not there.
294
+ if (cached?.signature === signature && fs.lstatSync(cached.store.vaultRoot, { throwIfNoEntry: false }) !== undefined) {
295
+ try { cached.store.checkAllocatedVault?.(); return cached.store } catch (error) { if (!isTypedRefusal(error)) throw error }
296
+ }
297
+ stores.delete(scope.scopeId)
255
298
  const store = seams.createRecoveryStore({ workspaceRoot, workspaceId, scopeId: scope.scopeId, repositoryRoots })
256
299
  stores.set(scope.scopeId, { signature, store })
257
300
  return store
@@ -318,15 +361,19 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
318
361
 
319
362
  // 1. Project and enablement. The digests are seeded before the project is
320
363
  // loaded for good, so the loaded project is never older than the index.
364
+ // A loader may answer a promise (one that composes a personal workspace off
365
+ // the event loop). A project bound to a personal workspace is loaded once:
366
+ // what a generation holds is fixed, and an input that changed after the load
367
+ // changes the validity key every build is checked against.
321
368
  if (project === null) {
322
- project = loadProject()
323
- reconcile({ index, files: listConfigFiles(project), prefix: CONFIG_PREFIX, full: true, lstat })
324
- project = loadProject()
369
+ project = await loadProject()
370
+ reconcile({ index, files: configFilesOf(project), prefix: CONFIG_PREFIX, full: true, lstat })
371
+ if (personalWorkspaceBindingOf(project) === null) project = await loadProject()
325
372
  } else {
326
- const config = reconcile({ index, files: listConfigFiles(project), prefix: CONFIG_PREFIX, full, hinted, lstat })
373
+ const config = reconcile({ index, files: configFilesOf(project), prefix: CONFIG_PREFIX, full, hinted, lstat })
327
374
  if (config.changes.length > 0) {
328
- project = loadProject()
329
- reconcile({ index, files: listConfigFiles(project), prefix: CONFIG_PREFIX, full: true, lstat })
375
+ project = await loadProject()
376
+ reconcile({ index, files: configFilesOf(project), prefix: CONFIG_PREFIX, full: true, lstat })
330
377
  }
331
378
  }
332
379
  const enablement = readObsidianEnablement(project)
@@ -351,14 +398,63 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
351
398
  if (!lock.acquired) return { state: 'busy', reason: 'engine-lock-held', lock: { reason: lock.reason, holder: lock.holder ?? null }, changes: [], scopes: [], pendingEdits: [], dispatched: [], lateWriters: [] }
352
399
  const machine = readMachineSettings({ workspaceRoot, workspaceId })
353
400
  ?? writeMachineSettings({ workspaceRoot, workspaceId, repositoryRoots, settings: defaultMachineSettings({ workspaceId, updatedAt: now }) })
401
+ const eligibility = fixedEligibility ?? eligibilityFor({ machine, project })
354
402
  const stateStore = createMaintenanceStateStore({ workspaceRoot, workspaceId })
355
403
  known = { stateStore, maintenanceMode: machine.maintenanceMode }
356
- const scopes = enablement.scopes.map((scope) => ({ scope, store: storeFor(scope, workspaceRoot, workspaceId, repositoryRoots) }))
404
+ // Each view's vault: allocated now where this workspace decided its vaults live, when it has none and was never
405
+ // published under the data root (vault-location.mjs). A view whose folder cannot be allocated is not prepared on
406
+ // this tick, and its freshness says why; the other views go on.
407
+ const allocationRefusals = new Map()
408
+ if (machine.decisions.location !== null) {
409
+ // The app's list, read once per tick and only when a view is about to be allocated. An engine given no reader
410
+ // coordinates with no app, and knows no list. A list that cannot be read allocates nothing: the view says why,
411
+ // and its folder is allocated at a later tick, once the list reads.
412
+ let list = null
413
+ const appList = async () => {
414
+ if (typeof readAppVaultList !== 'function') return { ok: true, vaults: null }
415
+ if (list === null) {
416
+ let answer
417
+ try { answer = await readAppVaultList() } catch { answer = null }
418
+ list = answer?.ok === true ? { ok: true, vaults: answer.vaults ?? {} } : { ok: false, code: typeof answer?.code === 'string' ? answer.code : 'obsidian-settings-unreadable' }
419
+ }
420
+ return list
421
+ }
422
+ // The folders allocated to the views; a record that cannot be read refuses only its own view, below.
423
+ const allocatedPaths = enablement.scopes.map((scope) => { try { return readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })?.path } catch { return undefined } }).filter((item) => typeof item === 'string')
424
+ for (const scope of enablement.scopes) {
425
+ try {
426
+ let vaults = null
427
+ // The list is read where a folder is about to be made: a first allocation, or an allocated folder that has gone.
428
+ const record = readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })
429
+ if (record === null ? !hasCommittedGeneration({ workspaceRoot, scopeId: scope.scopeId }) : allocatedFolderState(record) === 'missing') {
430
+ const known = await appList()
431
+ if (!known.ok) refuse('app-vault-list-unreadable', 'Obsidian\'s vault list could not be read, so no folder is allocated for this view yet; it is tried again at the next tick', { cause: known.code })
432
+ vaults = known.vaults
433
+ }
434
+ ensureVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId, location: machine.decisions.location, projectName: projectDisplayName(project), repositoryRoots, vaults, allocatedPaths, now })
435
+ } catch (error) {
436
+ const code = viewRefusalCode(error)
437
+ if (code === null) throw error
438
+ allocationRefusals.set(scope.scopeId, code)
439
+ }
440
+ }
441
+ }
442
+ // Each view's store, on its own: one whose vault cannot be placed (a record naming a folder inside a repository,
443
+ // say) is refused alone, and the other views go on.
444
+ const scopes = []
445
+ for (const scope of enablement.scopes) {
446
+ if (allocationRefusals.has(scope.scopeId)) continue
447
+ try { scopes.push({ scope, store: storeFor(scope, workspaceRoot, workspaceId, repositoryRoots) }) } catch (error) {
448
+ const code = viewRefusalCode(error)
449
+ if (code === null) throw error
450
+ allocationRefusals.set(scope.scopeId, code)
451
+ }
452
+ }
357
453
 
358
454
  watch([
359
455
  ...(project.repos ?? []).filter((repo) => !repo.external && typeof repo.path === 'string').map((repo) => ({ id: `repo:${repo.name}`, path: repo.path, recursive: true, keyPrefix: sourceKey(repo.name, ''), keyOf: (relative) => sourceKey(repo.name, relative) })),
360
456
  ...scopes.map(({ scope, store }) => ({ id: `vault:${scope.scopeId}`, path: store.vaultRoot, recursive: true, keyPrefix: vaultKey(scope.scopeId, ''), keyOf: (relative) => vaultKey(scope.scopeId, relative) })),
361
- ...[...new Set(listConfigFiles(project).map((file) => path.dirname(file.absolute)))].filter((directory) => fs.existsSync(directory))
457
+ ...[...new Set(configFilesOf(project).map((file) => path.dirname(file.absolute)))].filter((directory) => fs.existsSync(directory))
362
458
  .map((directory) => ({ id: `config:${directory}`, path: directory, recursive: false, keyPrefix: CONFIG_PREFIX, keyOf: (relative) => configKey(path.join(directory, relative)) })),
363
459
  ])
364
460
 
@@ -385,9 +481,12 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
385
481
  // Observation. The registered proposal adapter is handed a copy of the pending edits and observes the open ones
386
482
  // the object store does not know yet, a bounded number per tick: each is classified from its preserved bytes
387
483
  // against the source as it is now and recorded as what it is, in manual and in automatic mode alike, and no
388
- // source is written. What refused before anything was recorded is offered again on a full reconciliation only.
484
+ // source is written. What refused before anything was recorded is offered again on a full reconciliation only;
485
+ // for a personal workspace, also on the tick after one whose graph was pending, which offers every earlier
486
+ // refusal again.
389
487
  const proposalAdapter = extensions.get('proposal-adapter')
390
- const adapterContext = () => ({ project, workspaceRoot, workspaceId, repositoryRoots, edits: structuredClone(edits), clock, env })
488
+ // A personal workspace is never composed on this event loop by the adapter: its builds defer, as the engine's do.
489
+ const adapterContext = () => ({ project, workspaceRoot, workspaceId, repositoryRoots, edits: structuredClone(edits), clock, env, deferPersonalValidation: true })
391
490
  let observed = null
392
491
  if (proposalAdapter !== null && rules.observeEdits(proposalAdapter)) {
393
492
  try { observed = await proposalAdapter.observe(adapterContext(), { retryRefused: full }) } catch { observed = { adapterId: proposalAdapter.id, failed: 'proposal-adapter-threw' } }
@@ -455,7 +554,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
455
554
  if (full) { lastFullMs = nowMs; forceFull = false }
456
555
 
457
556
  // 7. Which views are invalid.
458
- declaredScopes = new Set(scopes.map(({ scope }) => scope.scopeId))
557
+ declaredScopes = new Set(enablement.scopes.map((scope) => scope.scopeId))
459
558
  const previous = readPreviousFreshness(stateStore)
460
559
  const entries = new Map()
461
560
  const unseen = new Set()
@@ -479,9 +578,21 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
479
578
  if (earlierLayout.has(scope.scopeId) && entry.state === 'current' && layoutHeldOf(scope.scopeId).length === 0) invalidate(scope.scopeId, null)
480
579
  }
481
580
  for (const change of changes) for (const { scope } of scopes) if (change.scopeId === undefined || change.scopeId === scope.scopeId) invalidate(scope.scopeId, change.changeClass)
581
+ // A bound personal workspace is composed again, off the event loop, at every full reconciliation, whatever else is
582
+ // being prepared: a cause outside its validity key can stop a generation holding without a file this engine
583
+ // observes. Its views are prepared again only when the composition refuses, or confirms another key.
584
+ let personalRefusal = null
585
+ if (full && personalWorkspaceBindingOf(project) !== null) {
586
+ const verdict = await validatePersonalWorkspace(project)
587
+ if (verdict.ok !== true) personalRefusal = verdict.code
588
+ if (verdict.ok !== true || verdict.changed) for (const { scope } of scopes) invalidate(scope.scopeId, null)
589
+ }
482
590
 
483
591
  // Invalidation is durable before any work: a view is not reported current while it is being rebuilt.
484
592
  for (const [scopeId, classes] of attempt) entries.set(scopeId, { ...demote(entries.get(scopeId), 'stale', 'invalidated', now), changeClasses: [...classes].sort() })
593
+ for (const [scopeId, code] of allocationRefusals) {
594
+ entries.set(scopeId, demote(previous?.scopes.find((entry) => entry.scopeId === scopeId && entry.state !== 'disabled') ?? blankEntry(scopeId, now), 'stale', code, now))
595
+ }
485
596
  for (const entry of previous?.scopes ?? []) if (!entries.has(entry.scopeId)) entries.set(entry.scopeId, demote(entry, 'disabled', 'scope-not-configured', now))
486
597
  const persist = () => stateStore.writeFreshness(freshnessDocument({ workspaceId, enablement: 'enabled', maintenanceMode: machine.maintenanceMode, now, entries }))
487
598
  persist()
@@ -490,7 +601,19 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
490
601
  if (attempt.size > 0) {
491
602
  let built = null
492
603
  try {
493
- const graph = seams.buildGraph({ project, eligibility, cache: graphCache, index })
604
+ if (personalRefusal !== null) refuse(personalRefusal, 'the personal workspace refused this generation; nothing is prepared from it', { source: 'personal-workspace' })
605
+ // A personal workspace whose validity key is not confirmed yet is composed off the event loop, then built again.
606
+ const build = () => seams.buildGraph({ project, eligibility, cache: graphCache, index, deferPersonalValidation: true })
607
+ let graph
608
+ try { graph = build() } catch (error) {
609
+ if (error?.code !== PERSONAL_VALIDATION_PENDING) throw error
610
+ const verdict = await validatePersonalWorkspace(project)
611
+ if (verdict?.ok !== true) refuse(verdict?.code ?? 'personal-binding-unrecognized', 'the personal workspace refused this generation; nothing is prepared from it', { source: 'personal-workspace' })
612
+ try { graph = build() } catch (again) {
613
+ if (again?.code !== PERSONAL_VALIDATION_PENDING) throw again
614
+ refuse('personal-graph-unconfirmed', 'the graph built for the personal workspace is not the one its composition built; nothing is prepared from it', { source: 'personal-workspace' })
615
+ }
616
+ }
494
617
  // The assets this graph lets a view copy are observed from now on, and hashed now so the snapshot pins
495
618
  // what observation saw. A withheld asset is not observed: its bytes can change no view.
496
619
  const formerAssetKeys = new Set(observedAssets.map((asset) => sourceKey(asset.repo, asset.path)))
@@ -508,6 +631,9 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
508
631
  if (!isTypedRefusal(error)) throw error
509
632
  for (const scopeId of attempt.keys()) entries.set(scopeId, demote(entries.get(scopeId), 'stale', error.code, now))
510
633
  if (REREAD_CODES.has(error.code)) forceFull = true
634
+ // The personal workspace refused the bound generation: the project is loaded again at the next tick, so a
635
+ // loader that now binds another generation is followed.
636
+ if (error.detail?.source === 'personal-workspace') project = null
511
637
  }
512
638
  for (const { scope, store } of built ? scopes.filter((item) => attempt.has(item.scope.scopeId)) : []) {
513
639
  const { scopeId } = scope
@@ -543,13 +669,29 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
543
669
  // that cannot be qualified never makes a current view stale. Any other publication builds the adapter first,
544
670
  // and an app that does not qualify keeps the publisher from being reached at all.
545
671
  const lazy = trusted()?.generationId === preparedGenerationId ? builtOnFirstUse(() => adapterFactory({ store, scope })) : null
546
- const adapter = lazy ?? await adapterFactory({ store, scope })
672
+ // A view never published into its allocated folder is not kept from its first publication by an app that
673
+ // does not qualify, while that app's list shows no entry for the folder: the publisher is reached with an
674
+ // adapter that coordinates with nothing, and may take its `direct-unheld` path. Any other outcome is the
675
+ // factory's refusal, as before.
676
+ let heldBackBy = null
677
+ let adapter = lazy
678
+ if (adapter === null) {
679
+ try { adapter = await adapterFactory({ store, scope }) } catch (error) {
680
+ if (!isTypedRefusal(error) || !(await unheldMayApply(store, trusted()))) throw error
681
+ heldBackBy = error
682
+ adapter = uncoordinatedAdapter(error)
683
+ }
684
+ }
547
685
  let result
548
686
  try {
549
687
  result = await seams.publishView({
550
688
  preparedView: prepared, protocolId: PROTOCOL_ID, expectedGeneration: trusted()?.generationId ?? null, recoveryStore: store, adapter, clock,
551
689
  ...(quietPeriodMs === undefined ? {} : { quietPeriodMs }),
690
+ ...(typeof readUnheldEvidence === 'function' ? { unheldEvidence: readUnheldEvidence, platform } : {}),
552
691
  })
692
+ // Stopped before its commit for want of an app that coordinates: the app's refusal is what the person has to
693
+ // act on, and `open` answers it before it would add the vault to the app.
694
+ if (heldBackBy !== null && (result.state === 'refused' || (result.state === 'updating' && result.notes.some((note) => note.blocking && note.outcome === 'editor-uncoordinated')))) throw heldBackBy
553
695
  } catch (error) {
554
696
  // The publisher asked for the app only to publish the committed generation again (a plugin file drifted),
555
697
  // and the app did not qualify: nothing was written, and the committed generation is read back as it is.
@@ -579,7 +721,8 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
579
721
  // Somebody wrote during the publication. The next tick preserves and queues it.
580
722
  else if (differing.length > 0 || editedUnderPublisher) settle('updating', 'read-back-differs', common)
581
723
  else if (common.generationId !== preparedGenerationId) settle('stale', 'committed-generation-differs', common)
582
- else settle('current', result.alreadyCommitted ? 'verified-by-read-back' : 'published-and-verified', { ...common, heldNotes: [], verified: true })
724
+ // Kept current while a plugin file waits for the app: the reason says so, and status shows it on the plugin line.
725
+ else settle('current', result.waitsForApp === true ? PLUGIN_FILES_WAIT_FOR_APP : result.alreadyCommitted ? 'verified-by-read-back' : 'published-and-verified', { ...common, heldNotes: [], verified: true })
583
726
  }
584
727
  } catch (error) {
585
728
  if (!isTypedRefusal(error)) { settle('stale', 'publisher-error'); persist(); throw error }
@@ -65,7 +65,7 @@ export function normalizeApplyResult(value) {
65
65
  // an apply operation replaces it.
66
66
  const OPERATION_NAME = /^[a-z][a-z0-9-]{0,31}$/
67
67
  // The operations the command ships. The service composes the same registry, so both refuse the same contributions.
68
- export const BUILT_IN_OPERATIONS = Object.freeze(['status', 'scope', 'audience', 'mode', 'policy', 'service', 'open', 'plugin', 'apply', 'help'])
68
+ export const BUILT_IN_OPERATIONS = Object.freeze(['status', 'settings', 'location', 'scope', 'view', 'audience', 'mode', 'policy', 'service', 'open', 'uninstall', 'plugin', 'apply', 'help'])
69
69
  export const REPLACEABLE_OPERATIONS = Object.freeze(['apply'])
70
70
 
71
71
  export function createCommandOperations({ reserved = [] } = {}) {
@@ -6,8 +6,9 @@
6
6
  export { ObsidianMaintenanceRefusal } from './errors.mjs'
7
7
  export { DISABLED_REASONS, ENABLEMENT_STATES, readObsidianEnablement } from './enablement.mjs'
8
8
  export {
9
- LOCAL_POINTER_SCHEMA, MACHINE_SETTINGS_SCHEMA, MAINTENANCE_MODES, authorizeAutomaticApply, defaultDataRoot, defaultMachineSettings, ensureWorkspaceIdentity,
10
- installApplyPolicy, localPointerPath, protectedRoots, readInstalledApplyPolicy, readLocalPointer, readMachineSettings, resolveDataRoot, revokeApplyPolicy, workspaceStateRoot, writeLocalPointer, writeMachineSettings,
9
+ DECISIONS, DECISION_SOURCES, LOCAL_POINTER_SCHEMA, MACHINE_SETTINGS_SCHEMA, MACHINE_SETTINGS_SCHEMA_V1, MAINTENANCE_MODES, ONLY_YOU_AUDIENCES, authorizeAutomaticApply,
10
+ defaultDataRoot, defaultMachineSettings, ensureWorkspaceIdentity, installApplyPolicy, localPointerPath, protectedRoots, readInstalledApplyPolicy, readLocalPointer,
11
+ readMachineSettings, resolveDataRoot, revokeApplyPolicy, withDecision, workspaceStateRoot, writeLocalPointer, writeMachineSettings,
11
12
  } from './machine-settings.mjs'
12
13
  export { APPLY_RESULT_STATUSES, BUILT_IN_OPERATIONS, EXTENSION_KINDS, REPLACEABLE_OPERATIONS, UNAVAILABLE_APPLY_OPERATION, createCommandOperations, createMaintenanceExtensions, createObsidianRegistry } from './extension-points.mjs'
13
14
  export { CONTRIBUTIONS_DIRECTORY, loadContributions } from './contributions.mjs'
@@ -20,19 +21,21 @@ export {
20
21
  } from './state-store.mjs'
21
22
  export { listConfigFiles, listSourceFiles, listVaultNotes, reconcile } from './observation.mjs'
22
23
  export { createFsWatcherFactory, createNullWatcherFactory } from './watchers.mjs'
23
- export { DEFAULT_ELIGIBILITY, assetEligibilityFor, createProductionSeams } from './pipeline.mjs'
24
+ export { DEFAULT_ELIGIBILITY, assetEligibilityFor, createProductionSeams, eligibilityFor, onlyYouEligibility } from './pipeline.mjs'
24
25
  export { DEFAULT_FULL_RECONCILIATION_INTERVAL_MS, DEFAULT_LATE_WRITER_WINDOW_MS, DEFAULT_PUBLICATION_RETRY_MS, DEFAULT_RETRY_INTERVAL_MS, createMaintenanceEngine } from './engine.mjs'
25
26
  export { ENGINE_LOCK_DIRECTORY, LOCK_TICKET_SCHEMA, acquirePrivateGenerationLock, createAbandonmentProof, inspectPrivateGenerationLock } from './private-lock.mjs'
26
27
  export { DEFAULT_MAX_BACKOFF_MS, DEFAULT_TICK_INTERVAL_MS, createTickLoop } from './tick-loop.mjs'
27
28
  export { HEALTH_SCHEMA, LOOPBACK_HOSTS, probeHealth, requestLoopback } from './service-client.mjs'
28
29
  export {
29
- CONSENT_COVERAGES, SERVICE_ERROR_SCHEMA, SERVICE_SETTINGS_SCHEMA, executableIdentity, publicRecord, readLastServiceError, readServiceRecord, readServiceSettings,
30
- removeServiceRecord, serviceNameFor, servicePaths, validateServiceRecord, writeServiceRecord, writeServiceSettings,
30
+ CONSENT_COVERAGES, LAST_STARTUP_SCHEMA, SERVICE_ERROR_SCHEMA, SERVICE_SETTINGS_SCHEMA, STARTUP_OUTCOMES, executableIdentity, publicRecord, readLastServiceError, readLastStartup,
31
+ readServiceRecord, readServiceSettings, removeServiceRecord, serviceNameFor, servicePaths, validateServiceRecord, writeServiceRecord, writeServiceSettings,
31
32
  } from './service-record.mjs'
32
33
  export { MAX_REQUEST_BYTES, SERVICE_OPERATIONS, createServiceServer } from './service-server.mjs'
33
34
  export { PLUGIN_BEARER_SCHEMA, PLUGIN_PRESENCE_SCHEMA, createPluginChannel, createPluginSessions, ensurePluginBearer, pluginBearerDirectory, pluginPresence, readPluginBearers } from './plugin-channel.mjs'
34
35
  export { PLUGIN_PRESENCE_REASONS, pluginPresenceOf, turnPluginOnNext, withPluginReportedVersion } from './plugin-presence.mjs'
35
36
  export { PLUGIN_CHOICE_SCHEMA, PLUGIN_CHOICE_STATES, confirmPluginEntry, confirmPluginSeen, currentPluginChoice, decidePluginChoice, pluginChoiceDirectory, readCommunityEntry, readPluginChoice, viewVaultRoot, writePluginChoice } from './plugin-choice.mjs'
36
- export { DEFAULT_SHUTDOWN_GRACE_MS, SERVICE_STATUS_SCHEMA, resolveServiceWorkspace, runMaintenanceService } from './service.mjs'
37
+ export { DEFAULT_SHUTDOWN_GRACE_MS, RELEASE_CHANGED, SERVICE_STATUS_SCHEMA, resolveServiceWorkspace, runMaintenanceService } from './service.mjs'
37
38
  export { DEFAULT_START_TIMEOUT_MS, DEFAULT_STOP_TIMEOUT_MS, SERVICE_STATES, readServiceStatusDocument, requestServiceTick, serviceStatus, startService, stopService } from './lifecycle.mjs'
38
- export { STARTUP_PLATFORMS, buildStartupAdapter } from './startup-adapters.mjs'
39
+ export { STARTUP_PLATFORMS, buildStartupAdapter, startupSearchPath } from './startup-adapters.mjs'
40
+ export { LOGIN_ITEM_SCHEMA, installLoginItem, loginItemStatus, removeLoginItem, resolveLoginItemEntry } from './login-item.mjs'
41
+ export { SERVICE_MANAGER_KINDS, createLaunchdManager, createSystemdManager } from './service-managers.mjs'
@@ -55,11 +55,14 @@ export const urlProcessed = (stdout) => typeof stdout === 'string' && /^processe
55
55
  //
56
56
  // A URL is never handed to the operating system for an app this launch
57
57
  // started: that would start or reach it while it starts, which is what drops
58
- // the other vaults' reopen flags. The vault was added flagged open, so a plainly
59
- // started app opens it anyway, and `open` waits for it.
60
- export async function runLaunchPlan(plan, { start, answered, handLink, osOpen, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), waitMs = 30_000, pollMs = 500, now = () => Date.now() }) {
58
+ // the other vaults' reopen flags. A vault added flagged open is opened anyway by
59
+ // a plainly started app, and `open` waits for it; one already listed but closed
60
+ // is handed the link once more instead (opening.mjs). `startedEarlier` says
61
+ // that an earlier launch of the same `open` started the app (a link handed to
62
+ // it once more): the link then goes through the app's tool only, as well.
63
+ export async function runLaunchPlan(plan, { start, answered, handLink, osOpen, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), waitMs = 30_000, pollMs = 500, now = () => Date.now(), startedEarlier = false }) {
61
64
  if (!plan?.ok) return { launched: false, reason: plan?.reason ?? 'launcher-refused' }
62
- let started = false
65
+ let started = startedEarlier === true
63
66
  let reason = 'url-accepted'
64
67
  for (const { step, uri } of plan.steps) {
65
68
  if (step === 'plain-start') {