@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,61 @@
1
+ import { execFile } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { refuse } from './errors.mjs'
6
+ import { createLaunchdManager, createSystemdManager, homeIsAccountHome } from './service-managers.mjs'
7
+
8
+ // The production service manager of a login item: the only code that runs
9
+ // launchctl or systemctl, or writes into ~/Library/LaunchAgents or the user's
10
+ // systemd folder.
11
+ //
12
+ // It is imported in exactly one place, dynamically: the command entry
13
+ // (src/commands/obsidian.mjs), only when it runs as the real command-line
14
+ // entry. Importing it leaves a trace below that the test suite asserts is
15
+ // absent. It refuses, before anything is looked up or run:
16
+ //
17
+ // - under the Node test runner (`real-login-item-under-test`), whatever
18
+ // environment it is handed: a test hands in its own manager;
19
+ // - when HOME is not the account's own home directory
20
+ // (`login-item-home-mismatch`): launchd and systemd register a unit in
21
+ // the account's real session whatever HOME says, so a private HOME must
22
+ // never install one;
23
+ // - on Windows (`startup-platform-unqualified`) and on any platform but
24
+ // macOS and Linux (`startup-platform-unsupported`).
25
+ globalThis[Symbol.for('mnstry.atelier.obsidian.production-service-manager-loaded')] = true
26
+
27
+ const RUN_TIMEOUT_MS = 15 * 1000
28
+ const SYSTEMCTL = ['/usr/bin/systemctl', '/bin/systemctl']
29
+
30
+ // No shell, in the root directory, bounded.
31
+ function runner(env) {
32
+ return (program, args) => new Promise((resolve) => {
33
+ execFile(program, args, { env, cwd: '/', timeout: RUN_TIMEOUT_MS, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
34
+ resolve({ status: error ? (typeof error.code === 'number' ? error.code : null) : 0, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') })
35
+ })
36
+ })
37
+ }
38
+
39
+ export function createProductionServiceManager({ platform = process.platform, env = process.env } = {}) {
40
+ if (process.env.NODE_TEST_CONTEXT !== undefined || env.NODE_TEST_CONTEXT !== undefined) refuse('real-login-item-under-test', 'the production login-item manager is never used under the test runner; a test hands in its own')
41
+ if (platform === 'win32') refuse('startup-platform-unqualified', 'operating-system startup on Windows has not been qualified', { platform })
42
+ if (platform !== 'darwin' && platform !== 'linux') refuse('startup-platform-unsupported', 'no login item is known for this platform', { platform: String(platform) })
43
+ const account = os.userInfo()
44
+ if (!homeIsAccountHome({ env, accountHome: account.homedir })) refuse('login-item-home-mismatch', 'HOME is not this account\'s home directory; a login item is registered in the account\'s real session whatever HOME says, so none is installed from here')
45
+ const run = runner(env)
46
+ if (platform === 'darwin') return createLaunchdManager({ run, uid: account.uid, directory: path.join(account.homedir, 'Library', 'LaunchAgents') })
47
+ const configHome = typeof env.XDG_CONFIG_HOME === 'string' && path.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path.join(account.homedir, '.config')
48
+ const systemctl = SYSTEMCTL.find((candidate) => { try { return fs.statSync(candidate).isFile() } catch { return false } })
49
+ if (systemctl === undefined) return unavailableManager({ directory: path.join(configHome, 'systemd', 'user') })
50
+ return createSystemdManager({ run, systemctl, directory: path.join(configHome, 'systemd', 'user') })
51
+ }
52
+
53
+ // Linux without systemctl: every change answers `login-item-unavailable`, and nothing is written.
54
+ function unavailableManager({ directory }) {
55
+ const unavailable = async () => ({ ok: false, code: 'login-item-unavailable', status: null, message: 'systemctl was not found' })
56
+ return {
57
+ kind: 'systemd-user-unit', platform: 'linux', directory, fileFor: (fileName) => path.join(directory, fileName),
58
+ readUnit: () => null, install: unavailable, start: unavailable, remove: async () => ({ ok: true, removed: false }),
59
+ inspect: async ({ fileName }) => ({ ok: true, file: path.join(directory, fileName), present: false, loaded: null, running: null, pid: null, lastExit: null, disabled: null }),
60
+ }
61
+ }
@@ -0,0 +1,275 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { atomicReplacePrivateText, readRegularTextNoFollow } from '../../project/private-state.mjs'
4
+
5
+ // The operating system's service managers, as a login item uses them: a
6
+ // launchd user agent on macOS and a systemd user unit on Linux.
7
+ //
8
+ // Each is built from the directory its unit files live in and `run`, the only
9
+ // way it starts a program: `run(program, args)` resolves { status, stdout,
10
+ // stderr }, with a null status when the program could not be run at all.
11
+ // Neither has a default here. The production ones are made only by the real
12
+ // command-line entry, through the production manager module, never under the
13
+ // test runner and never for a HOME that is not the account's own; tests hand
14
+ // in a `run` that records what it was asked and a temporary directory.
15
+ //
16
+ // What each does, in order:
17
+ //
18
+ // launchd install write the property list (mode 0644, atomically); boot
19
+ // the job out if it is loaded, because a loaded job keeps
20
+ // the definition it was loaded with, and wait until
21
+ // launchd has let it go; bootstrap it (RunAtLoad starts it);
22
+ // a file written where none was is removed again when
23
+ // launchd did not take it
24
+ // start kickstart -p: starts the job when it is not running and
25
+ // prints its PID either way
26
+ // remove bootout; delete the file
27
+ // inspect print (loaded, state, PID, last exit code) and
28
+ // print-disabled (switched off in System Settings)
29
+ // systemd install write the unit (mode 0644, atomically); daemon-reload;
30
+ // enable
31
+ // start start
32
+ // remove disable --now; delete the file; daemon-reload
33
+ // inspect show LoadState, ActiveState, SubState, UnitFileState,
34
+ // MainPID, ExecMainStatus
35
+ //
36
+ // Every answer is { ok: true, ... } or { ok: false, code, status, message }
37
+ // with one of these codes:
38
+ //
39
+ // login-item-unavailable no manager to install into (no systemd user
40
+ // instance: WSL, a container, no session bus)
41
+ // login-item-install-failed the manager refused the unit
42
+ // login-item-not-loaded a start of a job the manager does not have
43
+ // loaded (booted out, or not allowed to run)
44
+ // login-item-start-failed the manager did not start it for another reason
45
+ // login-item-remove-failed the manager did not let the job go
46
+ // login-item-file-unsafe the unit's file is a link or not a regular file
47
+ //
48
+ // The exit statuses and texts of launchctl and systemctl read here are the
49
+ // documented ones; they are exercised against recorded texts, never against
50
+ // this machine's own session.
51
+
52
+ export const SERVICE_MANAGER_KINDS = Object.freeze(['launchd-user-agent', 'systemd-user-unit'])
53
+ export const LAUNCHCTL = '/bin/launchctl'
54
+ const UNIT_FILE_MODE = 0o644
55
+ const LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
56
+ // launchctl: the service is not in the domain (print, kickstart), and the job was not loaded (bootout).
57
+ const LAUNCHD_NOT_FOUND = [113, 3]
58
+ const BOOTOUT_WAIT_MS = 10 * 1000
59
+ const BOOTOUT_POLL_MS = 100
60
+
61
+ // Whether HOME names the account's own home directory (`accountHome`, as the system records it). A login item is
62
+ // registered in the account's real session whatever HOME says, so only then is one installed.
63
+ export function homeIsAccountHome({ env, accountHome }) {
64
+ const home = env?.HOME
65
+ if (typeof home !== 'string' || !path.isAbsolute(home) || typeof accountHome !== 'string' || !path.isAbsolute(accountHome)) return false
66
+ if (path.resolve(home) === path.resolve(accountHome)) return true
67
+ try { return fs.realpathSync(home) === fs.realpathSync(accountHome) } catch { return false }
68
+ }
69
+
70
+ const failed = (code, answer, message) => ({ ok: false, code, status: answer?.status ?? null, message: message ?? firstLine(answer) })
71
+ // The first line of what a manager printed, bounded: never a whole output, which can carry an environment.
72
+ function firstLine(answer) {
73
+ const text = `${answer?.stderr ?? ''}\n${answer?.stdout ?? ''}`.split('\n').map((line) => line.trim()).find((line) => line !== '')
74
+ return text === undefined ? null : text.slice(0, 200)
75
+ }
76
+ const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms) })
77
+
78
+ function checkUnit({ label, fileName }) {
79
+ if (typeof label !== 'string' || !LABEL.test(label)) throw new TypeError('a login item has a plain label')
80
+ if (typeof fileName !== 'string' || fileName !== path.basename(fileName) || !fileName.startsWith(label)) throw new TypeError('a unit file name is the label and its suffix')
81
+ }
82
+
83
+ // The unit's file, owned by this module: written atomically with the mode a manager accepts (neither launchd nor
84
+ // systemd reads a unit anyone else can write), never through a link.
85
+ function unitFiles(directory) {
86
+ if (typeof directory !== 'string' || !path.isAbsolute(directory)) throw new TypeError('a service manager needs the absolute directory of its unit files')
87
+ const fileFor = (fileName) => path.join(directory, fileName)
88
+ return {
89
+ directory,
90
+ fileFor,
91
+ read(fileName) {
92
+ try { return readRegularTextNoFollow(fileFor(fileName)) } catch (error) {
93
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null
94
+ return undefined
95
+ }
96
+ },
97
+ write(fileName, text) {
98
+ try {
99
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 })
100
+ atomicReplacePrivateText(fileFor(fileName), text, UNIT_FILE_MODE)
101
+ return { ok: true }
102
+ } catch (error) {
103
+ return { ok: false, code: error?.code === undefined || /regular file|redirected/.test(String(error.message)) ? 'login-item-file-unsafe' : 'login-item-install-failed', status: null, message: String(error.code ?? 'not-written') }
104
+ }
105
+ },
106
+ remove(fileName) {
107
+ let before
108
+ try { before = fs.lstatSync(fileFor(fileName)) } catch (error) { if (error.code === 'ENOENT') return { ok: true, removed: false }; return { ok: false, code: 'login-item-remove-failed', status: null, message: String(error.code) } }
109
+ if (!before.isFile()) return { ok: false, code: 'login-item-file-unsafe', status: null, message: 'not a regular file' }
110
+ try { fs.unlinkSync(fileFor(fileName)); return { ok: true, removed: true } } catch (error) {
111
+ return error.code === 'ENOENT' ? { ok: true, removed: false } : { ok: false, code: 'login-item-remove-failed', status: null, message: String(error.code) }
112
+ }
113
+ },
114
+ present(fileName) { try { return fs.lstatSync(fileFor(fileName)).isFile() } catch { return false } },
115
+ }
116
+ }
117
+
118
+ // Pure. What `launchctl print <domain>/<label>` says about one job.
119
+ export function readLaunchdPrint(text) {
120
+ const value = (name) => { const match = new RegExp(`^\\s*${name} = (.+)$`, 'm').exec(String(text ?? '')); return match === null ? null : match[1].trim() }
121
+ const pid = Number(value('pid'))
122
+ const exit = Number(value('last exit code'))
123
+ return { state: value('state'), pid: Number.isSafeInteger(pid) && pid > 0 ? pid : null, lastExit: Number.isInteger(exit) ? exit : null }
124
+ }
125
+
126
+ // Pure. Whether `launchctl print-disabled <domain>` lists this label as disabled: true, false, or null when it is not
127
+ // listed. Releases of macOS print `=> disabled` / `=> enabled`, earlier ones `=> true` / `=> false`.
128
+ export function readLaunchdDisabled(text, label) {
129
+ for (const line of String(text ?? '').split('\n')) {
130
+ const match = /^\s*"([^"]+)"\s*=>\s*(\S+)\s*$/.exec(line)
131
+ if (match === null || match[1] !== label) continue
132
+ if (match[2] === 'disabled' || match[2] === 'true') return true
133
+ if (match[2] === 'enabled' || match[2] === 'false') return false
134
+ }
135
+ return null
136
+ }
137
+
138
+ export function createLaunchdManager({ run, uid, directory, waitMs = BOOTOUT_WAIT_MS, pollMs = BOOTOUT_POLL_MS } = {}) {
139
+ if (typeof run !== 'function') throw new TypeError('a service manager needs run')
140
+ if (!Number.isSafeInteger(uid) || uid < 0) throw new TypeError('a launchd manager needs the user id of its session')
141
+ const files = unitFiles(directory)
142
+ const domain = `gui/${uid}`
143
+ const target = (label) => `${domain}/${label}`
144
+ const launchctl = (...args) => run(LAUNCHCTL, args)
145
+ const loaded = async (label) => {
146
+ const answer = await launchctl('print', target(label))
147
+ return answer.status === 0 ? { loaded: true, answer } : { loaded: LAUNCHD_NOT_FOUND.includes(answer.status) ? false : null, answer }
148
+ }
149
+ // Boots the job out when launchd has it, and waits, bounded, until it is gone: bootstrap refuses a label still loaded.
150
+ async function bootout(label) {
151
+ const answer = await launchctl('bootout', target(label))
152
+ if (answer.status !== 0 && !LAUNCHD_NOT_FOUND.includes(answer.status)) return failed('login-item-remove-failed', answer)
153
+ const until = Date.now() + waitMs
154
+ for (;;) {
155
+ const now = await loaded(label)
156
+ if (now.loaded === false) return { ok: true, wasLoaded: answer.status === 0 }
157
+ if (Date.now() >= until) return failed('login-item-remove-failed', now.answer, 'launchd kept the job loaded')
158
+ await sleep(pollMs)
159
+ }
160
+ }
161
+ return {
162
+ kind: 'launchd-user-agent', platform: 'darwin', directory: files.directory, fileFor: files.fileFor,
163
+ readUnit({ label, fileName }) { checkUnit({ label, fileName }); return files.read(fileName) },
164
+ async install({ label, fileName, text }) {
165
+ checkUnit({ label, fileName })
166
+ // launchd loads every property list in LaunchAgents at login: one this installation wrote where none was is
167
+ // removed again when launchd did not take it.
168
+ const existed = files.present(fileName)
169
+ const written = files.write(fileName, text)
170
+ if (!written.ok) return written
171
+ const out = await bootout(label)
172
+ if (!out.ok) { if (!existed) files.remove(fileName); return out }
173
+ const answer = await launchctl('bootstrap', domain, files.fileFor(fileName))
174
+ if (answer.status !== 0) {
175
+ if (!existed) files.remove(fileName)
176
+ return failed(answer.status === null ? 'login-item-unavailable' : 'login-item-install-failed', answer)
177
+ }
178
+ return { ok: true, file: files.fileFor(fileName) }
179
+ },
180
+ async start({ label, fileName }) {
181
+ checkUnit({ label, fileName })
182
+ const answer = await launchctl('kickstart', '-p', target(label))
183
+ if (answer.status === 0) { const pid = Number(String(answer.stdout ?? '').trim()); return { ok: true, pid: Number.isSafeInteger(pid) && pid > 0 ? pid : null } }
184
+ return failed(LAUNCHD_NOT_FOUND.includes(answer.status) ? 'login-item-not-loaded' : 'login-item-start-failed', answer)
185
+ },
186
+ async remove({ label, fileName }) {
187
+ checkUnit({ label, fileName })
188
+ const out = await bootout(label)
189
+ if (!out.ok) return out
190
+ const removed = files.remove(fileName)
191
+ return removed.ok ? { ok: true, removed: removed.removed || out.wasLoaded } : removed
192
+ },
193
+ async inspect({ label, fileName }) {
194
+ checkUnit({ label, fileName })
195
+ const present = files.present(fileName)
196
+ const printed = await loaded(label)
197
+ const disabled = await launchctl('print-disabled', domain)
198
+ const job = printed.loaded === true ? readLaunchdPrint(printed.answer.stdout) : { state: null, pid: null, lastExit: null }
199
+ return {
200
+ ok: true, file: files.fileFor(fileName), present, loaded: printed.loaded, running: printed.loaded === true ? job.state === 'running' : printed.loaded === false ? false : null,
201
+ pid: job.pid, lastExit: job.lastExit, disabled: disabled.status === 0 ? readLaunchdDisabled(disabled.stdout, label) === true : null,
202
+ }
203
+ },
204
+ }
205
+ }
206
+
207
+ // Pure. The `KEY=value` lines of `systemctl show`.
208
+ export function readSystemdShow(text) {
209
+ const values = {}
210
+ for (const line of String(text ?? '').split('\n')) {
211
+ const at = line.indexOf('=')
212
+ if (at > 0) values[line.slice(0, at).trim()] = line.slice(at + 1).trim()
213
+ }
214
+ const pid = Number(values.MainPID)
215
+ const exit = Number(values.ExecMainStatus)
216
+ return {
217
+ loadState: values.LoadState ?? null, activeState: values.ActiveState ?? null, subState: values.SubState ?? null, unitFileState: values.UnitFileState ?? null,
218
+ pid: Number.isSafeInteger(pid) && pid > 0 ? pid : null, lastExit: values.ExecMainStatus === undefined || !Number.isInteger(exit) ? null : exit,
219
+ }
220
+ }
221
+
222
+ export function createSystemdManager({ run, systemctl, directory } = {}) {
223
+ if (typeof run !== 'function') throw new TypeError('a service manager needs run')
224
+ if (typeof systemctl !== 'string' || !path.isAbsolute(systemctl)) throw new TypeError('a systemd manager needs the absolute path of systemctl')
225
+ const files = unitFiles(directory)
226
+ const user = (...args) => run(systemctl, ['--user', ...args])
227
+ // A user instance that cannot be reached (no session bus: WSL, a container, a session without systemd) makes the
228
+ // first call fail; nothing is left behind by an installation it stopped.
229
+ const unreachable = (answer) => answer.status === null || /Failed to connect to bus|No medium found|not been booted with systemd|System has not been booted/i.test(`${answer.stderr ?? ''}`)
230
+ return {
231
+ kind: 'systemd-user-unit', platform: 'linux', directory: files.directory, fileFor: files.fileFor,
232
+ readUnit({ label, fileName }) { checkUnit({ label, fileName }); return files.read(fileName) },
233
+ async install({ label, fileName, text }) {
234
+ checkUnit({ label, fileName })
235
+ const existed = files.present(fileName)
236
+ const written = files.write(fileName, text)
237
+ if (!written.ok) return written
238
+ const reloaded = await user('daemon-reload')
239
+ if (reloaded.status !== 0) {
240
+ if (!existed) files.remove(fileName)
241
+ return failed(unreachable(reloaded) ? 'login-item-unavailable' : 'login-item-install-failed', reloaded)
242
+ }
243
+ const enabled = await user('enable', fileName)
244
+ if (enabled.status !== 0) return failed(unreachable(enabled) ? 'login-item-unavailable' : 'login-item-install-failed', enabled)
245
+ return { ok: true, file: files.fileFor(fileName) }
246
+ },
247
+ async start({ label, fileName }) {
248
+ checkUnit({ label, fileName })
249
+ const answer = await user('start', fileName)
250
+ if (answer.status === 0) return { ok: true, pid: null }
251
+ return failed(unreachable(answer) ? 'login-item-unavailable' : /not found|not loaded|does not exist/i.test(`${answer.stderr ?? ''}`) ? 'login-item-not-loaded' : 'login-item-start-failed', answer)
252
+ },
253
+ async remove({ label, fileName }) {
254
+ checkUnit({ label, fileName })
255
+ const disabled = await user('disable', '--now', fileName)
256
+ const missing = disabled.status !== 0 && /not found|not loaded|does not exist/i.test(`${disabled.stderr ?? ''}`)
257
+ if (disabled.status !== 0 && !missing) return failed(unreachable(disabled) ? 'login-item-unavailable' : 'login-item-remove-failed', disabled)
258
+ const removed = files.remove(fileName)
259
+ if (!removed.ok) return removed
260
+ await user('daemon-reload')
261
+ return { ok: true, removed: removed.removed || disabled.status === 0 }
262
+ },
263
+ async inspect({ label, fileName }) {
264
+ checkUnit({ label, fileName })
265
+ const present = files.present(fileName)
266
+ const answer = await user('show', '-p', 'LoadState,ActiveState,SubState,UnitFileState,MainPID,ExecMainStatus', fileName)
267
+ if (answer.status !== 0) return { ok: true, file: files.fileFor(fileName), present, loaded: null, running: null, pid: null, lastExit: null, disabled: null }
268
+ const unit = readSystemdShow(answer.stdout)
269
+ return {
270
+ ok: true, file: files.fileFor(fileName), present, loaded: unit.loadState === 'loaded', running: unit.activeState === 'active' || unit.activeState === 'activating',
271
+ pid: unit.pid, lastExit: unit.lastExit, disabled: unit.unitFileState === null ? null : !['enabled', 'enabled-runtime', 'linked', 'linked-runtime'].includes(unit.unitFileState),
272
+ }
273
+ },
274
+ }
275
+ }
@@ -11,11 +11,14 @@ import { LOOPBACK_HOSTS } from './service-client.mjs'
11
11
  // Private documents of the maintenance service of one workspace, under
