@open-mercato/cezar 0.9.1 → 0.9.2

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 (300) hide show
  1. package/README.md +19 -11
  2. package/dist/agent-config/catalog.d.ts +1 -1
  3. package/dist/agent-config/files.d.ts +1 -1
  4. package/dist/agent-config/service.d.ts +1 -1
  5. package/dist/agent-config/validate.d.ts +1 -1
  6. package/dist/automations/coordinator.d.ts +26 -0
  7. package/dist/automations/coordinator.js +66 -0
  8. package/dist/automations/coordinator.js.map +1 -0
  9. package/dist/automations/github-poller.d.ts +74 -0
  10. package/dist/automations/github-poller.js +234 -0
  11. package/dist/automations/github-poller.js.map +1 -0
  12. package/dist/automations/scheduler.d.ts +42 -0
  13. package/dist/automations/scheduler.js +192 -0
  14. package/dist/automations/scheduler.js.map +1 -0
  15. package/dist/automations/store.d.ts +65 -0
  16. package/dist/automations/store.js +298 -0
  17. package/dist/automations/store.js.map +1 -0
  18. package/dist/automations/task-template.d.ts +18 -0
  19. package/dist/automations/task-template.js +95 -0
  20. package/dist/automations/task-template.js.map +1 -0
  21. package/dist/automations/types.d.ts +251 -0
  22. package/dist/automations/types.js +158 -0
  23. package/dist/automations/types.js.map +1 -0
  24. package/dist/config.d.ts +9 -111
  25. package/dist/contract/agent-config.d.ts +151 -0
  26. package/dist/contract/automations.d.ts +911 -0
  27. package/dist/contract/events.d.ts +45 -0
  28. package/dist/contract/github.d.ts +467 -0
  29. package/dist/contract/health.d.ts +96 -0
  30. package/dist/contract/index.d.ts +15 -0
  31. package/dist/contract/index.js +1421 -0
  32. package/dist/contract/projects.d.ts +182 -0
  33. package/dist/contract/repo.d.ts +234 -0
  34. package/dist/contract/runs.d.ts +1226 -0
  35. package/dist/contract/skills.d.ts +225 -0
  36. package/dist/contract/workflows.d.ts +212 -0
  37. package/dist/contract/workspace.d.ts +442 -0
  38. package/dist/core/agent-env.d.ts +1 -1
  39. package/dist/core/agent-runner.d.ts +12 -1
  40. package/dist/core/agent-runner.js +13 -0
  41. package/dist/core/agent-runner.js.map +1 -1
  42. package/dist/core/ask.d.ts +45 -124
  43. package/dist/core/ask.js +103 -13
  44. package/dist/core/ask.js.map +1 -1
  45. package/dist/core/claude-cli-runner.d.ts +2 -2
  46. package/dist/core/claude-cli-runner.js +52 -9
  47. package/dist/core/claude-cli-runner.js.map +1 -1
  48. package/dist/core/claude-ui-mapper.d.ts +1 -1
  49. package/dist/core/codex-app-server-runner.d.ts +1 -1
  50. package/dist/core/codex-app-server-runner.js +55 -3
  51. package/dist/core/codex-app-server-runner.js.map +1 -1
  52. package/dist/core/codex-app-server-transport.d.ts +6 -1
  53. package/dist/core/codex-app-server-transport.js +12 -3
  54. package/dist/core/codex-app-server-transport.js.map +1 -1
  55. package/dist/core/codex-model-catalog.d.ts +1 -1
  56. package/dist/core/codex-ui-mapper.d.ts +5 -1
  57. package/dist/core/codex-ui-mapper.js +74 -17
  58. package/dist/core/codex-ui-mapper.js.map +1 -1
  59. package/dist/core/model-identity.d.ts +1 -1
  60. package/dist/core/model-presets.d.ts +2 -2
  61. package/dist/core/model-presets.js +1 -1
  62. package/dist/core/opencode-server-runner.d.ts +2 -2
  63. package/dist/core/opencode-ui-mapper.d.ts +4 -1
  64. package/dist/core/opencode-ui-mapper.js +45 -7
  65. package/dist/core/opencode-ui-mapper.js.map +1 -1
  66. package/dist/core/provider-auth.d.ts +62 -0
  67. package/dist/core/provider-auth.js +290 -0
  68. package/dist/core/provider-auth.js.map +1 -0
  69. package/dist/core/provider-availability.d.ts +3 -0
  70. package/dist/core/provider-availability.js +13 -0
  71. package/dist/core/provider-availability.js.map +1 -0
  72. package/dist/core/runner-factory.d.ts +1 -1
  73. package/dist/core/runner-model-catalog.d.ts +1 -1
  74. package/dist/core/tool-display.d.ts +1 -1
  75. package/dist/core/ui-events.d.ts +1 -1
  76. package/dist/core/ui-events.js +1 -1
  77. package/dist/git-diff-base.d.ts +60 -0
  78. package/dist/git-diff-base.js +54 -0
  79. package/dist/git-diff-base.js.map +1 -0
  80. package/dist/git-worktree.d.ts +37 -6
  81. package/dist/git-worktree.js +39 -9
  82. package/dist/git-worktree.js.map +1 -1
  83. package/dist/handoff.d.ts +2 -2
  84. package/dist/handoff.js +1 -1
  85. package/dist/index.js +65 -8
  86. package/dist/index.js.map +1 -1
  87. package/dist/paths.d.ts +16 -1
  88. package/dist/paths.js +24 -1
  89. package/dist/paths.js.map +1 -1
  90. package/dist/planner.d.ts +2 -2
  91. package/dist/planner.js +1 -1
  92. package/dist/planner.js.map +1 -1
  93. package/dist/release/manifests.d.ts +81 -0
  94. package/dist/release/manifests.js +74 -0
  95. package/dist/release/manifests.js.map +1 -0
  96. package/dist/release/snapshot.d.ts +9 -23
  97. package/dist/release/snapshot.js +9 -22
  98. package/dist/release/snapshot.js.map +1 -1
  99. package/dist/release/stable.d.ts +15 -29
  100. package/dist/release/stable.js +14 -27
  101. package/dist/release/stable.js.map +1 -1
  102. package/dist/runs/auto-name.d.ts +1 -1
  103. package/dist/runs/retention.d.ts +1 -1
  104. package/dist/runs/store.d.ts +142 -336
  105. package/dist/runs/store.js +136 -9
  106. package/dist/runs/store.js.map +1 -1
  107. package/dist/runs/ui-event-sink.d.ts +1 -1
  108. package/dist/server/app-type.d.ts +21 -0
  109. package/dist/server/app-type.js +2 -0
  110. package/dist/server/app-type.js.map +1 -0
  111. package/dist/server/capabilities.d.ts +6 -5
  112. package/dist/server/capabilities.js +11 -0
  113. package/dist/server/capabilities.js.map +1 -1
  114. package/dist/server/forge/github.d.ts +58 -151
  115. package/dist/server/forge/github.js +530 -6
  116. package/dist/server/forge/github.js.map +1 -1
  117. package/dist/server/forge/index.d.ts +10 -3
  118. package/dist/server/forge/index.js +14 -1
  119. package/dist/server/forge/index.js.map +1 -1
  120. package/dist/server/forge/types.d.ts +86 -1
  121. package/dist/server/git-changes.d.ts +8 -8
  122. package/dist/server/git-changes.js +14 -17
  123. package/dist/server/git-changes.js.map +1 -1
  124. package/dist/server/git.d.ts +2 -0
  125. package/dist/server/git.js +9 -0
  126. package/dist/server/git.js.map +1 -1
  127. package/dist/server/github.d.ts +3 -3
  128. package/dist/server/github.js +1 -1
  129. package/dist/server/github.js.map +1 -1
  130. package/dist/server/pr.d.ts +2 -2
  131. package/dist/server/project-context.d.ts +20 -3
  132. package/dist/server/project-context.js +30 -1
  133. package/dist/server/project-context.js.map +1 -1
  134. package/dist/server/provider-action-gate.d.ts +8 -0
  135. package/dist/server/provider-action-gate.js +54 -0
  136. package/dist/server/provider-action-gate.js.map +1 -0
  137. package/dist/server/provider-auth-runtime.d.ts +21 -0
  138. package/dist/server/provider-auth-runtime.js +66 -0
  139. package/dist/server/provider-auth-runtime.js.map +1 -0
  140. package/dist/server/server.d.ts +11142 -47
  141. package/dist/server/server.js +1714 -806
  142. package/dist/server/server.js.map +1 -1
  143. package/dist/server/validators.d.ts +97 -0
  144. package/dist/server/validators.js +86 -0
  145. package/dist/server/validators.js.map +1 -0
  146. package/dist/server/ws.d.ts +2 -2
  147. package/dist/server/ws.js +2 -2
  148. package/dist/server/ws.js.map +1 -1
  149. package/dist/server-install/engine.d.ts +1 -1
  150. package/dist/server-install/platforms/macosx-ngrok.d.ts +1 -1
  151. package/dist/server-install/platforms/ubuntu-vps.d.ts +16 -1
  152. package/dist/server-install/platforms/ubuntu-vps.js +83 -1
  153. package/dist/server-install/platforms/ubuntu-vps.js.map +1 -1
  154. package/dist/server-install/state.d.ts +1 -1
  155. package/dist/server-install/steps.d.ts +2 -2
  156. package/dist/server-install/strategies.d.ts +1 -1
  157. package/dist/server-install/types.d.ts +38 -424
  158. package/dist/server-install/ui.d.ts +1 -1
  159. package/dist/skills-banner.d.ts +7 -6
  160. package/dist/skills-banner.js +7 -6
  161. package/dist/skills-banner.js.map +1 -1
  162. package/dist/skills-remote.d.ts +2 -2
  163. package/dist/skills-update.d.ts +88 -0
  164. package/dist/skills-update.js +392 -0
  165. package/dist/skills-update.js.map +1 -0
  166. package/dist/skills.d.ts +2 -0
  167. package/dist/skills.js +3 -2
  168. package/dist/skills.js.map +1 -1
  169. package/dist/todos.d.ts +2 -28
  170. package/dist/todos.js +1 -1
  171. package/dist/ui-state.d.ts +16 -2
  172. package/dist/ui-state.js +14 -1
  173. package/dist/ui-state.js.map +1 -1
  174. package/dist/workflows/load.d.ts +1 -1
  175. package/dist/workflows/run.d.ts +53 -12
  176. package/dist/workflows/run.js +436 -64
  177. package/dist/workflows/run.js.map +1 -1
  178. package/dist/workflows/types.d.ts +59 -224
  179. package/dist/workflows/types.js +14 -0
  180. package/dist/workflows/types.js.map +1 -1
  181. package/dist/workspace/config.d.ts +68 -206
  182. package/dist/workspace/config.js +145 -14
  183. package/dist/workspace/config.js.map +1 -1
  184. package/dist/workspace/migrations.d.ts +1 -1
  185. package/dist/workspace/migrations.js +19 -4
  186. package/dist/workspace/migrations.js.map +1 -1
  187. package/dist/workspace/projects.d.ts +8 -2
  188. package/dist/workspace/projects.js +9 -3
  189. package/dist/workspace/projects.js.map +1 -1
  190. package/dist/workspace/semaphore.d.ts +6 -0
  191. package/dist/workspace/semaphore.js +14 -1
  192. package/dist/workspace/semaphore.js.map +1 -1
  193. package/dist/workspace/ui-state.d.ts +13 -3
  194. package/dist/workspace/ui-state.js +15 -5
  195. package/dist/workspace/ui-state.js.map +1 -1
  196. package/package.json +17 -39
  197. package/scripts/inline-contract.mjs +112 -0
  198. package/scripts/mock-claude.mjs +39 -1
  199. package/scripts/sync-readme.mjs +20 -0
  200. package/web/dist/assets/alert-dialog-ghffK6g9.js +1 -0
  201. package/web/dist/assets/arrow-down-Cz8qb71e.js +1 -0
  202. package/web/dist/assets/arrow-left-wcYKQXI_.js +1 -0
  203. package/web/dist/assets/bundle-mjs-BT31bpU6.js +1 -0
  204. package/web/dist/assets/centered-state-ZtnFHA-n.js +43 -0
  205. package/web/dist/assets/chevron-right-G4caF2QV.js +1 -0
  206. package/web/dist/assets/{chunk-BO2N2NFS-DtrdTCWa.js → chunk-BO2N2NFS-DE6qKn3r.js} +8 -8
  207. package/web/dist/assets/circle-check--KvBxXWp.js +1 -0
  208. package/web/dist/assets/circle-x-C3EFZ3xf.js +1 -0
  209. package/web/dist/assets/collapsible-B0JVzd0B.js +1 -0
  210. package/web/dist/assets/commit-list-wm6DPHNs.js +1 -0
  211. package/web/dist/assets/compare-variants-DbdP4Y5T.js +1 -0
  212. package/web/dist/assets/{core-DePtBHcl.js → core-BCsw8oQw.js} +1 -1
  213. package/web/dist/assets/diff-Bizc96qe.js +3 -0
  214. package/web/dist/assets/diff-stat-Dyfg3ltF.js +1 -0
  215. package/web/dist/assets/{diff-view-DCeSegKk.js → diff-view-CG8ek6tC.js} +4 -4
  216. package/web/dist/assets/dropdown-menu-C1Dlf35P.js +1 -0
  217. package/web/dist/assets/editable-title-5rC_QRBZ.js +1 -0
  218. package/web/dist/assets/{ellipsis-vertical-CNnSLn5a.js → ellipsis-vertical-DpGTRqC2.js} +1 -1
  219. package/web/dist/assets/{file-D4GBh1ao.js → file-DlUGpUlb.js} +1 -1
  220. package/web/dist/assets/folder-BAY6Vddy.js +1 -0
  221. package/web/dist/assets/{git-pull-request-DnR8Xh4_.js → git-pull-request-agOG0iGM.js} +1 -1
  222. package/web/dist/assets/git-toolbar-DGiNMmtY.js +1 -0
  223. package/web/dist/assets/github-Vp7lxYGQ.js +1 -0
  224. package/web/dist/assets/highlighted-body-OFNGDK62-BL-MfFgl.js +1 -0
  225. package/web/dist/assets/highlighter-CdHqIwFr.js +3 -0
  226. package/web/dist/assets/image-preview-BWGk5fPX.js +1 -0
  227. package/web/dist/assets/index-DSxyJpuK.css +2 -0
  228. package/web/dist/assets/index-DiawI6RX.js +6 -0
  229. package/web/dist/assets/lib-BQXq3kEf.js +1 -0
  230. package/web/dist/assets/lib-BxQXEXDF.js +1 -0
  231. package/web/dist/assets/markdown-BGBOGCA_.js +2 -0
  232. package/web/dist/assets/mermaid-GHXKKRXX-C4jxqUUb.js +1 -0
  233. package/web/dist/assets/new-task-form-BpAw_Dks.js +1 -0
  234. package/web/dist/assets/pill-Dvs-P0xy.js +1 -0
  235. package/web/dist/assets/project-router-Cx8NPoqn.js +1 -0
  236. package/web/dist/assets/prompt-templates-CSBYBR1I.js +15 -0
  237. package/web/dist/assets/react-runtime-CCIEwYL0.js +9 -0
  238. package/web/dist/assets/{refresh-cw-BT8E96Tm.js → refresh-cw-BoGdAykg.js} +1 -1
  239. package/web/dist/assets/repo-git-PmX-BNMo.js +1 -0
  240. package/web/dist/assets/rolldown-runtime-QTnfLwEv.js +1 -0
  241. package/web/dist/assets/run-diff-EcOkf3ti.js +3 -0
  242. package/web/dist/assets/run-header-BGiv6_qX.js +1 -0
  243. package/web/dist/assets/{search-x-BDjaMrzG.js → search-x-X3EpVhgs.js} +1 -1
  244. package/web/dist/assets/skill-empty-hint-CZLALx6l.js +1 -0
  245. package/web/dist/assets/skills-BiWfHmCR.js +1 -0
  246. package/web/dist/assets/skills-CC91ouGH.js +1 -0
  247. package/web/dist/assets/sparkles-Dt4q7pbG.js +1 -0
  248. package/web/dist/assets/{square-terminal-Ckjjn0vb.js → square-terminal-hwBL8XsC.js} +1 -1
  249. package/web/dist/assets/tab-link-DIlR5T07.js +1 -0
  250. package/web/dist/assets/task-changes-XkHDxzQA.js +1 -0
  251. package/web/dist/assets/task-commits-BTTA5k5T.js +1 -0
  252. package/web/dist/assets/task-files-Da8_4C9g.js +2 -0
  253. package/web/dist/assets/task-thread-DoNXUsHr.js +9 -0
  254. package/web/dist/assets/textarea-CUdokhkv.js +1 -0
  255. package/web/dist/assets/thread-loading-B8q8ukXJ.js +1 -0
  256. package/web/dist/assets/{trash-2-DAyZ0VSi.js → trash-2-DhVIdWv8.js} +1 -1
  257. package/web/dist/assets/{triangle-alert-Uy8rokUh.js → triangle-alert-XYT0DGCI.js} +1 -1
  258. package/web/dist/assets/{upload-CDdyPPo_.js → upload-BKjN59-1.js} +1 -1
  259. package/web/dist/assets/use-desktop-Bf5LS_P4.js +1 -0
  260. package/web/dist/assets/use-submit-shortcut-CzVyoAMd.js +1 -0
  261. package/web/dist/assets/utils-DsLuRXwV.js +64 -0
  262. package/web/dist/assets/workflows-o2QU_pvU.js +11 -0
  263. package/web/dist/assets/zoomable-image-7WyUiaVD.js +1 -0
  264. package/web/dist/index.html +32 -13
  265. package/scripts/dev.mjs +0 -74
  266. package/scripts/release-snapshot.mjs +0 -130
  267. package/scripts/release.mjs +0 -119
  268. package/web/dist/assets/arrow-left-CnSdp92h.js +0 -1
  269. package/web/dist/assets/bundle-mjs-BegPhL5c.js +0 -1
  270. package/web/dist/assets/centered-state-DOsTcJZF.js +0 -43
  271. package/web/dist/assets/commit-list-BrxBEzRK.js +0 -1
  272. package/web/dist/assets/compare-variants-Bv2c9ORF.js +0 -1
  273. package/web/dist/assets/dist-B6XQOhgM.js +0 -1
  274. package/web/dist/assets/git-toolbar-pIC1ubPM.js +0 -1
  275. package/web/dist/assets/github-DTTSG0ht.js +0 -1
  276. package/web/dist/assets/highlighted-body-OFNGDK62-BuFJwunK.js +0 -1
  277. package/web/dist/assets/image-preview-DEhW0TFz.js +0 -1
  278. package/web/dist/assets/index-Cijmlz7q.js +0 -30
  279. package/web/dist/assets/index-DzroxqBG.css +0 -2
  280. package/web/dist/assets/lib-DPEZDBUN.js +0 -1
  281. package/web/dist/assets/lib-uHpeUPzK.js +0 -1
  282. package/web/dist/assets/mermaid-GHXKKRXX-CN2zQue0.js +0 -1
  283. package/web/dist/assets/open-mercato-toBr6SOa.svg +0 -11
  284. package/web/dist/assets/project-router-BMx4e8m-.js +0 -1
  285. package/web/dist/assets/repo-git-BusYfa3t.js +0 -1
  286. package/web/dist/assets/run-diff-wOJ3odA-.js +0 -3
  287. package/web/dist/assets/run-header-Cg5incil.js +0 -1
  288. package/web/dist/assets/skill-empty-hint-DTUSp6Vl.js +0 -1
  289. package/web/dist/assets/skills-PpP2owbM.js +0 -1
  290. package/web/dist/assets/tab-link-F3uQrdm2.js +0 -1
  291. package/web/dist/assets/task-changes-iD9kS79I.js +0 -1
  292. package/web/dist/assets/task-commits-COkLaUri.js +0 -1
  293. package/web/dist/assets/task-files-Ckttz7XH.js +0 -2
  294. package/web/dist/assets/task-thread-CQD91VPt.js +0 -9
  295. package/web/dist/assets/textarea-Bj_LCreg.js +0 -1
  296. package/web/dist/assets/use-desktop-DUh0STzq.js +0 -3
  297. package/web/dist/assets/utils-B8s5qIcK.js +0 -1
  298. package/web/dist/assets/workflows-BFk8C0co.js +0 -11
  299. package/web/dist/assets/zoomable-image-PEDOqyLQ.js +0 -1
  300. /package/web/{open-mercato.svg → dist/open-mercato.svg} +0 -0
