@open-mercato/cezar 0.8.0 → 0.9.0-pr470.650

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 (247) hide show
  1. package/README.md +109 -14
  2. package/dist/agent-config/catalog.d.ts +64 -0
  3. package/dist/agent-config/catalog.js +205 -0
  4. package/dist/agent-config/catalog.js.map +1 -0
  5. package/dist/agent-config/files.d.ts +46 -0
  6. package/dist/agent-config/files.js +119 -0
  7. package/dist/agent-config/files.js.map +1 -0
  8. package/dist/agent-config/seed.d.ts +7 -0
  9. package/dist/agent-config/seed.js +90 -0
  10. package/dist/agent-config/seed.js.map +1 -0
  11. package/dist/agent-config/service.d.ts +44 -0
  12. package/dist/agent-config/service.js +71 -0
  13. package/dist/agent-config/service.js.map +1 -0
  14. package/dist/agent-config/validate.d.ts +21 -0
  15. package/dist/agent-config/validate.js +76 -0
  16. package/dist/agent-config/validate.js.map +1 -0
  17. package/dist/config.d.ts +34 -4
  18. package/dist/config.js +97 -2
  19. package/dist/config.js.map +1 -1
  20. package/dist/core/agent-env.js +21 -15
  21. package/dist/core/agent-env.js.map +1 -1
  22. package/dist/core/agent-runner.d.ts +16 -6
  23. package/dist/core/agent-runner.js +15 -3
  24. package/dist/core/agent-runner.js.map +1 -1
  25. package/dist/core/ask.d.ts +184 -0
  26. package/dist/core/ask.js +97 -0
  27. package/dist/core/ask.js.map +1 -0
  28. package/dist/core/backend-detect.d.ts +5 -5
  29. package/dist/core/backend-detect.js +37 -5
  30. package/dist/core/backend-detect.js.map +1 -1
  31. package/dist/core/claude-cli-runner.d.ts +17 -2
  32. package/dist/core/claude-cli-runner.js +8 -3
  33. package/dist/core/claude-cli-runner.js.map +1 -1
  34. package/dist/core/claude-ui-mapper.d.ts +9 -0
  35. package/dist/core/claude-ui-mapper.js +34 -3
  36. package/dist/core/claude-ui-mapper.js.map +1 -1
  37. package/dist/core/codex-app-server-runner.d.ts +15 -3
  38. package/dist/core/codex-app-server-runner.js +158 -127
  39. package/dist/core/codex-app-server-runner.js.map +1 -1
  40. package/dist/core/codex-app-server-transport.d.ts +30 -0
  41. package/dist/core/codex-app-server-transport.js +136 -0
  42. package/dist/core/codex-app-server-transport.js.map +1 -0
  43. package/dist/core/codex-model-catalog.d.ts +10 -0
  44. package/dist/core/codex-model-catalog.js +110 -0
  45. package/dist/core/codex-model-catalog.js.map +1 -0
  46. package/dist/core/codex-ui-mapper.d.ts +47 -2
  47. package/dist/core/codex-ui-mapper.js +247 -29
  48. package/dist/core/codex-ui-mapper.js.map +1 -1
  49. package/dist/core/model-identity.d.ts +113 -0
  50. package/dist/core/model-identity.js +119 -0
  51. package/dist/core/model-identity.js.map +1 -0
  52. package/dist/core/model-presets.d.ts +15 -0
  53. package/dist/core/model-presets.js +43 -0
  54. package/dist/core/model-presets.js.map +1 -0
  55. package/dist/core/opencode-server-runner.d.ts +1 -1
  56. package/dist/core/opencode-server-runner.js +7 -12
  57. package/dist/core/opencode-server-runner.js.map +1 -1
  58. package/dist/core/opencode-ui-mapper.d.ts +11 -7
  59. package/dist/core/opencode-ui-mapper.js +62 -19
  60. package/dist/core/opencode-ui-mapper.js.map +1 -1
  61. package/dist/core/pi-runner.d.ts +40 -0
  62. package/dist/core/pi-runner.js +49 -0
  63. package/dist/core/pi-runner.js.map +1 -0
  64. package/dist/core/process-usage.d.ts +3 -0
  65. package/dist/core/process-usage.js +12 -0
  66. package/dist/core/process-usage.js.map +1 -1
  67. package/dist/core/runner-factory.js +3 -0
  68. package/dist/core/runner-factory.js.map +1 -1
  69. package/dist/core/runner-model-catalog.d.ts +28 -0
  70. package/dist/core/runner-model-catalog.js +62 -0
  71. package/dist/core/runner-model-catalog.js.map +1 -0
  72. package/dist/core/secret-redaction.js.map +1 -1
  73. package/dist/core/tool-display.d.ts +2 -2
  74. package/dist/core/tool-display.js +24 -6
  75. package/dist/core/tool-display.js.map +1 -1
  76. package/dist/core/ui-events.d.ts +29 -2
  77. package/dist/git-worktree.d.ts +37 -4
  78. package/dist/git-worktree.js +175 -12
  79. package/dist/git-worktree.js.map +1 -1
  80. package/dist/handoff.d.ts +2 -2
  81. package/dist/handoff.js +2 -0
  82. package/dist/handoff.js.map +1 -1
  83. package/dist/index.js +55 -5
  84. package/dist/index.js.map +1 -1
  85. package/dist/install-as-command.d.ts +1 -1
  86. package/dist/install-as-command.js.map +1 -1
  87. package/dist/paths.d.ts +32 -2
  88. package/dist/paths.js +47 -2
  89. package/dist/paths.js.map +1 -1
  90. package/dist/runs/auto-name.js.map +1 -1
  91. package/dist/runs/retention.js.map +1 -1
  92. package/dist/runs/review-gate.js.map +1 -1
  93. package/dist/runs/store.d.ts +154 -60
  94. package/dist/runs/store.js +103 -19
  95. package/dist/runs/store.js.map +1 -1
  96. package/dist/runs/task-markers.d.ts +6 -2
  97. package/dist/runs/task-markers.js +25 -4
  98. package/dist/runs/task-markers.js.map +1 -1
  99. package/dist/runs/ui-event-sink.js.map +1 -1
  100. package/dist/server/capabilities.d.ts +22 -6
  101. package/dist/server/capabilities.js +85 -8
  102. package/dist/server/capabilities.js.map +1 -1
  103. package/dist/server/checkout.d.ts +128 -0
  104. package/dist/server/checkout.js +296 -0
  105. package/dist/server/checkout.js.map +1 -0
  106. package/dist/server/forge/github.d.ts +281 -10
  107. package/dist/server/forge/github.js +539 -23
  108. package/dist/server/forge/github.js.map +1 -1
  109. package/dist/server/forge/types.d.ts +48 -1
  110. package/dist/server/fs-browse.d.ts +105 -0
  111. package/dist/server/fs-browse.js +182 -0
  112. package/dist/server/fs-browse.js.map +1 -0
  113. package/dist/server/git-changes.d.ts +12 -3
  114. package/dist/server/git-changes.js +23 -5
  115. package/dist/server/git-changes.js.map +1 -1
  116. package/dist/server/github.d.ts +1 -1
  117. package/dist/server/open-in-app.d.ts +2 -1
  118. package/dist/server/open-in-app.js +1 -0
  119. package/dist/server/open-in-app.js.map +1 -1
  120. package/dist/server/project-context.d.ts +103 -0
  121. package/dist/server/project-context.js +153 -0
  122. package/dist/server/project-context.js.map +1 -0
  123. package/dist/server/server.d.ts +110 -0
  124. package/dist/server/server.js +1557 -215
  125. package/dist/server/server.js.map +1 -1
  126. package/dist/server/wsl.js.map +1 -1
  127. package/dist/server-install/platforms/macosx-ngrok.js.map +1 -1
  128. package/dist/server-install/platforms/ubuntu-vps.js +7 -0
  129. package/dist/server-install/platforms/ubuntu-vps.js.map +1 -1
  130. package/dist/server-install/state.js.map +1 -1
  131. package/dist/server-install/steps.js +6 -2
  132. package/dist/server-install/steps.js.map +1 -1
  133. package/dist/server-install/types.d.ts +28 -28
  134. package/dist/server-install/ui.js.map +1 -1
  135. package/dist/skills-banner.js.map +1 -1
  136. package/dist/skills-remote.d.ts +10 -4
  137. package/dist/skills-remote.js +31 -14
  138. package/dist/skills-remote.js.map +1 -1
  139. package/dist/skills.d.ts +29 -0
  140. package/dist/skills.js +72 -14
  141. package/dist/skills.js.map +1 -1
  142. package/dist/todos.d.ts +9 -7
  143. package/dist/todos.js +38 -19
  144. package/dist/todos.js.map +1 -1
  145. package/dist/workflows/run.d.ts +145 -10
  146. package/dist/workflows/run.js +608 -66
  147. package/dist/workflows/run.js.map +1 -1
  148. package/dist/workflows/types.d.ts +42 -42
  149. package/dist/workflows/types.js +2 -1
  150. package/dist/workflows/types.js.map +1 -1
  151. package/dist/workspace/config.d.ts +251 -0
  152. package/dist/workspace/config.js +162 -0
  153. package/dist/workspace/config.js.map +1 -0
  154. package/dist/workspace/migrations.d.ts +45 -0
  155. package/dist/workspace/migrations.js +132 -0
  156. package/dist/workspace/migrations.js.map +1 -0
  157. package/dist/workspace/projects-cli.d.ts +23 -0
  158. package/dist/workspace/projects-cli.js +105 -0
  159. package/dist/workspace/projects-cli.js.map +1 -0
  160. package/dist/workspace/projects.d.ts +81 -0
  161. package/dist/workspace/projects.js +214 -0
  162. package/dist/workspace/projects.js.map +1 -0
  163. package/dist/workspace/semaphore.d.ts +77 -0
  164. package/dist/workspace/semaphore.js +57 -0
  165. package/dist/workspace/semaphore.js.map +1 -0
  166. package/dist/workspace/ui-state.d.ts +23 -0
  167. package/dist/workspace/ui-state.js +42 -0
  168. package/dist/workspace/ui-state.js.map +1 -0
  169. package/package.json +3 -2
  170. package/scripts/mock-claude.mjs +136 -2
  171. package/web/dist/assets/arrow-left-CnSdp92h.js +1 -0
  172. package/web/dist/assets/centered-state-NF79blq7.js +43 -0
  173. package/web/dist/assets/{chunk-BO2N2NFS-BN7h-q1E.js → chunk-BO2N2NFS-DtrdTCWa.js} +4 -4
  174. package/web/dist/assets/commit-list-CL8vSWGw.js +1 -0
  175. package/web/dist/assets/compare-variants-mI-MnhuR.js +1 -0
  176. package/web/dist/assets/diff-view-Bhfvnnn9.js +6 -0
  177. package/web/dist/assets/dist-COA_NSud.js +1 -0
  178. package/web/dist/assets/ellipsis-vertical-CNnSLn5a.js +1 -0
  179. package/web/dist/assets/{file-tuE95HNX.js → file-D4GBh1ao.js} +1 -1
  180. package/web/dist/assets/{git-pull-request-BztcgsRZ.js → git-pull-request-DnR8Xh4_.js} +1 -1
  181. package/web/dist/assets/git-toolbar-rVtFyL4t.js +1 -0
  182. package/web/dist/assets/github-_t0ceKcO.js +1 -0
  183. package/web/dist/assets/{highlighted-body-OFNGDK62-CJIHtxqW.js → highlighted-body-OFNGDK62-BuFJwunK.js} +1 -1
  184. package/web/dist/assets/{image-preview-Ca0UgThg.js → image-preview-DEhW0TFz.js} +1 -1
  185. package/web/dist/assets/index-BfXFhAGQ.js +30 -0
  186. package/web/dist/assets/index-Mov2Rrbw.css +2 -0
  187. package/web/dist/assets/lib-DPEZDBUN.js +1 -0
  188. package/web/dist/assets/mermaid-GHXKKRXX-CN2zQue0.js +1 -0
  189. package/web/dist/assets/project-router-CBzGUqvS.js +1 -0
  190. package/web/dist/assets/{refresh-cw-B4NW5q60.js → refresh-cw-BT8E96Tm.js} +1 -1
  191. package/web/dist/assets/repo-git-C3dJiX3j.js +1 -0
  192. package/web/dist/assets/{run-diff-BKpV1kGk.js → run-diff-HEZcHwRr.js} +3 -3
  193. package/web/dist/assets/run-header-DldbROhB.js +1 -0
  194. package/web/dist/assets/{search-x-C6xG26dv.js → search-x-BDjaMrzG.js} +1 -1
  195. package/web/dist/assets/skill-empty-hint-DTUSp6Vl.js +1 -0
  196. package/web/dist/assets/skills-BIg29xLt.js +1 -0
  197. package/web/dist/assets/{square-terminal-D9l_Xx7B.js → square-terminal-Ckjjn0vb.js} +1 -1
  198. package/web/dist/assets/tab-link-DTQ9FBZY.js +1 -0
  199. package/web/dist/assets/task-changes-DRhQVnFN.js +1 -0
  200. package/web/dist/assets/task-commits-5StODPII.js +1 -0
  201. package/web/dist/assets/task-files-BuylxobG.js +2 -0
  202. package/web/dist/assets/task-thread-DnHR0k7k.js +9 -0
  203. package/web/dist/assets/textarea-Bj_LCreg.js +1 -0
  204. package/web/dist/assets/toml-CcmNWLt0.js +1 -0
  205. package/web/dist/assets/{trash-2-De8Z5Y5-.js → trash-2-DAyZ0VSi.js} +1 -1
  206. package/web/dist/assets/{triangle-alert-DJUyRhTV.js → triangle-alert-Uy8rokUh.js} +1 -1
  207. package/web/dist/assets/{upload-yxUM9nk5.js → upload-CDdyPPo_.js} +1 -1
  208. package/web/dist/assets/use-desktop-TyHAXhMR.js +3 -0
  209. package/web/dist/assets/utils-B8s5qIcK.js +1 -0
  210. package/web/dist/assets/workflows-CwDYZLvG.js +11 -0
  211. package/web/dist/assets/zoomable-image-PEDOqyLQ.js +1 -0
  212. package/web/dist/index.html +10 -19
  213. package/web/dist/assets/arrow-left-bEsJ-6jp.js +0 -1
  214. package/web/dist/assets/centered-state-DzJJpXgm.js +0 -43
  215. package/web/dist/assets/command-eBgVGftz.js +0 -1
  216. package/web/dist/assets/compare-variants-DhqGJuHF.js +0 -1
  217. package/web/dist/assets/dialog-Bf1Cpc3n.js +0 -1
  218. package/web/dist/assets/diff-view-Cqc8LRkj.js +0 -4
  219. package/web/dist/assets/dist-M2QYGAcm.js +0 -1
  220. package/web/dist/assets/dist-ZAIqO3UX.js +0 -1
  221. package/web/dist/assets/folder-20ea3OVM.js +0 -1
  222. package/web/dist/assets/git-toolbar-DAlBoSP5.js +0 -1
  223. package/web/dist/assets/github--sfM4X3j.js +0 -1
  224. package/web/dist/assets/highlighter-ZVyRi_Cx.js +0 -3
  225. package/web/dist/assets/index-DlKq8gF4.css +0 -2
  226. package/web/dist/assets/index-h4gyDJyh.js +0 -11
  227. package/web/dist/assets/loader-circle-Bnw4ga19.js +0 -1
  228. package/web/dist/assets/markdown-DxmCMr30.js +0 -1
  229. package/web/dist/assets/mermaid-GHXKKRXX-DghlHZS5.js +0 -1
  230. package/web/dist/assets/prompt-templates-CsGr-FXH.js +0 -15
  231. package/web/dist/assets/repo-git-CHF2oip_.js +0 -1
  232. package/web/dist/assets/run-header-3CGn6FAM.js +0 -1
  233. package/web/dist/assets/skill-detail-DsD4-dGp.js +0 -1
  234. package/web/dist/assets/skill-empty-hint-Cl3ESoPu.js +0 -1
  235. package/web/dist/assets/skills-5kRcjM1J.js +0 -1
  236. package/web/dist/assets/skills-B13iqWAN.js +0 -1
  237. package/web/dist/assets/tab-link-DkVxOa-q.js +0 -1
  238. package/web/dist/assets/task-changes-DtMiVVwO.js +0 -1
  239. package/web/dist/assets/task-commits-VbSLn_tW.js +0 -1
  240. package/web/dist/assets/task-files-qYDBG3Mx.js +0 -2
  241. package/web/dist/assets/task-thread-BfGERKlB.js +0 -7
  242. package/web/dist/assets/tasks-table-C5fwJmZ7.js +0 -1
  243. package/web/dist/assets/use-desktop-6PwFeCW3.js +0 -3
  244. package/web/dist/assets/utils-CzkLr94M.js +0 -1
  245. package/web/dist/assets/workflows-CnTcKlfV.js +0 -11
  246. package/web/dist/assets/x-DaR4ZfE-.js +0 -1
  247. package/web/dist/assets/zoomable-image-B-76XMUw.js +0 -1