12
12
  //
13
13
  // <data>/obsidian/<workspace-id>/state/service/
14
- // runtime.json the adapter record of the running service (service-state v1)
15
- // settings.json what this machine chose: loopback host, port, startup consent
16
- // last-error.json the last tick that failed for a reason nobody typed
17
- // service.log the operational log of the service process
18
- // start-lock/ serializes `start` for this workspace
14
+ // runtime.json the adapter record of the running service (service-state v1)
15
+ // settings.json what this machine chose: loopback host, port, startup consent
16
+ // last-error.json the last tick that failed for a reason nobody typed
17
+ // last-startup.json how the last start by the login item ended: started, or the refusal's code
18
+ // login-item.json the installed login item (login-item.mjs)
19
+ // service.log the operational log of the service process
20
+ // login-item.log what a login item's service printed before it could open service.log
21
+ // start-lock/ serializes `start` for this workspace
19
22
  //
20
23
  // Owner-only, replaced atomically, outside every repository and every vault.
21
24
  // Every document is validated on every read. One that does not validate, or
@@ -24,7 +27,9 @@ import { LOOPBACK_HOSTS } from './service-client.mjs'
24
27
 
25
28
  export const SERVICE_SETTINGS_SCHEMA = 'atelier-obsidian-service-settings/v1'