@@ -23,234 +23,87 @@ export declare const PROJECT_ID_RE: RegExp;
23
23
  * degrade per-key so one bad value never evicts the project.
24
24
  */
25
25
  declare const workspaceProjectSchema: z.ZodObject<{
26
- /** Unique slug — URL segment, sidebar key, worktree namespace. */
27
26
  id: z.ZodString;
28
- /** Absolute, realpath-normalized repo root (normalization is the writer's
29
- * job — `registerProject` in step 1.3; the schema only demands absolute). */
30
- root: z.ZodEffects<z.ZodString, string, string>;
31
- /** Display name (basename by default). `''` = caller derives a fallback. */
27
+ root: z.ZodString;
32
28
  name: z.ZodCatch<z.ZodString>;
33
29
  addedAt: z.ZodCatch<z.ZodString>;
34
30
  lastOpenedAt: z.ZodCatch<z.ZodString>;
35
- source: z.ZodCatch<z.ZodEnum<["local", "checkout"]>>;
36
- /** Per-project cap on concurrently running tasks. Absent = inherit the
37
- * workspace `resources.maxParallel`. Bounded like the workspace cap; a bad
38
- * value degrades to "inherit" (`.catch(undefined)`) rather than a hard
39
- * default, and `.passthrough()` preserves the key across cezar round-trips. */
31
+ source: z.ZodCatch<z.ZodEnum<{
32
+ checkout: "checkout";
33
+ local: "local";
34
+ }>>;
40
35
  maxParallel: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
41
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
42
- /** Unique slug — URL segment, sidebar key, worktree namespace. */
43
- id: z.ZodString;
44
- /** Absolute, realpath-normalized repo root (normalization is the writer's
45
- * job — `registerProject` in step 1.3; the schema only demands absolute). */
46
- root: z.ZodEffects<z.ZodString, string, string>;
47
- /** Display name (basename by default). `''` = caller derives a fallback. */
48
- name: z.ZodCatch<z.ZodString>;
49
- addedAt: z.ZodCatch<z.ZodString>;
50
- lastOpenedAt: z.ZodCatch<z.ZodString>;
51
- source: z.ZodCatch<z.ZodEnum<["local", "checkout"]>>;
52
- /** Per-project cap on concurrently running tasks. Absent = inherit the
53
- * workspace `resources.maxParallel`. Bounded like the workspace cap; a bad
54
- * value degrades to "inherit" (`.catch(undefined)`) rather than a hard
55
- * default, and `.passthrough()` preserves the key across cezar round-trips. */
56
- maxParallel: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
57
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
58
- /** Unique slug — URL segment, sidebar key, worktree namespace. */
59
- id: z.ZodString;
60
- /** Absolute, realpath-normalized repo root (normalization is the writer's
61
- * job — `registerProject` in step 1.3; the schema only demands absolute). */
62
- root: z.ZodEffects<z.ZodString, string, string>;
63
- /** Display name (basename by default). `''` = caller derives a fallback. */
64
- name: z.ZodCatch<z.ZodString>;
65
- addedAt: z.ZodCatch<z.ZodString>;
66
- lastOpenedAt: z.ZodCatch<z.ZodString>;
67
- source: z.ZodCatch<z.ZodEnum<["local", "checkout"]>>;
68
- /** Per-project cap on concurrently running tasks. Absent = inherit the
69
- * workspace `resources.maxParallel`. Bounded like the workspace cap; a bad
70
- * value degrades to "inherit" (`.catch(undefined)`) rather than a hard
71
- * default, and `.passthrough()` preserves the key across cezar round-trips. */
72
- maxParallel: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
73
- }, z.ZodTypeAny, "passthrough">>;
36
+ }, z.core.$loose>;
74
37
  export type WorkspaceProject = z.infer<typeof workspaceProjectSchema>;
