@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
@@ -12,7 +12,7 @@ import { processRunsRecordedExecutable } from './process-identity.mjs'
12
12
  import { DEFAULT_PROBE_TIMEOUT_MS, LOOPBACK_HOSTS, probeHealth, requestLoopback } from './service-client.mjs'
13
13
  import { SERVICE_ENTRY_PATH } from './service-entry-path.mjs'
14
14
  import {
15
- CONSENT_COVERAGES, SERVICE_SETTINGS_SCHEMA, executableIdentity, openServiceLog, publicRecord, readServiceRecord, readServiceSettings, releaseIdentity, removeServiceRecord,
15
+ CONSENT_COVERAGES, SERVICE_SETTINGS_SCHEMA, executableIdentity, openServiceLog, publicRecord, readLastStartup, readServiceRecord, readServiceSettings, releaseOfEntry, removeServiceRecord,
16
16
  serviceNameFor, servicePaths, writeServiceSettings,
17
17
  } from './service-record.mjs'
18
18
  import { resolveServiceWorkspace } from './service.mjs'
@@ -105,8 +105,18 @@ function freeLoopbackPort(host) {
105
105
  })
106
106
  }
107
107
 
108
- async function resolveSettings({ workspaceRoot, workspaceId }, { host, port, consent, now }) {
108
+ // The service settings of this workspace with the address and consent given, written when that changes anything: the
109
+ // first start needs a consent naming its actor, and a consent for another actor or coverage replaces the recorded one.
110
+ // Installing and removing a login item raise and lower the coverage through it (login-item.mjs).
111
+ export async function ensureServiceSettings(workspace, { host, port, consent, now }) {
112
+ return resolveSettings(workspace, { host, port, consent, now })
113
+ }
114
+
115
+ async function resolveSettings({ workspaceRoot, workspaceId }, { host, port, consent: given, now }) {
109
116
  const current = readServiceSettings({ workspaceRoot, workspaceId })
117
+ // A consent derived from the account of a person at a terminal (`derived: true`) stands only for a workspace that has
118
+ // none. Read here, under the start lock, so one recorded meanwhile by another run is never replaced or narrowed.
119
+ const consent = given?.derived === true && current !== null ? undefined : given
110
120
  if (host !== undefined && !LOOPBACK_HOSTS.includes(host)) refuse('service-address-not-loopback', 'the service host must be the literal 127.0.0.1 or ::1')
111
121
  if (port !== undefined && (!Number.isInteger(port) || port < 1024 || port > 65535)) refuse('service-address-not-loopback', 'the service port must be between 1024 and 65535')
112
122
  if (consent !== undefined && (typeof consent?.actor !== 'string' || !CONSENT_COVERAGES.includes(consent.coverage ?? 'service'))) refuse('startup-consent-required', 'a consent names its actor and what it covers')
@@ -121,11 +131,55 @@ async function resolveSettings({ workspaceRoot, workspaceId }, { host, port, con
121
131
  return changed ? writeServiceSettings({ workspaceRoot, workspaceId, settings: next }) : current
122
132
  }
123
133
 
134
+ // Asks the service manager of the installed login item to start the service, and waits, bounded, for this
135
+ // workspace's service to prove itself (healthy, or busy in its first tick) with a record naming the digest of the
136
+ // entry the item runs. The runtime identifier is the service's own there, so that digest is what makes it the one
137
+ // asked for. A service that refused under the item exited cleanly and recorded why (`last-startup.json`); that code is
138
+ // the answer. { result } or, when the manager did not start it (the item is not loaded: switched off in System
139
+ // Settings, or no user service manager), { fallback: code }.
140
+ async function startThroughLoginItem({ workspace, loginItem, entryPath, deadline, probeTimeoutMs, alive, rules }) {
141
+ const { workspaceRoot, workspaceId } = workspace
142
+ const lastStartup = () => { try { return JSON.stringify(readLastStartup(workspace)) } catch { return null } }
143
+ const before = lastStartup()
144
+ let asked
145
+ try { asked = await loginItem.start() } catch { asked = { ok: false, code: 'login-item-start-failed' } }
146
+ const item = { via: 'login-item', ...(asked?.refreshed === true ? { refreshed: true } : {}) }
147
+ if (asked?.ok !== true) return { fallback: typeof asked?.code === 'string' ? asked.code : 'login-item-start-failed', refreshed: asked?.refreshed === true }
148
+ // A unit written again on the way names the entry of now. One whose entry cannot be read cannot prove anything.
149
+ let expected
150
+ try { expected = executableIdentity(typeof asked.entryPath === 'string' ? asked.entryPath : entryPath).digest } catch { return { result: { state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: 'login-item-entry-missing', observed: null, logPath: servicePaths(workspaceRoot).loginItemLog, loginItem: item } } }
151
+ let last = null
152
+ const proves = (status) => status?.record?.executable?.digest === expected
153
+ while (Date.now() < deadline) {
154
+ last = await evaluate(workspace, { probeTimeoutMs, alive, rules }).catch((error) => ({ state: 'refused', reason: error.code ?? 'untyped-error' }))
155
+ if (last.state === 'healthy' && proves(last)) return { result: { ...shown(last), started: true, alreadyRunning: false, loginItem: item } }
156
+ const now = lastStartup()
157
+ if (now !== before && now !== null) {
158
+ const startup = JSON.parse(now)
159
+ if (startup?.outcome === 'refused') return { result: { state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: startup.code, observed: last.state ?? null, logPath: servicePaths(workspaceRoot).log, loginItem: item } }
160
+ }
161
+ await sleep(50)
162
+ }
163
+ // As for a child started here: a service that proved itself and went straight into a long first tick runs, and is
164
+ // answered as busy only once the wait for it to be healthy is over.
165
+ if (last?.state === 'busy' && proves(last)) return { result: { ...shown(last), started: true, alreadyRunning: false, busy: true, loginItem: item } }
166
+ return { result: { state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: 'health-never-proved-ownership', observed: last?.state ?? null, logPath: servicePaths(workspaceRoot).loginItemLog, loginItem: item } }
167
+ }
168
+
169
+ // `loginItem`, for a workspace with an installed login item: { start() }, which asks its service manager to start the
170
+ // service (login-item.mjs). The service is then started through it, never as a child beside it, so two starts never
171
+ // compete; `entryPath` is the entry the item runs. When the manager does not start it, the service is started as a
172
+ // child, and the answer says why (`loginItem: { via: 'child', reason }`).
173
+ //
174
+ // `replaceOutdated`: a proven runtime of this workspace that runs an earlier release than the entry this start would run
175
+ // (runtimeRelease) is stopped as `stopService` stops it and started again in its place, under the consent already
176
+ // recorded (`replaced: 'outdated'`); one of a later release is never replaced (`release: 'later'`). Without it, any
177
+ // proven runtime is answered as already running.
124
178
  export async function startService(options = {}, rules = LIFECYCLE_PRIMITIVES) {
125
179
  const {
126
180
  loadProject, dataRoot, host, port, consent, detached = false, entryPath = SERVICE_ENTRY_PATH, entryArgs = [], intervalMs,
127
181
  startTimeoutMs = DEFAULT_START_TIMEOUT_MS, probeTimeoutMs, clock = () => new Date(), env = process.env, platform = process.platform,
128
- spawn = childProcess.spawn, execPath = process.execPath, alive = isProcessAlive, randomBytes = cryptoRandomBytes,
182
+ spawn = childProcess.spawn, execPath = process.execPath, alive = isProcessAlive, randomBytes = cryptoRandomBytes, loginItem = null, replaceOutdated = false,
129
183
  } = options
130
184
  const { project, workspace } = context({ loadProject, dataRoot, env, platform, create: true })
131
185
  const { workspaceRoot, workspaceId } = workspace
@@ -146,11 +200,26 @@ export async function startService(options = {}, rules = LIFECYCLE_PRIMITIVES) {
146
200
 
147
201
  let child = null
148
202
  try {
149
- const before = await evaluate(workspace, { probeTimeoutMs, alive, rules })
150
- if (rules.isOurs(before)) return { ...shown(before), started: false, alreadyRunning: true }
203
+ let before = await evaluate(workspace, { probeTimeoutMs, alive, rules })
204
+ // A runtime of an earlier release, still running after an upgrade, is replaced when asked to; it is stopped through its
205
+ // own listener, as its owner stops it, and nothing else is ever stopped here.
206
+ let replaced = null
207
+ if (rules.isOurs(before)) {
208
+ const standing = replaceOutdated ? runtimeRelease(before, entryPath) : null
209
+ if (standing === 'later') return { ...shown(before), started: false, alreadyRunning: true, release: 'later' }
210
+ if (standing !== 'outdated') return { ...shown(before), started: false, alreadyRunning: true }
211
+ const stopped = await stopService({ loadProject, dataRoot, env, platform, probeTimeoutMs, alive }, rules)
212
+ if (!stopped.stopped) return { ...shown(before), started: false, alreadyRunning: true, release: 'outdated', reason: stopped.reason ?? 'the-runtime-was-not-stopped' }
213
+ replaced = 'outdated'
214
+ before = await evaluate(workspace, { probeTimeoutMs, alive, rules })
215
+ }
151
216
  // Running, and in a long tick: nothing is started beside it and nothing replaces it.
152
217
  if (before.state === 'busy') return { ...shown(before), started: false, alreadyRunning: true, busy: true }
153
- if (rules.refusesOccupied(before)) refuse('service-port-occupied', 'something that is not this service answers on the loopback port; it is never taken over', { reason: before.reason, address: before.address })
218
+ // With a login item, a listener that has not written its record yet may be the service its manager is starting
219
+ // right now (it listens before it records itself): the start below asks the manager, which starts nothing beside
220
+ // a running job, and accepts only a runtime that proves itself. Anything else stays occupied and is never adopted.
221
+ const starting = loginItem !== null && before.state === 'occupied' && before.reason === 'a-listener-without-a-record'
222
+ if (rules.refusesOccupied(before) && !starting) refuse('service-port-occupied', 'something that is not this service answers on the loopback port; it is never taken over', { reason: before.reason, address: before.address })
154
223
  // `stale-record` and `pid-not-ours`: nothing listens, so the recorded runtime is not serving. Its PID is never signalled;
155
224
  // the new service replaces the record once it listens. Nothing under recovery or staging is touched on the way.
156
225
  const settings = await resolveSettings(workspace, { host, port, consent, now: isoTime(clock) })
@@ -159,6 +228,15 @@ export async function startService(options = {}, rules = LIFECYCLE_PRIMITIVES) {
159
228
  if (moved.kind !== 'refused') refuse('service-port-occupied', 'something already answers on the selected loopback port; it is never taken over', { address: { host: settings.host, port: settings.port } })
160
229
  }
161
230
 
231
+ // An installed login item starts it. Only when its manager does not is a child started, and the answer says why.
232
+ let fallback = null
233
+ if (loginItem !== null) {
234
+ const managed = await startThroughLoginItem({ workspace, loginItem, entryPath, deadline, probeTimeoutMs, alive, rules })
235
+ if (managed.result) return replaced === null ? managed.result : { ...managed.result, replaced }
236
+ fallback = { via: 'child', reason: managed.fallback, ...(managed.refreshed ? { refreshed: true } : {}) }
237
+ }
238
+ const withFallback = (result) => ({ ...result, ...(fallback === null ? {} : { loginItem: fallback }), ...(replaced === null ? {} : { replaced }) })
239
+
162
240
  const runtimeId = `rt-${randomBytes(16).toString('hex')}`
163
241
  const executable = executableIdentity(entryPath)
164
242
  const log = openServiceLog(workspaceRoot)
@@ -180,19 +258,19 @@ export async function startService(options = {}, rules = LIFECYCLE_PRIMITIVES) {
180
258
  last = await evaluate(workspace, { probeTimeoutMs, alive, rules }).catch((error) => ({ state: 'refused', reason: error.code ?? 'untyped-error' }))
181
259
  if (last.state === 'healthy' && last.record.runtimeId === runtimeId && last.record.pid === child.pid) {
182
260
  if (detached) child.unref()
183
- return { ...shown(last), started: true, alreadyRunning: false, ...(detached ? {} : { child }) }
261
+ return withFallback({ ...shown(last), started: true, alreadyRunning: false, ...(detached ? {} : { child }) })
184
262
  }
185
263
  await sleep(50)
186
264
  }
187
265
  // The child created here wrote the record and went straight into a long first tick: it runs, and is not stopped.
188
266
  if (exited === null && last?.state === 'busy' && last.record?.runtimeId === runtimeId && last.record?.pid === child.pid) {
189
267
  if (detached) child.unref()
190
- return { ...shown(last), started: true, alreadyRunning: false, busy: true, ...(detached ? {} : { child }) }
268
+ return withFallback({ ...shown(last), started: true, alreadyRunning: false, busy: true, ...(detached ? {} : { child }) })
191
269
  }
192
270
  // Ownership was never proven: only the child created here is stopped, by its handle.
193
271
  if (exited === null) { child.kill(); const until = Date.now() + 5000; while (exited === null && Date.now() < until) await sleep(25) }
194
272
  removeServiceRecord({ workspaceRoot, workspaceId, runtimeId, pid: child.pid })
195
- return { state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: exited?.error ?? (exited?.code === null || exited === null ? 'health-never-proved-ownership' : `service-exited-${exited.code}`), observed: last?.state ?? null, logPath: log.file }
273
+ return withFallback({ state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: exited?.error ?? (exited?.code === null || exited === null ? 'health-never-proved-ownership' : `service-exited-${exited.code}`), observed: last?.state ?? null, logPath: log.file })
196
274
  } finally {
197
275
  lock.release()
198
276
  }
@@ -242,7 +320,7 @@ async function askProvenRuntime(options, rules, { method, operation, timeoutMs,
242
320
 
243
321
  // Pure. How the release a runtime recorded at its start (`executable.ext.release`, see releaseIdentity: the package
244
322
  // version and a digest of every runtime module) stands to the installed one, `installed` being { entry, release }, the
245
- // digest of the entry this command would start and releaseIdentity():
323
+ // digest of the entry this command would start and the release that entry's package holds now (releaseOfEntry):
246
324
  // current the same version, entry module and modules;
247
325
  // outdated an earlier version; the same version with another entry module or other modules; or no release named,
248
326
  // as releases before the one that began recording it record none;
@@ -261,11 +339,13 @@ export function releaseStanding(executable, installed) {
261
339
  }
262
340
 
263
341
  // How the release the proven runtime runs stands to the installed one (releaseStanding), `entryPath` being the entry
264
- // this command would start. Null when the runtime is not proven, or the installed side cannot be read.
342
+ // this command would start: its digest and the release are both read from that entry, so with a login item the
343
+ // installed release is the project's own package, read through the path the item names, whichever package this command
344
+ // runs from. Null when the runtime is not proven, or the installed side cannot be read.
265
345
  export function runtimeRelease(status, entryPath) {
266
346
  if (status?.state !== 'healthy' || typeof status.record?.executable?.digest !== 'string' || typeof entryPath !== 'string') return null
267
347
  let installed
268
- try { installed = { entry: executableIdentity(entryPath).digest, release: releaseIdentity() } } catch { return null }
348
+ try { installed = { entry: executableIdentity(entryPath).digest, release: releaseOfEntry(entryPath) } } catch { return null }
269
349
  return releaseStanding(status.record.executable, installed)
270
350
  }
271
351
 
@@ -0,0 +1,332 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../project/private-state.mjs'
6
+ import { canonicalJson, closedObject, isoTime } from './documents.mjs'
7
+ import { refuse } from './errors.mjs'
8
+ import { ensureServiceSettings } from './lifecycle.mjs'
9
+ import { ensureWorkspaceIdentity, readLocalPointer, resolveDataRoot } from './machine-settings.mjs'
10
+ import { SERVICE_MANAGER_KINDS } from './service-managers.mjs'
11
+ import { readLastStartup, readServiceSettings, serviceNameFor, servicePaths, writeServiceSettings } from './service-record.mjs'
12
+ import { resolveServiceWorkspace } from './service.mjs'
13
+ import { STARTUP_PLATFORMS, buildStartupAdapter, startupSearchPath } from './startup-adapters.mjs'
14
+
15
+ // A login item: the operating system starts this workspace's maintenance
16
+ // service when the person logs in, so a vault is fresh after a restart
17
+ // without anything being run.
18
+ //
19
+ // macOS a launchd user agent: ~/Library/LaunchAgents/ai.mnstry.atelier.<project>.<workspace-id>.plist
20
+ // Linux a systemd user unit: ~/.config/systemd/user/atelier-obsidian-<workspace-id>.service
21
+ // Windows not offered (startup-platform-unqualified)
22
+ //
23
+ // One per workspace, each with its own label, port, log and consent, each
24
+ // running its own project's installed release. The unit runs the service
25
+ // entry of the package installed in the project, named by its path, so an
26
+ // upgrade in the project is what the next start runs; the Node that ran the
27
+ // installing command, by its real path, so a per-shell link of a version
28
+ // manager is never named; the search path the person had then, absolute
29
+ // entries only; the data root the workspace was resolved under and the
30
+ // workspace, so the service at login finds the same workspace whatever its
31
+ // manager's environment carries (XDG_DATA_HOME, the overlay's variable), and
32
+ // records a refusal there even when the project no longer leads to it; and
33
+ // `--startup`, under which the service refuses to run without a consent that
34
+ // covers startup.
35
+ //
36
+ // Installing records that consent first, since the manager starts the
37
+ // service as soon as it loads the unit, then hands the text to the injected
38
+ // service manager (service-managers.mjs) and keeps
39
+ //
40
+ // state/service/login-item.json atelier-obsidian-login-item/v1
41
+ //
42
+ // { label, file, digest of the text, program: { node, entry }, searchPath }.
43
+ // The label is allocated once and kept. Once installed, the service is
44
+ // started through the manager (loginItemStarter), never as a child beside
45
+ // it; a unit that differs from what would be written now (a Node that was
46
+ // removed, another entry, an earlier release's format) is written again on
47
+ // that way and reloaded, keeping the search path recorded at installation.
48
+ // Removing lowers the consent to the service alone first, so a unit left
49
+ // behind by a failed removal could only refuse, then removes the unit and
50
+ // the record. A person who switched the item off (System Settings, or
51
+ // `systemctl --user disable`) is not overridden: the manager is asked first,
52
+ // the unit is neither written again nor started through it, and the service
53
+ // is started as a child for that command only.
54
+
55
+ export const LOGIN_ITEM_SCHEMA = 'atelier-obsidian-login-item/v1'
56
+ export const LOGIN_ITEM_PACKAGE = '@mnstry/atelier'
57
+ const ENTRY_IN_PACKAGE = Object.freeze(['src', 'runtime', 'obsidian', 'service-main.mjs'])
58
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
59
+ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
60
+ const DIGEST = /^sha256:[0-9a-f]{64}$/
61
+ const CONTROL = /[\u0000-\u001f\u007f]/
62
+ // A package runner's cache: npx, pnpm dlx, bunx. A package there can be removed by the runner at any time.
63
+ const RUNNER_CACHES = Object.freeze([/[\\/]_npx[\\/]/, /[\\/]pnpm[\\/]dlx[\\/]/, /[\\/]\.bun[\\/]install[\\/]cache[\\/]/])
64
+
65
+ const sha256 = (text) => `sha256:${createHash('sha256').update(text).digest('hex')}`
66
+ const isAbsolutePlain = (value) => typeof value === 'string' && path.isAbsolute(value) && !CONTROL.test(value)
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // The record
70
+ // ---------------------------------------------------------------------------
71
+
72
+ function validateLoginItem(document, workspaceId) {
73
+ const code = 'invalid-login-item'
74
+ closedObject(document, { required: ['schema', 'workspaceId', 'kind', 'label', 'file', 'digest', 'program', 'searchPath', 'installedAt', 'updatedAt'] }, code, 'the login item record')
75
+ closedObject(document.program, { required: ['node', 'entry'] }, code, 'the program of the login item')
76
+ const ok = document.schema === LOGIN_ITEM_SCHEMA && document.workspaceId === workspaceId && SERVICE_MANAGER_KINDS.includes(document.kind)
77
+ && typeof document.label === 'string' && IDENTIFIER.test(document.label) && isAbsolutePlain(document.file) && path.basename(document.file).startsWith(document.label)
78
+ && typeof document.digest === 'string' && DIGEST.test(document.digest) && isAbsolutePlain(document.program.node) && isAbsolutePlain(document.program.entry)
79
+ && (document.searchPath === null || (typeof document.searchPath === 'string' && document.searchPath.split(':').every(isAbsolutePlain)))
80
+ && TIMESTAMP.test(document.installedAt) && TIMESTAMP.test(document.updatedAt)
81
+ if (!ok) refuse(code, 'the login item record is malformed')
82
+ return document
83
+ }
84
+
85
+ export function readLoginItemRecord({ workspaceRoot, workspaceId }) {
86
+ let text
87
+ try { text = readRegularTextNoFollow(servicePaths(workspaceRoot).loginItem) } catch (error) {
88
+ if (error.code === 'ENOENT') return null
89
+ return refuse('invalid-login-item', 'the login item record cannot be read', { cause: error.code ?? 'unreadable' })
90
+ }
91
+ let document
92
+ try { document = JSON.parse(text) } catch { return refuse('invalid-login-item', 'the login item record is not JSON') }
93
+ return validateLoginItem(document, workspaceId)
94
+ }
95
+
96
+ function writeLoginItemRecord({ workspaceRoot, workspaceId, record }) {
97
+ validateLoginItem(record, workspaceId)
98
+ const directory = ensureContainedPrivateDirectory({ workspaceRoot, directory: servicePaths(workspaceRoot).directory, label: 'Obsidian service state' })
99
+ atomicReplacePrivateText(path.join(directory, 'login-item.json'), canonicalJson(record))
100
+ return record
101
+ }
102
+
103
+ function removeLoginItemRecord({ workspaceRoot }) {
104
+ try { fs.unlinkSync(servicePaths(workspaceRoot).loginItem); return true } catch (error) { if (error.code === 'ENOENT') return false; throw error }
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // What a unit is made of
109
+ // ---------------------------------------------------------------------------
110
+
111
+ // The part of a launchd label that stands for the project: lower-case letters, digits and dashes, at most 40, from
112
+ // its name, so a person who looks in ~/Library/LaunchAgents can tell whose it is. Pure.
113
+ export function labelSlug(name) {
114
+ const slug = String(name ?? '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40).replace(/-+$/, '')
115
+ return slug === '' ? 'project' : slug
116
+ }
117
+
118
+ // The label of this workspace's login item, as it is allocated the first time. Pure.
119
+ export function loginItemLabel({ platform, projectName, workspaceId }) {
120
+ const id = String(workspaceId).replaceAll(':', '_')
121
+ return platform === 'darwin' ? `ai.mnstry.atelier.${labelSlug(projectName)}.${id}` : serviceNameFor(workspaceId).replaceAll(':', '_')
122
+ }
123
+
124
+ // The name people read for a project: its configured `name`, else the name of its folder.
125
+ export const projectNameOf = (project) => (typeof project?.config?.name === 'string' && project.config.name.trim() !== '' ? project.config.name : path.basename(project?.configDir ?? 'project'))
126
+
127
+ // Absolute folders whose contents do not outlive a restart, or not for long.
128
+ export function temporaryRoots({ tmpdir = os.tmpdir() } = {}) {
129
+ const roots = new Set(['/tmp', '/private/tmp', '/var/tmp', '/private/var/tmp'])
130
+ if (isAbsolutePlain(tmpdir)) { roots.add(path.resolve(tmpdir)); try { roots.add(fs.realpathSync(tmpdir)) } catch { /* only as given */ } }
131
+ return [...roots]
132
+ }
133
+
134
+ const inside = (file, root) => file === root || file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)
135
+ const isFile = (file) => { try { return fs.statSync(file).isFile() } catch { return false } }
136
+
137
+ // The entry a login item runs: the service entry of the package installed for this project, found as Node finds a
138
+ // package from the project's folder (in its node_modules or a folder's above), named by that path and not its real
139
+ // path, so an upgrade in place is what the next start runs; else `ownEntry`, the entry of the package this command
140
+ // runs from, unless that one is in a package runner's cache (npx) or a temporary folder, which are gone after a
141
+ // restart (`login-item-needs-installed-package`). { entryPath, source: 'project' | 'command' }.
142
+ export function resolveLoginItemEntry({ project, ownEntry, temporary = temporaryRoots(), exists = isFile } = {}) {
143
+ if (typeof project?.configDir === 'string' && path.isAbsolute(project.configDir)) {
144
+ for (let directory = path.resolve(project.configDir); ; directory = path.dirname(directory)) {
145
+ const candidate = path.join(directory, 'node_modules', ...LOGIN_ITEM_PACKAGE.split('/'), ...ENTRY_IN_PACKAGE)
146
+ if (exists(candidate)) return { entryPath: candidate, source: 'project' }
147
+ if (path.dirname(directory) === directory) break
148
+ }
149
+ }
150
+ const transient = typeof ownEntry !== 'string' || !path.isAbsolute(ownEntry) || RUNNER_CACHES.some((pattern) => pattern.test(ownEntry)) || temporary.some((root) => inside(path.resolve(ownEntry), root))
151
+ if (transient) refuse('login-item-needs-installed-package', 'a login item runs the package installed in the project, and this project has none; the command runs from a package runner\'s cache or a temporary folder, which may be gone after a restart')
152
+ return { entryPath: ownEntry, source: 'command' }
153
+ }
154
+
155
+ // The Node a unit names: the one running this command, by its real path.
156
+ export const realNodePath = (execPath = process.execPath) => { try { return fs.realpathSync(execPath) } catch { return execPath } }
157
+
158
+ // The data root this workspace is resolved under for this command (the flag, the pointer, the overlay's preference or
159
+ // the platform default), which a unit names.
160
+ export const loginItemDataRoot = ({ project, dataRoot, env = process.env, platform = process.platform }) => resolveDataRoot({ dataRoot, pointer: readLocalPointer(project), project, env, platform })
161
+
162
+ // The unit this workspace's login item would have now. `dataRoot` is the resolved one (loginItemDataRoot), always named;
163
+ // `entryArgs` follow `--project`, `--data-root` and `--workspace-id`. Pure but for the builder's refusals.
164
+ export function planLoginItem({ platform, project, workspaceRoot, workspaceId, dataRoot, label, entryPath, entryArgs = [], nodePath, searchPath = null }) {
165
+ if (!isAbsolutePlain(dataRoot)) throw new TypeError('a login item names the absolute data root of its workspace')
166
+ if (typeof workspaceId !== 'string' || workspaceId === '') throw new TypeError('a login item names its workspace')
167
+ const args = [`--project=${project.configPath}`, `--data-root=${dataRoot}`, `--workspace-id=${workspaceId}`, ...entryArgs]
168
+ const unit = buildStartupAdapter({ platform, label, nodePath, entryPath, args, logPath: servicePaths(workspaceRoot).loginItemLog, searchPath })
169
+ return { ...unit, label, digest: sha256(unit.text), program: { node: nodePath, entry: entryPath }, searchPath }
170
+ }
171
+
172
+ function checkManager(manager, platform) {
173
+ if (!STARTUP_PLATFORMS.includes(platform)) buildStartupAdapter({ platform })
174
+ const operations = ['readUnit', 'install', 'start', 'remove', 'inspect']
175
+ if (manager === null || typeof manager !== 'object' || !operations.every((name) => typeof manager[name] === 'function') || !SERVICE_MANAGER_KINDS.includes(manager.kind)) {
176
+ throw new TypeError('a login item needs an injected service manager')
177
+ }
178
+ if (manager.platform !== platform) throw new TypeError('the service manager is not this platform\'s')
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Install, remove, start through it, and what it looks like
183
+ // ---------------------------------------------------------------------------
184
+
185
+ // Installs, or installs again, this workspace's login item. `consent` ({ actor }) is the person's consent to the
186
+ // service running at login; without it, a recorded consent must already cover startup (`startup-consent-required`).
187
+ // `ownEntry` and `entryArgs` are the entry this command would start and its arguments (`--adapter=...`).
188
+ // `beforeInstall({ entryPath })`, when given, runs once the consent is recorded and before the manager loads the unit,
189
+ // which starts the service at once: the command stops a service of an earlier release there, so that start runs the
190
+ // entry the unit names instead of finding the earlier one running.
191
+ // { installed: true, label, file, kind, entry: { path, source }, node, searchPath } or, when the manager did not take
192
+ // the unit, { installed: false, reason, label } with the consent recorded before, or, when none was, the one just
193
+ // recorded covering the service alone.
194
+ export async function installLoginItem(options = {}) {
195
+ const { loadProject, dataRoot, env = process.env, platform = process.platform, manager, consent, ownEntry, entryArgs = [], nodePath = realNodePath(), pathValue = env.PATH, clock = () => new Date(), temporary = temporaryRoots(), beforeInstall = null } = options
196
+ checkManager(manager, platform)
197
+ const project = loadProject()
198
+ ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }) })
199
+ const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform, create: true })
200
+ const { entryPath, source } = resolveLoginItemEntry({ project, ownEntry, temporary })
201
+ const recorded = readLoginItemRecord(workspace)
202
+ const label = recorded?.label ?? loginItemLabel({ platform, projectName: projectNameOf(project), workspaceId: workspace.workspaceId })
203
+ const plan = planLoginItem({
204
+ platform, project, workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId, dataRoot: loginItemDataRoot({ project, dataRoot, env, platform }), label, entryPath, entryArgs, nodePath,
205
+ searchPath: startupSearchPath(pathValue, { temporary }),
206
+ })
207
+
208
+ // The consent first: the manager starts the service as soon as it loads the unit.
209
+ const now = isoTime(clock)
210
+ const before = readServiceSettings(workspace)
211
+ if (consent === undefined && before?.consent.coverage !== 'service-and-startup') refuse('startup-consent-required', 'a login item needs a consent that covers the service starting at login; name who gives it')
212
+ if (consent !== undefined && (typeof consent?.actor !== 'string' || consent.actor === '')) refuse('startup-consent-required', 'a consent names its actor')
213
+ await ensureServiceSettings(workspace, { consent: consent === undefined ? undefined : { actor: consent.actor, coverage: 'service-and-startup' }, now })
214
+ if (typeof beforeInstall === 'function') await beforeInstall({ entryPath })
215
+
216
+ const installed = await manager.install({ label, fileName: plan.fileName, text: plan.text })
217
+ if (!installed.ok) {
218
+ restoreConsent(workspace, before, now)
219
+ return { installed: false, reason: installed.code, label, message: installed.message ?? null }
220
+ }
221
+ writeLoginItemRecord({
222
+ ...workspace,
223
+ record: { schema: LOGIN_ITEM_SCHEMA, workspaceId: workspace.workspaceId, kind: manager.kind, label, file: installed.file, digest: plan.digest, program: plan.program, searchPath: plan.searchPath, installedAt: recorded?.installedAt ?? now, updatedAt: now },
224
+ })
225
+ return { installed: true, label, file: installed.file, kind: manager.kind, entry: { path: entryPath, source }, node: nodePath, searchPath: plan.searchPath }
226
+ }
227
+
228
+ // Back to the consent that was recorded before an installation the manager refused: the service may still run, as a
229
+ // child, under the consent it had.
230
+ function restoreConsent(workspace, before, now) {
231
+ const current = readServiceSettings(workspace)
232
+ if (current === null) return
233
+ const consent = before?.consent ?? { ...current.consent, coverage: 'service' }
234
+ if (JSON.stringify(consent) !== JSON.stringify(current.consent)) writeServiceSettings({ ...workspace, settings: { ...current, consent, updatedAt: now } })
235
+ }
236
+
237
+ // Removes this workspace's login item: the consent is lowered to the service alone first, then the manager unloads
238
+ // and deletes the unit (a service it runs is stopped as it stops it), then the record goes. { removed, label, file }
239
+ // or { removed: false, reason } when the manager kept it.
240
+ export async function removeLoginItem(options = {}) {
241
+ const { loadProject, dataRoot, env = process.env, platform = process.platform, manager, clock = () => new Date() } = options
242
+ checkManager(manager, platform)
243
+ const project = loadProject()
244
+ const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
245
+ if (!workspace?.workspaceRoot) return { removed: false, reason: 'workspace-not-prepared' }
246
+ const record = readLoginItemRecord(workspace)
247
+ const label = record?.label ?? loginItemLabel({ platform, projectName: projectNameOf(project), workspaceId: workspace.workspaceId })
248
+ const fileName = record === null ? `${label}${manager.kind === 'launchd-user-agent' ? '.plist' : '.service'}` : path.basename(record.file)
249
+ const now = isoTime(clock)
250
+ const settings = readServiceSettings(workspace)
251
+ if (settings?.consent.coverage === 'service-and-startup') await ensureServiceSettings(workspace, { consent: { actor: settings.consent.actor, coverage: 'service' }, now })
252
+ const removed = await manager.remove({ label, fileName })
253
+ if (!removed.ok) return { removed: false, reason: removed.code, label, message: removed.message ?? null }
254
+ removeLoginItemRecord(workspace)
255
+ return { removed: removed.removed === true || record !== null, label, file: manager.fileFor(fileName) }
256
+ }
257
+
258
+ // The unit this workspace's installed login item would have now, keeping its label and the search path recorded when
259
+ // it was installed; null when no entry for it can be found from here (the command runs from a package runner and the
260
+ // project has no package installed): what is installed then stays as it is.
261
+ export function currentLoginItemPlan({ record, project, workspaceRoot, dataRoot, env = process.env, platform, ownEntry, entryArgs = [], nodePath = realNodePath(), temporary = temporaryRoots() }) {
262
+ try {
263
+ const { entryPath } = resolveLoginItemEntry({ project, ownEntry, temporary })
264
+ return planLoginItem({
265
+ platform, project, workspaceRoot, workspaceId: record.workspaceId, dataRoot: loginItemDataRoot({ project, dataRoot, env, platform }), label: record.label, entryPath, entryArgs, nodePath,
266
+ searchPath: record.searchPath,
267
+ })
268
+ } catch (error) {
269
+ if (error?.code === 'login-item-needs-installed-package' || error?.code === 'startup-adapter-input-invalid') return null
270
+ throw error
271
+ }
272
+ }
273
+
274
+ // { entryPath, start() } for startService: the entry the item runs, and a start through its manager. The manager is
275
+ // asked first whether the person switched the item off (System Settings, `systemctl --user disable`): one that is off
276
+ // is neither written again, reloaded, enabled nor started (`login-item-switched-off`), and startService starts a child
277
+ // for that command only. A unit that differs from `plan` is written again and reloaded first (the answer then carries
278
+ // `refreshed: true`); a unit whose file is gone was removed by somebody and is not put back.
279
+ export function loginItemStarter({ workspace, manager, record, plan = null, clock = () => new Date() }) {
280
+ const fileName = path.basename(record.file)
281
+ let entryPath = record.program.entry
282
+ return {
283
+ get entryPath() { return entryPath },
284
+ async start() {
285
+ let refreshed = false
286
+ const text = manager.readUnit({ label: record.label, fileName })
287
+ if (text === null) return { ok: false, code: 'login-item-file-missing', refreshed }
288
+ let seen = null
289
+ try { seen = await manager.inspect({ label: record.label, fileName }) } catch { seen = null }
290
+ if (seen?.ok === true && seen.disabled === true) return { ok: false, code: 'login-item-switched-off', refreshed }
291
+ if (plan !== null && plan.fileName === fileName && text !== plan.text) {
292
+ const installed = await manager.install({ label: record.label, fileName, text: plan.text })
293
+ if (!installed.ok) return { ok: false, code: installed.code, refreshed }
294
+ writeLoginItemRecord({ ...workspace, record: { ...record, file: installed.file, digest: plan.digest, program: plan.program, updatedAt: isoTime(clock) } })
295
+ entryPath = plan.program.entry
296
+ refreshed = true
297
+ }
298
+ const started = await manager.start({ label: record.label, fileName })
299
+ return { ...started, refreshed, entryPath }
300
+ },
301
+ }
302
+ }
303
+
304
+ // What this workspace's login item looks like: the record, what the manager says about it, and how the last start by
305
+ // it ended. `manager` is a manager, or a function that answers one (asked only when an item is recorded), or null
306
+ // (it cannot be asked from here): the record alone is reported then.
307
+ // state loaded | not-loaded | switched-off | file-missing | unknown
308
+ export async function loginItemStatus({ workspace, manager: given = null }) {
309
+ const lastStartup = (() => { try { return readLastStartup(workspace) } catch { return null } })()
310
+ let record
311
+ try { record = readLoginItemRecord(workspace) } catch (error) { return { installed: null, reason: error.code ?? 'invalid-login-item', lastStartup } }
312
+ if (record === null) return { installed: false, lastStartup }
313
+ // A unit whose entry or Node is gone cannot start: its manager tries again every minute until it is removed or installed again.
314
+ const present = (file) => { try { return fs.statSync(file).isFile() } catch { return false } }
315
+ const shown = {
316
+ installed: true, label: record.label, file: record.file, kind: record.kind, entry: record.program.entry, node: record.program.node, installedAt: record.installedAt,
317
+ programPresent: present(record.program.entry) && present(record.program.node), lastStartup,
318
+ }
319
+ const manager = typeof given === 'function' ? await given() : given
320
+ if (manager === null) return { ...shown, state: 'unknown', inspected: false }
321
+ const fileName = path.basename(record.file)
322
+ let seen = null
323
+ try { seen = await manager.inspect({ label: record.label, fileName }) } catch { seen = null }
324
+ let text = null
325
+ try { text = manager.readUnit({ label: record.label, fileName }) } catch { text = null }
326
+ const state = seen === null || seen.ok !== true ? 'unknown'
327
+ : seen.present === false ? 'file-missing'
328
+ : seen.disabled === true ? 'switched-off'
329
+ : seen.loaded === true ? 'loaded'
330
+ : seen.loaded === false ? 'not-loaded' : 'unknown'
331
+ return { ...shown, state, inspected: true, running: seen?.running ?? null, pid: seen?.pid ?? null, lastExit: seen?.lastExit ?? null, changedOnDisk: typeof text === 'string' && sha256(text) !== record.digest }
332
+ }