26
29
  export const SERVICE_ERROR_SCHEMA = 'atelier-obsidian-service-last-error/v1'
30
+ export const LAST_STARTUP_SCHEMA = 'atelier-obsidian-last-startup/v1'
27
31
  export const CONSENT_COVERAGES = Object.freeze(['service', 'service-and-startup'])
32
+ export const STARTUP_OUTCOMES = Object.freeze(['started', 'refused'])
28
33
 
29
34
  const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
30
35
  const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
@@ -41,6 +46,7 @@ export function servicePaths(workspaceRoot) {
41
46
  return {
42
47
  stateLocation: path.join(workspaceRoot, 'state'), directory, record: path.join(directory, 'runtime.json'), settings: path.join(directory, 'settings.json'),
43
48
  lastError: path.join(directory, 'last-error.json'), log: path.join(directory, 'service.log'), startLock: path.join(directory, 'start-lock'),
49
+ lastStartup: path.join(directory, 'last-startup.json'), loginItem: path.join(directory, 'login-item.json'), loginItemLog: path.join(directory, 'login-item.log'),
44
50
  }
45
51
  }
46
52
 
@@ -73,29 +79,60 @@ const releases = new Map()
73
79
  // maintains; outside those it reads nothing else of the package but the
74
80
  // version in `package.json`. A release that changes any of it, and not only