75
38
  declare const workspaceConfigSchema: z.ZodObject<{
76
- /** Migration cursor (src/workspace/migrations.ts). Absent/bad → 0, which
77
- * means "run every migration" — each one is idempotent, so that is safe. */
78
- schemaVersion: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
79
- /** Root exposed by the Add project folder browser. Environment supplies
80
- * the zero-config default; an explicit workspace value wins thereafter. */
81
- browseRoot: z.ZodCatch<z.ZodDefault<z.ZodString>>;
82
- /** Checkout root for GUI-cloned projects. Stored as written (a literal
83
- * `~` is expanded by the checkout flow, not here); validated writable
84
- * when *changed*, never at load. */
85
- projectsDir: z.ZodCatch<z.ZodDefault<z.ZodString>>;
86
- resources: z.ZodCatch<z.ZodDefault<z.ZodObject<{
87
- /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
88
- maxParallel: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
89
- /** Per-task memory ceiling in MiB; null = no limit (matches the file's
90
- * literal `"memoryLimitMb": null` in the spec's Data Model). */
91
- memoryLimitMb: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
92
- /** Default worktree retention for projects that don't override it. */
93
- worktreeRetentionDefault: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
94
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
95
- /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
96
- maxParallel: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
97
- /** Per-task memory ceiling in MiB; null = no limit (matches the file's
98
- * literal `"memoryLimitMb": null` in the spec's Data Model). */
99
- memoryLimitMb: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
100
- /** Default worktree retention for projects that don't override it. */
101
- worktreeRetentionDefault: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
102
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
103
- /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
104
- maxParallel: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
105
- /** Per-task memory ceiling in MiB; null = no limit (matches the file's
106
- * literal `"memoryLimitMb": null` in the spec's Data Model). */
107
- memoryLimitMb: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
108
- /** Default worktree retention for projects that don't override it. */
109
- worktreeRetentionDefault: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
110
- }, z.ZodTypeAny, "passthrough">>>>;
111
- /** Per-entry salvage: a corrupt entry is dropped, the rest of the registry
112
- * survives (a whole-array `.catch([])` would evict every project over one
113
- * bad row). */
114
- projects: z.ZodEffects<z.ZodCatch<z.ZodDefault<z.ZodArray<z.ZodUnknown, "many">>>, z.objectOutputType<{
115
- /** Unique slug — URL segment, sidebar key, worktree namespace. */
116
- id: z.ZodString;
117
- /** Absolute, realpath-normalized repo root (normalization is the writer's
118
- * job — `registerProject` in step 1.3; the schema only demands absolute). */
119
- root: z.ZodEffects<z.ZodString, string, string>;
120
- /** Display name (basename by default). `''` = caller derives a fallback. */
121
- name: z.ZodCatch<z.ZodString>;
122
- addedAt: z.ZodCatch<z.ZodString>;
123
- lastOpenedAt: z.ZodCatch<z.ZodString>;
124
- source: z.ZodCatch<z.ZodEnum<["local", "checkout"]>>;
125
- /** Per-project cap on concurrently running tasks. Absent = inherit the
126
- * workspace `resources.maxParallel`. Bounded like the workspace cap; a bad
127
- * value degrades to "inherit" (`.catch(undefined)`) rather than a hard
128
- * default, and `.passthrough()` preserves the key across cezar round-trips. */
129
- maxParallel: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
130
- }, z.ZodTypeAny, "passthrough">[], unknown>;
131
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
132
- /** Migration cursor (src/workspace/migrations.ts). Absent/bad → 0, which
133
- * means "run every migration" — each one is idempotent, so that is safe. */
134
39
  schemaVersion: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
135
- /** Root exposed by the Add project folder browser. Environment supplies
136
- * the zero-config default; an explicit workspace value wins thereafter. */
137
40
  browseRoot: z.ZodCatch<z.ZodDefault<z.ZodString>>;
138
- /** Checkout root for GUI-cloned projects. Stored as written (a literal
139
- * `~` is expanded by the checkout flow, not here); validated writable
140
- * when *changed*, never at load. */
141
41
  projectsDir: z.ZodCatch<z.ZodDefault<z.ZodString>>;
142
- resources: z.ZodCatch<z.ZodDefault<z.ZodObject<{
143
- /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
42
+ skillsAutoUpdate: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
43
+ resources: z.ZodCatch<z.ZodPrefault<z.ZodObject<{
144
44
  maxParallel: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
145
- /** Per-task memory ceiling in MiB; null = no limit (matches the file's
146
- * literal `"memoryLimitMb": null` in the spec's Data Model). */
45
+ maxMonitoringSessions: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
46
+ monitoringWakeIntervalMinutes: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
147
47
  memoryLimitMb: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
148
- /** Default worktree retention for projects that don't override it. */
149
48
  worktreeRetentionDefault: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
150
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
151
- /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
152
- maxParallel: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
153
- /** Per-task memory ceiling in MiB; null = no limit (matches the file's
154
- * literal `"memoryLimitMb": null` in the spec's Data Model). */
155
- memoryLimitMb: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
156
- /** Default worktree retention for projects that don't override it. */
157
- worktreeRetentionDefault: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
158
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
159
- /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
160
- maxParallel: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
161
- /** Per-task memory ceiling in MiB; null = no limit (matches the file's
162
- * literal `"memoryLimitMb": null` in the spec's Data Model). */
163
- memoryLimitMb: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
164
- /** Default worktree retention for projects that don't override it. */
165
- worktreeRetentionDefault: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
166
- }, z.ZodTypeAny, "passthrough">>>>;
167
- /** Per-entry salvage: a corrupt entry is dropped, the rest of the registry
168
- * survives (a whole-array `.catch([])` would evict every project over one
169
- * bad row). */
170
- projects: z.ZodEffects<z.ZodCatch<z.ZodDefault<z.ZodArray<z.ZodUnknown, "many">>>, z.objectOutputType<{
171
- /** Unique slug — URL segment, sidebar key, worktree namespace. */
172
- id: z.ZodString;
173
- /** Absolute, realpath-normalized repo root (normalization is the writer's
174
- * job — `registerProject` in step 1.3; the schema only demands absolute). */
175
- root: z.ZodEffects<z.ZodString, string, string>;
176
- /** Display name (basename by default). `''` = caller derives a fallback. */
177
- name: z.ZodCatch<z.ZodString>;
178
- addedAt: z.ZodCatch<z.ZodString>;
179
- lastOpenedAt: z.ZodCatch<z.ZodString>;
180
- source: z.ZodCatch<z.ZodEnum<["local", "checkout"]>>;
181
- /** Per-project cap on concurrently running tasks. Absent = inherit the
182
- * workspace `resources.maxParallel`. Bounded like the workspace cap; a bad
183
- * value degrades to "inherit" (`.catch(undefined)`) rather than a hard
184
- * default, and `.passthrough()` preserves the key across cezar round-trips. */
185
- maxParallel: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
186
- }, z.ZodTypeAny, "passthrough">[], unknown>;
187
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
188
- /** Migration cursor (src/workspace/migrations.ts). Absent/bad → 0, which
189
- * means "run every migration" — each one is idempotent, so that is safe. */
190
- schemaVersion: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
191
- /** Root exposed by the Add project folder browser. Environment supplies
192
- * the zero-config default; an explicit workspace value wins thereafter. */
193
- browseRoot: z.ZodCatch<z.ZodDefault<z.ZodString>>;
194
- /** Checkout root for GUI-cloned projects. Stored as written (a literal
195
- * `~` is expanded by the checkout flow, not here); validated writable
196
- * when *changed*, never at load. */
197
- projectsDir: z.ZodCatch<z.ZodDefault<z.ZodString>>;
198
- resources: z.ZodCatch<z.ZodDefault<z.ZodObject<{
199
- /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
200
- maxParallel: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
201
- /** Per-task memory ceiling in MiB; null = no limit (matches the file's
202
- * literal `"memoryLimitMb": null` in the spec's Data Model). */
203
- memoryLimitMb: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
204
- /** Default worktree retention for projects that don't override it. */
205
- worktreeRetentionDefault: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
206
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
207
- /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
208
- maxParallel: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
209
- /** Per-task memory ceiling in MiB; null = no limit (matches the file's
210
- * literal `"memoryLimitMb": null` in the spec's Data Model). */
211
- memoryLimitMb: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
212
- /** Default worktree retention for projects that don't override it. */
213
- worktreeRetentionDefault: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
214
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
215
- /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
216
- maxParallel: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
217
- /** Per-task memory ceiling in MiB; null = no limit (matches the file's
218
- * literal `"memoryLimitMb": null` in the spec's Data Model). */
219
- memoryLimitMb: z.ZodCatch<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
220
- /** Default worktree retention for projects that don't override it. */
221
- worktreeRetentionDefault: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
222
- }, z.ZodTypeAny, "passthrough">>>>;
223
- /** Per-entry salvage: a corrupt entry is dropped, the rest of the registry
224
- * survives (a whole-array `.catch([])` would evict every project over one
225
- * bad row). */
226
- projects: z.ZodEffects<z.ZodCatch<z.ZodDefault<z.ZodArray<z.ZodUnknown, "many">>>, z.objectOutputType<{
227
- /** Unique slug — URL segment, sidebar key, worktree namespace. */
228
- id: z.ZodString;
229
- /** Absolute, realpath-normalized repo root (normalization is the writer's
230
- * job — `registerProject` in step 1.3; the schema only demands absolute). */
231
- root: z.ZodEffects<z.ZodString, string, string>;
232
- /** Display name (basename by default). `''` = caller derives a fallback. */
233
- name: z.ZodCatch<z.ZodString>;
234
- addedAt: z.ZodCatch<z.ZodString>;
235
- lastOpenedAt: z.ZodCatch<z.ZodString>;
236
- source: z.ZodCatch<z.ZodEnum<["local", "checkout"]>>;
237
- /** Per-project cap on concurrently running tasks. Absent = inherit the
238
- * workspace `resources.maxParallel`. Bounded like the workspace cap; a bad
239
- * value degrades to "inherit" (`.catch(undefined)`) rather than a hard
240
- * default, and `.passthrough()` preserves the key across cezar round-trips. */
241
- maxParallel: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
242
- }, z.ZodTypeAny, "passthrough">[], unknown>;
243
- }, z.ZodTypeAny, "passthrough">>;
49
+ }, z.core.$loose>>>;
50
+ composerDefaults: z.ZodCatch<z.ZodDefault<z.ZodObject<{
51
+ autonomous: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
52
+ worktree: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
53
+ }, z.core.$loose>>>;
54
+ disabledProviders: z.ZodPipe<z.ZodCatch<z.ZodDefault<z.ZodArray<z.ZodUnknown>>>, z.ZodTransform<("claude" | "codex" | "opencode")[], unknown[]>>;
55
+ projects: z.ZodPipe<z.ZodCatch<z.ZodDefault<z.ZodArray<z.ZodUnknown>>>, z.ZodTransform<{
56
+ [x: string]: unknown;
57
+ id: string;
58
+ root: string;
59
+ name: string;
60
+ addedAt: string;
61
+ lastOpenedAt: string;
62
+ source: "checkout" | "local";
63
+ maxParallel?: number | undefined;
64
+ }[], unknown[]>>;
65
+ }, z.core.$loose>;
244
66
  export type WorkspaceConfig = z.infer<typeof workspaceConfigSchema>;
