@qodeca/xezar 0.10.1

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 (481) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +810 -0
  3. package/dist/agent-config/account-identity.d.ts +44 -0
  4. package/dist/agent-config/account-identity.js +128 -0
  5. package/dist/agent-config/account-identity.js.map +1 -0
  6. package/dist/agent-config/catalog.d.ts +71 -0
  7. package/dist/agent-config/catalog.js +223 -0
  8. package/dist/agent-config/catalog.js.map +1 -0
  9. package/dist/agent-config/files.d.ts +46 -0
  10. package/dist/agent-config/files.js +119 -0
  11. package/dist/agent-config/files.js.map +1 -0
  12. package/dist/agent-config/model-settings/claude.d.ts +2 -0
  13. package/dist/agent-config/model-settings/claude.js +11 -0
  14. package/dist/agent-config/model-settings/claude.js.map +1 -0
  15. package/dist/agent-config/model-settings/codex.d.ts +2 -0
  16. package/dist/agent-config/model-settings/codex.js +16 -0
  17. package/dist/agent-config/model-settings/codex.js.map +1 -0
  18. package/dist/agent-config/model-settings/opencode.d.ts +2 -0
  19. package/dist/agent-config/model-settings/opencode.js +8 -0
  20. package/dist/agent-config/model-settings/opencode.js.map +1 -0
  21. package/dist/agent-config/model-settings/pi.d.ts +12 -0
  22. package/dist/agent-config/model-settings/pi.js +18 -0
  23. package/dist/agent-config/model-settings/pi.js.map +1 -0
  24. package/dist/agent-config/model-settings/shared.d.ts +9 -0
  25. package/dist/agent-config/model-settings/shared.js +93 -0
  26. package/dist/agent-config/model-settings/shared.js.map +1 -0
  27. package/dist/agent-config/model-settings/types.d.ts +9 -0
  28. package/dist/agent-config/model-settings/types.js +2 -0
  29. package/dist/agent-config/model-settings/types.js.map +1 -0
  30. package/dist/agent-config/models.d.ts +9 -0
  31. package/dist/agent-config/models.js +32 -0
  32. package/dist/agent-config/models.js.map +1 -0
  33. package/dist/agent-config/seed.d.ts +7 -0
  34. package/dist/agent-config/seed.js +90 -0
  35. package/dist/agent-config/seed.js.map +1 -0
  36. package/dist/agent-config/service.d.ts +44 -0
  37. package/dist/agent-config/service.js +71 -0
  38. package/dist/agent-config/service.js.map +1 -0
  39. package/dist/agent-config/validate.d.ts +21 -0
  40. package/dist/agent-config/validate.js +76 -0
  41. package/dist/agent-config/validate.js.map +1 -0
  42. package/dist/automations/coordinator.d.ts +26 -0
  43. package/dist/automations/coordinator.js +66 -0
  44. package/dist/automations/coordinator.js.map +1 -0
  45. package/dist/automations/github-poller.d.ts +74 -0
  46. package/dist/automations/github-poller.js +234 -0
  47. package/dist/automations/github-poller.js.map +1 -0
  48. package/dist/automations/scheduler.d.ts +42 -0
  49. package/dist/automations/scheduler.js +192 -0
  50. package/dist/automations/scheduler.js.map +1 -0
  51. package/dist/automations/store.d.ts +65 -0
  52. package/dist/automations/store.js +298 -0
  53. package/dist/automations/store.js.map +1 -0
  54. package/dist/automations/task-template.d.ts +18 -0
  55. package/dist/automations/task-template.js +95 -0
  56. package/dist/automations/task-template.js.map +1 -0
  57. package/dist/automations/types.d.ts +255 -0
  58. package/dist/automations/types.js +159 -0
  59. package/dist/automations/types.js.map +1 -0
  60. package/dist/config.d.ts +77 -0
  61. package/dist/config.js +242 -0
  62. package/dist/config.js.map +1 -0
  63. package/dist/contract/agent-config.d.ts +153 -0
  64. package/dist/contract/agent-profiles.d.ts +347 -0
  65. package/dist/contract/automations.d.ts +935 -0
  66. package/dist/contract/events.d.ts +104 -0
  67. package/dist/contract/github.d.ts +597 -0
  68. package/dist/contract/health.d.ts +102 -0
  69. package/dist/contract/index.d.ts +16 -0
  70. package/dist/contract/index.js +1953 -0
  71. package/dist/contract/projects.d.ts +210 -0
  72. package/dist/contract/repo.d.ts +234 -0
  73. package/dist/contract/runs.d.ts +1527 -0
  74. package/dist/contract/skills.d.ts +234 -0
  75. package/dist/contract/workflows.d.ts +218 -0
  76. package/dist/contract/workspace.d.ts +579 -0
  77. package/dist/core/agent-env.d.ts +31 -0
  78. package/dist/core/agent-env.js +371 -0
  79. package/dist/core/agent-env.js.map +1 -0
  80. package/dist/core/agent-model-policy.d.ts +10 -0
  81. package/dist/core/agent-model-policy.js +36 -0
  82. package/dist/core/agent-model-policy.js.map +1 -0
  83. package/dist/core/agent-profiles.d.ts +62 -0
  84. package/dist/core/agent-profiles.js +90 -0
  85. package/dist/core/agent-profiles.js.map +1 -0
  86. package/dist/core/agent-runner.d.ts +211 -0
  87. package/dist/core/agent-runner.js +68 -0
  88. package/dist/core/agent-runner.js.map +1 -0
  89. package/dist/core/ask.d.ts +115 -0
  90. package/dist/core/ask.js +256 -0
  91. package/dist/core/ask.js.map +1 -0
  92. package/dist/core/backend-detect.d.ts +16 -0
  93. package/dist/core/backend-detect.js +155 -0
  94. package/dist/core/backend-detect.js.map +1 -0
  95. package/dist/core/claude-cli-runner.d.ts +65 -0
  96. package/dist/core/claude-cli-runner.js +479 -0
  97. package/dist/core/claude-cli-runner.js.map +1 -0
  98. package/dist/core/claude-model-catalog.d.ts +24 -0
  99. package/dist/core/claude-model-catalog.js +208 -0
  100. package/dist/core/claude-model-catalog.js.map +1 -0
  101. package/dist/core/claude-ui-mapper.d.ts +79 -0
  102. package/dist/core/claude-ui-mapper.js +597 -0
  103. package/dist/core/claude-ui-mapper.js.map +1 -0
  104. package/dist/core/codex-app-server-runner.d.ts +41 -0
  105. package/dist/core/codex-app-server-runner.js +584 -0
  106. package/dist/core/codex-app-server-runner.js.map +1 -0
  107. package/dist/core/codex-app-server-transport.d.ts +35 -0
  108. package/dist/core/codex-app-server-transport.js +150 -0
  109. package/dist/core/codex-app-server-transport.js.map +1 -0
  110. package/dist/core/codex-model-catalog.d.ts +10 -0
  111. package/dist/core/codex-model-catalog.js +110 -0
  112. package/dist/core/codex-model-catalog.js.map +1 -0
  113. package/dist/core/codex-ui-mapper.d.ts +109 -0
  114. package/dist/core/codex-ui-mapper.js +764 -0
  115. package/dist/core/codex-ui-mapper.js.map +1 -0
  116. package/dist/core/model-identity.d.ts +125 -0
  117. package/dist/core/model-identity.js +132 -0
  118. package/dist/core/model-identity.js.map +1 -0
  119. package/dist/core/model-presets.d.ts +21 -0
  120. package/dist/core/model-presets.js +91 -0
  121. package/dist/core/model-presets.js.map +1 -0
  122. package/dist/core/ndjson.d.ts +6 -0
  123. package/dist/core/ndjson.js +23 -0
  124. package/dist/core/ndjson.js.map +1 -0
  125. package/dist/core/opencode-model-catalog.d.ts +32 -0
  126. package/dist/core/opencode-model-catalog.js +153 -0
  127. package/dist/core/opencode-model-catalog.js.map +1 -0
  128. package/dist/core/opencode-server-runner.d.ts +32 -0
  129. package/dist/core/opencode-server-runner.js +535 -0
  130. package/dist/core/opencode-server-runner.js.map +1 -0
  131. package/dist/core/opencode-ui-mapper.d.ts +111 -0
  132. package/dist/core/opencode-ui-mapper.js +668 -0
  133. package/dist/core/opencode-ui-mapper.js.map +1 -0
  134. package/dist/core/pi-runner.d.ts +24 -0
  135. package/dist/core/pi-runner.js +349 -0
  136. package/dist/core/pi-runner.js.map +1 -0
  137. package/dist/core/pi-ui-mapper.d.ts +34 -0
  138. package/dist/core/pi-ui-mapper.js +270 -0
  139. package/dist/core/pi-ui-mapper.js.map +1 -0
  140. package/dist/core/process-usage.d.ts +66 -0
  141. package/dist/core/process-usage.js +193 -0
  142. package/dist/core/process-usage.js.map +1 -0
  143. package/dist/core/provider-auth.d.ts +154 -0
  144. package/dist/core/provider-auth.js +509 -0
  145. package/dist/core/provider-auth.js.map +1 -0
  146. package/dist/core/provider-availability.d.ts +3 -0
  147. package/dist/core/provider-availability.js +14 -0
  148. package/dist/core/provider-availability.js.map +1 -0
  149. package/dist/core/runner-factory.d.ts +8 -0
  150. package/dist/core/runner-factory.js +25 -0
  151. package/dist/core/runner-factory.js.map +1 -0
  152. package/dist/core/runner-model-catalog.d.ts +28 -0
  153. package/dist/core/runner-model-catalog.js +62 -0
  154. package/dist/core/runner-model-catalog.js.map +1 -0
  155. package/dist/core/secret-redaction.d.ts +37 -0
  156. package/dist/core/secret-redaction.js +115 -0
  157. package/dist/core/secret-redaction.js.map +1 -0
  158. package/dist/core/shell-env.d.ts +28 -0
  159. package/dist/core/shell-env.js +56 -0
  160. package/dist/core/shell-env.js.map +1 -0
  161. package/dist/core/tool-display.d.ts +31 -0
  162. package/dist/core/tool-display.js +179 -0
  163. package/dist/core/tool-display.js.map +1 -0
  164. package/dist/core/ui-events.d.ts +332 -0
  165. package/dist/core/ui-events.js +22 -0
  166. package/dist/core/ui-events.js.map +1 -0
  167. package/dist/core/usage-limit.d.ts +42 -0
  168. package/dist/core/usage-limit.js +209 -0
  169. package/dist/core/usage-limit.js.map +1 -0
  170. package/dist/core/usage.d.ts +11 -0
  171. package/dist/core/usage.js +15 -0
  172. package/dist/core/usage.js.map +1 -0
  173. package/dist/core/v1-text-coalescer.d.ts +36 -0
  174. package/dist/core/v1-text-coalescer.js +72 -0
  175. package/dist/core/v1-text-coalescer.js.map +1 -0
  176. package/dist/git-diff-base.d.ts +85 -0
  177. package/dist/git-diff-base.js +180 -0
  178. package/dist/git-diff-base.js.map +1 -0
  179. package/dist/git-refs.d.ts +11 -0
  180. package/dist/git-refs.js +14 -0
  181. package/dist/git-refs.js.map +1 -0
  182. package/dist/git-worktree.d.ts +161 -0
  183. package/dist/git-worktree.js +508 -0
  184. package/dist/git-worktree.js.map +1 -0
  185. package/dist/handoff.d.ts +62 -0
  186. package/dist/handoff.js +154 -0
  187. package/dist/handoff.js.map +1 -0
  188. package/dist/index.d.ts +2 -0
  189. package/dist/index.js +684 -0
  190. package/dist/index.js.map +1 -0
  191. package/dist/install-as-command.d.ts +40 -0
  192. package/dist/install-as-command.js +52 -0
  193. package/dist/install-as-command.js.map +1 -0
  194. package/dist/pack-check.d.ts +17 -0
  195. package/dist/pack-check.js +27 -0
  196. package/dist/pack-check.js.map +1 -0
  197. package/dist/paths.d.ts +140 -0
  198. package/dist/paths.js +192 -0
  199. package/dist/paths.js.map +1 -0
  200. package/dist/planner.d.ts +27 -0
  201. package/dist/planner.js +268 -0
  202. package/dist/planner.js.map +1 -0
  203. package/dist/release/manifests.d.ts +78 -0
  204. package/dist/release/manifests.js +57 -0
  205. package/dist/release/manifests.js.map +1 -0
  206. package/dist/release/snapshot.d.ts +67 -0
  207. package/dist/release/snapshot.js +97 -0
  208. package/dist/release/snapshot.js.map +1 -0
  209. package/dist/release/stable.d.ts +33 -0
  210. package/dist/release/stable.js +50 -0
  211. package/dist/release/stable.js.map +1 -0
  212. package/dist/runs/agent-tmpdir.d.ts +41 -0
  213. package/dist/runs/agent-tmpdir.js +183 -0
  214. package/dist/runs/agent-tmpdir.js.map +1 -0
  215. package/dist/runs/arm-repo-handle.d.ts +17 -0
  216. package/dist/runs/arm-repo-handle.js +24 -0
  217. package/dist/runs/arm-repo-handle.js.map +1 -0
  218. package/dist/runs/auto-name.d.ts +75 -0
  219. package/dist/runs/auto-name.js +179 -0
  220. package/dist/runs/auto-name.js.map +1 -0
  221. package/dist/runs/event-history.d.ts +50 -0
  222. package/dist/runs/event-history.js +613 -0
  223. package/dist/runs/event-history.js.map +1 -0
  224. package/dist/runs/retention.d.ts +60 -0
  225. package/dist/runs/retention.js +86 -0
  226. package/dist/runs/retention.js.map +1 -0
  227. package/dist/runs/review-gate.d.ts +18 -0
  228. package/dist/runs/review-gate.js +21 -0
  229. package/dist/runs/review-gate.js.map +1 -0
  230. package/dist/runs/run-index.d.ts +19 -0
  231. package/dist/runs/run-index.js +43 -0
  232. package/dist/runs/run-index.js.map +1 -0
  233. package/dist/runs/store.d.ts +490 -0
  234. package/dist/runs/store.js +1283 -0
  235. package/dist/runs/store.js.map +1 -0
  236. package/dist/runs/task-markers.d.ts +28 -0
  237. package/dist/runs/task-markers.js +74 -0
  238. package/dist/runs/task-markers.js.map +1 -0
  239. package/dist/runs/task-refs.d.ts +25 -0
  240. package/dist/runs/task-refs.js +74 -0
  241. package/dist/runs/task-refs.js.map +1 -0
  242. package/dist/runs/ui-event-sink.d.ts +80 -0
  243. package/dist/runs/ui-event-sink.js +209 -0
  244. package/dist/runs/ui-event-sink.js.map +1 -0
  245. package/dist/server/app-type.d.ts +21 -0
  246. package/dist/server/app-type.js +2 -0
  247. package/dist/server/app-type.js.map +1 -0
  248. package/dist/server/capabilities.d.ts +70 -0
  249. package/dist/server/capabilities.js +152 -0
  250. package/dist/server/capabilities.js.map +1 -0
  251. package/dist/server/checkout.d.ts +128 -0
  252. package/dist/server/checkout.js +296 -0
  253. package/dist/server/checkout.js.map +1 -0
  254. package/dist/server/forge/github.d.ts +535 -0
  255. package/dist/server/forge/github.js +2544 -0
  256. package/dist/server/forge/github.js.map +1 -0
  257. package/dist/server/forge/index.d.ts +40 -0
  258. package/dist/server/forge/index.js +72 -0
  259. package/dist/server/forge/index.js.map +1 -0
  260. package/dist/server/forge/types.d.ts +267 -0
  261. package/dist/server/forge/types.js +2 -0
  262. package/dist/server/forge/types.js.map +1 -0
  263. package/dist/server/fs-browse.d.ts +105 -0
  264. package/dist/server/fs-browse.js +182 -0
  265. package/dist/server/fs-browse.js.map +1 -0
  266. package/dist/server/git-changes.d.ts +213 -0
  267. package/dist/server/git-changes.js +555 -0
  268. package/dist/server/git-changes.js.map +1 -0
  269. package/dist/server/git.d.ts +28 -0
  270. package/dist/server/git.js +112 -0
  271. package/dist/server/git.js.map +1 -0
  272. package/dist/server/github.d.ts +9 -0
  273. package/dist/server/github.js +8 -0
  274. package/dist/server/github.js.map +1 -0
  275. package/dist/server/launch-key.d.ts +7 -0
  276. package/dist/server/launch-key.js +33 -0
  277. package/dist/server/launch-key.js.map +1 -0
  278. package/dist/server/open-in-app.d.ts +66 -0
  279. package/dist/server/open-in-app.js +211 -0
  280. package/dist/server/open-in-app.js.map +1 -0
  281. package/dist/server/open-in-terminal.d.ts +70 -0
  282. package/dist/server/open-in-terminal.js +179 -0
  283. package/dist/server/open-in-terminal.js.map +1 -0
  284. package/dist/server/pr.d.ts +7 -0
  285. package/dist/server/pr.js +7 -0
  286. package/dist/server/pr.js.map +1 -0
  287. package/dist/server/project-context.d.ts +120 -0
  288. package/dist/server/project-context.js +192 -0
  289. package/dist/server/project-context.js.map +1 -0
  290. package/dist/server/provider-action-gate.d.ts +8 -0
  291. package/dist/server/provider-action-gate.js +56 -0
  292. package/dist/server/provider-action-gate.js.map +1 -0
  293. package/dist/server/provider-auth-runtime.d.ts +21 -0
  294. package/dist/server/provider-auth-runtime.js +66 -0
  295. package/dist/server/provider-auth-runtime.js.map +1 -0
  296. package/dist/server/server.d.ts +13578 -0
  297. package/dist/server/server.js +5284 -0
  298. package/dist/server/server.js.map +1 -0
  299. package/dist/server/static-ui.d.ts +49 -0
  300. package/dist/server/static-ui.js +92 -0
  301. package/dist/server/static-ui.js.map +1 -0
  302. package/dist/server/validators.d.ts +97 -0
  303. package/dist/server/validators.js +86 -0
  304. package/dist/server/validators.js.map +1 -0
  305. package/dist/server/ws.d.ts +83 -0
  306. package/dist/server/ws.js +216 -0
  307. package/dist/server/ws.js.map +1 -0
  308. package/dist/server/wsl.d.ts +25 -0
  309. package/dist/server/wsl.js +92 -0
  310. package/dist/server/wsl.js.map +1 -0
  311. package/dist/server-install/engine.d.ts +50 -0
  312. package/dist/server-install/engine.js +324 -0
  313. package/dist/server-install/engine.js.map +1 -0
  314. package/dist/server-install/platforms/macosx-ngrok.d.ts +6 -0
  315. package/dist/server-install/platforms/macosx-ngrok.js +348 -0
  316. package/dist/server-install/platforms/macosx-ngrok.js.map +1 -0
  317. package/dist/server-install/platforms/ubuntu-vps.d.ts +63 -0
  318. package/dist/server-install/platforms/ubuntu-vps.js +997 -0
  319. package/dist/server-install/platforms/ubuntu-vps.js.map +1 -0
  320. package/dist/server-install/state.d.ts +52 -0
  321. package/dist/server-install/state.js +182 -0
  322. package/dist/server-install/state.js.map +1 -0
  323. package/dist/server-install/steps.d.ts +109 -0
  324. package/dist/server-install/steps.js +334 -0
  325. package/dist/server-install/steps.js.map +1 -0
  326. package/dist/server-install/strategies.d.ts +4 -0
  327. package/dist/server-install/strategies.js +20 -0
  328. package/dist/server-install/strategies.js.map +1 -0
  329. package/dist/server-install/types.d.ts +287 -0
  330. package/dist/server-install/types.js +121 -0
  331. package/dist/server-install/types.js.map +1 -0
  332. package/dist/server-install/ui.d.ts +42 -0
  333. package/dist/server-install/ui.js +130 -0
  334. package/dist/server-install/ui.js.map +1 -0
  335. package/dist/skills-banner.d.ts +22 -0
  336. package/dist/skills-banner.js +41 -0
  337. package/dist/skills-banner.js.map +1 -0
  338. package/dist/skills-remote.d.ts +84 -0
  339. package/dist/skills-remote.js +468 -0
  340. package/dist/skills-remote.js.map +1 -0
  341. package/dist/skills-update.d.ts +88 -0
  342. package/dist/skills-update.js +392 -0
  343. package/dist/skills-update.js.map +1 -0
  344. package/dist/skills.d.ts +82 -0
  345. package/dist/skills.js +251 -0
  346. package/dist/skills.js.map +1 -0
  347. package/dist/todos.d.ts +48 -0
  348. package/dist/todos.js +201 -0
  349. package/dist/todos.js.map +1 -0
  350. package/dist/ui-state.d.ts +26 -0
  351. package/dist/ui-state.js +38 -0
  352. package/dist/ui-state.js.map +1 -0
  353. package/dist/update-check.d.ts +12 -0
  354. package/dist/update-check.js +41 -0
  355. package/dist/update-check.js.map +1 -0
  356. package/dist/workflows/load.d.ts +15 -0
  357. package/dist/workflows/load.js +56 -0
  358. package/dist/workflows/load.js.map +1 -0
  359. package/dist/workflows/run.d.ts +803 -0
  360. package/dist/workflows/run.js +3483 -0
  361. package/dist/workflows/run.js.map +1 -0
  362. package/dist/workflows/types.d.ts +145 -0
  363. package/dist/workflows/types.js +194 -0
  364. package/dist/workflows/types.js.map +1 -0
  365. package/dist/workspace/agent-accounts.d.ts +153 -0
  366. package/dist/workspace/agent-accounts.js +304 -0
  367. package/dist/workspace/agent-accounts.js.map +1 -0
  368. package/dist/workspace/agent-profiles.d.ts +78 -0
  369. package/dist/workspace/agent-profiles.js +115 -0
  370. package/dist/workspace/agent-profiles.js.map +1 -0
  371. package/dist/workspace/config.d.ts +169 -0
  372. package/dist/workspace/config.js +379 -0
  373. package/dist/workspace/config.js.map +1 -0
  374. package/dist/workspace/migrations.d.ts +45 -0
  375. package/dist/workspace/migrations.js +147 -0
  376. package/dist/workspace/migrations.js.map +1 -0
  377. package/dist/workspace/projects-cli.d.ts +25 -0
  378. package/dist/workspace/projects-cli.js +172 -0
  379. package/dist/workspace/projects-cli.js.map +1 -0
  380. package/dist/workspace/projects.d.ts +109 -0
  381. package/dist/workspace/projects.js +250 -0
  382. package/dist/workspace/projects.js.map +1 -0
  383. package/dist/workspace/semaphore.d.ts +179 -0
  384. package/dist/workspace/semaphore.js +195 -0
  385. package/dist/workspace/semaphore.js.map +1 -0
  386. package/dist/workspace/ui-state.d.ts +34 -0
  387. package/dist/workspace/ui-state.js +53 -0
  388. package/dist/workspace/ui-state.js.map +1 -0
  389. package/package.json +85 -0
  390. package/scripts/check-pack.mjs +46 -0
  391. package/scripts/inline-contract.mjs +112 -0
  392. package/scripts/install-as-command.mjs +120 -0
  393. package/scripts/mock-claude.mjs +524 -0
  394. package/scripts/mock-pi-rpc.mjs +83 -0
  395. package/scripts/sync-readme.mjs +20 -0
  396. package/scripts/test-process-usage.mjs +47 -0
  397. package/web/dist/assets/alert-dialog-CGskMvgc.js +1 -0
  398. package/web/dist/assets/arrow-down-G8adPhbu.js +1 -0
  399. package/web/dist/assets/arrow-left-BJsIW6A0.js +1 -0
  400. package/web/dist/assets/bundle-mjs-BT31bpU6.js +1 -0
  401. package/web/dist/assets/centered-state-B_V_Ai2h.js +43 -0
  402. package/web/dist/assets/chevron-right-CFzn-von.js +1 -0
  403. package/web/dist/assets/chunk-BO2N2NFS-DE6qKn3r.js +131 -0
  404. package/web/dist/assets/collapsible-B_8uYwHb.js +1 -0
  405. package/web/dist/assets/commit-list-C4Dv6vUL.js +1 -0
  406. package/web/dist/assets/compare-variants-SR2zZfZq.js +1 -0
  407. package/web/dist/assets/core-BCsw8oQw.js +12 -0
  408. package/web/dist/assets/css-BsVw1vtW.js +1 -0
  409. package/web/dist/assets/diff-CbozNUJq.js +3 -0
  410. package/web/dist/assets/diff-stat-btAmKFJr.js +1 -0
  411. package/web/dist/assets/diff-view-BpFTcG3R.js +6 -0
  412. package/web/dist/assets/diff-woXpYk--.js +1 -0
  413. package/web/dist/assets/dropdown-menu-Z80KMInL.js +1 -0
  414. package/web/dist/assets/editable-title-wS5NWwob.js +1 -0
  415. package/web/dist/assets/ellipsis-vertical-Bmi2u2TG.js +1 -0
  416. package/web/dist/assets/engine-javascript-FQvzRocB.js +141 -0
  417. package/web/dist/assets/file-DKoYWRsv.js +1 -0
  418. package/web/dist/assets/folder-u01HScRT.js +1 -0
  419. package/web/dist/assets/git-toolbar-DZlZjTGz.js +1 -0
  420. package/web/dist/assets/github-CFURo9kf.js +1 -0
  421. package/web/dist/assets/go-rLFTqkRN.js +1 -0
  422. package/web/dist/assets/highlighted-body-OFNGDK62-BL-MfFgl.js +1 -0
  423. package/web/dist/assets/highlighter-C8DK1_sG.js +3 -0
  424. package/web/dist/assets/html-BY76lXLH.js +1 -0
  425. package/web/dist/assets/image-preview-BR3iIIhf.js +1 -0
  426. package/web/dist/assets/index-BcI5hug3.css +2 -0
  427. package/web/dist/assets/index-DlC81_GI.js +7 -0
  428. package/web/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  429. package/web/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  430. package/web/dist/assets/javascript-BgS3c2Ky.js +1 -0
  431. package/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  432. package/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  433. package/web/dist/assets/json-qhed-kSA.js +1 -0
  434. package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
  435. package/web/dist/assets/jsx-Bz0zcwM4.js +1 -0
  436. package/web/dist/assets/lib-BQXq3kEf.js +1 -0
  437. package/web/dist/assets/lib-BxQXEXDF.js +1 -0
  438. package/web/dist/assets/markdown-5FuCZDA4.js +2 -0
  439. package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
  440. package/web/dist/assets/mermaid-GHXKKRXX-C4jxqUUb.js +1 -0
  441. package/web/dist/assets/new-task-form-BydHBbef.js +1 -0
  442. package/web/dist/assets/pill-vNzaaYxj.js +1 -0
  443. package/web/dist/assets/project-router-BbCzrHKs.js +1 -0
  444. package/web/dist/assets/prompt-templates-B7Fs2b1S.js +15 -0
  445. package/web/dist/assets/python-gzcpVVnB.js +1 -0
  446. package/web/dist/assets/react-runtime-CCIEwYL0.js +9 -0
  447. package/web/dist/assets/refresh-cw-DGliez3E.js +1 -0
  448. package/web/dist/assets/repo-git-CJFDMauC.js +1 -0
  449. package/web/dist/assets/rolldown-runtime-QTnfLwEv.js +1 -0
  450. package/web/dist/assets/run-diff-Bq9vkSNK.js +3 -0
  451. package/web/dist/assets/run-header-Cd8mSKBG.js +1 -0
  452. package/web/dist/assets/rust-Cfkwpbl8.js +1 -0
  453. package/web/dist/assets/search-x-69n_672Y.js +1 -0
  454. package/web/dist/assets/shellscript-CLZ0U2zV.js +1 -0
  455. package/web/dist/assets/skill-empty-hint-DTu0LzaJ.js +1 -0
  456. package/web/dist/assets/skills-Cs-auniL.js +1 -0
  457. package/web/dist/assets/skills-D4P-Hgey.js +1 -0
  458. package/web/dist/assets/sparkles-B8agSxBZ.js +1 -0
  459. package/web/dist/assets/sql-BsFa4tDR.js +1 -0
  460. package/web/dist/assets/square-terminal-Cf4Og6ct.js +1 -0
  461. package/web/dist/assets/tab-link-Cp2-CJS8.js +1 -0
  462. package/web/dist/assets/task-changes-DYgNXOyY.js +1 -0
  463. package/web/dist/assets/task-commits-BRpZSz1G.js +1 -0
  464. package/web/dist/assets/task-files-pKOwSyLh.js +2 -0
  465. package/web/dist/assets/task-thread-Bfyqqy-t.js +9 -0
  466. package/web/dist/assets/textarea--cpYshee.js +1 -0
  467. package/web/dist/assets/thread-loading-GyH2Atwo.js +1 -0
  468. package/web/dist/assets/toml-CcmNWLt0.js +1 -0
  469. package/web/dist/assets/trash-2--ZtrzUx3.js +1 -0
  470. package/web/dist/assets/triangle-alert-D5tJwRD2.js +1 -0
  471. package/web/dist/assets/tsx-udAQXfEw.js +1 -0
  472. package/web/dist/assets/typescript-bsJCZSQ-.js +1 -0
  473. package/web/dist/assets/upload-SnwVxKOA.js +1 -0
  474. package/web/dist/assets/use-desktop-C31lOFUD.js +1 -0
  475. package/web/dist/assets/use-submit-shortcut-JY8M_MPL.js +1 -0
  476. package/web/dist/assets/utils-Bb8ohcr3.js +64 -0
  477. package/web/dist/assets/workflows-BmOehP-A.js +11 -0
  478. package/web/dist/assets/yaml-rwi0_p6S.js +1 -0
  479. package/web/dist/assets/zoomable-image-0G3IBXWC.js +1 -0
  480. package/web/dist/index.html +80 -0
  481. package/web/dist/xezar.svg +14 -0