75
81
  // the entry, differs. A package without `plugins/` reads as one with an empty
76
- // one. Computed once per process and package root, so a service keeps the
77
- // identity of the code it loaded.
78
- export function releaseIdentity({ root = PACKAGE_ROOT } = {}) {
79
- if (!releases.has(root)) {
80
- const hash = createHash('sha256')
81
- const walk = (relative, { optional = false } = {}) => {
82
- let entries
83
- try { entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true }) } catch (error) { if (optional && error.code === 'ENOENT') return; throw error }
84
- for (const entry of entries.sort(byName)) {
85
- const child = `${relative}/${entry.name}`
86
- if (entry.isDirectory()) walk(child)
87
- else if (entry.isFile()) hash.update(`${child}\0${createHash('sha256').update(fs.readFileSync(path.join(root, child))).digest('hex')}\n`)
88
- }
82
+ // one.
83
+
84
+ // Every regular file a release identity covers, in a fixed order: `visit(relative, absolute)`. A missing `src/` or
85
+ // `contracts/` throws (the package cannot be read); a missing `plugins/` is empty.
86
+ export function walkRelease(root, visit) {
87
+ const walk = (relative, { optional = false } = {}) => {
88
+ let entries
89
+ try { entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true }) } catch (error) { if (optional && error.code === 'ENOENT') return; throw error }
90
+ for (const entry of entries.sort(byName)) {
91
+ const child = `${relative}/${entry.name}`
92
+ if (entry.isDirectory()) walk(child)
93
+ else if (entry.isFile()) visit(child, path.join(root, child))
89
94
  }
90
- for (const part of ['src', 'contracts']) walk(part)
91
- walk('plugins', { optional: true })
92
- let version = null
93
- try { const read = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version; version = typeof read === 'string' ? read : null } catch { version = null }
94
- releases.set(root, Object.freeze({ version, digest: `sha256:${hash.digest('hex')}` }))
95
95
  }