67
+ /** Resolve the auto-update preference without mutating or materializing it. */
68
+ export declare function effectiveSkillsAutoUpdate(config: Pick<WorkspaceConfig, 'skillsAutoUpdate'>, env?: NodeJS.ProcessEnv): boolean;
69
+ export declare function effectiveComposerDefault(stored: boolean | undefined, envValue: string | undefined, fallback: boolean): boolean;
245
70
  /** The in-memory default — what a missing/corrupt file behaves like. */
246
71
  export declare function defaultWorkspaceConfig(): WorkspaceConfig;
72
+ /**
73
+ * The last-known-good copy of a NON-EMPTY registry, written beside the config
74
+ * by every successful merge-write. The registry is cheap to rebuild in theory
75
+ * ("open the project again"), but in practice it is a hand-curated list, and a
76
+ * single bad write — a crash between `writeFileSync` and `rename`, a full
77
+ * disk, a stray process — costs the user every entry. One extra file, no
78
+ * configuration, and `loadWorkspaceConfig` falls back to it.
79
+ *
80
+ * Removing `~/.cezar` still resets cezar completely; removing only
81
+ * `config.json` no longer does, because this snapshot restores it.
82
+ *
83
+ * A cezar older than this change does not refresh the snapshot, so on a machine
84
+ * that alternates between versions it can lag behind the registry — which only
85
+ * shows if the config file is also lost, and the worst case is a project the
86
+ * user unregistered reappearing. Cheap next to losing the whole list.
87
+ */
88
+ export declare function workspaceConfigBackupPath(path?: string): string;
247
89
  /**
248
90
  * Read `~/.cezar/config.json` on demand — never cached, never throws. A
249
91
  * missing file is the zero-config default (silent); an unreadable or
250
92
  * malformed one degrades to the same default with a one-line warning and is
251
93
  * left on disk untouched (the next successful merge-write replaces it).
94
+ *
95
+ * Before degrading, a missing, empty, or corrupt file is restored from the
96
+ * `config.json.bak` snapshot when that still holds projects. The restore is
97
+ * read-only: the recovered registry is handed back in memory and lands on disk
98
+ * again through the next merge-write, so a read never writes. A file that
99
+ * parses and simply has no projects is NOT restored — that is a user who
100
+ * removed their last project, not a lost registry.
101
+ *
102
+ * `path` defaults to the current `workspaceConfigPath()`, but a caller that
103
+ * will also WRITE passes the path it resolved itself — see
104
+ * `mergeWriteWorkspaceConfig` for why resolving it twice is a data-loss bug.
252
105
  */
