@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,282 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { validateProjectConfigDoc } from '../../project/config.mjs'
4
+ import { MAX_EXPANSION_DEPTH, OBSIDIAN_EXT_KEY, ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
5
+ import { readObsidianEnablement } from './enablement.mjs'
6
+ import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
7
+
8
+ // The views a project declares, written into its `atelier.project.json` for a
9
+ // person, so nobody edits JSON by hand: `view add`, and the view a project
10
+ // that never used Obsidian gets on its first open.
11
+ //
12
+ // The project file is committed source. Atelier changes it only when asked,
13
+ // shows the exact change as a diff, and never commits. It rewrites a file only
14
+ // when the file is in the form Atelier writes JSON in (two-space indentation,
15
+ // with LF or CRLF line ends, with or without a final one), so the change is
16
+ // the Obsidian member and nothing else; and only over the bytes it read. The
17
+ // workspace pointer is only ever written into an ignored `.atelier-local/`, so
18
+ // a project where that folder is not ignored gets the ignore line in the same
19
+ // change.
20
+
21
+ export const EXT_SETTINGS_SCHEMA = 'atelier-obsidian-ext-settings/v1'
22
+ // Every note this machine may show, in one view that is the default. Its name reads well in a command
23
+ // (`--scope everything`) and in a vault's name (`harbor-notes (everything)`).
24
+ export const DEFAULT_VIEW = Object.freeze({ scopeId: 'everything', mode: 'full', selector: Object.freeze({ all: true }) })
25
+ const LOCAL_STATE_LINE = '.atelier-local/'
26
+ const SCOPE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
27
+ const MAX_EXPANSION_NODES = 100000
28
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
29
+
30
+ const linesOf = (text) => { const lines = text.split(/\r?\n/); if (lines.at(-1) === '') lines.pop(); return lines }
31
+ // A hunk's range: the first line and the count, or the line before an empty range.
32
+ const range = (start, count) => (count === 0 ? `${start},0` : `${start + 1},${count}`)
33
+
34
+ // The lines that differ between two texts, as one unified hunk with up to three lines of context on each side. Enough
35
+ // for a change that inserts or replaces one run of lines, which is every change made here.
36
+ export function unifiedDiff(before, after, { label }) {
37
+ if (before === after) return ''
38
+ const [left, right] = [linesOf(before), linesOf(after)]
39
+ let prefix = 0
40
+ while (prefix < left.length && prefix < right.length && left[prefix] === right[prefix]) prefix += 1
41
+ let suffix = 0
42
+ while (suffix < left.length - prefix && suffix < right.length - prefix && left[left.length - 1 - suffix] === right[right.length - 1 - suffix]) suffix += 1
43
+ const context = 3
44
+ const start = Math.max(0, prefix - context)
45
+ const leftEnd = Math.min(left.length, left.length - suffix + context)
46
+ const rightEnd = Math.min(right.length, right.length - suffix + context)
47
+ const lines = [
48
+ `--- ${label}`, `+++ ${label}`, `@@ -${range(start, leftEnd - start)} +${range(start, rightEnd - start)} @@`,
49
+ ...left.slice(start, prefix).map((line) => ` ${line}`),
50
+ ...left.slice(prefix, left.length - suffix).map((line) => `-${line}`),
51
+ ...right.slice(prefix, right.length - suffix).map((line) => `+${line}`),
52
+ ...right.slice(right.length - suffix, rightEnd).map((line) => ` ${line}`),
53
+ ]
54
+ return `${lines.join('\n')}\n`
55
+ }
56
+
57
+ const jsonText = (document, { eol, final }) => {
58
+ const text = JSON.stringify(document, null, 2)
59
+ return `${eol === '\n' ? text : text.replaceAll('\n', '\r\n')}${final ? eol : ''}`
60
+ }
61
+
62
+ // The form `text` is `document` written in, or null when it is in none Atelier writes.
63
+ function formOf(text, document) {
64
+ for (const eol of ['\n', '\r\n']) for (const final of [true, false]) if (text === jsonText(document, { eol, final })) return { eol, final }
65
+ return null
66
+ }
67
+
68
+ // A file Atelier may replace: a regular file, not a link to one elsewhere. `bytes` is null when there is none.
69
+ function readRegularFile(file, { code, what }) {
70
+ const stat = fs.lstatSync(file, { throwIfNoEntry: false })
71
+ if (stat === undefined) return { bytes: null, mode: null }
72
+ if (!stat.isFile()) refuse('project-file-not-regular', `${what} is not a regular file, so Atelier does not rewrite it; make the change by hand`, { file: path.basename(file) })
73
+ try { return { bytes: fs.readFileSync(file), mode: stat.mode & 0o777 } } catch (error) { return refuse(code, `${what} cannot be read`, { cause: error.code ?? 'unreadable' }) }
74
+ }
75
+
76
+ // The text of a file's bytes, only when they are UTF-8 that decodes and encodes back to exactly them: a file in another
77
+ // encoding is never rewritten, since a byte that is not UTF-8 would be replaced without the diff showing it.
78
+ function textOf(bytes, refusal) {
79
+ const text = bytes.toString('utf8')
80
+ if (!Buffer.from(text, 'utf8').equals(bytes)) refusal()
81
+ return text
82
+ }
83
+
84
+ // The settings as enablement will read them, or a typed refusal naming what is wrong.
85
+ function checkedSettings(document, settings) {
86
+ try {
87
+ readObsidianEnablement({ config: { ...document, ext: { ...(isPlainObject(document.ext) ? document.ext : {}), [OBSIDIAN_EXT_KEY]: settings } } })
88
+ } catch (error) {
89
+ if (error instanceof ObsidianMaintenanceRefusal || error instanceof ObsidianContractRefusal) refuse('invalid-view', 'the views would not satisfy the Obsidian settings contract; nothing was written', { errors: error.detail?.errors ?? [] })
90
+ throw error
91
+ }
92
+ return settings
93
+ }
94
+
95
+ // The project file with `settings` as its Obsidian member: { file, bytes, after, mode, diff }, `bytes` being the ones
96
+ // it was made from. `document` is the file as read. Refuses a file that is not in a form Atelier writes, since
97
+ // rewriting it would change more than the member, and names the member to add by hand.
98
+ function planMember({ file, bytes, before, mode, document, settings }) {
99
+ const form = isPlainObject(document) && (document.ext === undefined || isPlainObject(document.ext)) ? formOf(before, document) : null
100
+ if (form === null) {
101
+ refuse('project-config-format-unknown', `${path.basename(file)} is not in the form Atelier writes (two-space JSON), so rewriting it would change more than the Obsidian member; nothing was written. Add the member by hand, under "ext"`, { member: { [OBSIDIAN_EXT_KEY]: settings } })
102
+ }
103
+ const next = { ...document, ext: { ...(document.ext ?? {}), [OBSIDIAN_EXT_KEY]: settings } }
104
+ const errors = validateProjectConfigDoc(next)
105
+ if (errors.length > 0) refuse('invalid-view', 'the project configuration would not be valid; nothing was written', { errors })
106
+ const after = jsonText(next, form)
107
+ return { file, what: path.basename(file), bytes, after, mode, diff: unifiedDiff(before, after, { label: path.basename(file) }) }
108
+ }
109
+
110
+ // The ignore line a project needs before the workspace pointer can be written:
111
+ // { needed, file, bytes, after, mode, diff }.
112
+ function planIgnore(project) {
113
+ if (project?.localState?.ignored === true) return { needed: false }
114
+ const file = path.join(project.configDir, '.gitignore')
115
+ const { bytes, mode } = readRegularFile(file, { code: 'gitignore-unreadable', what: 'the project\'s .gitignore' })
116
+ const before = bytes === null ? '' : textOf(bytes, () => refuse('gitignore-not-utf8', `the project's .gitignore is not UTF-8, so Atelier does not rewrite it; add the line ${LOCAL_STATE_LINE} to it by hand; nothing was written`, { line: LOCAL_STATE_LINE }))
117
+ const eol = before.includes('\r\n') ? '\r\n' : '\n'
118
+ const after = `${before}${before === '' || before.endsWith('\n') ? '' : eol}${LOCAL_STATE_LINE}${eol}`
119
+ return { needed: true, file, what: 'the project\'s .gitignore', bytes, after, mode, diff: unifiedDiff(before, after, { label: '.gitignore' }) }
120
+ }
121
+
122
+ // The project file as it is now: its bytes and their text, its mode, and its document (null when it is not JSON).
123
+ function readProjectFile(project) {
124
+ const file = project?.configPath
125
+ if (typeof file !== 'string') refuse('project-config-unreadable', 'this project has no configuration file to write the view into')
126
+ const { bytes, mode } = readRegularFile(file, { code: 'project-config-unreadable', what: path.basename(file) })
127
+ if (bytes === null) refuse('project-config-unreadable', `${path.basename(file)} is not there`)
128
+ // Not UTF-8: in no form Atelier writes. The member is named for the person when the rest of the file can be read.
129
+ let before
130
+ try { before = textOf(bytes, () => { throw new SyntaxError('not UTF-8') }) } catch {
131
+ return { file, bytes, before: null, mode, document: null }
132
+ }
133
+ let document = null
134
+ try { document = JSON.parse(before) } catch { document = null }
135
+ return { file, bytes, before, mode, document }
136
+ }
137
+
138
+ // A folder as the repository-relative prefix of the notes a view selects, `./` and a trailing `/` dropped; null for
139
+ // the whole repository.
140
+ function folderPrefix(text) {
141
+ const cleaned = String(text).replace(/^(?:\.\/)+/, '').replace(/\/+$/, '')
142
+ if (cleaned === '' || cleaned === '.') return null
143
+ if (cleaned.startsWith('/') || cleaned.startsWith('~') || /^[A-Za-z]:/.test(cleaned) || cleaned.includes('\\') || cleaned.includes('\u0000') || cleaned.split('/').includes('..')) {
144
+ refuse('invalid-view', 'a folder is written relative to its repository\'s root, with "/", and without ".." (docs/notes, say)', { folder: String(text).slice(0, 200) })
145
+ }
146
+ return cleaned
147
+ }
148
+
149
+ // `DEPTH:MAX`: follow outgoing links up to DEPTH steps from the selected notes, adding at most MAX notes in all.
150
+ function expansionOf(text) {
151
+ const match = /^([0-9]{1,2}):([0-9]{1,6})$/.exec(String(text))
152
+ const depth = match ? Number(match[1]) : NaN
153
+ const maxNodes = match ? Number(match[2]) : NaN
154
+ if (!(depth >= 1 && depth <= MAX_EXPANSION_DEPTH && maxNodes >= 1 && maxNodes <= MAX_EXPANSION_NODES)) {
155
+ refuse('invalid-view', `--expand is DEPTH:MAX, a depth from 1 to ${MAX_EXPANSION_DEPTH} and at most ${MAX_EXPANSION_NODES} notes in all (2:200, say)`)
156
+ }
157
+ return { depth, maxNodes, direction: 'outgoing', order: 'canonical-id' }
158
+ }
159
+
160
+ // The view `view add` asks for: every note (`all`, a full view), or a scoped view of the notes under folders of one
161
+ // repository or with a tag, optionally expanded along outgoing links. `repositories` are the names of the project's
162
+ // managed repositories; `repo` may be left out when there is one. (The contract's `type` selector matches a note's
163
+ // file type, not its `kg.type`, so it is not offered here.)
164
+ export function viewFromRequest({ scopeId, all = false, folders = [], repo, tag, expand, repositories = [] }) {
165
+ if (typeof scopeId !== 'string' || !SCOPE_ID.test(scopeId)) refuse('usage', 'a view is named by an identifier: a letter or digit, then letters, digits, ".", "_", ":" or "-", 128 at most')
166
+ const kinds = [all === true, folders.length > 0, tag !== undefined].filter(Boolean).length
167
+ if (kinds !== 1) refuse('usage', 'a view selects its notes with exactly one of --all, --folder PATH (as often as needed) or --tag T')
168
+ if (repo !== undefined && folders.length === 0) refuse('usage', '--repo names the repository of --folder')
169
+ if (all === true && expand !== undefined) refuse('usage', 'a view of every note has nothing to expand')
170
+ const expansion = expand === undefined ? undefined : expansionOf(expand)
171
+ let selector
172
+ if (all === true) selector = { all: true }
173
+ else if (tag !== undefined) selector = { tag }
174
+ else {
175
+ if (repo === undefined && repositories.length !== 1) refuse('view-repository-ambiguous', 'this project enrols more than one repository; name the one the folders are in with --repo', { repositories })
176
+ const repoId = repo ?? repositories[0]
177
+ if (!repositories.includes(repoId)) refuse('unknown-repo', 'the project enrols no repository of that name', { repositories })
178
+ const prefixes = [...new Set(folders.map(folderPrefix))]
179
+ const members = prefixes.includes(null) ? [{ repo: repoId }] : prefixes.map((pathPrefix) => ({ repo: repoId, pathPrefix }))
180
+ selector = members.length === 1 ? members[0] : { union: members }
181
+ }
182
+ return { scopeId, mode: all === true ? 'full' : 'scoped', selector, ...(expansion === undefined ? {} : { expansion }) }
183
+ }
184
+
185
+ // One more view: the project's Obsidian member with `scope` added (the member made, enabled, when there is none), and
186
+ // the default when asked or when it is the first view. Refuses a view whose name is taken. Writes nothing:
187
+ // { settings, member, ignore, diff }, for writeViewPlan.
188
+ export function planViewAdd(project, { scope, makeDefault = false }) {
189
+ const source = readProjectFile(project)
190
+ const document = isPlainObject(source.document) ? source.document : project.config
191
+ const present = isPlainObject(document?.ext) ? document.ext[OBSIDIAN_EXT_KEY] : undefined
192
+ const scopes = Array.isArray(present?.scopes) ? present.scopes : []
193
+ if (scopes.some((item) => item?.scopeId === scope.scopeId)) refuse('view-exists', 'the project declares a view of this name already', { scopeId: scope.scopeId })
194
+ // A member of its own keeps its keys in their order; a new one is written as the documentation shows it.
195
+ const next = present === undefined
196
+ ? { schema: EXT_SETTINGS_SCHEMA, enabled: true, defaultScopeId: scope.scopeId, scopes: [scope] }
197
+ : { ...present, scopes: [...scopes, scope], ...(makeDefault || scopes.length === 0 ? { defaultScopeId: scope.scopeId } : {}) }
198
+ const settings = checkedSettings(document ?? {}, next)
199
+ const member = planMember({ ...source, settings })
200
+ const ignore = planIgnore(project)
201
+ return { settings, member, ignore, diff: `${member.diff}${ignore.needed ? ignore.diff : ''}` }
202
+ }
203
+
204
+ // A planned file still holds exactly the bytes its plan was made from (a file that was not there is still not there).
205
+ function assertUnchanged({ file, what, bytes: planned }) {
206
+ const { bytes } = readRegularFile(file, { code: 'project-file-unreadable', what })
207
+ if (planned === null ? bytes !== null : bytes === null || !bytes.equals(planned)) {
208
+ refuse('project-config-changed', `${path.basename(file)} changed while the change was prepared; nothing was written`, { file: path.basename(file) })
209
+ }
210
+ }
211
+
212
+ // The temporary file a planned file's new text is written to, beside it: `.<name>.<pid>.<ms>.atelier.tmp`.
213
+ const temporaryOf = (file) => path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.atelier.tmp`)
214
+ const isAlive = (pid) => { try { process.kill(pid, 0); return true } catch (error) { return error.code === 'EPERM' } }
215
+ // Temporary files an earlier run left beside a planned file when it stopped between writing and renaming (a crash):
216
+ // removed, unless the run that made them still runs, so none is left to be committed.
217
+ function removeLeftovers(file) {
218
+ const pattern = new RegExp(`^\\.${path.basename(file).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.([0-9]+)\\.[0-9]+\\.atelier\\.tmp$`)
219
+ let names = []
220
+ try { names = fs.readdirSync(path.dirname(file)) } catch { return }
221
+ for (const name of names) {
222
+ const match = pattern.exec(name)
223
+ if (match !== null && (Number(match[1]) === process.pid || !isAlive(Number(match[1])))) fs.rmSync(path.join(path.dirname(file), name), { force: true })
224
+ }
225
+ }
226
+
227
+ // A planned file's new text in its temporary file, keeping its mode, and on the disk before any file is replaced.
228
+ function writeTemporary({ file, after, mode }) {
229
+ const temporary = temporaryOf(file)
230
+ const descriptor = fs.openSync(temporary, 'wx', mode ?? 0o666)
231
+ try {
232
+ fs.writeFileSync(descriptor, after)
233
+ if (mode !== null) fs.fchmodSync(descriptor, mode)
234
+ fs.fsyncSync(descriptor)
235
+ } catch (error) {
236
+ fs.closeSync(descriptor)
237
+ fs.rmSync(temporary, { force: true })
238
+ throw error
239
+ } finally {
240
+ try { fs.closeSync(descriptor) } catch { /* closed above */ }
241
+ }
242
+ return temporary
243
+ }
244
+
245
+ // The folder of a replaced file, on the disk, so the renames survive a crash. A system that cannot sync a folder
246
+ // (Windows) keeps the renames all the same.
247
+ function syncFolder(folder) {
248
+ let descriptor
249
+ try { descriptor = fs.openSync(folder, 'r'); fs.fsyncSync(descriptor) } catch { /* not supported here */ } finally { if (descriptor !== undefined) fs.closeSync(descriptor) }
250
+ }
251
+
252
+ // Writes a plan of planViewAdd when every file it changes still holds the bytes it was made from: every new text is
253
+ // written and synced to its temporary file first, then the files are replaced, the ignore line first, so a project
254
+ // file that names a view never waits on it. A failure before the first replacement changes nothing
255
+ // (`project-files-not-written`); one after it says which files changed (`project-files-partly-written`). No temporary
256
+ // file is left. Commits nothing. Answers the files written. `rename` is injectable for tests.
257
+ export function writeViewPlan(plan, { rename = fs.renameSync } = {}) {
258
+ const targets = [...(plan.ignore.needed ? [plan.ignore] : []), plan.member]
259
+ for (const target of targets) removeLeftovers(target.file)
260
+ for (const target of targets) assertUnchanged(target)
261
+ const prepared = []
262
+ const discard = () => { for (const { temporary } of prepared) fs.rmSync(temporary, { force: true }) }
263
+ try {
264
+ for (const target of targets) prepared.push({ target, temporary: writeTemporary(target) })
265
+ } catch (error) {
266
+ discard()
267
+ refuse('project-files-not-written', 'the change could not be written; nothing was changed', { cause: error.code ?? String(error.message) })
268
+ }
269
+ const written = []
270
+ for (const { target, temporary } of prepared) {
271
+ try { rename(temporary, target.file) } catch (error) {
272
+ discard()
273
+ const names = (list) => list.map((file) => path.basename(file))
274
+ const rest = targets.map((item) => item.file).filter((file) => !written.includes(file))
275
+ if (written.length === 0) refuse('project-files-not-written', 'the change could not be written; nothing was changed', { cause: error.code ?? String(error.message) })
276
+ refuse('project-files-partly-written', `${names(written).join(', ')} changed, and ${names(rest).join(', ')} could not be; finish the change by hand, or undo it`, { written: names(written), notWritten: names(rest), cause: error.code ?? String(error.message) })
277
+ }
278
+ written.push(target.file)
279
+ }
280
+ for (const folder of new Set(written.map((file) => path.dirname(file)))) syncFolder(folder)
281
+ return { written }
282
+ }
@@ -0,0 +1,48 @@
1
+ import readline from 'node:readline'
2
+
3
+ // Questions for a person at a terminal, one line each, over the streams the
4
+ // caller gives, so a test types the answers. A question never waits forever:
5
+ // no line within the timeout (10 minutes), or the end of the input, is no
6
+ // answer, so an agent that was handed a terminal still never hangs. Whether a
7
+ // person is there at all is decided before anything is asked
8
+ // (src/commands/obsidian.mjs `isInteractive`).
9
+
10
+ export const ANSWER_TIMEOUT_MS = 10 * 60 * 1000
11
+ const MAX_ANSWER_LENGTH = 4096
12
+ const clean = (line) => line.slice(0, MAX_ANSWER_LENGTH).trim()
13
+
14
+ // `ask(question)` writes the question and answers the next line typed, trimmed, or null when none came. A line typed
15
+ // before its question is kept for it. `close()` stops reading.
16
+ export function createQuestioner({ input, output, timeoutMs = ANSWER_TIMEOUT_MS }) {
17
+ const typed = []
18
+ const waiting = []
19
+ let ended = false
20
+ const lines = readline.createInterface({ input, terminal: false, crlfDelay: Infinity })
21
+ lines.on('line', (line) => { const next = waiting.shift(); if (next === undefined) typed.push(line); else next(line) })
22
+ lines.on('close', () => { ended = true; for (const next of waiting.splice(0)) next(null) })
23
+ return {
24
+ ask(question) {
25
+ output.write(question)
26
+ if (typed.length > 0) return Promise.resolve(clean(typed.shift()))
27
+ if (ended) return Promise.resolve(null)
28
+ return new Promise((resolve) => {
29
+ const answer = (line) => { clearTimeout(timer); resolve(line === null ? null : clean(line)) }
30
+ const timer = setTimeout(() => { waiting.splice(waiting.indexOf(answer), 1); output.write('\n'); resolve(null) }, timeoutMs)
31
+ waiting.push(answer)
32
+ })
33
+ },
34
+ close() { lines.close() },
35
+ }
36
+ }
37
+
38
+ // A go-ahead: Enter, y or yes is yes; n or no is no; anything else is asked again, three times in all. null: no answer.
39
+ export async function askGoAhead(questioner, question) {
40
+ for (let attempt = 0; attempt < 3; attempt += 1) {
41
+ const answer = await questioner.ask(question)
42
+ if (answer === null) return null
43
+ const word = answer.toLowerCase()
44
+ if (word === '' || word === 'y' || word === 'yes') return true
45
+ if (word === 'n' || word === 'no') return false
46
+ }
47
+ return null
48
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { packageRootOfEntry, readReleaseIdentity, walkRelease } from './service-record.mjs'
4
+
5
+ // Whether the package a service runs from changed on disk since it started.
6
+ //
7
+ // A service started by a login item runs until the person logs out. When the
8
+ // package it was started from is upgraded meanwhile (`npm i -D
9
+ // @mnstry/atelier@<new>` in the project), it would go on running the release
10
+ // it loaded, and load any module it had not needed yet from the new one. So,
11
+ // under `--startup`, the service asks after every tick whether its package
12
+ // changed, finishes that tick and exits with a code its service manager
13
+ // restarts (service.mjs, service-main.mjs): the next process runs the new
14
+ // release.
15
+ //
16
+ // The package is read through the path the service was started by, not its
17
+ // real path: a linked install (`file:`, `npm link`) that is pointed elsewhere
18
+ // is then seen. What is compared is the release identity (readReleaseIdentity
19
+ // in service-record.mjs, the one a service records at its start): the package
20
+ // version and a digest of every file under `src/`, `contracts/` and
21
+ // `plugins/`, by relative path and content. It is cheap to ask: the files'
22
+ // status (inode, size, change and modification time) is compared first, and
23
+ // the digest is computed only when that differs. A package that cannot be read right now (an install in
24
+ // progress) is not a change yet; it is asked again after the next tick.
25
+
26
+ export { packageRootOfEntry, readReleaseIdentity }
27
+
28
+ // The status of every file the identity covers and of package.json, as one comparable text. A package replaced by an
29
+ // install has new inodes and change times even where the archive kept the modification times.
30
+ export function releaseStatSignature({ root }) {
31
+ const lines = []
32
+ const stat = (relative, absolute) => { const found = fs.lstatSync(absolute); lines.push(`${relative}\0${found.dev}\0${found.ino}\0${found.size}\0${found.mtimeMs}\0${found.ctimeMs}`) }
33
+ walkRelease(root, stat)
34
+ stat('package.json', path.join(root, 'package.json'))
35
+ return lines.join('\n')
36
+ }
37
+
38
+ // { root, started, changed() }. `started` is the identity the service runs, read when the watch is made; `changed()`
39
+ // answers true once the package on disk is another release, and false while it is the same or cannot be read.
40
+ export function createReleaseWatch({ root, identityOf = readReleaseIdentity, signatureOf = releaseStatSignature } = {}) {
41
+ if (typeof root !== 'string' || !path.isAbsolute(root)) throw new TypeError('a release watch needs the absolute root of a package')
42
+ let signature = signatureOf({ root })
43
+ const started = identityOf({ root })
44
+ return {
45
+ root, started,
46
+ changed() {
47
+ let now
48
+ try { now = signatureOf({ root }) } catch { return false }
49
+ if (now === signature) return false
50
+ let identity
51
+ try { identity = identityOf({ root }) } catch { return false }
52
+ if (identity.version === started.version && identity.digest === started.digest) { signature = now; return false }
53
+ return true
54
+ },
55
+ }
56
+ }
@@ -3,20 +3,29 @@ import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { firstString, parseArgs, resolveProjectConfig } from '../../project/config.mjs'
6
- import { runMaintenanceService } from './service.mjs'
6
+ import { workspaceStateRoot } from './machine-settings.mjs'
7
+ import { RELEASE_CHANGED, resolveServiceWorkspace, runMaintenanceService } from './service.mjs'
7
8
 
8
9
  // The process of the maintenance service. `start` runs it as a child; a unit
9
10
  // installed at the operating-system level runs it with `--startup`.
10
11
  //
11
12
  // --project=<absolute atelier.project.json> required
12
13
  // --data-root=<absolute directory> optional; otherwise the pointer, the overlay or the platform default
14
+ // --workspace-id=<identifier> named by a login item: under `--startup`, where a refusal is recorded
15
+ // when the project no longer leads to a workspace
13
16
  // --runtime-id=<identifier> given by `start`, so it can recognise the child it created
14
17
  // --interval-ms=<milliseconds> optional
15
- // --startup run by an operating-system unit; needs a consent that covers startup
18
+ // --startup run by a login item; needs a consent that covers startup
16
19
  // --adapter=obsidian-cli the editor adapter; there is no default
17
20
  //
18
21
  // Without `--adapter` the process refuses before it listens or ticks: reaching
19
22
  // a running app is a decision of whoever starts the service, never a default.
23
+ //
24
+ // Exit codes: 0 after a clean stop, and under `--startup` after a refusal too;
25
+ // 2 (EXIT_REFUSED) after a refusal otherwise; 75 (EXIT_RELEASE_CHANGED) under
26
+ // `--startup` once the package changed on disk; 1 after an error without a
27
+ // code. A refusal is any error that carries a string code, Node's system
28
+ // errors (EACCES, EADDRNOTAVAIL, EPERM) included.
20
29
 
21
30
  // Reaching a real app happens here and nowhere else in the service: the
22
31
  // production probe and the CLI transport are imported only once `--adapter`
@@ -33,52 +42,121 @@ import { runMaintenanceService } from './service.mjs'
33
42
  // only read here.
34
43
  const ADAPTERS = Object.freeze({
35
44
  'obsidian-cli': async () => {
36
- const [{ createObsidianCliAdapter, defaultObsidianProcessProbe }, { createProductionAppProbe }, { createQualifiedAdapterFactory }, { obsidianUserDataDir, readObsidianSettings }, { appStateSignature }] = await Promise.all([
45
+ const [{ createObsidianCliAdapter, defaultObsidianProcessProbe }, { createProductionAppProbe }, { createQualifiedAdapterFactory }, { obsidianSandboxedBuild, obsidianUserDataDir, readObsidianSettings }, { appStateSignature }, { readAppVaultListForAllocation }, { createProductionUnheldEvidence }] = await Promise.all([
37
46
  import('../../projection/obsidian/publication/transport.mjs'), import('./app-production-seams.mjs'), import('./app-capability.mjs'),
38
- import('../../projection/obsidian/publication/vault-list.mjs'), import('./app-registration.mjs'),
47
+ import('../../projection/obsidian/publication/vault-list.mjs'), import('./app-registration.mjs'), import('./vault-location.mjs'), import('./unheld-evidence.mjs'),
39
48
  ])
40
49
  const adapterFactory = createQualifiedAdapterFactory({ appProbe: createProductionAppProbe(), createAdapter: ({ qualification }) => createObsidianCliAdapter({ qualification }) })
41
50
  const userDataDir = obsidianUserDataDir()
42
51
  const observeApp = () => appStateSignature({ processes: defaultObsidianProcessProbe(), settings: userDataDir === null ? null : readObsidianSettings({ userDataDir }) })
52
+ // The app's own vault list, read from its file and never written, so a new vault's folder is never allocated inside
53
+ // a vault it lists nor under a name one of its vaults has; a list that cannot be read allocates nothing.
54
+ const readAppVaultList = () => readAppVaultListForAllocation({
55
+ read: () => (userDataDir === null ? { ok: false, code: 'obsidian-settings-location-unknown' } : readObsidianSettings({ userDataDir })),
56
+ sandboxed: obsidianSandboxedBuild() !== null,
57
+ })
43
58
  return {
44
- adapterFactory, engineOptions: { observeApp },
59
+ // The same file tells the publisher whether a view's first publication may go into a vault no Obsidian lists yet.
60
+ adapterFactory, engineOptions: { observeApp, readAppVaultList, readUnheldEvidence: createProductionUnheldEvidence() },
45
61
  appStatus: () => { const known = adapterFactory.lastQualification(); return known === null ? null : { outcome: known.outcome, reason: known.reason, version: known.version, floor: known.floor } },
46
62
  }
47
63
  },
48
64
  })
49
65
 
66
+ // What the tests read: which reader each adapter hands its engine. Calling one builds the production seams, and
67
+ // reaches no app and no file until the engine uses them.
68
+ export { ADAPTERS as SERVICE_ADAPTERS }
69
+
70
+ import { createReleaseWatch, packageRootOfEntry } from './release-watch.mjs'
50
71
  import { SERVICE_ENTRY_PATH } from './service-entry-path.mjs'
72
+ import { LAST_STARTUP_SCHEMA, openServiceLog, writeLastStartup } from './service-record.mjs'
51
73
 
52
74
  export { SERVICE_ENTRY_PATH }
53
75
  export const EXIT_REFUSED = 2
76
+ // A service a login item started exits with this code when the package it runs from changed on disk: its service
77
+ // manager restarts it (a non-zero exit), on the release now installed. EX_TEMPFAIL.
78
+ export const EXIT_RELEASE_CHANGED = 75
54
79
 
55
- const log = (entry) => { try { process.stdout.write(`${JSON.stringify(entry)}\n`) } catch { /* a closed log never ends the service */ } }
80
+ const printed = (entry) => { try { process.stdout.write(`${JSON.stringify(entry)}\n`) } catch { /* a closed log never ends the service */ } }
81
+
82
+ // The workspace of a service a login item started, when it can be found: where it keeps its log and records how its
83
+ // start ended. When the project no longer leads to one (it moved, its pointer is gone), the workspace the unit names by
84
+ // its data root and identity, if that exists; it is never created here. Null otherwise; a refusal is then printed
85
+ // only, into the unit's own output file.
86
+ function startupWorkspace({ loadProject, dataRoot, workspaceId, env = process.env, platform = process.platform }) {
87
+ try {
88
+ const workspace = resolveServiceWorkspace({ project: loadProject(), dataRoot, env, platform })
89
+ if (workspace?.workspaceRoot) return workspace
90
+ } catch { /* the workspace the unit names, below */ }
91
+ if (typeof dataRoot !== 'string' || !path.isAbsolute(dataRoot) || typeof workspaceId !== 'string') return null
92
+ try {
93
+ const root = workspaceStateRoot(path.resolve(dataRoot), workspaceId)
94
+ return fs.lstatSync(root).isDirectory() ? { workspaceId, workspaceRoot: fs.realpathSync(root) } : null
95
+ } catch { return null }
96
+ }
97
+
98
+ // The service's own log, bounded as under `start`, which hands a child that same file. Under a login item the output
99
+ // goes to the unit's file instead, which nothing bounds; it keeps only what happens before this log is open.
100
+ function workspaceLog(workspaceRoot) {
101
+ let descriptor
102
+ try { descriptor = openServiceLog(workspaceRoot).descriptor } catch { return printed }
103
+ return (entry) => { try { fs.writeSync(descriptor, `${JSON.stringify(entry)}\n`) } catch { /* a closed log never ends the service */ } }
104
+ }
105
+
106
+ function recordStartup(workspace, { at, outcome, code }) {
107
+ if (workspace === null) return
108
+ try { writeLastStartup({ ...workspace, document: { schema: LAST_STARTUP_SCHEMA, workspaceId: workspace.workspaceId, at, outcome, code } }) } catch { /* reported by the log only */ }
109
+ }
56
110
 
57
111
  // The common body of a service process: run, end cleanly on a signal, exit 0
58
112
  // after a clean stop and EXIT_REFUSED when the service refused to run.
113
+ //
114
+ // Started by a login item (`startup`), it logs into its workspace's bounded
115
+ // service log, records how the start ended (`last-startup.json`), exits 0 on
116
+ // a refusal as well, so the service manager does not start it again every
117
+ // minute over something that does not change by itself, and exits
118
+ // EXIT_RELEASE_CHANGED after the tick that found another release on disk.
119
+ // Anything else that ends it (an error nobody typed, a signal) is a crash,
120
+ // and the manager restarts it after its throttle.
59
121
  export async function runServiceProcess(options) {
122
+ const startup = options.startup === true
123
+ const workspace = startup ? startupWorkspace(options) : null
124
+ const log = workspace === null ? printed : workspaceLog(workspace.workspaceRoot)
60
125
  try {
61
- const service = await runMaintenanceService({ log, ...options })
126
+ const service = await runMaintenanceService({ log, invokedAs: process.argv[1], ...options })
127
+ if (startup) recordStartup(workspace, { at: new Date().toISOString(), outcome: 'started', code: null })
62
128
  for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, () => { void service.shutdown(`signal-${signal}`) })
63
- await service.done
129
+ const { reason } = await service.done
64
130
  // Everything durable is on disk by now; nothing left over may keep a stopped service's PID alive.
65
- process.exit(0)
131
+ process.exit(reason === RELEASE_CHANGED ? EXIT_RELEASE_CHANGED : 0)
66
132
  } catch (error) {
67
- log({ at: new Date().toISOString(), event: 'refused', code: error?.code ?? 'untyped-error', message: String(error?.message ?? error) })
68
- process.exitCode = typeof error?.code === 'string' ? EXIT_REFUSED : 1
133
+ const at = new Date().toISOString()
134
+ const code = typeof error?.code === 'string' ? error.code : null
135
+ log({ at, event: 'refused', code: code ?? 'untyped-error', message: String(error?.message ?? error) })
136
+ if (startup && code !== null) { recordStartup(workspace, { at, outcome: 'refused', code }); process.exitCode = 0; return }
137
+ process.exitCode = code !== null ? EXIT_REFUSED : 1
69
138
  }
70
139
  }
71
140
 
141
+ // The release a service a login item started runs, read through the path it was started by (`argv[1]`), so an
142
+ // upgrade, or a linked install pointed elsewhere, is seen. Null when that package cannot be read.
143
+ function watchRelease(invokedAs) {
144
+ const root = packageRootOfEntry(invokedAs) ?? packageRootOfEntry(SERVICE_ENTRY_PATH)
145
+ if (root === null) return null
146
+ try { return createReleaseWatch({ root }) } catch { return null }
147
+ }
148
+
72
149
  export function serviceOptionsFromArgv(argv, { env = process.env } = {}) {
73
150
  const args = parseArgs(argv)
74
151
  const configPath = firstString(args.project)
75
152
  if (!configPath || !path.isAbsolute(configPath)) throw Object.assign(new Error('--project must be the absolute path of a project configuration'), { code: 'service-arguments-invalid' })
76
153
  const dataRoot = firstString(args['data-root']) ?? undefined
154
+ const workspaceId = firstString(args['workspace-id']) ?? undefined
77
155
  const intervalMs = args['interval-ms'] === undefined ? undefined : Number(args['interval-ms'])
78
156
  if (intervalMs !== undefined && (!Number.isInteger(intervalMs) || intervalMs < 1)) throw Object.assign(new Error('--interval-ms must be a positive integer'), { code: 'service-arguments-invalid' })
79
157
  return {
80
158
  loadProject: () => resolveProjectConfig({ argv: [`--project=${configPath}`], cwd: path.dirname(configPath), env, writeLocalState: false }),
81
- ...(dataRoot === undefined ? {} : { dataRoot }), ...(intervalMs === undefined ? {} : { intervalMs }),
159
+ ...(dataRoot === undefined ? {} : { dataRoot }), ...(workspaceId === undefined ? {} : { workspaceId }), ...(intervalMs === undefined ? {} : { intervalMs }),
82
160
  ...(firstString(args['runtime-id']) ? { runtimeId: firstString(args['runtime-id']) } : {}),
83
161
  startup: args.startup === true, adapter: firstString(args.adapter),
84
162
  }
@@ -87,13 +165,18 @@ export function serviceOptionsFromArgv(argv, { env = process.env } = {}) {
87
165
  const invokedDirectly = (() => { try { return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)) } catch { return false } })()
88
166
 
89
167
  if (invokedDirectly) {
168
+ const argv = process.argv.slice(2)
169
+ // As early as this entry can: its static imports have already loaded most of the modules, so a release that changes
170
+ // between this process's start and this reading is not noticed until the next change (docs/obsidian.md says so).
171
+ const releaseWatch = argv.includes('--startup') ? watchRelease(process.argv[1]) : null
90
172
  let options = null
91
173
  try {
92
- options = serviceOptionsFromArgv(process.argv.slice(2))
174
+ options = serviceOptionsFromArgv(argv)
93
175
  if (!options.adapter || !Object.hasOwn(ADAPTERS, options.adapter)) throw Object.assign(new Error('no editor adapter was selected; pass --adapter explicitly'), { code: 'service-adapter-not-selected' })
94
176
  } catch (error) {
95
- log({ at: new Date().toISOString(), event: 'refused', code: error.code ?? 'untyped-error', message: error.message })
96
- process.exitCode = EXIT_REFUSED
177
+ printed({ at: new Date().toISOString(), event: 'refused', code: error.code ?? 'untyped-error', message: error.message })
178
+ // Under a login item arguments that do not work never will: it ends cleanly, and is not started again every minute.
179
+ process.exitCode = argv.includes('--startup') ? 0 : EXIT_REFUSED
97
180
  options = null
98
181
  }
99
182
  if (options) {
@@ -101,6 +184,6 @@ if (invokedDirectly) {
101
184
  const [{ loadContributions }, { createObsidianRegistry }] = await Promise.all([import('./contributions.mjs'), import('./extension-points.mjs')])
102
185
  const registry = createObsidianRegistry({ contributions: await loadContributions() })
103
186
  const { engineOptions, ...seams } = await ADAPTERS[adapter]()
104
- await runServiceProcess({ ...rest, entryPath: SERVICE_ENTRY_PATH, ...seams, engineOptions: { ...engineOptions, extensions: registry.extensions } })
187
+ await runServiceProcess({ ...rest, entryPath: SERVICE_ENTRY_PATH, ...seams, engineOptions: { ...engineOptions, extensions: registry.extensions }, releaseWatch })
105
188
  }
106
189
  }