96
+ for (const part of ['src', 'contracts']) walk(part)
97
+ walk('plugins', { optional: true })
98
+ }
99
+
100
+ // The release identity of the package at `root`, read now and never cached: { version, digest }.
101
+ export function readReleaseIdentity({ root }) {
102
+ if (typeof root !== 'string' || !path.isAbsolute(root)) throw new TypeError('a release identity is read from the absolute root of a package')
103
+ const hash = createHash('sha256')
104
+ walkRelease(root, (relative, absolute) => { hash.update(`${relative}\0${createHash('sha256').update(fs.readFileSync(absolute)).digest('hex')}\n`) })
105
+ let version = null
106
+ try { const read = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version; version = typeof read === 'string' ? read : null } catch { version = null }
107
+ return Object.freeze({ version, digest: `sha256:${hash.digest('hex')}` })
108
+ }
109
+
110
+ // readReleaseIdentity, computed once per process and package root, so a service keeps the identity of the code it
111
+ // loaded.
112
+ export function releaseIdentity({ root = PACKAGE_ROOT } = {}) {
113
+ if (!releases.has(root)) releases.set(root, readReleaseIdentity({ root }))
96
114
  return releases.get(root)
97
115
  }
98
116
 
117
+ const ENTRY_IN_PACKAGE = Object.freeze(['src', 'runtime', 'obsidian', 'service-main.mjs'])
118
+
119
+ // The root of the package whose service entry is `entryPath` (`<root>/src/runtime/obsidian/service-main.mjs`), as that
120
+ // path names it, without resolving links; null for any other entry.
121
+ export function packageRootOfEntry(entryPath) {
122
+ if (typeof entryPath !== 'string' || !path.isAbsolute(entryPath)) return null
123
+ const parts = path.resolve(entryPath).split(path.sep)
124
+ if (parts.length <= ENTRY_IN_PACKAGE.length || ENTRY_IN_PACKAGE.some((part, index) => parts[parts.length - ENTRY_IN_PACKAGE.length + index] !== part)) return null
125
+ return parts.slice(0, parts.length - ENTRY_IN_PACKAGE.length).join(path.sep) || path.sep
126
+ }
127
+
128
+ // The release that starting `entryPath` would run, read now from the package that path names (so a link pointed
129
+ // elsewhere, or a `file:` install, is the package it leads to now); for an entry that is not a package's service entry,
130
+ // this package's own, as it was loaded.
131
+ export function releaseOfEntry(entryPath) {
132
+ const root = packageRootOfEntry(entryPath)
133
+ return root === null ? releaseIdentity() : readReleaseIdentity({ root })
134
+ }
135
+
99
136
  // ---------------------------------------------------------------------------