253
- export declare function loadWorkspaceConfig(): Promise<WorkspaceConfig>;
106
+ export declare function loadWorkspaceConfig(path?: string): Promise<WorkspaceConfig>;
254
107
  /**
255
108
  * The tmp path an atomic write stages through — UNIQUE PER WRITE, never a
256
109
  * fixed `${path}.tmp`. `~/.cezar/` is shared by every cezar process on the
@@ -276,6 +129,15 @@ export declare function atomicWriteJsonSync(path: string, value: unknown): void;
276
129
  * boot). The mutator may mutate its argument in place or return a replacement.
277
130
  * Returns the config that was written. Throws on write failure (e.g. a
278
131
  * read-only home) — degrading is the caller's policy, per house rules.
132
+ *
133
+ * The path is resolved ONCE, before the `await`, and the same value feeds the
134
+ * read and the write. Resolving it twice used to lose the whole registry:
135
+ * `workspaceConfigPath()` re-reads `CEZ_HOME` on every call, so if the variable
136
+ * changed while the read was in flight — a test's `afterEach` dropping its pin
137
+ * after a timeout is the way this happens in practice — the read came from one
138
+ * home and the write landed in another, replacing that file's registry with a
139
+ * config it never held. One resolution keeps a merge-write inside exactly one
140
+ * file, whatever the environment does mid-flight.
279
141
  */