@@ -0,0 +1,1283 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { z } from 'zod';
6
+ import { collectSecretValues, redactDeep, redactSecrets } from '../core/secret-redaction.js';
7
+ // Pure, dependency-free reference helpers — the same sanity bound the marker parser applies.
8
+ import { MAX_REF } from './task-refs.js';
9
+ // Type-only module (zod + nothing else), so this cannot cycle back into the store.
10
+ import { workflowDefSchema } from '../workflows/types.js';
11
+ import { RUNNER_IDS } from '../core/agent-runner.js';
12
+ const usageCounterSchema = z.number().finite().nonnegative();
13
+ /**
14
+ * A runner id as it may appear in a PERSISTED record, normalized to the three
15
+ * ids the rest of xezar speaks (#547).
16
+ *
17
+ * `claude-cli` is the legacy spelling of `claude` — still a member of
18
+ * `AgentBackend` and still accepted by `createRunner`, and named by
19
+ * `BACKWARD_COMPATIBILITY.md` §3 as an id `runs.json` keeps parseable. The enum
20
+ * here did not accept it, so that promise was false: the loader `safeParse`s the
21
+ * WHOLE array, so one record carrying it would have dropped every run in the
22
+ * file — the exact failure mode §3 exists to warn about.
23
+ *
24
+ * Parse-and-fold rather than widen: the legacy id is accepted on the way in and
25
+ * collapsed to `claude`, so no consumer, wire type or contract schema ever sees
26
+ * a fourth runner. The narrowing is one-way and permanent (the index is
27
+ * re-serialized from the parsed records), which is what "old run records
28
+ * normalise identically to `claude`" in `core/model-identity.ts` has always
29
+ * claimed. Use ONLY for read-back of stored state — request bodies, settings and
30
+ * workflow step defs stay the three selectable ids (`RunnerId`), because nothing
31
+ * should be able to ASK for the legacy spelling.
32
+ */
33
+ const storedRunnerSchema = z
34
+ .enum([...RUNNER_IDS, 'claude-cli'])
35
+ .transform((id) => (id === 'claude-cli' ? 'claude' : id));
36
+ const stepStateSchema = z.object({
37
+ id: z.string(),
38
+ name: z.string(),
39
+ kind: z.enum(['agent', 'check']),
40
+ status: z.enum(['pending', 'running', 'waiting', 'review', 'done', 'failed', 'cancelled', 'skipped']),
41
+ iterations: z.number(),
42
+ tokensUsed: z.number(),
43
+ inputTokens: usageCounterSchema.optional(),
44
+ outputTokens: usageCounterSchema.optional(),
45
+ usageInvocationsStarted: usageCounterSchema.optional(),
46
+ usageInvocationsObserved: usageCounterSchema.optional(),
47
+ usageTurnsStarted: usageCounterSchema.optional(),
48
+ usageTurnsRecorded: usageCounterSchema.optional(),
49
+ usageInvocationEpoch: usageCounterSchema.optional(),
50
+ startedAt: z.string().optional(),
51
+ finishedAt: z.string().optional(),
52
+ error: z.string().optional(),
53
+ /** Latest backend-owned session id, used for same-backend Continue. */
54
+ sessionId: z.string().optional(),
55
+ /** Backend that owns `sessionId`. Optional so pre-affinity runs.json files still parse;
56
+ * `storedRunnerSchema` so a legacy `claude-cli` folds to `claude` instead of failing (#547). */
57
+ backend: storedRunnerSchema.optional(),
58
+ /** Agent profile (account) this step actually spawned under — `default`, or a stored profile
59
+ * id (spec 2026-07-29-agent-profiles). Recorded rather than re-derived because a session id
60
+ * only means something inside the config dir that created it: `sessionId` and `profileId` are
61
+ * a PAIR. Without it, changing the project's account would silently make Continue resume
62
+ * against the wrong account's session store. Absent = the discovered default. */
63
+ profileId: z.string().optional(),
64
+ /** Dollar cost reported by the claude CLI for this step's turns. */
65
+ costUsd: z.number().optional(),
66
+ });
67
+ /** One prompt message stacked onto a run while it waits for a free agent slot
68
+ * (#472). Folded into `{{task}}` at dequeue by `hydrateQueuedInput`; never
69
+ * delivered as its own turn — a follow-up turn would reach only the first step
70
+ * of a chain and would race the opening turn. */
71
+ const queuedMessageSchema = z.object({
72
+ id: z.string(),
73
+ text: z.string(),
74
+ /** `/api/v1/runs/:id/images/…` URLs — the base64 never enters `runs.json`. */
75
+ images: z.array(z.string()).optional(),
76
+ createdAt: z.string(),
77
+ });
78
+ /** Exported for `./run-index.ts`, the read-only reader of the same file. Nothing else should
79
+ * parse `runs.json` — see `reconcileLoadedRun` for why a second parser is a correctness risk. */
80
+ export const runRecordSchema = z.object({
81
+ id: z.string(),
82
+ title: z.string(),
83
+ /** Display title (#389): the auto-derived summary of the first agent turn,
84
+ * or the user's inline edit (`PATCH /api/runs/:id` sets it together with
85
+ * `title` so edits always win). The UI shows `titleSummary ?? title`. */
86
+ titleSummary: z.string().optional(),
87
+ /** `git diff --shortstat` of the worktree vs its base, refreshed on every
88
+ * turn-end (#389) — what the quick list / table shows without a git call.
89
+ * `repointed` (#751) is optional and only ever written as `true`: it marks the
90
+ * runs whose numbers were narrowed to uncommitted work because the agent had
91
+ * checked another branch out into the worktree. Optional is load-bearing here —
92
+ * `runs.json` is `safeParse`d as one array, so a required addition would
93
+ * silently drop every pre-existing run. */
94
+ diffStat: z
95
+ .object({
96
+ adds: z.number(),
97
+ dels: z.number(),
98
+ files: z.number(),
99
+ repointed: z.boolean().optional(),
100
+ })
101
+ .optional(),
102
+ workflow: z.string(),
103
+ task: z.string(),
104
+ /** Prompt messages stacked onto this run while it was queued (#472). Optional:
105
+ * `undefined` on every pre-#472 record reads as an empty stack. Like `task`,
106
+ * `text` is the user's own prompt and is replayed into `{{task}}`, so it is
107
+ * deliberately NOT in `redactPatch`'s field list — scrubbing it would corrupt
108
+ * the run the same way scrubbing `task` would. */
109
+ queuedMessages: z.array(queuedMessageSchema).optional(),
110
+ /** URLs of images attached to the initial task prompt, for the thread's first bubble
111
+ * (#image-display) — persisted like agent screenshots, served from `/images/`. */
112
+ taskImages: z.array(z.string()).optional(),
113
+ model: z.string().optional(),
114
+ /** Canonical provider/model identity (#405) — the normalised `provider/model`
115
+ * (e.g. `anthropic/claude-opus-4-8`) the run actually used, resolved from the
116
+ * free-text `model` against the chosen runner. Additive and optional: pre-#405
117
+ * records carry only `model`, and it stays the human/hand-edit surface; this
118
+ * is the parseable identity cost attribution and reproducible replay key off.
119
+ *
120
+ * Read in production by the session header's agent badge (#546), which shows it
121
+ * whenever it says something `model` does not — so this is no longer a
122
+ * write-only field whose next reader has to guess whether it is load-bearing. */
123
+ modelIdentity: z.string().optional(),
124
+ /** Agent backend this run used — drives "open in CLI" resume command. `storedRunnerSchema`
125
+ * so a legacy `claude-cli` record folds to `claude` instead of failing the whole index (#547). */
126
+ runner: storedRunnerSchema.optional(),
127
+ /** Per-task agent-account override from the composer (spec 2026-07-29-agent-profiles), applying
128
+ * to steps that run on `runner`. Steps on a DIFFERENT backend still resolve from the project's
129
+ * own selection — an override for Claude says nothing about which Codex account a mixed
130
+ * workflow's codex step should use. Absent = follow the project. */
131
+ agentProfile: z.string().optional(),
132
+ /** Echo of the extra system prompt this run actually used (R2): the
133
+ * `POST /api/runs` override, or the `config.json` default it fell back to.
134
+ * Deliberately NOT the full composed prompt — skill bodies and the handoff
135
+ * contract are derivable from the persisted workflow and would bloat the
136
+ * index. Resolved at execute time (a queued run picks up config edits). */
137
+ systemPrompt: z.string().optional(),
138
+ /** Per-task follow-up inbox contract (spec 007, #444). Missing on old runs
139
+ * means enabled — the historical behavior. */
140
+ generateFollowups: z.boolean().optional(),
141
+ /** Autonomous mode (#489): the run was started with the "autonomous" checkbox,
142
+ * so it never parks at `waiting` (auto-nudge) and — once persisted here —
143
+ * never parks at the terminal `review` gate either (`settleSuccess` + the
144
+ * group-pick winner-park read it). Additive-safe: absent = falsy = not
145
+ * autonomous. Set at creation from `WorkflowInput.autonomous`. */
146
+ autonomous: z.boolean().optional(),
147
+ /** Optional provenance for tasks launched by a project GitHub automation. */
148
+ automation: z
149
+ .object({
150
+ automationId: z.string(),
151
+ automationRevision: z.number().int().positive(),
152
+ receiptId: z.string(),
153
+ event: z.string(),
154
+ githubUrl: z.string().url(),
155
+ })
156
+ .optional(),
157
+ status: z.enum(['queued', 'running', 'waiting', 'review', 'done', 'failed', 'cancelled']),
158
+ /** Sub-state of `running` (spec 2026-07-18-subagent-monitoring-status, #490):
159
+ * `monitoring` while the agent is still working on its own downstream work.
160
+ * Optional/absent on old runs; cleared when the run resumes or ends. */
161
+ activity: z.enum(['monitoring']).optional(),
162
+ /** Exact server-computed deadline for the next automatic monitoring check. */
163
+ monitoringWakeAt: z.string().datetime().optional().catch(undefined),
164
+ /** True only for the live epoch that exhausted all automatic monitoring checks. */
165
+ monitoringWakeCapReached: z.boolean().optional(),
166
+ /**
167
+ * Exact deadline at which a run stopped by a provider USAGE LIMIT resumes itself
168
+ * (spec 2026-08-03-auto-resume-after-usage-limit) — the reset instant the provider named plus a
169
+ * short grace. Present only while such a resume is pending: the run is `failed`, the timer is
170
+ * armed, and the cockpit says so. Deliberately survives a restart (`RunStore.open` keeps it) —
171
+ * it is what lets `recover()` re-arm a wait that may be hours long.
172
+ */
173
+ autoResumeAt: z.string().datetime().optional().catch(undefined),
174
+ /** Consecutive automatic resumes since the last human turn — the safety cap's counter.
175
+ * Persisted so a restart cannot reset a loop back to zero. */
176
+ autoResumeAttempts: z.number().int().min(0).optional().catch(undefined),
177
+ createdAt: z.string(),
178
+ startedAt: z.string().optional(),
179
+ finishedAt: z.string().optional(),
180
+ tokensUsed: z.number(),
181
+ inputTokens: usageCounterSchema.optional(),
182
+ outputTokens: usageCounterSchema.optional(),
183
+ costUsd: z.number().optional(),
184
+ /** First GitHub PR URL spotted in the transcript (the janitor trick). */
185
+ pullRequestUrl: z.string().optional(),
186
+ /** The PR this task is ABOUT (#407, spec 2026-07-16-pr-autodiscovery):
187
+ * auto-discovered from conversation references for tasks that work on an
188
+ * existing PR (review/continue/merge). Display-only tier — `pullRequestUrl`
189
+ * (the PR this task CREATED) always wins, and action gates ignore this. */
190
+ referencedPullRequestUrl: z.string().optional(),
191
+ /** The PR/issue number this task is ABOUT (spec 2026-07-17-task-auto-naming):
192
+ * regex-extracted from the task prompt, upgradable by the namer's
193
+ * cross-checked output. Display tier — never gates actions. */
194
+ prNumber: z.number().optional(),
195
+ issueNumber: z.number().optional(),
196
+ /** Provenance for an `issueNumber` seeded by referenced-issue discovery.
197
+ * Persisted so ambiguity can revoke only the janitor's own value, including
198
+ * after a restart. Any prompt, namer, or marker write clears this flag. */
199
+ referencedIssueNumberSeeded: z.boolean().optional(),
200
+ /** Who owns the display title: `user` (PATCH rename — never auto-overwritten),
201
+ * `marker` (agent-declared via `XEZ:TITLE`, spec 2026-07-18-task-ref-markers —
202
+ * beats the namer, silences live refresh) or `auto` (namer-owned — a later
203
+ * namer result may replace it). Missing on old runs = legacy behavior (auto
204
+ * fills only an unset titleSummary). Precedence: user > marker > auto. */
205
+ titleOrigin: z.enum(['user', 'auto', 'marker']).optional(),
206
+ /** References the agent itself declared via `XEZ:PR=` / `XEZ:ISSUE=` markers
207
+ * (spec 2026-07-18-task-ref-markers). Presence of a kind makes it
208
+ * authoritative: the namer may no longer write that kind, and a declared PR
209
+ * owns the referenced tier's resolution. */
210
+ markerRefs: z.object({ pr: z.number().optional(), issue: z.number().optional() }).optional(),
211
+ /** Distinct PR URLs spotted so far — the referenced tier's working set,
212
+ * persisted so a resumed run keeps disambiguating against the full history
213
+ * instead of re-adopting the next URL as "the only one". Capped. */
214
+ referencedPrCandidates: z.array(z.string()).optional(),
215
+ /** The issue this task is ABOUT (spec 2026-07-21-report-ref-discovery):
216
+ * auto-discovered from `github.com/…/issues/N` links in the conversation,
217
+ * mirroring the referenced-PR tier. Display-only; never gates actions. */
218
+ referencedIssueUrl: z.string().optional(),
219
+ /** Distinct issue URLs spotted so far — the referenced-issue working set,
220
+ * persisted like `referencedPrCandidates`. Capped. */
221
+ referencedIssueCandidates: z.array(z.string()).optional(),
222
+ /** Explicit execution policy. `false` means the run intentionally uses the repo root;
223
+ * absent on older runs and for the default isolated-worktree mode. */
224
+ worktree: z.literal(false).optional(),
225
+ /** Task worktree (spec 006) — absent for in-place runs and after explicit cleanup. */
226
+ worktreePath: z.string().optional(),
227
+ /** The task's own branch (`xez/<id8>`), created off `baseBranch`. */
228
+ branch: z.string().optional(),
229
+ /** Stable baseline for session git views: a worktree's fork ref, or an in-place run's starting commit. */
230
+ baseBranch: z.string().optional(),
231
+ /** Set when count-based retention (#483) reclaimed this run's worktree
232
+ * *directory* (the `xez/<id8>` branch is kept). Presence means "materialized
233
+ * dir gone, recoverable via `git worktree add`"; it excludes the run from the
234
+ * retention budget until the dir is re-materialized (resume clears it). */
235
+ worktreeReclaimedAt: z.string().optional(),
236
+ /** Parallel variants (spec 010): tasks sharing a groupId are one group. */
237
+ groupId: z.string().optional(),
238
+ /** Variant letter within the group — 'A' | 'B' | 'C' (kept as a string). */
239
+ variant: z.string().optional(),
240
+ /** Peak resident memory (bytes) / process count observed across the run's
241
+ * agent process trees (#348) — written when a session's telemetry ends.
242
+ * Optional: old runs.json files and `ps`-less platforms have neither. */
243
+ peakRssBytes: z.number().optional(),
244
+ peakProcCount: z.number().optional(),
245
+ archived: z.boolean().default(false),
246
+ archivedAt: z.string().optional(),
247
+ /** Pinned to the top of this project's task list (#935): plain per-task state, the
248
+ * same class as `archived` and `seenAt`. Optional with NO default, unlike `archived`:
249
+ * absent is what every runs.json written before this carries and it already means
250
+ * "not pinned", so nothing needs filling in on parse and an unpin can simply delete
251
+ * the key rather than persist a `false` older xezars never wrote. */
252
+ pinned: z.boolean().optional(),
253
+ pinnedAt: z.string().optional(),
254
+ /** Read receipt (#unread-done-items): the ISO time the cockpit last opened this
255
+ * run's thread. A finished run reads as "unread" until it has been seen since it
256
+ * finished — see `isUnread()` in the cockpit's `lib/read-state.ts`. Absent on old
257
+ * runs, on every run not yet opened, and on one `setUnread` put back to unread
258
+ * (#775) — the unread rule treats all three alike. */
259
+ seenAt: z.string().optional(),
260
+ currentStepId: z.string().optional(),
261
+ error: z.string().optional(),
262
+ steps: z.array(stepStateSchema),
263
+ /** Full workflow definition, persisted so a `queued` run can be re-enqueued
264
+ * after a restart (#367) — including ad-hoc "(planned)" chains that exist
265
+ * nowhere else.
266
+ *
267
+ * Typed, not `z.record(z.string(), z.unknown())`: this key goes out over the
268
+ * wire on every run route, and `unknown` is wider than anything the server
269
+ * can serialize — which made the route's own type (hono's `JSONValue`, whose
270
+ * index signature admits `object | symbol | undefined`) impossible for the
271
+ * contract to describe. `.catch(undefined)` keeps an older or hand-edited
272
+ * entry from failing the whole index parse: a def that no longer fits simply
273
+ * drops, and `reviveWorkflow` falls back to the catalog by name.
274
+ *
275
+ * That drop is PERMANENT, not per-boot — the index is re-serialized from the
276
+ * parsed records (`saveNow`), so the next save writes runs.json back without
277
+ * it. Harmless for a catalog workflow, which re-resolves by name; fatal for
278
+ * the ad-hoc "(planned)" chain this field exists to preserve, which has no
279
+ * catalog entry to fall back to. Nothing written since #367 fails the schema
280
+ * (`name`, `source` and `steps` have been on every persisted def), so tighten
281
+ * `workflowStepSchema` only with that in mind: a narrowing here silently eats
282
+ * queued runs rather than degrading them. */
283
+ workflowDef: workflowDefSchema.optional().catch(undefined),
284
+ });
285
+ const MAX_RUNS_KEPT = 300;
286
+ const MAX_ARCHIVED_KEPT = 500;
287
+ const PR_URL_RE = /https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+/;
288
+ const ISSUE_URL_RE = /https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/\d+/;
289
+ // The transcript auto-link is convenience only (the cockpit's own `gh pr create` path sets the
290
+ // URL authoritatively). Adopt a PR URL ONLY when the agent actually CREATED one — a task that
291
+ // reviews or merely references an existing PR must not get mislabeled with its number (#fake-pr).
292
+ const CREATED_PR_RE = /\b(?:gh\s+pr\s+create|pull\s*request\s+created|created\s+(?:a\s+)?(?:draft\s+)?(?:pr|pull\s*request)|opened\s+(?:a\s+)?(?:draft\s+)?pull\s*request)\b/i;
293
+ /** Referenced-tier working-set cap (spec 2026-07-16-pr-autodiscovery): past
294
+ * this many distinct PRs the conversation is a survey, not a subject. */
295
+ const MAX_PR_CANDIDATES = 8;
296
+ /** `https://github.com/qodeca/xezar/pull/402` → `qodeca/xezar`, lowercased.
297
+ * Undefined for anything that is not a `<host>/<owner>/<repo>/<kind>/<n>` forge URL. */
298
+ function refUrlRepo(url) {
299
+ const parts = url.split('/');
300
+ const owner = parts[parts.length - 4];
301
+ const name = parts[parts.length - 3];
302
+ return owner && name ? `${owner}/${name}`.toLowerCase() : undefined;
303
+ }
304
+ /**
305
+ * May the referenced tier ADOPT this URL as the task's subject? (#945)
306
+ *
307
+ * The tier was text-scoped but never repo-scoped: `PR_URL_RE` matches any
308
+ * `github.com/<owner>/<repo>/pull/N`, so a research task that cites one upstream PR handed the
309
+ * resolver exactly one candidate and it became the task's identity — an `oko` task wearing
310
+ * `supabase/cli#6056`. Nothing compared the URL's repository with the project's own.
311
+ *
312
+ * A foreign URL is adoptable only when the TASK PROMPT corroborates it: the prompt names that
313
+ * `owner/repo`, which a pasted URL does inherently. That is the trust boundary this module already
314
+ * uses elsewhere — the prompt and the agent's own turn text are trusted, scraped tool output is
315
+ * not — and it is what keeps the legitimate cross-repo case working (#819:
316
+ * `om-auto-fix-pr https://github.com/open-mercato/open-mercato/pull/1977` started from xezar).
317
+ *
318
+ * Unknown handle → today's behavior exactly (`AGENTS.md` zero config: degrade, never fail). An
319
+ * unparseable URL is left alone for the same reason — the guard only ever removes an association
320
+ * it can PROVE is foreign.
321
+ *
322
+ * Note what this does not touch: `referenced*Candidates` keep recording every URL as evidence.
323
+ * The fix changes what is *promoted*, never what is *collected* (the #526 rule).
324
+ */
325
+ function isRepoScopedRef(url, task, handle) {
326
+ if (!handle)
327
+ return true;
328
+ const repo = refUrlRepo(url);
329
+ if (!repo)
330
+ return true;
331
+ if (repo === `${handle.owner}/${handle.name}`.toLowerCase())
332
+ return true;
333
+ return task.toLowerCase().includes(repo);
334
+ }
335
+ /**
336
+ * Every scannable string of one persisted event: the v1 top-level fields plus
337
+ * the protocol-v2 `item.*` content (nested — the reason v2 streams were
338
+ * invisible to the janitor, #407). Reasoning items are skipped: thinking text
339
+ * speculates about PRs the task never touches.
340
+ */
341
+ /**
342
+ * Archiving IS resigning from a task, so an archived run can never carry a pending usage-limit
343
+ * resume (spec 2026-08-03-auto-resume-after-usage-limit). The rule lives HERE rather than in the
344
+ * archive route because the bulk "Archive finished" sweep never goes through that route, and a
345
+ * user who archives fifty finished tasks has resigned from all fifty.
346
+ *
347
+ * The engine needs no telling: its timer re-reads the record before it fires and no sweep re-arms
348
+ * an archived run, so a cleared field is the whole cancellation.
349
+ */
350
+ function clearPendingAutoResume(run) {
351
+ run.autoResumeAt = undefined;
352
+ run.autoResumeAttempts = undefined;
353
+ }
354
+ /**
355
+ * Archiving is resigning from a task, so it retires the pin too (#935) — a pin on a task the
356
+ * user has filed away is stale by definition, and the archived view collapses into one bucket
357
+ * anyway, so a surviving pin would be invisible state waiting to surprise whoever unarchives.
358
+ *
359
+ * Here rather than in the pin route for the same reason `clearPendingAutoResume` is here: the
360
+ * bulk "Archive finished" sweep never goes through a route, and it has to obey the rule too.
361
+ *
362
+ * Deleted, not set to `false`: absent is what every reader treats as unpinned, and it is the
363
+ * shape a xezar that has never heard of pins already writes.
364
+ */
365
+ function clearPin(run) {
366
+ delete run.pinned;
367
+ delete run.pinnedAt;
368
+ }
369
+ function eventTextFragments(event) {
370
+ const fragments = [];
371
+ for (const key of ['text', 'result', 'message']) {
372
+ const value = event[key];
373
+ if (typeof value === 'string')
374
+ fragments.push(value);
375
+ }
376
+ const item = event.item;
377
+ if (item && typeof item === 'object' && item.kind !== 'reasoning') {
378
+ const it = item;
379
+ for (const key of ['text', 'title', 'output']) {
380
+ const value = it[key];
381
+ if (typeof value === 'string')
382
+ fragments.push(value);
383
+ }
384
+ if (typeof it.input === 'string') {
385
+ fragments.push(it.input);
386
+ }
387
+ else if (it.input !== undefined) {
388
+ try {
389
+ fragments.push(JSON.stringify(it.input));
390
+ }
391
+ catch {
392
+ // circular input — skip it
393
+ }
394
+ }
395
+ }
396
+ return fragments;
397
+ }
398
+ /**
399
+ * Where a CREATION CLAIM may come from — the trust boundary the created tier was missing.
400
+ *
401
+ * `CREATED_PR_RE` used to be matched against everything an event carried, tool OUTPUT included,
402
+ * so a transcript that merely QUOTES a `gh pr create` line handed the run a PR it never opened.
403
+ * Not hypothetical: the task that fixed the reference chips printed another run's stored events
404
+ * while investigating them, and xezar read `"title": "Ran gh pr create --repo …"` out of that
405
+ * dump and adopted a PR from a DIFFERENT repository as its own — permanently, because the first
406
+ * created URL wins and the real `gh pr create` that followed was never looked at.
407
+ *
408
+ * So the claim must come from the agent's own words, or from the tool title xezar itself renders
409
+ * from the command it saw run. Tool output and tool input are the transcript of the world, not a
410
+ * statement about this run. The URL is still read from the whole event — `gh` prints it in the
411
+ * output — because it is the CLAIM that needs a trustworthy source, not the link.
412
+ */
413
+ function eventCreationClaimFragments(event) {
414
+ const fragments = [];
415
+ // A `tool-result` event's `result` IS raw command output; on every other event the top-level
416
+ // text is the agent's own.
417
+ if (event.type !== 'tool-result') {
418
+ for (const key of ['text', 'result', 'message']) {
419
+ const value = event[key];
420
+ if (typeof value === 'string')
421
+ fragments.push(value);
422
+ }
423
+ }
424
+ const item = event.item;
425
+ if (item && typeof item === 'object') {
426
+ const it = item;
427
+ if (it.kind === 'message' && it.role === 'assistant' && typeof it.text === 'string') {
428
+ fragments.push(it.text);
429
+ }
430
+ if (it.kind === 'tool' && typeof it.title === 'string')
431
+ fragments.push(it.title);
432
+ }
433
+ return fragments;
434
+ }
435
+ /** Agent-authored event text, matching the trust boundary used by task markers.
436
+ * Tool titles, inputs, and outputs remain visible to the referenced-URL tier,
437
+ * but must never promote an issue into the shared `issueNumber` field (#538). */
438
+ function eventAgentTextFragments(event) {
439
+ const fragments = [];
440
+ for (const key of ['text', 'result']) {
441
+ const value = event[key];
442
+ if (typeof value === 'string')
443
+ fragments.push(value);
444
+ }
445
+ const item = event.item;
446
+ if (item && typeof item === 'object') {
447
+ const it = item;
448
+ if (it.kind === 'message' && it.role === 'assistant' && typeof it.text === 'string') {
449
+ fragments.push(it.text);
450
+ }
451
+ }
452
+ return fragments;
453
+ }
454
+ /**
455
+ * The referenced tier's resolution rule, shared by the PR and issue janitors:
456
+ * a marker-declared number (spec 2026-07-18-task-ref-markers) owns the answer
457
+ * outright — only a candidate URL ending in that number resolves, and a
458
+ * contradiction clears the chip. Without a declaration: one distinct URL is
459
+ * the subject; among several, the one whose number the task prompt names (and
460
+ * only when exactly one matches); otherwise ambiguous — no chip beats a wrong
461
+ * chip.
462
+ *
463
+ * Whatever that produces is then repo-scoped (#945): a winner from another repository that the
464
+ * prompt does not corroborate is vetoed — see `isRepoScopedRef`. The veto is applied to the
465
+ * RESULT rather than to the candidate list on purpose, so the guard stays strictly subtractive:
466
+ * filtering first would let a project-local candidate win a two-candidate race today's rule calls
467
+ * ambiguous, which is a wider behavior change than the defect warrants. As written this function
468
+ * can only ever lose a value, never gain one.
469
+ */
470
+ function resolveReferencedRef(candidates, task, declared, handle) {
471
+ const resolved = resolveCandidate(candidates, task, declared);
472
+ if (resolved === undefined)
473
+ return undefined;
474
+ return isRepoScopedRef(resolved, task, handle) ? resolved : undefined;
475
+ }
476
+ /** The pre-#945 resolution rule, unchanged — see `resolveReferencedRef` for the contract. */
477
+ function resolveCandidate(candidates, task, declared) {
478
+ if (declared !== undefined)
479
+ return candidates.find((url) => url.endsWith(`/${declared}`));
480
+ if (candidates.length === 1)
481
+ return candidates[0];
482
+ const named = candidates.filter((url) => {
483
+ const num = url.split('/').pop() ?? '';
484
+ // `\d` boundaries only: they reject `170` inside `4170` yet still match a
485
+ // number written as `#4170`, ` 4170`, or inside a pasted `…/pull/4170`.
486
+ return num !== '' && new RegExp(`(?<!\\d)#?${num}(?!\\d)`).test(task);
487
+ });
488
+ return named.length === 1 ? named[0] : undefined;
489
+ }
490
+ /** The number a forge URL's last segment names (`…/pull/402` → 402), or undefined. */
491
+ function refUrlNumber(url) {
492
+ if (!url)
493
+ return undefined;
494
+ const n = Number(url.split('/').pop());
495
+ return Number.isInteger(n) && n > 0 && n < MAX_REF ? n : undefined;
496
+ }
497
+ /**
498
+ * The PR declaration the REFERENCED tier is allowed to act on.
499
+ *
500
+ * `XEZ:PR=N` means one of two things depending on when the agent writes it: on the way in it
501
+ * names the PR the task is ABOUT, and once the task has opened a PR of its own the marker
502
+ * contract asks it to re-declare with the new number ("Re-emit with the new number if the subject
503
+ * changes (e.g. you open a PR later in the task)"). A declaration naming the PR this run CREATED
504
+ * is therefore a statement about the CREATED tier, which `pullRequestUrl` already carries — and
505
+ * feeding it to the referenced tier ERASES the about-PR, because `resolveReferencedRef` clears
506
+ * the chip when no candidate matches the declared number (a task on #4326 that opened
507
+ * #5366 dropped from two chips to one the moment it declared #5366).
508
+ *
509
+ * Both tiers stay true instead: the created PR is the created PR, and the reference resolves as
510
+ * if that declaration had not been made — which is exactly what it was before the task opened
511
+ * anything.
512
+ */
513
+ function referencedPrDeclaration(run) {
514
+ const declared = run.markerRefs?.pr;
515
+ if (declared === undefined)
516
+ return undefined;
517
+ return declared === refUrlNumber(run.pullRequestUrl) ? undefined : declared;
518
+ }
519
+ /**
520
+ * The PR URL a creation phrase *introduces*, or undefined. The created URL is
521
+ * the first one at or after the `CREATED_PR_RE` phrase — a PR the same event
522
+ * merely referenced *earlier* (e.g. the issue's own linked `…/pull/1`) must not
523
+ * be mistaken for the one just created (#495). Falls back to the last URL
524
+ * *before* the phrase for `gh` orderings that print the URL first. Selection
525
+ * only — the caller decides whether creation phrasing is present.
526
+ */
527
+ function createdPrUrl(haystack) {
528
+ const phrase = CREATED_PR_RE.exec(haystack);
529
+ if (!phrase)
530
+ return undefined;
531
+ const after = PR_URL_RE.exec(haystack.slice(phrase.index));
532
+ if (after)
533
+ return after[0];
534
+ let before;
535
+ for (const m of haystack.slice(0, phrase.index).matchAll(new RegExp(PR_URL_RE.source, 'g'))) {
536
+ before = m[0];
537
+ }
538
+ return before;
539
+ }
540
+ /**
541
+ * Reconcile one record just read off disk with the fact that whichever process wrote it is gone.
542
+ *
543
+ * Mutates and returns `run`. Extracted from `RunStore.open` so the read-only index reader
544
+ * (`./run-index.ts`) answers the SAME question about a `running` row on disk. Two parsers that
545
+ * disagree here is a visible bug, not an internal one: the cockpit would show a task as running
546
+ * in the ⌘K index and failed the moment you opened it.
547
+ *
548
+ * `keepLive` (#367): leave `queued`/`running`/`waiting` untouched so the caller can recover them
549
+ * (RunManager.recover re-queues queued runs, resumes interrupted ones). Without it — one-shot CLI
550
+ * paths that never recover, and the index reader, which has no manager at all — live-looking runs
551
+ * are marked failed so no ghost stays behind.
552
+ */
553
+ export function reconcileLoadedRun(run, opts) {
554
+ // A run that was live when the previous process exited can never finish —
555
+ // surface that instead of a forever-"running" ghost. `review` survives
556
+ // restarts on purpose: the gate is pure data (worktree + branch + record)
557
+ // with no live process, so the diff panel, Send back (resume) and Draft PR
558
+ // all still work.
559
+ if (!opts?.keepLive &&
560
+ (run.status === 'running' || run.status === 'queued' || run.status === 'waiting')) {
561
+ run.status = 'failed';
562
+ run.error = 'interrupted — xezar process exited during the run';
563
+ run.finishedAt = run.finishedAt ?? new Date().toISOString();
564
+ for (const step of run.steps) {
565
+ if (step.status === 'running' || step.status === 'waiting')
566
+ step.status = 'failed';
567
+ }
568
+ }
569
+ if (!['running', 'waiting', 'queued'].includes(run.status)) {
570
+ run.activity = undefined;
571
+ run.monitoringWakeAt = undefined;
572
+ }
573
+ // A pending usage-limit resume survives the restart on purpose (the wait can be
574
+ // hours) — `RunManager.recover()` re-arms it from this field. It can only mean
575
+ // anything on a `failed` run, so anywhere else it is stale bookkeeping.
576
+ if (run.status !== 'failed')
577
+ run.autoResumeAt = undefined;
578
+ // The wake counter is intentionally process-local, so a restarted process
579
+ // starts a fresh epoch instead of displaying a stale cap.
580
+ run.monitoringWakeCapReached = undefined;
581
+ // Heal a record written before `referencedPrDeclaration` existed: a task that re-declared
582
+ // `XEZ:PR` with the PR it had just CREATED cleared the PR it was ABOUT, because no candidate
583
+ // could match the created number. The evidence is all still on the record — only the
584
+ // conclusion drawn from it was wrong — so re-resolve without that declaration instead of
585
+ // asking for a migration. Deliberately one-directional: it only runs on a record that HAS no
586
+ // referenced PR, so it can never take one away from a record written by an older xezar whose
587
+ // candidate list no longer explains it. `prNumber` is not recoverable this way (the
588
+ // declaration overwrote it) and is left alone — the restored URL is what paints the chip.
589
+ if (run.referencedPullRequestUrl === undefined &&
590
+ run.markerRefs?.pr !== undefined &&
591
+ referencedPrDeclaration(run) === undefined) {
592
+ run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, undefined);
593
+ }
594
+ // Deliberately UNSCOPED by repo (#945), unlike every other `resolveReferencedRef` call. Neither
595
+ // caller has a handle to pass: `RunStore.open` is synchronous and the handle costs a `gh` spawn
596
+ // (which is why it is armed afterwards, by `setRepoHandle`), and the read-only index reader
597
+ // (`./run-index.ts`) has no repo root at all. Passing `undefined` here is not a gap — it is the
598
+ // no-handle path the guard is specified to take. The foreign-URL heal runs in `setRepoHandle`'s
599
+ // sweep the moment the handle lands, and the index reader picks the healed values up from
600
+ // `runs.json` on its next read.
601
+ return run;
602
+ }
603
+ /**
604
+ * File-backed run store: `runs.json` index (atomic tmp+rename writes, the
605
+ * pattern from @xezar/core's IssueStore) plus one append-only NDJSON event
606
+ * file per run. Also the in-process event bus the SSE endpoints subscribe to:
607
+ * emits `('run', RunRecord)` and `('event', { runId, event: RunEvent })`.
608
+ */
609
+ export class RunStore extends EventEmitter {
610
+ dataDir;
611
+ runs = new Map();
612
+ saveTimer = null;
613
+ /** The repository this project IS (#945), armed after `open()` by `setRepoHandle`. Undefined
614
+ * until it arrives and `null` when it cannot be known — both mean "unscoped", which is
615
+ * exactly the pre-#945 behavior. */
616
+ repoHandle;
617
+ constructor(dataDir) {
618
+ super();
619
+ this.dataDir = dataDir;
620
+ this.setMaxListeners(100);
621
+ }
622
+ /** See `reconcileLoadedRun` for what `keepLive` (#367) decides about live-looking rows. */
623
+ static open(dataDir, opts) {
624
+ mkdirSync(join(dataDir, 'runs'), { recursive: true });
625
+ const store = new RunStore(dataDir);
626
+ const indexPath = join(dataDir, 'runs.json');
627
+ if (existsSync(indexPath)) {
628
+ try {
629
+ const raw = JSON.parse(readFileSync(indexPath, 'utf8'));
630
+ const parsed = z.array(runRecordSchema).safeParse(raw);
631
+ if (parsed.success) {
632
+ for (const run of parsed.data) {
633
+ store.runs.set(run.id, reconcileLoadedRun(run, opts));
634
+ }
635
+ }
636
+ }
637
+ catch {
638
+ // corrupt index — start fresh; event files stay on disk untouched
639
+ }
640
+ }
641
+ return store;
642
+ }
643
+ /**
644
+ * Tell the store which repository this project IS (#945), so the referenced tier stops adopting
645
+ * another repo's PR/issue as the task's subject. See `isRepoScopedRef` for the rule.
646
+ *
647
+ * A setter rather than an `open()` option because `open()` is synchronous and the handle costs a
648
+ * `gh` spawn: callers arm this in the background so boot never waits on the network. `null` is a
649
+ * first-class answer meaning "cannot be known" (no `gh`, no remote, a non-git root) and leaves
650
+ * the store in exactly its pre-#945 behavior.
651
+ *
652
+ * Arming also HEALS records already poisoned by the un-scoped rule, on the `reconcileLoadedRun`
653
+ * precedent: the evidence is all still on the record (`referenced*Candidates`), only the
654
+ * conclusion drawn from it was wrong, so re-deciding beats asking for a migration. It rewrites
655
+ * values, never the format, and is one-directional by construction — see `rescopeRun`.
656
+ */
657
+ setRepoHandle(handle) {
658
+ this.repoHandle = handle;
659
+ if (!handle)
660
+ return; // nothing to prove foreign against
661
+ // `touch` per healed run: the cockpit is already live when the handle lands, so a corrected
662
+ // chip has to reach the open page over SSE, not just the next `runs.json` write.
663
+ for (const run of this.runs.values()) {
664
+ if (this.rescopeRun(run))
665
+ this.touch(run);
666
+ }
667
+ }
668
+ /**
669
+ * Drop this run's referenced PR/issue if the project's handle proves it foreign and the prompt
670
+ * does not corroborate it (#945). Returns whether anything changed.
671
+ *
672
+ * One-directional by construction: it only ever clears fields, so a record written by an older
673
+ * xezar — or read by one after this ran — is never worse off, and a downgrade sees a record whose
674
+ * format is untouched and whose cleared fields were already optional.
675
+ */
676
+ rescopeRun(run) {
677
+ let changed = false;
678
+ if (run.referencedPullRequestUrl &&
679
+ !isRepoScopedRef(run.referencedPullRequestUrl, run.task, this.repoHandle)) {
680
+ run.referencedPullRequestUrl = undefined;
681
+ changed = true;
682
+ }
683
+ if (run.referencedIssueUrl &&
684
+ !isRepoScopedRef(run.referencedIssueUrl, run.task, this.repoHandle)) {
685
+ run.referencedIssueUrl = undefined;
686
+ changed = true;
687
+ // Take back the number this janitor seeded from that very URL — the same revoke
688
+ // `trackReferencedIssues` performs when ambiguity clears a resolution. A `prNumber`-style
689
+ // number the prompt, namer or a marker owns is NOT ours to touch, which is exactly what
690
+ // `referencedIssueNumberSeeded` records.
691
+ if (run.referencedIssueNumberSeeded) {
692
+ run.issueNumber = undefined;
693
+ run.referencedIssueNumberSeeded = undefined;
694
+ }
695
+ }
696
+ return changed;
697
+ }
698
+ listRuns() {
699
+ return [...this.runs.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
700
+ }
701
+ getRun(id) {
702
+ return this.runs.get(id);
703
+ }
704
+ createRun(input) {
705
+ const run = {
706
+ id: randomUUID(),
707
+ // Scrubbed on the way in, exactly as `updateRun` scrubs it on the way
708
+ // through (#456 review) — a token pasted into the prompt otherwise sat
709
+ // verbatim in `runs.json` from creation. `task` is deliberately NOT
710
+ // scrubbed: it is the user's own prompt and is replayed into `{{task}}`
711
+ // when a queued run is revived after a restart (#367), so redacting it
712
+ // would corrupt the revived run.
713
+ title: this.redactText(input.title),
714
+ workflow: input.workflow,
715
+ task: input.task,
716
+ model: input.model,
717
+ runner: input.runner,
718
+ agentProfile: input.agentProfile,
719
+ generateFollowups: input.generateFollowups,
720
+ autonomous: input.autonomous,
721
+ worktree: input.worktree,
722
+ groupId: input.groupId,
723
+ variant: input.variant,
724
+ status: 'queued',
725
+ createdAt: new Date().toISOString(),
726
+ tokensUsed: 0,
727
+ archived: false,
728
+ steps: input.steps.map((s) => ({
729
+ ...s,
730
+ status: 'pending',
731
+ iterations: 0,
732
+ tokensUsed: 0,
733
+ })),
734
+ };
735
+ // A prompt that pastes a PR or issue URL is already about that item — seed
736
+ // both referenced tiers so queued runs can expose the reference before the
737
+ // first agent event (#407, #554).
738
+ this.trackReferencedPrs(run, input.task);
739
+ this.trackReferencedIssues(run, input.task);
740
+ this.runs.set(run.id, run);
741
+ this.pruneOldRuns();
742
+ this.touch(run);
743
+ return run;
744
+ }
745
+ updateRun(id, patch) {
746
+ const run = this.runs.get(id);
747
+ if (!run)
748
+ return undefined;
749
+ if (Object.prototype.hasOwnProperty.call(patch, 'issueNumber')) {
750
+ delete run.referencedIssueNumberSeeded;
751
+ }
752
+ const normalized = { ...patch };
753
+ if (normalized.status && !['running', 'waiting', 'queued'].includes(normalized.status)) {
754
+ normalized.activity = undefined;
755
+ normalized.monitoringWakeAt = undefined;
756
+ normalized.monitoringWakeCapReached = undefined;
757
+ }
758
+ // …and the mirror image for the usage-limit resume (spec
759
+ // 2026-08-03-auto-resume-after-usage-limit): it is a promise made ABOUT a failed run, so a
760
+ // run coming back to life — the resume itself, a user Continue, a re-queue — retires it.
761
+ // The manager's timer re-checks the record before it fires, so a cleared field is enough.
762
+ if (normalized.status && ['running', 'waiting', 'queued'].includes(normalized.status)) {
763
+ normalized.autoResumeAt = undefined;
764
+ }
765
+ Object.assign(run, this.redactPatch(normalized));
766
+ this.touch(run);
767
+ return run;
768
+ }
769
+ /**
770
+ * Scrub the free-text fields of a record patch (#427 review). Redacting only
771
+ * events left a hole: `titleSummary` is derived from the RAW first agent turn
772
+ * and `error` from raw process output, so a token the agent echoed was
773
+ * `[REDACTED]` in the NDJSON yet verbatim in `runs.json` — the file the "no
774
+ * secrets in state files" rule names explicitly. These three are the only
775
+ * patch fields carrying agent/process text; the rest are ids, enums, counters
776
+ * and URLs, and running the scrubber over them would only risk mangling them.
777
+ *
778
+ * `StepState.error` is the step-level counterpart and is scrubbed the same
779
+ * way in `updateStep` — `run.ts` feeds the SAME `err.message` string to both
780
+ * calls, so redacting only the run-level copy left the token verbatim one
781
+ * field away (#456 review).
782
+ */
783
+ redactPatch(patch) {
784
+ if (process.env.XEZ_REDACT_SECRETS === '0')
785
+ return patch;
786
+ const out = { ...patch };
787
+ for (const field of ['title', 'titleSummary', 'error']) {
788
+ const value = out[field];
789
+ if (typeof value === 'string')
790
+ out[field] = this.redactText(value);
791
+ }
792
+ return out;
793
+ }
794
+ /**
795
+ * Step-level counterpart of `redactPatch` (#456 review). `error` is the only
796
+ * free-text `StepState` field — it is set from raw `err.message` /process
797
+ * output (`run.ts` `finishStep`), and `touch()` fans the whole record out
798
+ * over SSE, so an unscrubbed copy leaked to `runs.json` AND to the browser.
799
+ * The remaining fields are ids, enums, counters and timestamps.
800
+ */
801
+ redactStepPatch(patch) {
802
+ if (process.env.XEZ_REDACT_SECRETS === '0')
803
+ return patch;
804
+ if (typeof patch.error !== 'string')
805
+ return patch;
806
+ return { ...patch, error: this.redactText(patch.error) };
807
+ }
808
+ /** Append a step to an existing run (used by "Continue" — spec 003). */
809
+ addStep(runId, step) {
810
+ const run = this.runs.get(runId);
811
+ if (!run || run.steps.some((s) => s.id === step.id))
812
+ return;
813
+ run.steps.push({ ...step, status: 'pending', iterations: 0, tokensUsed: 0 });
814
+ this.touch(run);
815
+ }
816
+ updateStep(runId, stepId, patch) {
817
+ const run = this.runs.get(runId);
818
+ const step = run?.steps.find((s) => s.id === stepId);
819
+ if (!run || !step)
820
+ return;
821
+ Object.assign(step, this.redactStepPatch(patch));
822
+ run.tokensUsed = run.steps.reduce((sum, s) => sum + s.tokensUsed, 0);
823
+ const startedAgentSteps = run.steps.filter((candidate) => candidate.kind === 'agent' && candidate.iterations > 0);
824
+ const directionalComplete = startedAgentSteps.length > 0 &&
825
+ startedAgentSteps.every((candidate) => candidate.usageInvocationsStarted !== undefined &&
826
+ candidate.usageInvocationsObserved !== undefined &&
827
+ candidate.usageInvocationsObserved > 0 &&
828
+ candidate.usageInvocationsStarted === candidate.usageInvocationsObserved &&
829
+ candidate.usageTurnsStarted !== undefined &&
830
+ candidate.usageTurnsRecorded !== undefined &&
831
+ candidate.usageTurnsStarted > 0 &&
832
+ candidate.usageTurnsStarted === candidate.usageTurnsRecorded &&
833
+ candidate.inputTokens !== undefined &&
834
+ candidate.outputTokens !== undefined);
835
+ run.inputTokens = directionalComplete
836
+ ? startedAgentSteps.reduce((sum, candidate) => sum + (candidate.inputTokens ?? 0), 0)
837
+ : undefined;
838
+ run.outputTokens = directionalComplete
839
+ ? startedAgentSteps.reduce((sum, candidate) => sum + (candidate.outputTokens ?? 0), 0)
840
+ : undefined;
841
+ const cost = run.steps.reduce((sum, s) => sum + (s.costUsd ?? 0), 0);
842
+ run.costUsd = cost > 0 ? cost : undefined;
843
+ this.touch(run);
844
+ }
845
+ setArchived(id, archived) {
846
+ const run = this.runs.get(id);
847
+ if (!run)
848
+ return undefined;
849
+ run.archived = archived;
850
+ run.archivedAt = archived ? new Date().toISOString() : undefined;
851
+ if (archived) {
852
+ clearPendingAutoResume(run);
853
+ clearPin(run);
854
+ }
855
+ this.touch(run);
856
+ return run;
857
+ }
858
+ /** Pin one run to the top of this project's task list, or unpin it (#935). Mirrors
859
+ * `setArchived`: sets the fields, then persists + broadcasts via `touch`, so the updated
860
+ * record rides the existing `run` SSE with no new event. Idempotent — re-pinning a pinned
861
+ * run just re-stamps `pinnedAt`.
862
+ *
863
+ * Unconditional by design, like `setUnread`: pinning is legal for every status, and WHICH
864
+ * runs are worth offering the action on is UI policy (`runActionFlags` in the cockpit). */
865
+ setPinned(id, pinned) {
866
+ const run = this.runs.get(id);
867
+ if (!run)
868
+ return undefined;
869
+ if (pinned) {
870
+ run.pinned = true;
871
+ run.pinnedAt = new Date().toISOString();
872
+ }
873
+ else {
874
+ clearPin(run);
875
+ }
876
+ this.touch(run);
877
+ return run;
878
+ }
879
+ /** Bulk-archive every finished run; returns how many were archived. */
880
+ archiveFinished() {
881
+ let count = 0;
882
+ for (const run of this.runs.values()) {
883
+ if (!run.archived && ['done', 'failed', 'cancelled'].includes(run.status)) {
884
+ run.archived = true;
885
+ run.archivedAt = new Date().toISOString();
886
+ clearPendingAutoResume(run);
887
+ clearPin(run);
888
+ this.touch(run);
889
+ count++;
890
+ }
891
+ }
892
+ return count;
893
+ }
894
+ /** Mark one run as read (#unread-done-items): stamp the read receipt now. Mirrors
895
+ * `setArchived` — sets the field then persists + broadcasts via `touch`, so the
896
+ * updated record rides the existing `run` SSE with no new event. Idempotent by
897
+ * design: opening an already-read thread just re-stamps a later `seenAt`. */
898
+ setRead(id) {
899
+ const run = this.runs.get(id);
900
+ if (!run)
901
+ return undefined;
902
+ run.seenAt = new Date().toISOString();
903
+ this.touch(run);
904
+ return run;
905
+ }
906
+ /** Mark one run as UNread (#775): drop the read receipt so the run rejoins the unread
907
+ * list. The inverse of `setRead` and, like it, `touch`es so the updated record rides the
908
+ * existing `run` SSE.
909
+ *
910
+ * Deleting the field rather than adding a "manually unread" flag is the whole point:
911
+ * absent `seenAt` is ALREADY what every reader treats as unread (`isUnread` in the
912
+ * cockpit's read-state.ts, and `markAllRead`'s clause-for-clause copy of it below), so
913
+ * clearing needs no new state and writes a shape any older xezar already parses.
914
+ *
915
+ * Deliberately unconditional: clearing a receipt is always a legal write, so this
916
+ * succeeds for an already-unread run (idempotent) and for statuses that can never wear
917
+ * the marker. WHETHER the action means anything for a given run is UI policy, and lives
918
+ * in the cockpit's `runActionFlags` — the same split the rest of the store keeps. */
919
+ setUnread(id) {
920
+ const run = this.runs.get(id);
921
+ if (!run)
922
+ return undefined;
923
+ delete run.seenAt;
924
+ this.touch(run);
925
+ return run;
926
+ }
927
+ /** Bulk mark-read: stamp every currently-unread finished run; returns the count.
928
+ * "Unread" here is the same rule the cockpit paints (`isUnread` in read-state.ts),
929
+ * clause for clause:
930
+ * - a `done` or `failed` run that finished and has not been seen since;
931
+ * - cancelled runs are never unread — you stopped them yourself;
932
+ * - archived ones never are either, since archiving is a stronger "done with this"
933
+ * than reading;
934
+ * - and a `failed` run with a pending `autoResumeAt` is not a done item AT ALL
935
+ * (`isScheduledResume`, spec 2026-08-03-auto-resume-after-usage-limit): it has an
936
+ * appointment to pick the work back up, so there is no outcome to have missed.
937
+ *
938
+ * Keeping the two rules identical is what makes the returned count the number the
939
+ * cockpit's unread badge was showing. The `autoResumeAt` clause is the one that drifted
940
+ * (#803): `isUnread` gained it with auto-resume and this sweep did not, so a task waiting
941
+ * out a usage limit was uncounted by the badge but stamped read by the sweep — and this
942
+ * comment asserted an invariant the code no longer held.
943
+ *
944
+ * This rule lives in two languages of the same repo, which is why it has now drifted
945
+ * once. The cockpit cannot import it (`packages/web` does not depend on the service, and
946
+ * should not), so a single definition would have to move to `packages/contract` — the one
947
+ * package both sides already import. Worth doing; deliberately not done here, because
948
+ * widening the contract package's remit from "shapes" to "behavior" is a design change
949
+ * that deserves its own review rather than riding along in a bug fix. Until then: EDIT
950
+ * BOTH, and the case-table tests on either side are what catch you if you don't. */
951
+ markAllRead() {
952
+ const now = new Date().toISOString();
953
+ let count = 0;
954
+ for (const run of this.runs.values()) {
955
+ const unread = !run.archived &&
956
+ (run.status === 'done' || run.status === 'failed') &&
957
+ !(run.status === 'failed' && run.autoResumeAt !== undefined) &&
958
+ run.finishedAt !== undefined &&
959
+ (run.seenAt === undefined || run.seenAt < run.finishedAt);
960
+ if (!unread)
961
+ continue;
962
+ run.seenAt = now;
963
+ this.touch(run);
964
+ count++;
965
+ }
966
+ return count;
967
+ }
968
+ appendEvent(runId, event) {
969
+ const run = this.runs.get(runId);
970
+ if (!run)
971
+ throw new Error(`unknown run: ${runId}`);
972
+ const seq = this.nextSeq(runId);
973
+ // Scrub credentials before the event touches disk or the live wire (#427):
974
+ // tool-result output is persisted verbatim and served back over the API, so
975
+ // a secret in an agent's command output would otherwise land in `.ai/xezar/`.
976
+ const full = this.redact({ ...event, seq, ts: new Date().toISOString() });
977
+ // Sync append keeps event order without a write queue; local NDJSON
978
+ // appends at agent-event rates are effectively free.
979
+ appendFileSync(this.eventsPath(runId), `${JSON.stringify(full)}\n`, 'utf8');
980
+ this.emit('event', { runId, event: full });
981
+ // The janitor trick: agents print the PR URL after `gh pr create` — the
982
+ // first one spotted in the transcript becomes the run's PR link. Scans v1
983
+ // fields AND nested v2 `item.*` content (#407). A URL without the created
984
+ // phrasing still feeds the referenced tier (the PR the task is about) —
985
+ // and the phrasing itself is only believed from a source that can speak
986
+ // FOR this run (`eventCreationClaimFragments`), never from quoted output.
987
+ const haystack = eventTextFragments(full).join(' ');
988
+ const agentHaystack = eventAgentTextFragments(full).join(' ');
989
+ // The creation CLAIM is read from a narrower source than the URL is
990
+ // (`eventCreationClaimFragments`), which is why the two are searched
991
+ // together rather than the haystack alone: the phrase must land in the
992
+ // trusted prefix, and the link may come from anywhere after it.
993
+ const claim = eventCreationClaimFragments(full).join(' ');
994
+ if (haystack.length > 0) {
995
+ let changed = false;
996
+ if (!run.pullRequestUrl) {
997
+ const created = CREATED_PR_RE.test(claim) ? createdPrUrl(`${claim} ${haystack}`) : undefined;
998
+ if (created) {
999
+ this.updateRun(runId, { pullRequestUrl: created });
1000
+ // Adopting the created tier can RELEASE a declaration the referenced tier was holding
1001
+ // (see `referencedPrDeclaration`), so re-resolve here too: the about-PR must come back
1002
+ // whether the marker arrived before the creation evidence or after it.
1003
+ const resolved = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, referencedPrDeclaration(run), this.repoHandle);
1004
+ if (resolved !== run.referencedPullRequestUrl) {
1005
+ run.referencedPullRequestUrl = resolved;
1006
+ changed = true;
1007
+ }
1008
+ }
1009
+ else if (PR_URL_RE.test(haystack) && this.trackReferencedPrs(run, haystack)) {
1010
+ changed = true;
1011
+ }
1012
+ }
1013
+ // Issue links feed their own referenced tier regardless of PR state —
1014
+ // a task that created a PR can still be ABOUT an issue
1015
+ // (spec 2026-07-21-report-ref-discovery).
1016
+ if (ISSUE_URL_RE.test(haystack) &&
1017
+ this.trackReferencedIssues(run, haystack, agentHaystack)) {
1018
+ changed = true;
1019
+ }
1020
+ if (changed)
1021
+ this.touch(run);
1022
+ }
1023
+ return full;
1024
+ }
1025
+ /**
1026
+ * Fold every PR URL in `haystack` into the run's referenced-tier working
1027
+ * set and re-resolve `referencedPullRequestUrl` (spec
1028
+ * 2026-07-16-pr-autodiscovery). Mutates the record in place — the caller
1029
+ * owns persistence/fan-out — and reports whether anything changed.
1030
+ */
1031
+ trackReferencedPrs(run, haystack) {
1032
+ const seen = new Set(run.referencedPrCandidates ?? []);
1033
+ const before = seen.size;
1034
+ for (const match of haystack.matchAll(new RegExp(PR_URL_RE.source, 'g'))) {
1035
+ if (seen.size >= MAX_PR_CANDIDATES)
1036
+ break;
1037
+ seen.add(match[0]);
1038
+ }
1039
+ if (seen.size === before)
1040
+ return false;
1041
+ run.referencedPrCandidates = [...seen];
1042
+ run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates, run.task, referencedPrDeclaration(run), this.repoHandle);
1043
+ return true;
1044
+ }
1045
+ /**
1046
+ * The issue-side mirror of `trackReferencedPrs` (spec
1047
+ * 2026-07-21-report-ref-discovery): fold every issue URL in `haystack` into
1048
+ * the working set and re-resolve `referencedIssueUrl`. An unambiguous
1049
+ * resolution also seeds `issueNumber` when nothing owns that field yet —
1050
+ * marker and namer both outrank this janitor and overwrite it freely.
1051
+ */
1052
+ trackReferencedIssues(run, haystack, seedHaystack = haystack) {
1053
+ const seen = new Set(run.referencedIssueCandidates ?? []);
1054
+ const before = seen.size;
1055
+ for (const match of haystack.matchAll(new RegExp(ISSUE_URL_RE.source, 'g'))) {
1056
+ if (seen.size >= MAX_PR_CANDIDATES)
1057
+ break;
1058
+ seen.add(match[0]);
1059
+ }
1060
+ const candidatesChanged = seen.size !== before;
1061
+ if (candidatesChanged)
1062
+ run.referencedIssueCandidates = [...seen];
1063
+ const prev = run.referencedIssueUrl;
1064
+ run.referencedIssueUrl = resolveReferencedRef(run.referencedIssueCandidates ?? [], run.task, run.markerRefs?.issue, this.repoHandle);
1065
+ let numberChanged = false;
1066
+ if (run.markerRefs?.issue === undefined && ISSUE_URL_RE.test(seedHaystack)) {
1067
+ if (run.referencedIssueUrl && run.issueNumber === undefined) {
1068
+ const n = Number(run.referencedIssueUrl.split('/').pop());
1069
+ if (Number.isInteger(n) && n > 0) {
1070
+ run.issueNumber = n;
1071
+ run.referencedIssueNumberSeeded = true;
1072
+ numberChanged = true;
1073
+ }
1074
+ }
1075
+ else if (!run.referencedIssueUrl && prev && run.referencedIssueNumberSeeded) {
1076
+ // Ambiguity revoked the resolution — take back the number this janitor
1077
+ // seeded from it. No chip beats a wrong chip.
1078
+ delete run.issueNumber;
1079
+ delete run.referencedIssueNumberSeeded;
1080
+ numberChanged = true;
1081
+ }
1082
+ }
1083
+ return candidatesChanged || run.referencedIssueUrl !== prev || numberChanged;
1084
+ }
1085
+ /**
1086
+ * Apply agent-declared reference markers (spec 2026-07-18-task-ref-markers).
1087
+ * Marker values are authoritative for the display tier: they overwrite the
1088
+ * regex/namer numbers, and a declared PR re-resolves the referenced URL
1089
+ * against the candidate working set — including down to `undefined` when no
1090
+ * candidate matches (a wrong chip is worse than no chip). The created tier
1091
+ * (`pullRequestUrl`) is deliberately untouched.
1092
+ *
1093
+ * One declaration is NOT a statement about the referenced tier: the number of the PR this run
1094
+ * itself created. See `referencedPrDeclaration` — the marker contract asks the agent to
1095
+ * re-declare after it opens a PR, and taking that literally cost the task the PR it was about.
1096
+ */
1097
+ applyMarkerRefs(runId, refs) {
1098
+ const run = this.runs.get(runId);
1099
+ if (!run || (refs.pr === undefined && refs.issue === undefined))
1100
+ return run;
1101
+ run.markerRefs = {
1102
+ ...run.markerRefs,
1103
+ ...(refs.pr !== undefined ? { pr: refs.pr } : {}),
1104
+ ...(refs.issue !== undefined ? { issue: refs.issue } : {}),
1105
+ };
1106
+ // `prNumber` is the about-PR as well (it is what paints a numeric-only chip), so a
1107
+ // re-declaration naming the created PR only FILLS it — it never overwrites the number the
1108
+ // task came in with, which is still the PR this task is about.
1109
+ if (refs.pr !== undefined && (run.prNumber === undefined || refs.pr !== refUrlNumber(run.pullRequestUrl))) {
1110
+ run.prNumber = refs.pr;
1111
+ }
1112
+ if (refs.issue !== undefined) {
1113
+ run.issueNumber = refs.issue;
1114
+ delete run.referencedIssueNumberSeeded;
1115
+ }
1116
+ if (run.markerRefs.pr !== undefined) {
1117
+ run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, referencedPrDeclaration(run), this.repoHandle);
1118
+ }
1119
+ if (run.markerRefs.issue !== undefined) {
1120
+ run.referencedIssueUrl = resolveReferencedRef(run.referencedIssueCandidates ?? [], run.task, run.markerRefs.issue, this.repoHandle);
1121
+ }
1122
+ this.touch(run);
1123
+ return run;
1124
+ }
1125
+ /**
1126
+ * Fan an event out to live subscribers WITHOUT writing it to the NDJSON
1127
+ * file — the channel for coalesced `item.delta` flushes (protocol-v2
1128
+ * performance guardrail: raw deltas never hit disk; replay = the persisted
1129
+ * snapshots). Stamped with `seq`/`ts` like persisted lines so the live
1130
+ * wire keeps one ordering axis; the seq simply never appears in a replay
1131
+ * (gaps are fine — dedup compares with `>`).
1132
+ */
1133
+ emitEphemeral(runId, event) {
1134
+ const full = this.redact({ ...event, seq: this.nextSeq(runId), ts: new Date().toISOString() });
1135
+ this.emit('event', { runId, event: full });
1136
+ return full;
1137
+ }
1138
+ /** Lazily-collected concrete secret values from the host env (#427). */
1139
+ secretValues = null;
1140
+ /**
1141
+ * Scrub known credential values / token shapes from an event before it is
1142
+ * persisted or fanned out. On by default; `XEZ_REDACT_SECRETS=0` opts out.
1143
+ */
1144
+ redact(event) {
1145
+ if (process.env.XEZ_REDACT_SECRETS === '0')
1146
+ return event;
1147
+ return redactDeep(event, this.hostSecrets());
1148
+ }
1149
+ /** Best-effort scrub of one free-text string bound for `runs.json`. Honors
1150
+ * the `XEZ_REDACT_SECRETS=0` opt-out itself so every caller inherits it. */
1151
+ redactText(text) {
1152
+ if (process.env.XEZ_REDACT_SECRETS === '0')
1153
+ return text;
1154
+ return redactSecrets(text, this.hostSecrets());
1155
+ }
1156
+ hostSecrets() {
1157
+ if (this.secretValues === null)
1158
+ this.secretValues = collectSecretValues();
1159
+ return this.secretValues;
1160
+ }
1161
+ readEvents(runId) {
1162
+ try {
1163
+ const raw = readFileSync(this.eventsPath(runId), 'utf8');
1164
+ return raw
1165
+ .split('\n')
1166
+ .filter(Boolean)
1167
+ .map((line) => {
1168
+ try {
1169
+ return JSON.parse(line);
1170
+ }
1171
+ catch {
1172
+ return null;
1173
+ }
1174
+ })
1175
+ .filter((e) => e !== null);
1176
+ }
1177
+ catch {
1178
+ return [];
1179
+ }
1180
+ }
1181
+ deleteRun(id) {
1182
+ const existed = this.runs.delete(id);
1183
+ if (existed) {
1184
+ try {
1185
+ rmSync(this.eventsPath(id), { force: true });
1186
+ rmSync(this.handoffPath(id), { force: true }); // spec 007: the journal goes with the task
1187
+ rmSync(this.imagesDir(id), { recursive: true, force: true }); // agent screenshots
1188
+ }
1189
+ catch {
1190
+ // best effort — the index is authoritative
1191
+ }
1192
+ this.seqs.delete(id);
1193
+ this.scheduleSave();
1194
+ this.emit('deleted', id);
1195
+ }
1196
+ return existed;
1197
+ }
1198
+ /** Write the index out now (used on shutdown). */
1199
+ flush() {
1200
+ if (this.saveTimer) {
1201
+ clearTimeout(this.saveTimer);
1202
+ this.saveTimer = null;
1203
+ }
1204
+ this.saveNow();
1205
+ }
1206
+ // ---- internals -----------------------------------------------------------
1207
+ seqs = new Map();
1208
+ nextSeq(runId) {
1209
+ const next = (this.seqs.get(runId) ?? this.rehydrateSeq(runId)) + 1;
1210
+ this.seqs.set(runId, next);
1211
+ return next;
1212
+ }
1213
+ /** After a restart the in-memory counter is empty while the run's NDJSON file
1214
+ * keeps the history. Restarting from 1 would collide with the seqs a client
1215
+ * already replayed — its `seq > maxSeq` dedup then silently drops every
1216
+ * resumed event, even across a reload (the frozen-transcript symptom class
1217
+ * of #424). One file read on the first post-restart append per run. */
1218
+ rehydrateSeq(runId) {
1219
+ let max = 0;
1220
+ for (const event of this.readEvents(runId)) {
1221
+ if (typeof event.seq === 'number' && event.seq > max)
1222
+ max = event.seq;
1223
+ }
1224
+ return max;
1225
+ }
1226
+ eventsPath(runId) {
1227
+ return join(this.dataDir, 'runs', `${runId}.ndjson`);
1228
+ }
1229
+ /** Same location `handoffPath()` in handoff.ts produces — inlined to keep
1230
+ * the store free of upward imports. */
1231
+ handoffPath(runId) {
1232
+ return join(this.dataDir, 'runs', `${runId}.handoff.md`);
1233
+ }
1234
+ /** Agent screenshots persisted by the run manager (see persistImage). */
1235
+ imagesDir(runId) {
1236
+ return join(this.dataDir, 'runs', `${runId}-images`);
1237
+ }
1238
+ touch(run) {
1239
+ this.scheduleSave();
1240
+ this.emit('run', run);
1241
+ }
1242
+ pruneOldRuns() {
1243
+ const all = this.listRuns();
1244
+ const stalePool = [
1245
+ ...all.filter((r) => !r.archived).slice(MAX_RUNS_KEPT),
1246
+ ...all.filter((r) => r.archived).slice(MAX_ARCHIVED_KEPT),
1247
+ ];
1248
+ for (const stale of stalePool) {
1249
+ this.runs.delete(stale.id);
1250
+ try {
1251
+ rmSync(this.eventsPath(stale.id), { force: true });
1252
+ rmSync(this.handoffPath(stale.id), { force: true });
1253
+ rmSync(this.imagesDir(stale.id), { recursive: true, force: true });
1254
+ }
1255
+ catch {
1256
+ // best effort
1257
+ }
1258
+ }
1259
+ }
1260
+ /** Debounced so token-usage updates don't rewrite the index per event. */
1261
+ scheduleSave() {
1262
+ if (this.saveTimer)
1263
+ return;
1264
+ this.saveTimer = setTimeout(() => {
1265
+ this.saveTimer = null;
1266
+ this.saveNow();
1267
+ }, 300);
1268
+ this.saveTimer.unref?.();
1269
+ }
1270
+ saveNow() {
1271
+ const indexPath = join(this.dataDir, 'runs.json');
1272
+ const tmpPath = `${indexPath}.tmp`;
1273
+ try {
1274
+ writeFileSync(tmpPath, JSON.stringify(this.listRuns(), null, 2), 'utf8');
1275
+ renameSync(tmpPath, indexPath);
1276
+ }
1277
+ catch (err) {
1278
+ const message = err instanceof Error ? err.message : String(err);
1279
+ console.error(`[xez] failed to save runs.json: ${message}`);
1280
+ }
1281
+ }
1282
+ }
1283
+ //# sourceMappingURL=store.js.map