100
137
  // The adapter record
101
138
  // ---------------------------------------------------------------------------
@@ -195,6 +232,33 @@ export function writeLastServiceError({ workspaceRoot, workspaceId, document })
195
232
  return document
196
233
  }
197
234
 
235
+ // ---------------------------------------------------------------------------
236
+ // How the last start by a login item ended
237
+ // ---------------------------------------------------------------------------
238
+
239
+ // { schema, workspaceId, at, outcome: 'started' | 'refused', code }: the code of the refusal, null for a start. Written by
240
+ // the service when it runs with `--startup`, so a login item that exits cleanly on a refusal (and is therefore not
241
+ // started again in a loop) still says why.
242
+ function validateLastStartup(document, workspaceId) {
243
+ const code = 'invalid-service-last-startup'
244
+ closedObject(document, { required: ['schema', 'workspaceId', 'at', 'outcome', 'code'] }, code, 'the last startup')
245
+ const ok = document.schema === LAST_STARTUP_SCHEMA && document.workspaceId === workspaceId && TIMESTAMP.test(document.at) && STARTUP_OUTCOMES.includes(document.outcome)
246
+ && (document.outcome === 'started' ? document.code === null : typeof document.code === 'string' && /^[A-Za-z0-9_.:-]{1,64}$/.test(document.code))
247
+ if (!ok) refuse(code, 'the last startup is malformed')
248
+ return document
249
+ }
250
+
251
+ export function readLastStartup({ workspaceRoot, workspaceId }) {
252
+ const document = readJson(servicePaths(workspaceRoot).lastStartup, 'invalid-service-last-startup', 'the last startup')
253
+ return document === null ? null : validateLastStartup(document, workspaceId)
254
+ }
255
+
256
+ export function writeLastStartup({ workspaceRoot, workspaceId, document }) {
257
+ validateLastStartup(document, workspaceId)
258
+ atomicReplacePrivateText(path.join(serviceDirectory(workspaceRoot), 'last-startup.json'), canonicalJson(document))
259
+ return document
260
+ }
261
+
198
262
  // The log is bounded: past the ceiling it becomes the one previous log, and a new one begins.