280
142
  export declare function mergeWriteWorkspaceConfig(mutator: (config: WorkspaceConfig) => WorkspaceConfig | void): Promise<WorkspaceConfig>;
281
143
  export {};
@@ -3,7 +3,8 @@ import { chmodSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { dirname } from 'node:path';
5
5
  import { z } from 'zod';
6
- import { workspaceConfigPath } from '../paths.js';
6
+ import { PROVIDER_IDS } from '../core/provider-auth.js';
7
+ import { assertCezarHomeWriteIsSandboxed, workspaceConfigPath } from '../paths.js';
7
8
  /**
8
9
  * `~/.cezar/config.json` — the per-user workspace config + project registry
9
10
  * (spec 2026-07-20-multi-project-workspace). House rules from the spec's Data
@@ -50,6 +51,10 @@ const resourcesSchema = z
50
51
  .object({
51
52
  /** Workspace-wide parallel-task cap (moved from per-repo config.json). */
52
53
  maxParallel: z.number().int().min(1).max(16).default(2).catch(2),
54
+ /** Extra durable `CEZ:MONITORING` sessions exempt from the active-task cap. */
55
+ maxMonitoringSessions: z.number().int().min(0).max(16).default(2).catch(2),
56
+ /** Optional cadence for re-checking monitored work; null parks at zero model cost. */
57
+ monitoringWakeIntervalMinutes: z.number().int().min(1).max(60).nullable().default(null).catch(null),
53
58
  /** Per-task memory ceiling in MiB; null = no limit (matches the file's
54
59
  * literal `"memoryLimitMb": null` in the spec's Data Model). */
55
60
  memoryLimitMb: z.number().int().min(0).max(1_048_576).nullable().default(null).catch(null),
@@ -57,10 +62,30 @@ const resourcesSchema = z
57
62
  worktreeRetentionDefault: z.number().int().min(0).max(1000).default(10).catch(10),
58
63
  })
59
64
  .passthrough();
65
+ const composerDefaultsSchema = z
66
+ .object({
67
+ autonomous: z.boolean().optional().catch(undefined),
68
+ worktree: z.boolean().optional().catch(undefined),
69
+ })
70
+ .passthrough();
60
71
  const workspacePathSchema = (envName, fallback) => {
61
72
  const defaultValue = () => process.env[envName]?.trim() || fallback;
62
73
  return z.string().min(1).max(4096).default(defaultValue).catch(defaultValue);
63
74
  };
75
+ const providerIdSet = new Set(PROVIDER_IDS);
76
+ const disabledProvidersSchema = z
77
+ .array(z.unknown())
78
+ .default(() => [])
79
+ .catch(() => [])
80
+ .transform((values) => {
81
+ const seen = new Set();
82
+ for (const value of values) {
83
+ if (typeof value !== 'string' || !providerIdSet.has(value))
84
+ continue;
85
+ seen.add(value);
86
+ }
87
+ return PROVIDER_IDS.filter((provider) => seen.has(provider));
88
+ });
64
89
  const workspaceConfigSchema = z