package/README.md CHANGED
@@ -5,8 +5,8 @@
5
5
  **Parallel coding agents orchestrator** — a local cockpit for running and
6
6
  tracking AI coding-agent tasks in your repo.
7
7
 
8
- Type a task, pick a workflow and an agent — **Claude Code, Codex or OpenCode,
9
- or a mix of them per step** — and watch it work live: steps, tool calls,
8
+ Type a task, pick a workflow and an agent — **Claude Code, Codex or OpenCode
9
+ (experimental), or a mix of them per step** — and watch it work live: steps, tool calls,
10
10
  tokens, diffs, in a browser cockpit that runs entirely on your machine.
11
11
  Your CLI logins, your `gh`, your files. No accounts, no database, no cloud.
12
12
 
@@ -21,7 +21,7 @@ your phone, working your backlog while you're away.
21
21
 
22
22
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](#license)
23
23
  ![Node 20+](https://img.shields.io/badge/Node-20%2B-339933)
24
- ![TypeScript 5.x](https://img.shields.io/badge/TypeScript-5.x-3178c6)
24
+ ![TypeScript 7.x](https://img.shields.io/badge/TypeScript-7.x-3178c6)
25
25
  ![Zero config](https://img.shields.io/badge/config-zero-success)
26
26
  ![No database](https://img.shields.io/badge/database-none-success)
27
27
 
@@ -106,7 +106,7 @@ and an orchestrator keeps a whole queue of them moving.
106
106
  Take it over interactively in one click (`claude --resume <id>`), or continue it
107
107
  in-process from the cockpit.
108
108
  - 🔀 **Locked into one agent vendor.** Most tools wed you to a single CLI. cezar
109
- drives **Claude Code, Codex and OpenCode** through one runner seam — set a
109
+ drives **Claude Code, Codex and OpenCode (experimental)** through one runner seam — set a
110
110
  default, pick a backend per task, or mix them inside one workflow (implement
111
111
  with one agent, review with another) — and through **OpenCode** you can point
112
112
  a run at **open-source or local models**, not just the big vendors. See
@@ -251,7 +251,8 @@ Three words, no jargon — **task**, **skill**, **chain**:
251
251
  Five moves that make the cockpit worth the browser tab:
252
252
 
253
253
  - 🗃️ **Queue + orchestration.** Start as many tasks as you like: cezar runs up to
254
- `maxParallel` at once (default **2**; a non-git directory always runs one) and
254
+ `maxParallel` at once across every project (default **2**; a non-git directory
255
+ always runs one) and
255
256
  holds the rest in a FIFO queue with visible positions (`#1`, `#2`, …). Cancel a
256
257
  queued task before it starts; the queue even survives a cockpit restart —
257
258
  everything still `queued` is re-enqueued in order. It's the orchestration layer
@@ -303,6 +304,71 @@ a GitHub page.
303
304
 
304
305
  ---
305
306
 
307
+ ## Multiple projects, one cockpit
308
+
309
+ One `cezar serve` hosts **every repo you work in**, not just the one you started
310
+ it in. Each repo cezar boots in registers itself in a per-user registry at
311
+ `~/.cezar/config.json` — the workspace file that also holds the global knobs
312
+ (the parallel cap, the memory ceiling, the browse root, and the checkout root). Nothing is added to
313
+ the repo: per-project state stays exactly where it was, in that repo's
314
+ `.ai/cezar/`.
315
+
316
+ Every view is project-scoped:
317
+
318
+ ```
319
+ /p/<projectId>/ tasks · git · github · skills · workflows · settings
320
+ ```
321
+
322
+ `<projectId>` is a slug derived from the folder name (`my-app`, then `my-app-2`
323
+ on a collision), and `/p/default/…` always means the project cezar was started
324
+ in. The sidebar shows one collapsible group per project — each with its own nav
325
+ and task list — and the new-task composer names the project it will run in.
326
+
327
+ **Adding a project** — the **+** button beside *New task*:
328
+
329
+ - 📂 **Open local folder…** browses from the configured browse root
330
+ (**Settings → Projects**, default `~/`) in a folder picker and
331
+ registers the folder you pick.
332
+ - ⬇️ **Clone from GitHub…** clones with your logged-in `gh` into the checkout
333
+ root (**Settings → Projects**, default `~/cezar/projects`) with live progress,
334
+ then registers the clone. Close the dialog and the clone is killed and its
335
+ partial directory removed.
336
+
337
+ Removing a project (**Settings → Projects**) drops the registry entry only — the
338
+ repo and its `.ai/cezar/` are never touched, so re-adding it later finds all its
339
+ tasks intact. The project cezar is currently serving can't be removed: it
340
+ re-registers itself at the next start.
341
+
342
+ **From the terminal** — the same registry, no cockpit required (handy over ssh):
343
+
344
+ ```bash
345
+ cezar projects # list: id, branch or status, path
346
+ cezar projects add ~/code/api # register a folder (defaults to the current repo)
347
+ cezar projects remove api # drop the registry entry; the repo is untouched
348
+ ```
349
+
350
+ These read and write `~/.cezar/config.json` directly, so they work with the
351
+ server stopped, and `CEZ_HOME` selects which workspace they operate on.
352
+
353
+ Settings split along the same line: **Agents**, **Worktrees**, **Bookmarklets**,
354
+ **Prompt templates** and **MCP** describe one repo and live under
355
+ `/p/<projectId>/settings`; **Appearance**, **Notifications**, **Resources**,
356
+ **Projects** and **Keyboard** are yours or the machine's and live at
357
+ `/settings/global`.
358
+
359
+ **Old URLs keep working.** Every unprefixed path — `/`, `/tasks/<id>`,
360
+ `/settings`, and the whole `/api/…` surface — still answers exactly as before,
361
+ bound to the project cezar was started in; the cockpit redirects flat paths to
362
+ their `/p/<boot>/…` twin. Existing bookmarks, bookmarklets and scripts need no
363
+ change.
364
+
365
+ > **Hosted cockpit?** The folder picker is confined to the independent browse
366
+ > root. Set `CEZ_BROWSE_ROOT` narrowly before first boot (or save it in
367
+ > **Settings → Projects**) when a remote viewer should not enumerate the host's
368
+ > whole home. Clones continue to use the separate checkout root.
369
+
370
+ ---
371
+
306
372
  ## Workflow format
307
373
 
308
374
  A workflow is a small YAML file in `.ai/cezar/workflows/`:
@@ -316,7 +382,7 @@ steps:
316
382
  prompt: "{{task}}"
317
383
  skill: project-conventions # optional — from .ai/skills or .ai/cezar/skills
318
384
  # model: opus # optional per-step model override
319
- # runner: codex # optional per-step backend: claude · codex · opencode
385
+ # runner: codex # optional per-step backend: claude · codex · opencode · pi
320
386
  # allowedTools: [Read, Edit, Write, Grep, Glob, Bash]
321
387
  - id: verify
322
388
  name: Verify
@@ -362,10 +428,13 @@ Useful environment variables:
362
428
  | `CEZ_DRY_RUN=1` | Use the bundled mock instead of the real `claude` CLI — the entire cockpit works offline, for demos and development. |
363
429
  | `CEZ_APPROVAL_GATE=1` | Opt into Claude's interactive approval UI; by default, unapproved tools are denied without interrupting the run. |
364
430
  | `CEZ_FOLLOWUPS=1` | Turn on the global follow-up **Inbox**: agents are asked to leave follow-ups in `todos.json` when they finish, and the Inbox view appears. Off by default — each task's own **Notes** handoff journal runs either way. |
365
- | `CEZ_AUTOSAVE=1` | Re-enable the periodic (90 s) `cezar autosave` commit in task worktrees. Off by default (#471) — turn-end and pre-PR flushes always run, so branches still end complete. |
431
+ | `CEZ_AUTOSAVE=1` | Re-enable the periodic (90 s) autosave commit in task worktrees. Off by default (#471) — turn-end and pre-PR flushes always run, so branches still end complete. Every autosave names its trigger in the commit subject (`cezar autosave (periodic)` vs `(turn end)` / `(run finalize)` / `(pre-PR)`), so the flushes you keep are distinguishable from the timer you disabled. |
366
432
  | `CEZ_CLAUDE_BIN=/path/to/claude` | Override which `claude` binary is used. |
367
433
  | `CEZ_CODEX_BIN=/path/to/codex` | Override which `codex` binary is used. |
368
434
  | `CEZ_OPENCODE_BIN=/path/to/opencode` | Override which `opencode` binary is used. |
435
+ | `CEZ_PI_BIN=/path/to/pi` | Override which `pi` binary is used. |
436
+ | `CEZ_BROWSE_ROOT=~/` | Default root for **Add project → Open local folder…**. The picker cannot navigate above it; a saved workspace value overrides the environment default and must name an existing folder. |
437
+ | `CEZ_PROJECTS_DIR=~/cezar/projects` | Default destination for **Clone from GitHub**. Saved workspace settings override it, and missing directories are created recursively. |
369
438
  | `GITHUB_TOKEN` | Fallback for GitHub reads/PRs when `gh` isn't authenticated. |
370
439
  | `CEZ_ENV_PASSTHROUGH=A,B` | Forward these extra host env vars to spawned agents. By default agents get a least-privilege env (safe shell/toolchain vars + the backend's own auth + `GITHUB_TOKEN` + `CEZ_*`), not your full environment — use this to add a var an agent needs. |
371
440
  | `CEZ_AGENT_ENV_FULL=1` | Escape hatch: give spawned agents the full host environment (pre-hardening behavior). Off by default; only set it if you understand that this hands every host secret to the agent process. |
@@ -373,23 +442,29 @@ Useful environment variables:
373
442
  | `CEZ_TITLE_UPDATES=0` | Turn off the live task-title refresh (namer re-runs on each turn end). The Settings → Agents toggle overrides this default. |
374
443
  | `CEZ_AUTONAME=0` | Disable ALL LLM task naming (creation + live) — titles stay heuristic (`437: /om-auto-review-pr`). Under `CEZ_DRY_RUN=1` naming is already off unless forced with `CEZ_AUTONAME=1`. |
375
444
  | `CEZ_REVIEW_GATE=1` | Turn ON the optional diff-first review gate (#489): a successful, non-autonomous run with changes parks at `review` (Accept / Send back / Draft PR) instead of finishing. Off by default — changed runs settle to `done` with the diff left in the worktree. Only `1` enables. The Settings → Agents toggle overrides this; autonomous runs always skip it. |
376
- | `CEZ_NO_BANNER=1` | Skip the `open-mercato/skills` banner on `cezar serve` startup. Dismissing the same banner in the cockpit silences the terminal one too. |
445
+ | `CEZ_NO_BANNER=1` | Skip the `open-mercato/skills` banner on `cezar serve` startup. (The cockpit no longer shows a banner its skills now live on the Skills page's Manage panel — so this env var is the terminal banner's only switch.) |
377
446
 
378
447
  ---
379
448
 
380
449
  ## Coding agent backends
381
450
 
382
451
  cezar is not married to one vendor. Every agent step runs through a single
383
- `AgentRunner` seam with three built-in backends:
452
+ `AgentRunner` seam with four built-in backends:
384
453
 
385
454
  | Backend | CLI | How cezar drives it | Tool access |
386
455
  |---|---|---|---|
387
456
  | **Claude Code** (default) | [`claude`](https://github.com/anthropics/claude-code) | Headless `stream-json` mode. | Per-tool `--allowedTools` (`bashAllowlist` scopes `Bash`); `dontAsk` denies unapproved tools without prompting (`CEZ_APPROVAL_GATE=1` → `acceptEdits` + approval UI). |
388
- | **Codex** | [`codex`](https://github.com/openai/codex) | `codex app-server` — JSON-RPC over stdio, the same transport the Codex IDE extensions use. | Ignores `allowedTools`; runs its own `workspace-write` sandbox with `approvalPolicy: never` and network access on. |
389
- | **OpenCode** | [`opencode`](https://opencode.ai) | `opencode serve` — a local HTTP server with an SSE event stream. | Ignores `allowedTools` entirely; every permission is auto-approved. |
457
+ | **Codex** | [`codex`](https://github.com/openai/codex) | `codex app-server` — JSON-RPC over stdio, the same transport the Codex IDE extensions use. | Ignores `allowedTools`; the default auto mode uses `danger-full-access` with `approvalPolicy: never` (`CEZ_CODEX_NETWORK=0` opts into the network-blocked `workspace-write` sandbox). |
458
+ | **OpenCode** _(experimental)_ | [`opencode`](https://opencode.ai) | `opencode serve` — a local HTTP server with an SSE event stream. | Ignores `allowedTools` entirely; every permission is auto-approved. |
459
+ | **pi** _(experimental)_ | `pi` | Headless Claude-compatible `stream-json`; models are picked with the `provider/model` convention. | Same as Claude Code — the runner delegates to the Claude CLI driver, so per-tool `--allowedTools` applies and unapproved tools are denied. |
460
+
461
+ > ⚠️ **OpenCode and pi support are experimental.** Both runners work but are less
462
+ > battle-tested than the Claude Code and Codex backends, and OpenCode auto-approves
463
+ > every permission (it ignores `allowedTools`). Treat them as previews and expect
464
+ > rough edges.
390
465
 
391
466
  On startup cezar probes which CLIs are installed and the cockpit only offers
392
- the backends it found — install any one of the three and you're operational.
467
+ the backends it found — install any one of the four and you're operational.
393
468
 
394
469
  **Pick a backend at three levels** (most specific wins):
395
470
 
@@ -473,9 +548,8 @@ never blocks startup):
473
548
  // path as `./name`, not a bare `name`. Pin `ref` to a full commit SHA to freeze
474
549
  // the source against a moving branch head — cezar verifies it resolves to
475
550
  // exactly that commit, and reports it as `team.commit`.
476
- "maxParallel": 2, // how many tasks may run at once (non-git dirs always run 1)
477
551
  "worktreeRetention": 10, // keep the last N finished worktrees on disk; 0 = unlimited (branch always kept)
478
- "defaultRunner": "claude", // agent backend: "claude" (default) · "codex" · "opencode"
552
+ "defaultRunner": "claude", // agent backend: "claude" (default) · "codex" · "opencode" · "pi"
479
553
  "plannerModel": "sonnet", // model the "Plan first" button uses to draft chains
480
554
  "baseBranch": "develop" // branch worktrees fork from + PRs target (also settable in the Git tab)
481
555
  }
@@ -484,6 +558,27 @@ never blocks startup):
484
558
  Run data (`runs.json`, NDJSON event logs, worktrees, `todos.json`) is
485
559
  git-ignored automatically; your workflows and skills stay committable.
486
560
 
561
+ Settings that belong to *you* rather than to a repo — the parallel cap
562
+ (`maxParallel`, default **2**), the per-task memory ceiling and the checkout
563
+ root — live once in `~/.cezar/config.json`, alongside the
564
+ [project registry](#multiple-projects-one-cockpit), and are edited from
565
+ **Settings → Resources** and **Settings → Projects**. A `maxParallel` left over
566
+ in a repo's `.ai/cezar/config.json` is imported into the workspace file the
567
+ first time cezar boots there, and ignored afterwards.
568
+
569
+ ### Editing the agents' own config (Settings → Agent config)
570
+
571
+ cezar picks *which* agent runs; **Settings → Agent config** lets you edit *how* it
572
+ behaves — the raw config files Claude, Codex and OpenCode read for settings,
573
+ MCP, and memory. In the multi-project cockpit the section is project-scoped:
574
+ repo-relative files resolve from the selected project's root, while user-scope
575
+ files continue to resolve from the agent's home.
576
+
577
+ Each file keeps its native format and vendor-documented precedence. Tracked
578
+ files reach task worktrees after commit; Claude's gitignored personal layer is
579
+ seeded into each run's worktree. Editing is a local-machine capability, so a
580
+ hosted cockpit (`CEZ_REMOTE=1`) is read-only and never serves home-file contents.
581
+
487
582
  ---
488
583
 
489
584
  ## Local development
@@ -0,0 +1,64 @@
1
+ import type { RunnerId } from '../core/agent-runner.js';
2
+ /**
3
+ * The catalog of coding-agent config files cezar can surface and edit (spec
4
+ * `.ai/specs/2026-07-16-agent-config-files.md`). This file is the ONLY place
5
+ * vendor knowledge lives: where each agent keeps its files, at which scope, in
6
+ * what format, and — the load-bearing part — the vendor's OWN documented
7
+ * precedence, quoted so a UI label never claims a merge cezar does not perform.
8
+ *
9
+ * Hardcoding is the design. cezar's value here is knowing where the files are
10
+ * and what the docs say; an unknown file is not shown rather than guessed at.
11
+ * A raw editor cannot drift on a vendor's *schema*; it can drift on *paths and
12
+ * precedence strings*, so every entry carries a `docsUrl` and this table is the
13
+ * single maintenance surface. Facts verified against primary docs 2026-07-16.
14
+ */
15
+ export type ConfigFormat = 'json' | 'jsonc' | 'toml' | 'markdown';
16
+ export type ConfigScope = 'user' | 'project' | 'local';
17
+ /** `settings` = behavior knobs; `memory` = instruction/markdown; `mcp` = a dedicated MCP file. */
18
+ export type ConfigKind = 'settings' | 'memory' | 'mcp';
19
+ /**
20
+ * Git status *by convention* — it drives the honest label, it is not read from
21
+ * git. The seed path re-checks with `git check-ignore` before trusting it.
22
+ */
23
+ export type ConfigTracked = 'tracked' | 'gitignored' | 'outside-repo';
24
+ /** Resolved home directories per agent, injected so the catalog stays pure and testable. */
25
+ export interface AgentHomePaths {
26
+ /** `~/.claude` */
27
+ claude: string;
28
+ /** `$CODEX_HOME` or `~/.codex` */
29
+ codex: string;
30
+ /** `$XDG_CONFIG_HOME/opencode` or `~/.config/opencode` */
31
+ opencodeConfig: string;
32
+ }
33
+ export interface ConfigFileDef {
34
+ /** Stable, opaque, URL-safe. The ONLY thing a client may name (traversal-proof). */
35
+ id: string;
36
+ /** Every runner that reads this file. `<repo>/AGENTS.md` is one file, two readers. */
37
+ runners: RunnerId[];
38
+ kind: ConfigKind;
39
+ scope: ConfigScope;
40
+ /** Absolute path, resolved per request so `$CODEX_HOME`/`$XDG_CONFIG_HOME` are honoured. */
41
+ resolve: (repoRoot: string, home: AgentHomePaths) => string;
42
+ /** What the user sees, e.g. `~/.claude/settings.json`, `.claude/settings.local.json`. */
43
+ label: string;
44
+ format: ConfigFormat;
45
+ tracked: ConfigTracked;
46
+ /** True only for Claude's gitignored personal layer — the files seeded into a run's worktree. */
47
+ seeded?: boolean;
48
+ /** True when this file holds MCP server definitions (drives the MCP section's filter). */
49
+ holdsMcp?: boolean;
50
+ /** VERBATIM from the vendor docs. Never computed, never generic. */
51
+ precedence: string;
52
+ /** Documented mid-run reload behaviour, or undefined when the vendor is silent. */
53
+ hotReload?: string;
54
+ docsUrl: string;
55
+ }
56
+ /**
57
+ * The table. Order is presentation order: per runner, then user → project →
58
+ * local so each scope ladder reads top (broad) to bottom (specific).
59
+ */
60
+ export declare const CONFIG_FILES: ConfigFileDef[];
61
+ /** The whole catalog. */
62
+ export declare function listConfigFiles(): ConfigFileDef[];
63
+ /** Look up one entry by its stable id, or undefined when the id is unknown. */
64
+ export declare function findConfigFile(id: string): ConfigFileDef | undefined;
@@ -0,0 +1,205 @@
1
+ import { join } from 'node:path';
2
+ const CLAUDE_SETTINGS_DOCS = 'https://code.claude.com/docs/en/settings';
3
+ const CLAUDE_MEMORY_DOCS = 'https://code.claude.com/docs/en/memory';
4
+ const CLAUDE_MCP_DOCS = 'https://code.claude.com/docs/en/mcp';
5
+ const CODEX_CONFIG_DOCS = 'https://developers.openai.com/codex/config-reference';
6
+ const CODEX_AGENTS_DOCS = 'https://developers.openai.com/codex/guides/agents-md';
7
+ const OPENCODE_CONFIG_DOCS = 'https://opencode.ai/docs/config/';
8
+ const OPENCODE_RULES_DOCS = 'https://opencode.ai/docs/rules/';
9
+ /**
10
+ * The table. Order is presentation order: per runner, then user → project →
11
+ * local so each scope ladder reads top (broad) to bottom (specific).
12
+ */
13
+ export const CONFIG_FILES = [
14
+ // ---- Claude Code ----
15
+ {
16
+ id: 'claude.user.settings',
17
+ runners: ['claude'],
18
+ kind: 'settings',
19
+ scope: 'user',
20
+ resolve: (_repo, home) => join(home.claude, 'settings.json'),
21
+ label: '~/.claude/settings.json',
22
+ format: 'json',
23
+ tracked: 'outside-repo',
24
+ precedence: 'Lowest priority. Project and local settings override it key by key — except permission rules, which merge across all scopes.',
25
+ hotReload: 'Edits to most keys — including permissions and hooks — apply to a running session without a restart.',
26
+ docsUrl: CLAUDE_SETTINGS_DOCS,
27
+ },
28
+ {
29
+ id: 'claude.project.settings',
30
+ runners: ['claude'],
31
+ kind: 'settings',
32
+ scope: 'project',
33
+ resolve: (repo) => join(repo, '.claude', 'settings.json'),
34
+ label: '.claude/settings.json',
35
+ format: 'json',
36
+ tracked: 'tracked',
37
+ precedence: 'Overrides user settings key by key (permission rules merge). Local settings override this.',
38
+ hotReload: 'Edits to most keys — including permissions and hooks — apply to a running session without a restart.',
39
+ docsUrl: CLAUDE_SETTINGS_DOCS,
40
+ },
41
+ {
42
+ id: 'claude.local.settings',
43
+ runners: ['claude'],
44
+ kind: 'settings',
45
+ scope: 'local',
46
+ resolve: (repo) => join(repo, '.claude', 'settings.local.json'),
47
+ label: '.claude/settings.local.json',
48
+ format: 'json',
49
+ tracked: 'gitignored',
50
+ seeded: true,
51
+ precedence: 'Highest of the file scopes — overrides project and user (permission rules merge). Git-ignored; copied into each run’s worktree so it takes effect immediately.',
52
+ hotReload: 'Edits to most keys — including permissions and hooks — apply to a running session without a restart.',
53
+ docsUrl: CLAUDE_SETTINGS_DOCS,
54
+ },
55
+ {
56
+ id: 'claude.project.mcp',
57
+ runners: ['claude'],
58
+ kind: 'mcp',
59
+ scope: 'project',
60
+ resolve: (repo) => join(repo, '.mcp.json'),
61
+ label: '.mcp.json',
62
+ format: 'json',
63
+ tracked: 'tracked',
64
+ holdsMcp: true,
65
+ precedence: 'Project-scoped MCP servers (key: mcpServers), shared via version control. Each requires approval before use; user- and local-scoped servers live in ~/.claude.json, which cezar does not edit.',
66
+ docsUrl: CLAUDE_MCP_DOCS,
67
+ },
68
+ {
69
+ id: 'claude.user.memory',
70
+ runners: ['claude'],
71
+ kind: 'memory',
72
+ scope: 'user',
73
+ resolve: (_repo, home) => join(home.claude, 'CLAUDE.md'),
74
+ label: '~/.claude/CLAUDE.md',
75
+ format: 'markdown',
76
+ tracked: 'outside-repo',
77
+ precedence: 'Not overridden — every CLAUDE.md that loads is concatenated, this user file first.',
78
+ docsUrl: CLAUDE_MEMORY_DOCS,
79
+ },
80
+ {
81
+ id: 'claude.project.memory',
82
+ runners: ['claude'],
83
+ kind: 'memory',
84
+ scope: 'project',
85
+ resolve: (repo) => join(repo, 'CLAUDE.md'),
86
+ label: 'CLAUDE.md',
87
+ format: 'markdown',
88
+ tracked: 'tracked',
89
+ precedence: 'Concatenated after the user file, not replacing it. Claude does not read AGENTS.md — import it here with @AGENTS.md. Runs read the committed copy.',
90
+ docsUrl: CLAUDE_MEMORY_DOCS,
91
+ },
92
+ {
93
+ id: 'claude.local.memory',
94
+ runners: ['claude'],
95
+ kind: 'memory',
96
+ scope: 'local',
97
+ resolve: (repo) => join(repo, 'CLAUDE.local.md'),
98
+ label: 'CLAUDE.local.md',
99
+ format: 'markdown',
100
+ tracked: 'gitignored',
101
+ seeded: true,
102
+ precedence: 'Loads alongside CLAUDE.md, concatenated last. Git-ignored; copied into each run’s worktree so it takes effect immediately.',
103
+ docsUrl: CLAUDE_MEMORY_DOCS,
104
+ },
105
+ // ---- Codex ----
106
+ {
107
+ id: 'codex.user.config',
108
+ runners: ['codex'],
109
+ kind: 'settings',
110
+ scope: 'user',
111
+ resolve: (_repo, home) => join(home.codex, 'config.toml'),
112
+ label: '~/.codex/config.toml',
113
+ format: 'toml',
114
+ tracked: 'outside-repo',
115
+ holdsMcp: true,
116
+ precedence: 'User-level defaults. A trusted project’s .codex/config.toml overrides these; some keys (provider, auth, telemetry) cannot be overridden at project scope. MCP servers live here under [mcp_servers.<id>].',
117
+ docsUrl: CODEX_CONFIG_DOCS,
118
+ },
119
+ {
120
+ id: 'codex.project.config',
121
+ runners: ['codex'],
122
+ kind: 'settings',
123
+ scope: 'project',
124
+ resolve: (repo) => join(repo, '.codex', 'config.toml'),
125
+ label: '.codex/config.toml',
126
+ format: 'toml',
127
+ tracked: 'tracked',
128
+ holdsMcp: true,
129
+ precedence: 'Applies only in projects you have trusted. Some keys (provider, auth, telemetry) cannot be overridden here. MCP servers go under [mcp_servers.<id>]. Runs read the committed copy.',
130
+ docsUrl: CODEX_CONFIG_DOCS,
131
+ },
132
+ {
133
+ id: 'codex.user.memory',
134
+ runners: ['codex'],
135
+ kind: 'memory',
136
+ scope: 'user',
137
+ resolve: (_repo, home) => join(home.codex, 'AGENTS.md'),
138
+ label: '~/.codex/AGENTS.md',
139
+ format: 'markdown',
140
+ tracked: 'outside-repo',
141
+ precedence: 'Global instructions, read first (an AGENTS.override.md beside it wins if present). Project AGENTS.md files are concatenated after and override it.',
142
+ docsUrl: CODEX_AGENTS_DOCS,
143
+ },
144
+ // ---- OpenCode ----
145
+ {
146
+ id: 'opencode.user.config',
147
+ runners: ['opencode'],
148
+ kind: 'settings',
149
+ scope: 'user',
150
+ resolve: (_repo, home) => join(home.opencodeConfig, 'opencode.json'),
151
+ label: '~/.config/opencode/opencode.json',
152
+ format: 'jsonc',
153
+ tracked: 'outside-repo',
154
+ holdsMcp: true,
155
+ precedence: 'Global config. Merged with the project config, not replaced — later configs override earlier ones only for conflicting keys. MCP servers live under the "mcp" key.',
156
+ docsUrl: OPENCODE_CONFIG_DOCS,
157
+ },
158
+ {
159
+ id: 'opencode.project.config',
160
+ runners: ['opencode'],
161
+ kind: 'settings',
162
+ scope: 'project',
163
+ resolve: (repo) => join(repo, 'opencode.json'),
164
+ label: 'opencode.json',
165
+ format: 'jsonc',
166
+ tracked: 'tracked',
167
+ holdsMcp: true,
168
+ precedence: 'Merged over the global config per conflicting key (not a wholesale replace). MCP servers live under the "mcp" key. Runs read the committed copy.',
169
+ docsUrl: OPENCODE_CONFIG_DOCS,
170
+ },
171
+ {
172
+ id: 'opencode.user.memory',
173
+ runners: ['opencode'],
174
+ kind: 'memory',
175
+ scope: 'user',
176
+ resolve: (_repo, home) => join(home.opencodeConfig, 'AGENTS.md'),
177
+ label: '~/.config/opencode/AGENTS.md',
178
+ format: 'markdown',
179
+ tracked: 'outside-repo',
180
+ precedence: 'Global rules. First match wins across scopes: if a project AGENTS.md exists, this global file is not read at all.',
181
+ docsUrl: OPENCODE_RULES_DOCS,
182
+ },
183
+ // ---- Shared: <repo>/AGENTS.md is read by BOTH Codex and OpenCode ----
184
+ {
185
+ id: 'project.agents',
186
+ runners: ['codex', 'opencode'],
187
+ kind: 'memory',
188
+ scope: 'project',
189
+ resolve: (repo) => join(repo, 'AGENTS.md'),
190
+ label: 'AGENTS.md',
191
+ format: 'markdown',
192
+ tracked: 'tracked',
193
+ precedence: 'Read by Codex and OpenCode (Claude ignores it). Codex concatenates it root-down; OpenCode uses the first match and prefers it over CLAUDE.md. Runs read the committed copy.',
194
+ docsUrl: OPENCODE_RULES_DOCS,
195
+ },
196
+ ];
197
+ /** The whole catalog. */
198
+ export function listConfigFiles() {
199
+ return CONFIG_FILES;
200
+ }
201
+ /** Look up one entry by its stable id, or undefined when the id is unknown. */
202
+ export function findConfigFile(id) {
203
+ return CONFIG_FILES.find((f) => f.id === id);
204
+ }
205
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/agent-config/catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { type ConfigFileDef } from './catalog.js';
2
+ /**
3
+ * Read and write the coding agents' own config files, addressed by catalog id
4
+ * (never by a client-supplied path, so traversal is impossible by
5
+ * construction). Every function degrades — a missing file is "absent", an
6
+ * unreadable one is an honest error — and none throw. Writes validate first,
7
+ * refuse a stale overwrite via a content hash, write atomically through
8
+ * symlinks, and never touch a byte the user did not type.
9
+ */
10
+ /** sha256 of the exact file bytes. mtime is coarse and lies across filesystems. */
11
+ declare function hashBytes(content: string): string;
12
+ export interface ReadResult {
13
+ id: string;
14
+ path: string;
15
+ exists: boolean;
16
+ content: string;
17
+ /** sha256 of the bytes, or null when the file does not exist. */
18
+ version: string | null;
19
+ }
20
+ export type WriteOutcome = {
21
+ ok: true;
22
+ read: ReadResult;
23
+ } | {
24
+ ok: false;
25
+ status: 400 | 409 | 500;
26
+ error: string;
27
+ };
28
+ declare function resolvePath(def: ConfigFileDef, repoRoot: string, env: NodeJS.ProcessEnv): string;
29
+ /** Read a config file by id. Unknown id → null; absent file → exists:false; unreadable → thrown-free error string via `error`. */
30
+ export declare function readConfigFile(id: string, repoRoot: string, env?: NodeJS.ProcessEnv): Promise<ReadResult | {
31
+ error: string;
32
+ } | null>;
33
+ /**
34
+ * Write a config file by id. Validates the content against the file's format,
35
+ * refuses when `version` does not match what is on disk (stale / lost-update),
36
+ * creates the parent dir on demand, and writes atomically through any symlink
37
+ * rather than replacing the link. `version: null` means "I expect no file to
38
+ * exist yet" — the create path.
39
+ */
40
+ export declare function writeConfigFile(id: string, content: string, version: string | null, repoRoot: string, env?: NodeJS.ProcessEnv): Promise<WriteOutcome | null>;
41
+ /** Whether a path currently exists (used by the listing to report `exists`/`size`). */
42
+ export declare function statConfigPath(path: string): Promise<{
43
+ exists: boolean;
44
+ size: number;
45
+ }>;
46
+ export { resolvePath, hashBytes };
@@ -0,0 +1,119 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, realpath, rename, stat, writeFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { agentHomePaths } from '../paths.js';
5
+ import { findConfigFile } from './catalog.js';
6
+ import { validateConfig } from './validate.js';
7
+ /**
8
+ * Read and write the coding agents' own config files, addressed by catalog id
9
+ * (never by a client-supplied path, so traversal is impossible by
10
+ * construction). Every function degrades — a missing file is "absent", an
11
+ * unreadable one is an honest error — and none throw. Writes validate first,
12
+ * refuse a stale overwrite via a content hash, write atomically through
13
+ * symlinks, and never touch a byte the user did not type.
14
+ */
15
+ /** sha256 of the exact file bytes. mtime is coarse and lies across filesystems. */
16
+ function hashBytes(content) {
17
+ return createHash('sha256').update(content, 'utf8').digest('hex');
18
+ }
19
+ function resolvePath(def, repoRoot, env) {
20
+ return def.resolve(repoRoot, agentHomePaths(env));
21
+ }
22
+ /** Read a config file by id. Unknown id → null; absent file → exists:false; unreadable → thrown-free error string via `error`. */
23
+ export async function readConfigFile(id, repoRoot, env = process.env) {
24
+ const def = findConfigFile(id);
25
+ if (!def)
26
+ return null;
27
+ const path = resolvePath(def, repoRoot, env);
28
+ try {
29
+ const content = await readFile(path, 'utf8');
30
+ return { id, path, exists: true, content, version: hashBytes(content) };
31
+ }
32
+ catch (err) {
33
+ const e = err;
34
+ if (e.code === 'ENOENT')
35
+ return { id, path, exists: false, content: '', version: null };
36
+ return { error: e.message };
37
+ }
38
+ }
39
+ /**
40
+ * Write a config file by id. Validates the content against the file's format,
41
+ * refuses when `version` does not match what is on disk (stale / lost-update),
42
+ * creates the parent dir on demand, and writes atomically through any symlink
43
+ * rather than replacing the link. `version: null` means "I expect no file to
44
+ * exist yet" — the create path.
45
+ */
46
+ export async function writeConfigFile(id, content, version, repoRoot, env = process.env) {
47
+ const def = findConfigFile(id);
48
+ if (!def)
49
+ return null;
50
+ const check = validateConfig(content, def.format);
51
+ if (!check.ok)
52
+ return { ok: false, status: 400, error: `Invalid ${def.format}: ${check.error}` };
53
+ const path = resolvePath(def, repoRoot, env);
54
+ // Stale-write guard: the version the caller read must still match disk.
55
+ let current = null;
56
+ try {
57
+ current = await readFile(path, 'utf8');
58
+ }
59
+ catch (err) {
60
+ if (err.code !== 'ENOENT') {
61
+ return { ok: false, status: 500, error: err.message };
62
+ }
63
+ }
64
+ // Footgun guard: emptying a populated file passes format validation (empty is
65
+ // valid TOML/markdown, and JSON just errors) but silently wipes real config —
66
+ // unrecoverable for the gitignored/home files that aren't in git. Refuse it;
67
+ // deleting a config file is a deliberate act, not a stray select-all-delete.
68
+ if (content.trim() === '' && current !== null && current.trim() !== '') {
69
+ return {
70
+ ok: false,
71
+ status: 400,
72
+ error: 'refusing to overwrite a non-empty config file with empty content — delete the file manually if you mean to remove it',
73
+ };
74
+ }
75
+ const currentVersion = current === null ? null : hashBytes(current);
76
+ if (currentVersion !== version) {
77
+ return {
78
+ ok: false,
79
+ status: 409,
80
+ error: currentVersion === null
81
+ ? 'the file no longer exists on disk — reload before saving'
82
+ : 'the file changed on disk since you opened it — reload before saving',
83
+ };
84
+ }
85
+ try {
86
+ // Resolve the real target so an atomic rename writes THROUGH a symlink
87
+ // (e.g. ~/.claude → a dotfiles repo) instead of replacing the link itself.
88
+ let target = path;
89
+ try {
90
+ target = await realpath(path);
91
+ }
92
+ catch {
93
+ // file/link absent — target stays as the resolved path (the create case)
94
+ }
95
+ await mkdir(dirname(target), { recursive: true });
96
+ // Unique per write (not just per process) so two concurrent saves of the same
97
+ // file can't rename the same tmp path over each other and tear the bytes.
98
+ const tmp = `${target}.cez-tmp-${process.pid}-${randomUUID()}`;
99
+ await writeFile(tmp, content, 'utf8');
100
+ await rename(tmp, target);
101
+ const written = await readFile(target, 'utf8');
102
+ return { ok: true, read: { id, path, exists: true, content: written, version: hashBytes(written) } };
103
+ }
104
+ catch (err) {
105
+ return { ok: false, status: 500, error: err.message };
106
+ }
107
+ }
108
+ /** Whether a path currently exists (used by the listing to report `exists`/`size`). */
109
+ export async function statConfigPath(path) {
110
+ try {
111
+ const s = await stat(path);
112
+ return { exists: true, size: s.size };
113
+ }
114
+ catch {
115
+ return { exists: false, size: 0 };
116
+ }
117
+ }
118
+ export { resolvePath, hashBytes };
119
+ //# sourceMappingURL=files.js.map