199
263
  export const SERVICE_LOG_MAX_BYTES = 1024 * 1024
200
264
 
@@ -1,5 +1,6 @@
1
1
  import { randomBytes as cryptoRandomBytes } from 'node:crypto'
2
2
  import fs from 'node:fs'
3
+ import path from 'node:path'
3
4
  import { AtelierDiagnosticError } from '../../project/config.mjs'
4
5
  import { ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
5
6
  import { prepareView as productionPrepareView } from '../../projection/obsidian/materialize/index.mjs'
@@ -48,9 +49,16 @@ import { createFsWatcherFactory } from './watchers.mjs'
48
49
  //
49
50
  // `adapterFactory` has no default here either: whoever starts the service
50
51
  // decides whether it may reach a running app.
52
+ //
53
+ // Started by a login item (`startup`), the service also asks `releaseWatch`
54
+ // after every tick whether the package it runs from changed on disk; when it
55
+ // did, it finishes that tick and shuts down with the reason RELEASE_CHANGED,
56
+ // which its process turns into an exit code the service manager restarts, on
57
+ // the new release (release-watch.mjs, service-main.mjs).
51
58
 
52
59
  export const SERVICE_STATUS_SCHEMA = 'atelier-obsidian-service-status/v1'
53
60
  export const DEFAULT_SHUTDOWN_GRACE_MS = 30 * 1000
61
+ export const RELEASE_CHANGED = 'release-changed'
54
62
 
55
63
  // Where a workspace keeps its private state, without creating an identity.
56
64
  export function resolveServiceWorkspace({ project, dataRoot, env = process.env, platform = process.platform, create = false }) {
@@ -85,6 +93,11 @@ export async function runMaintenanceService(options = {}) {
85
93
  engineOptions = {}, createEngine = createMaintenanceEngine,
86
94
  // What the adapter factory last learned about the installed app, when it qualifies one. Codes and versions only.
87
95
  appStatus = null,
96
+ // Under `startup`: { changed() } (createReleaseWatch), asked after every tick.
97
+ releaseWatch = null,
98
+ // The entry as this process was started by it (`process.argv[1]`), when that is not its real path: a login item
99
+ // names the installed entry by its path. It is recorded, so the process table can still prove this process ours.
100
+ invokedAs = null,
88
101
  } = options
89
102
  if (typeof loadProject !== 'function') throw new TypeError('the service needs loadProject')
90
103
  if (typeof adapterFactory !== 'function') throw new TypeError('the service needs an adapterFactory')
@@ -107,6 +120,7 @@ export async function runMaintenanceService(options = {}) {
107
120
  }
108
121
 
109
122
  const executable = executableIdentity(entryPath)
123
+ const alias = typeof invokedAs === 'string' && path.isAbsolute(invokedAs) && path.resolve(invokedAs) !== executable.path ? path.resolve(invokedAs) : null
110
124
  const startedAt = isoTime(clock)
111
125
  const identity = { serviceName: serviceNameFor(workspaceId), workspaceId, runtimeId, pid, host, port, executableDigest: executable.digest, startedAt }
112
126
  const bearer = randomBytes(32).toString('base64url')
@@ -126,7 +140,7 @@ export async function runMaintenanceService(options = {}) {
126
140
  // view without the plugin for this tick; the view itself is prepared as ever.
127
141
  const pluginFor = (scopeId) => {
128
142
  try {
129
- const vaultRoot = viewVaultRoot(workspaceRoot, scopeId)
143
+ const vaultRoot = viewVaultRoot(workspaceRoot, scopeId, workspaceId)
130
144
  const { choice, community } = decidePluginChoice({ workspaceRoot, workspaceId, scopeId, vaultRoot, clock })
131
145
  const off = choice.state === 'off'
132
146
  const bearer = ensurePluginBearer({ workspaceRoot, workspaceId, scopeId, randomBytes, clock })
@@ -193,6 +207,18 @@ export async function runMaintenanceService(options = {}) {
193
207
  log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
194
208
  }
195
209
  }
210
+ // What a publication of this tick could not change is no drift to ask about again (plugin-drift.mjs).
211
+ const settleDriftLooks = () => {
212
+ try { pluginDrift.settle([...pluginChannel.bearers().keys()]) } catch (error) {
213
+ log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
214
+ }
215
+ }
216
+
217
+ // A package that changed on disk: another release, loaded module by module into this process from then on.
218
+ const releaseChanged = () => {
219
+ if (!startup || releaseWatch === null) return false
220
+ try { return releaseWatch.changed() === true } catch { return false }
221
+ }
196
222
 
197
223
  const loop = createTickLoop({
198
224
  intervalMs, maxBackoffMs, onOutcome: recordOutcome,
@@ -200,7 +226,14 @@ export async function runMaintenanceService(options = {}) {
200
226
  // The service runs only while its record names it: a replaced or removed record ends it, cleanly.
201
227
  if (!recordIsOurs()) { void shutdown('record-no-longer-names-this-runtime'); return { state: 'stopping', reason: 'record-no-longer-names-this-runtime' } }
202
228
  askForDriftedViews()
203
- return engine.tick()
229
+ try {
230
+ const report = await engine.tick()
231
+ if (report?.state === 'ticked') settleDriftLooks()
232
+ return report
233
+ } finally {
234
+ // After the tick, whether it succeeded or not: the next one runs in a process of the release now on disk.
235
+ if (!stopping && releaseChanged()) { log({ at: isoTime(clock), event: RELEASE_CHANGED }); void shutdown(RELEASE_CHANGED) }
236
+ }
204
237
  },
205
238
  })
206
239
 
@@ -296,7 +329,7 @@ export async function runMaintenanceService(options = {}) {
296
329
  workspaceRoot, workspaceId,
297
330
  record: {
298
331
  schema: 'atelier-obsidian-service-state/v1', contractVersion: '1.0.0', workspaceId, serviceName: identity.serviceName, host, port, runtimeId, pid,
299
- executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity() } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
332
+ executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity(), ...(alias === null ? {} : { invokedAs: alias }) } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
300
333
  health: { status: 'healthy', checkedAt: startedAt }, consent: settings.consent, ext: { bearer },
301
334
  },
302
335
  })