65
90
  .object({
66
91
  /** Migration cursor (src/workspace/migrations.ts). Absent/bad → 0, which
@@ -73,9 +98,16 @@ const workspaceConfigSchema = z
73
98
  * `~` is expanded by the checkout flow, not here); validated writable
74
99
  * when *changed*, never at load. */
75
100
  projectsDir: workspacePathSchema('CEZ_PROJECTS_DIR', '~/cezar/projects'),
101
+ /** Optional auto-update override. Absence inherits the environment/default
102
+ * and must stay absent on unrelated merge-writes. */
103
+ skillsAutoUpdate: z.boolean().optional().catch(undefined),
76
104
  // Function-form default/catch: mutators (step 1.3's registerProject) edit
77
105
  // these objects in place, so parses must never share one reference.
78
- resources: resourcesSchema.default(() => ({})).catch(() => resourcesSchema.parse({})),
106
+ resources: resourcesSchema.prefault(() => ({})).catch(() => resourcesSchema.parse({})),
107
+ /** Optional New Task policy. Missing keys inherit exact 0/1 environment seeds. */
108
+ composerDefaults: composerDefaultsSchema.default(() => ({})).catch(() => ({})),
109
+ /** Host-wide provider preferences; absent means every provider is enabled. */
110
+ disabledProviders: disabledProvidersSchema,
79
111
  /** Per-entry salvage: a corrupt entry is dropped, the rest of the registry
80
112
  * survives (a whole-array `.catch([])` would evict every project over one
81
113
  * bad row). */
@@ -89,33 +121,108 @@ const workspaceConfigSchema = z
89
121
  })),
90
122
  })
91
123
  .passthrough();
124
+ /** Resolve the auto-update preference without mutating or materializing it. */
125
+ export function effectiveSkillsAutoUpdate(config, env = process.env) {
126
+ if (config.skillsAutoUpdate !== undefined)
127
+ return config.skillsAutoUpdate;
128
+ if (env.CEZ_SKILLS_AUTO_UPDATE === '0')
129
+ return false;
130
+ if (env.CEZ_SKILLS_AUTO_UPDATE === '1')
131
+ return true;
132
+ return true;
133
+ }
134
+ export function effectiveComposerDefault(stored, envValue, fallback) {
135
+ if (stored !== undefined)
136
+ return stored;
137
+ if (envValue === '0')
138
+ return false;
139
+ if (envValue === '1')
140
+ return true;
141
+ return fallback;
142
+ }
92
143
  /** The in-memory default — what a missing/corrupt file behaves like. */
93
144
  export function defaultWorkspaceConfig() {
94
145
  return workspaceConfigSchema.parse({});
95
146
  }
147
+ /**
148
+ * The last-known-good copy of a NON-EMPTY registry, written beside the config
149
+ * by every successful merge-write. The registry is cheap to rebuild in theory
150
+ * ("open the project again"), but in practice it is a hand-curated list, and a
151
+ * single bad write — a crash between `writeFileSync` and `rename`, a full
152
+ * disk, a stray process — costs the user every entry. One extra file, no
153
+ * configuration, and `loadWorkspaceConfig` falls back to it.
154
+ *
155
+ * Removing `~/.cezar` still resets cezar completely; removing only
156
+ * `config.json` no longer does, because this snapshot restores it.
157
+ *
158
+ * A cezar older than this change does not refresh the snapshot, so on a machine
159
+ * that alternates between versions it can lag behind the registry — which only
160
+ * shows if the config file is also lost, and the worst case is a project the
161
+ * user unregistered reappearing. Cheap next to losing the whole list.
162
+ */
163
+ export function workspaceConfigBackupPath(path = workspaceConfigPath()) {
164
+ return `${path}.bak`;
165
+ }
166
+ function parseWorkspaceConfig(raw) {
167
+ try {
168
+ const parsed = workspaceConfigSchema.safeParse(JSON.parse(raw));
169
+ return parsed.success ? parsed.data : null;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ /** The snapshot is only worth restoring while it still holds projects — an
176
+ * empty one carries no information the defaults do not already have. */
177
+ async function loadWorkspaceConfigBackup(path) {
178
+ let raw;
179
+ try {
180
+ raw = await readFile(workspaceConfigBackupPath(path), 'utf8');
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ const parsed = parseWorkspaceConfig(raw);
186
+ return parsed && parsed.projects.length > 0 ? parsed : null;
187
+ }
96
188
  /**
97
189
  * Read `~/.cezar/config.json` on demand — never cached, never throws. A
98
190
  * missing file is the zero-config default (silent); an unreadable or
99
191
  * malformed one degrades to the same default with a one-line warning and is
100
192
  * left on disk untouched (the next successful merge-write replaces it).
193
+ *
194
+ * Before degrading, a missing, empty, or corrupt file is restored from the
195
+ * `config.json.bak` snapshot when that still holds projects. The restore is
196
+ * read-only: the recovered registry is handed back in memory and lands on disk
197
+ * again through the next merge-write, so a read never writes. A file that
198
+ * parses and simply has no projects is NOT restored — that is a user who
199
+ * removed their last project, not a lost registry.
200
+ *
201
+ * `path` defaults to the current `workspaceConfigPath()`, but a caller that
202
+ * will also WRITE passes the path it resolved itself — see
203
+ * `mergeWriteWorkspaceConfig` for why resolving it twice is a data-loss bug.
101
204
  */
102
- export async function loadWorkspaceConfig() {
103
- const path = workspaceConfigPath();
104
- let raw;
205
+ export async function loadWorkspaceConfig(path = workspaceConfigPath()) {
206
+ let raw = null;
105
207
  try {
106
208
  raw = await readFile(path, 'utf8');
107
209
  }
108
210
  catch {
109
- return defaultWorkspaceConfig();
211
+ // missing/unreadable — the backup below is the next chance
110
212
  }
111
- try {
112
- const parsed = workspaceConfigSchema.safeParse(JSON.parse(raw));
113
- if (parsed.success)
114
- return parsed.data;
213
+ if (raw !== null && raw.trim() !== '') {
214
+ const parsed = parseWorkspaceConfig(raw);
215
+ if (parsed)
216
+ return parsed;
115
217
  }
116
- catch {
117
- // malformed JSON — fall through to the warning + defaults
218
+ const restored = await loadWorkspaceConfigBackup(path);
219
+ if (restored) {
220
+ const cause = raw === null ? 'is missing' : 'is empty or corrupt';
221
+ console.warn(`[cez] workspace config ${path} ${cause} — restored ${restored.projects.length} project(s) from ${workspaceConfigBackupPath(path)}`);
222
+ return restored;
118
223
  }
224
+ if (raw === null)
225
+ return defaultWorkspaceConfig();
119
226
  console.warn(`[cez] workspace config ${path} is corrupt — using defaults (registry rebuilds)`);
120
227
  return defaultWorkspaceConfig();
121
228
  }
@@ -137,6 +244,7 @@ export function atomicTmpPath(path) {
137
244
  * shared by the workspace config and ui-state writers. Throws on write
138
245
  * failure (e.g. a read-only home) — degrading is the caller's policy. */
139
246
  export function atomicWriteJsonSync(path, value) {
247
+ assertCezarHomeWriteIsSandboxed(path);
140
248
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
141
249
  const tmp = atomicTmpPath(path);
142
250
  writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
@@ -157,11 +265,34 @@ export function atomicWriteJsonSync(path, value) {
157
265
  * boot). The mutator may mutate its argument in place or return a replacement.
158
266
  * Returns the config that was written. Throws on write failure (e.g. a
159
267
  * read-only home) — degrading is the caller's policy, per house rules.
268
+ *
269
+ * The path is resolved ONCE, before the `await`, and the same value feeds the
270
+ * read and the write. Resolving it twice used to lose the whole registry:
271
+ * `workspaceConfigPath()` re-reads `CEZ_HOME` on every call, so if the variable
272
+ * changed while the read was in flight — a test's `afterEach` dropping its pin
273
+ * after a timeout is the way this happens in practice — the read came from one
274
+ * home and the write landed in another, replacing that file's registry with a
275
+ * config it never held. One resolution keeps a merge-write inside exactly one
276
+ * file, whatever the environment does mid-flight.
160
277
  */
161
278
  export async function mergeWriteWorkspaceConfig(mutator) {
162
- const current = await loadWorkspaceConfig();
279
+ const path = workspaceConfigPath();
280
+ const current = await loadWorkspaceConfig(path);
163
281
  const next = mutator(current) ?? current;
164
- atomicWriteJsonSync(workspaceConfigPath(), next);
282
+ atomicWriteJsonSync(path, next);
283
+ // Refresh the snapshot after EVERY successful write, including an emptied
284
+ // registry (#731). Skipping the empty case left a stale non-empty backup:
285
+ // removing the last project, then losing config.json, resurrected the project
286
+ // the user had deliberately unregistered. An empty snapshot is not restored on
287
+ // load (see loadWorkspaceConfigBackup), so recovery now settles on the empty
288
+ // registry the user intended rather than the old projects.
289
+ try {
290
+ atomicWriteJsonSync(workspaceConfigBackupPath(path), next);
291
+ }
292
+ catch {
293
+ // Best-effort: the registry itself is already safely on disk, and a
294
+ // failed snapshot must never turn a successful write into an error.
295
+ }
165
296
  return next;
166
297
  }
167
298
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/workspace/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAEzD;;;;GAIG;AACH,MAAM,sBAAsB,GAAG,CAAC;KAC7B,MAAM,CAAC;IACN,kEAAkE;IAClE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC;IACnC;kFAC8E;IAC9E,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,uBAAuB,CAAC;IAC3F,4EAA4E;IAC5E,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;IACnC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;IACrC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;IAC1C,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;IACpD;;;oFAGgF;IAChF,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC;CACzE,CAAC;KACD,WAAW,EAAE,CAAC;AAIjB,MAAM,eAAe,GAAG,CAAC;KACtB,MAAM,CAAC;IACN,0EAA0E;IAC1E,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAChE;qEACiE;IACjE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;IAC1F,sEAAsE;IACtE,wBAAwB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;CAClF,CAAC;KACD,WAAW,EAAE,CAAC;AAEjB,MAAM,mBAAmB,GAAG,CAAC,OAA+C,EAAE,QAAgB,EAAE,EAAE;IAChG,MAAM,YAAY,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,QAAQ,CAAC;IACpE,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;AAC/E,CAAC,CAAC;AAEF,MAAM,qBAAqB,GAAG,CAAC;KAC5B,MAAM,CAAC;IACN;iFAC6E;IAC7E,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1D;gFAC4E;IAC5E,UAAU,EAAE,mBAAmB,CAAC,iBAAiB,EAAE,IAAI,CAAC;IACxD;;yCAEqC;IACrC,WAAW,EAAE,mBAAmB,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IACxE,0EAA0E;IAC1E,oEAAoE;IACpE,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrF;;oBAEgB;IAChB,QAAQ,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SAClB,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;SACjB,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;SACf,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CACrB,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QACxB,MAAM,MAAM,GAAG,sBAAsB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACvD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7C,CAAC,CAAC,CACH;CACJ,CAAC;KACD,WAAW,EAAE,CAAC;AAIjB,wEAAwE;AACxE,MAAM,UAAU,sBAAsB;IACpC,OAAO,qBAAqB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,MAAM,IAAI,GAAG,mBAAmB,EAAE,CAAC;IACnC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,sBAAsB,EAAE,CAAC;IAClC,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAChE,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;IAC5D,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,0BAA0B,IAAI,kDAAkD,CAAC,CAAC;IAC/F,OAAO,sBAAsB,EAAE,CAAC;AAClC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC;AACxE,CAAC;AAED;;0EAE0E;AAC1E,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,KAAc;IAC9D,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3D,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IAChC,aAAa,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7F,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACtB,IAAI,CAAC;QACH,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,4CAA4C;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,YAAY;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,OAA4D;IAE5D,MAAM,OAAO,GAAG,MAAM,mBAAmB,EAAE,CAAC;IAC5C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC;IACzC,mBAAmB,CAAC,mBAAmB,EAAE,EAAE,IAAI,CAAC,CAAC;IACjD,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/workspace/config.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- export { mergeWriteWorkspaceUiState } from './ui-state.js';
1
+ export { mergeWriteWorkspaceUiState } from './ui-state.ts';
2
2
  /**
3
3
  * Workspace config migrations (spec 2026-07-20-multi-project-workspace,
4
4
  * "Migrations"). Deliberately tiny and **config-files-only** — run state
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { workspaceUiStateSchema } from '../contract/index.js';
3
4
  import { cezarHomeDir, workspaceConfigPath } from '../paths.js';
4
5
  import { readUiState } from '../ui-state.js';
5
6
  import { loadWorkspaceConfig, mergeWriteWorkspaceConfig } from './config.js';
@@ -82,13 +83,27 @@ const migration001 = {
82
83
  if (!bootRepoRoot)
83
84
  return;
84
85
  const repoUiState = await readUiState(bootRepoRoot);
85
- const uiKeys = ['appearance', 'notifications'].filter((key) => repoUiState[key] !== undefined);
86
- if (uiKeys.length === 0)
86
+ // The two prefs that went GLOBAL at step 3.5, read out of the per-repo bag and validated with
87
+ // the DESTINATION's own field schemas. `notifications` is not named by the per-repo contract
88
+ // at all (it left at 3.5), so it arrives through the open catchall as `unknown` — parsing it
89
+ // here is what makes it assignable, and it is also the honest thing: `GET /workspace/ui-state`
90
+ // promises these two keys' shapes, so a hand-edited value must not be promoted into the global
91
+ // file unchecked. A value that does not parse is simply not imported — never a failed boot.
92
+ const shape = workspaceUiStateSchema.shape;
93
+ const importable = {};
94
+ const appearance = shape.appearance.safeParse(repoUiState.appearance);
95
+ if (appearance.success && appearance.data !== undefined)
96
+ importable.appearance = appearance.data;
97
+ const notifications = shape.notifications.safeParse(repoUiState.notifications);
98
+ if (notifications.success && notifications.data !== undefined) {
99
+ importable.notifications = notifications.data;
100
+ }
101
+ if (Object.keys(importable).length === 0)
87
102
  return; // nothing to import — don't create the file
88
103
  await mergeWriteWorkspaceUiState((state) => {
89
- for (const key of uiKeys) {
104
+ for (const [key, value] of Object.entries(importable)) {
90
105
  if (state[key] === undefined)
91
- state[key] = repoUiState[key];
106
+ state[key] = value;
92
107
  }
93
108
  });
94
109
  },