@open-mercato/cezar 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +19 -11
  2. package/dist/agent-config/catalog.d.ts +1 -1
  3. package/dist/agent-config/files.d.ts +1 -1
  4. package/dist/agent-config/service.d.ts +1 -1
  5. package/dist/agent-config/validate.d.ts +1 -1
  6. package/dist/automations/coordinator.d.ts +26 -0
  7. package/dist/automations/coordinator.js +66 -0
  8. package/dist/automations/coordinator.js.map +1 -0
  9. package/dist/automations/github-poller.d.ts +74 -0
  10. package/dist/automations/github-poller.js +234 -0
  11. package/dist/automations/github-poller.js.map +1 -0
  12. package/dist/automations/scheduler.d.ts +42 -0
  13. package/dist/automations/scheduler.js +192 -0
  14. package/dist/automations/scheduler.js.map +1 -0
  15. package/dist/automations/store.d.ts +65 -0
  16. package/dist/automations/store.js +298 -0
  17. package/dist/automations/store.js.map +1 -0
  18. package/dist/automations/task-template.d.ts +18 -0
  19. package/dist/automations/task-template.js +95 -0
  20. package/dist/automations/task-template.js.map +1 -0
  21. package/dist/automations/types.d.ts +251 -0
  22. package/dist/automations/types.js +158 -0
  23. package/dist/automations/types.js.map +1 -0
  24. package/dist/config.d.ts +9 -111
  25. package/dist/contract/agent-config.d.ts +151 -0
  26. package/dist/contract/automations.d.ts +911 -0
  27. package/dist/contract/events.d.ts +45 -0
  28. package/dist/contract/github.d.ts +467 -0
  29. package/dist/contract/health.d.ts +96 -0
  30. package/dist/contract/index.d.ts +15 -0
  31. package/dist/contract/index.js +1421 -0
  32. package/dist/contract/projects.d.ts +182 -0
  33. package/dist/contract/repo.d.ts +234 -0
  34. package/dist/contract/runs.d.ts +1226 -0
  35. package/dist/contract/skills.d.ts +225 -0
  36. package/dist/contract/workflows.d.ts +212 -0
  37. package/dist/contract/workspace.d.ts +442 -0
  38. package/dist/core/agent-env.d.ts +1 -1
  39. package/dist/core/agent-runner.d.ts +12 -1
  40. package/dist/core/agent-runner.js +13 -0
  41. package/dist/core/agent-runner.js.map +1 -1
  42. package/dist/core/ask.d.ts +45 -124
  43. package/dist/core/ask.js +103 -13
  44. package/dist/core/ask.js.map +1 -1
  45. package/dist/core/claude-cli-runner.d.ts +2 -2
  46. package/dist/core/claude-cli-runner.js +52 -9
  47. package/dist/core/claude-cli-runner.js.map +1 -1
  48. package/dist/core/claude-ui-mapper.d.ts +1 -1
  49. package/dist/core/codex-app-server-runner.d.ts +1 -1
  50. package/dist/core/codex-app-server-runner.js +55 -3
  51. package/dist/core/codex-app-server-runner.js.map +1 -1
  52. package/dist/core/codex-app-server-transport.d.ts +6 -1
  53. package/dist/core/codex-app-server-transport.js +12 -3
  54. package/dist/core/codex-app-server-transport.js.map +1 -1
  55. package/dist/core/codex-model-catalog.d.ts +1 -1
  56. package/dist/core/codex-ui-mapper.d.ts +5 -1
  57. package/dist/core/codex-ui-mapper.js +74 -17
  58. package/dist/core/codex-ui-mapper.js.map +1 -1
  59. package/dist/core/model-identity.d.ts +1 -1
  60. package/dist/core/model-presets.d.ts +2 -2
  61. package/dist/core/model-presets.js +1 -1
  62. package/dist/core/opencode-server-runner.d.ts +2 -2
  63. package/dist/core/opencode-ui-mapper.d.ts +4 -1
  64. package/dist/core/opencode-ui-mapper.js +45 -7
  65. package/dist/core/opencode-ui-mapper.js.map +1 -1
  66. package/dist/core/provider-auth.d.ts +62 -0
  67. package/dist/core/provider-auth.js +290 -0
  68. package/dist/core/provider-auth.js.map +1 -0
  69. package/dist/core/provider-availability.d.ts +3 -0
  70. package/dist/core/provider-availability.js +13 -0
  71. package/dist/core/provider-availability.js.map +1 -0
  72. package/dist/core/runner-factory.d.ts +1 -1
  73. package/dist/core/runner-model-catalog.d.ts +1 -1
  74. package/dist/core/tool-display.d.ts +1 -1
  75. package/dist/core/ui-events.d.ts +1 -1
  76. package/dist/core/ui-events.js +1 -1
  77. package/dist/git-diff-base.d.ts +60 -0
  78. package/dist/git-diff-base.js +54 -0
  79. package/dist/git-diff-base.js.map +1 -0
  80. package/dist/git-worktree.d.ts +37 -6
  81. package/dist/git-worktree.js +39 -9
  82. package/dist/git-worktree.js.map +1 -1
  83. package/dist/handoff.d.ts +2 -2
  84. package/dist/handoff.js +1 -1
  85. package/dist/index.js +65 -8
  86. package/dist/index.js.map +1 -1
  87. package/dist/paths.d.ts +16 -1
  88. package/dist/paths.js +24 -1
  89. package/dist/paths.js.map +1 -1
  90. package/dist/planner.d.ts +2 -2
  91. package/dist/planner.js +1 -1
  92. package/dist/planner.js.map +1 -1
  93. package/dist/release/manifests.d.ts +81 -0
  94. package/dist/release/manifests.js +74 -0
  95. package/dist/release/manifests.js.map +1 -0
  96. package/dist/release/snapshot.d.ts +9 -23
  97. package/dist/release/snapshot.js +9 -22
  98. package/dist/release/snapshot.js.map +1 -1
  99. package/dist/release/stable.d.ts +15 -29
  100. package/dist/release/stable.js +14 -27
  101. package/dist/release/stable.js.map +1 -1
  102. package/dist/runs/auto-name.d.ts +1 -1
  103. package/dist/runs/retention.d.ts +1 -1
  104. package/dist/runs/store.d.ts +142 -336
  105. package/dist/runs/store.js +136 -9
  106. package/dist/runs/store.js.map +1 -1
  107. package/dist/runs/ui-event-sink.d.ts +1 -1
  108. package/dist/server/app-type.d.ts +21 -0
  109. package/dist/server/app-type.js +2 -0
  110. package/dist/server/app-type.js.map +1 -0
  111. package/dist/server/capabilities.d.ts +6 -5
  112. package/dist/server/capabilities.js +11 -0
  113. package/dist/server/capabilities.js.map +1 -1
  114. package/dist/server/forge/github.d.ts +58 -151
  115. package/dist/server/forge/github.js +530 -6
  116. package/dist/server/forge/github.js.map +1 -1
  117. package/dist/server/forge/index.d.ts +10 -3
  118. package/dist/server/forge/index.js +14 -1
  119. package/dist/server/forge/index.js.map +1 -1
  120. package/dist/server/forge/types.d.ts +86 -1
  121. package/dist/server/git-changes.d.ts +8 -8
  122. package/dist/server/git-changes.js +14 -17
  123. package/dist/server/git-changes.js.map +1 -1
  124. package/dist/server/git.d.ts +2 -0
  125. package/dist/server/git.js +9 -0
  126. package/dist/server/git.js.map +1 -1
  127. package/dist/server/github.d.ts +3 -3
  128. package/dist/server/github.js +1 -1
  129. package/dist/server/github.js.map +1 -1
  130. package/dist/server/pr.d.ts +2 -2
  131. package/dist/server/project-context.d.ts +20 -3
  132. package/dist/server/project-context.js +30 -1
  133. package/dist/server/project-context.js.map +1 -1
  134. package/dist/server/provider-action-gate.d.ts +8 -0
  135. package/dist/server/provider-action-gate.js +54 -0
  136. package/dist/server/provider-action-gate.js.map +1 -0
  137. package/dist/server/provider-auth-runtime.d.ts +21 -0
  138. package/dist/server/provider-auth-runtime.js +66 -0
  139. package/dist/server/provider-auth-runtime.js.map +1 -0
  140. package/dist/server/server.d.ts +11142 -47
  141. package/dist/server/server.js +1714 -806
  142. package/dist/server/server.js.map +1 -1
  143. package/dist/server/validators.d.ts +97 -0
  144. package/dist/server/validators.js +86 -0
  145. package/dist/server/validators.js.map +1 -0
  146. package/dist/server/ws.d.ts +2 -2
  147. package/dist/server/ws.js +2 -2
  148. package/dist/server/ws.js.map +1 -1
  149. package/dist/server-install/engine.d.ts +1 -1
  150. package/dist/server-install/platforms/macosx-ngrok.d.ts +1 -1
  151. package/dist/server-install/platforms/ubuntu-vps.d.ts +16 -1
  152. package/dist/server-install/platforms/ubuntu-vps.js +83 -1
  153. package/dist/server-install/platforms/ubuntu-vps.js.map +1 -1
  154. package/dist/server-install/state.d.ts +1 -1
  155. package/dist/server-install/steps.d.ts +2 -2
  156. package/dist/server-install/strategies.d.ts +1 -1
  157. package/dist/server-install/types.d.ts +38 -424
  158. package/dist/server-install/ui.d.ts +1 -1
  159. package/dist/skills-banner.d.ts +7 -6
  160. package/dist/skills-banner.js +7 -6
  161. package/dist/skills-banner.js.map +1 -1
  162. package/dist/skills-remote.d.ts +2 -2
  163. package/dist/skills-update.d.ts +88 -0
  164. package/dist/skills-update.js +392 -0
  165. package/dist/skills-update.js.map +1 -0
  166. package/dist/skills.d.ts +2 -0
  167. package/dist/skills.js +3 -2
  168. package/dist/skills.js.map +1 -1
  169. package/dist/todos.d.ts +2 -28
  170. package/dist/todos.js +1 -1
  171. package/dist/ui-state.d.ts +16 -2
  172. package/dist/ui-state.js +14 -1
  173. package/dist/ui-state.js.map +1 -1
  174. package/dist/workflows/load.d.ts +1 -1
  175. package/dist/workflows/run.d.ts +53 -12
  176. package/dist/workflows/run.js +436 -64
  177. package/dist/workflows/run.js.map +1 -1
  178. package/dist/workflows/types.d.ts +59 -224
  179. package/dist/workflows/types.js +14 -0
  180. package/dist/workflows/types.js.map +1 -1
  181. package/dist/workspace/config.d.ts +68 -206
  182. package/dist/workspace/config.js +145 -14
  183. package/dist/workspace/config.js.map +1 -1
  184. package/dist/workspace/migrations.d.ts +1 -1
  185. package/dist/workspace/migrations.js +19 -4
  186. package/dist/workspace/migrations.js.map +1 -1
  187. package/dist/workspace/projects.d.ts +8 -2
  188. package/dist/workspace/projects.js +9 -3
  189. package/dist/workspace/projects.js.map +1 -1
  190. package/dist/workspace/semaphore.d.ts +6 -0
  191. package/dist/workspace/semaphore.js +14 -1
  192. package/dist/workspace/semaphore.js.map +1 -1
  193. package/dist/workspace/ui-state.d.ts +13 -3
  194. package/dist/workspace/ui-state.js +15 -5
  195. package/dist/workspace/ui-state.js.map +1 -1
  196. package/package.json +17 -39
  197. package/scripts/inline-contract.mjs +112 -0
  198. package/scripts/mock-claude.mjs +39 -1
  199. package/scripts/sync-readme.mjs +20 -0
  200. package/web/dist/assets/alert-dialog-ghffK6g9.js +1 -0
  201. package/web/dist/assets/arrow-down-Cz8qb71e.js +1 -0
  202. package/web/dist/assets/arrow-left-wcYKQXI_.js +1 -0
  203. package/web/dist/assets/bundle-mjs-BT31bpU6.js +1 -0
  204. package/web/dist/assets/centered-state-ZtnFHA-n.js +43 -0
  205. package/web/dist/assets/chevron-right-G4caF2QV.js +1 -0
  206. package/web/dist/assets/{chunk-BO2N2NFS-DtrdTCWa.js → chunk-BO2N2NFS-DE6qKn3r.js} +8 -8
  207. package/web/dist/assets/circle-check--KvBxXWp.js +1 -0
  208. package/web/dist/assets/circle-x-C3EFZ3xf.js +1 -0
  209. package/web/dist/assets/collapsible-B0JVzd0B.js +1 -0
  210. package/web/dist/assets/commit-list-wm6DPHNs.js +1 -0
  211. package/web/dist/assets/compare-variants-DbdP4Y5T.js +1 -0
  212. package/web/dist/assets/{core-DePtBHcl.js → core-BCsw8oQw.js} +1 -1
  213. package/web/dist/assets/diff-Bizc96qe.js +3 -0
  214. package/web/dist/assets/diff-stat-Dyfg3ltF.js +1 -0
  215. package/web/dist/assets/{diff-view-DCeSegKk.js → diff-view-CG8ek6tC.js} +4 -4
  216. package/web/dist/assets/dropdown-menu-C1Dlf35P.js +1 -0
  217. package/web/dist/assets/editable-title-5rC_QRBZ.js +1 -0
  218. package/web/dist/assets/{ellipsis-vertical-CNnSLn5a.js → ellipsis-vertical-DpGTRqC2.js} +1 -1
  219. package/web/dist/assets/{file-D4GBh1ao.js → file-DlUGpUlb.js} +1 -1
  220. package/web/dist/assets/folder-BAY6Vddy.js +1 -0
  221. package/web/dist/assets/{git-pull-request-DnR8Xh4_.js → git-pull-request-agOG0iGM.js} +1 -1
  222. package/web/dist/assets/git-toolbar-DGiNMmtY.js +1 -0
  223. package/web/dist/assets/github-Vp7lxYGQ.js +1 -0
  224. package/web/dist/assets/highlighted-body-OFNGDK62-BL-MfFgl.js +1 -0
  225. package/web/dist/assets/highlighter-CdHqIwFr.js +3 -0
  226. package/web/dist/assets/image-preview-BWGk5fPX.js +1 -0
  227. package/web/dist/assets/index-DSxyJpuK.css +2 -0
  228. package/web/dist/assets/index-DiawI6RX.js +6 -0
  229. package/web/dist/assets/lib-BQXq3kEf.js +1 -0
  230. package/web/dist/assets/lib-BxQXEXDF.js +1 -0
  231. package/web/dist/assets/markdown-BGBOGCA_.js +2 -0
  232. package/web/dist/assets/mermaid-GHXKKRXX-C4jxqUUb.js +1 -0
  233. package/web/dist/assets/new-task-form-BpAw_Dks.js +1 -0
  234. package/web/dist/assets/pill-Dvs-P0xy.js +1 -0
  235. package/web/dist/assets/project-router-Cx8NPoqn.js +1 -0
  236. package/web/dist/assets/prompt-templates-CSBYBR1I.js +15 -0
  237. package/web/dist/assets/react-runtime-CCIEwYL0.js +9 -0
  238. package/web/dist/assets/{refresh-cw-BT8E96Tm.js → refresh-cw-BoGdAykg.js} +1 -1
  239. package/web/dist/assets/repo-git-PmX-BNMo.js +1 -0
  240. package/web/dist/assets/rolldown-runtime-QTnfLwEv.js +1 -0
  241. package/web/dist/assets/run-diff-EcOkf3ti.js +3 -0
  242. package/web/dist/assets/run-header-BGiv6_qX.js +1 -0
  243. package/web/dist/assets/{search-x-BDjaMrzG.js → search-x-X3EpVhgs.js} +1 -1
  244. package/web/dist/assets/skill-empty-hint-CZLALx6l.js +1 -0
  245. package/web/dist/assets/skills-BiWfHmCR.js +1 -0
  246. package/web/dist/assets/skills-CC91ouGH.js +1 -0
  247. package/web/dist/assets/sparkles-Dt4q7pbG.js +1 -0
  248. package/web/dist/assets/{square-terminal-Ckjjn0vb.js → square-terminal-hwBL8XsC.js} +1 -1
  249. package/web/dist/assets/tab-link-DIlR5T07.js +1 -0
  250. package/web/dist/assets/task-changes-XkHDxzQA.js +1 -0
  251. package/web/dist/assets/task-commits-BTTA5k5T.js +1 -0
  252. package/web/dist/assets/task-files-Da8_4C9g.js +2 -0
  253. package/web/dist/assets/task-thread-DoNXUsHr.js +9 -0
  254. package/web/dist/assets/textarea-CUdokhkv.js +1 -0
  255. package/web/dist/assets/thread-loading-B8q8ukXJ.js +1 -0
  256. package/web/dist/assets/{trash-2-DAyZ0VSi.js → trash-2-DhVIdWv8.js} +1 -1
  257. package/web/dist/assets/{triangle-alert-Uy8rokUh.js → triangle-alert-XYT0DGCI.js} +1 -1
  258. package/web/dist/assets/{upload-CDdyPPo_.js → upload-BKjN59-1.js} +1 -1
  259. package/web/dist/assets/use-desktop-Bf5LS_P4.js +1 -0
  260. package/web/dist/assets/use-submit-shortcut-CzVyoAMd.js +1 -0
  261. package/web/dist/assets/utils-DsLuRXwV.js +64 -0
  262. package/web/dist/assets/workflows-o2QU_pvU.js +11 -0
  263. package/web/dist/assets/zoomable-image-7WyUiaVD.js +1 -0
  264. package/web/dist/index.html +32 -13
  265. package/scripts/dev.mjs +0 -74
  266. package/scripts/release-snapshot.mjs +0 -130
  267. package/scripts/release.mjs +0 -119
  268. package/web/dist/assets/arrow-left-CnSdp92h.js +0 -1
  269. package/web/dist/assets/bundle-mjs-BegPhL5c.js +0 -1
  270. package/web/dist/assets/centered-state-DOsTcJZF.js +0 -43
  271. package/web/dist/assets/commit-list-BrxBEzRK.js +0 -1
  272. package/web/dist/assets/compare-variants-Bv2c9ORF.js +0 -1
  273. package/web/dist/assets/dist-B6XQOhgM.js +0 -1
  274. package/web/dist/assets/git-toolbar-pIC1ubPM.js +0 -1
  275. package/web/dist/assets/github-DTTSG0ht.js +0 -1
  276. package/web/dist/assets/highlighted-body-OFNGDK62-BuFJwunK.js +0 -1
  277. package/web/dist/assets/image-preview-DEhW0TFz.js +0 -1
  278. package/web/dist/assets/index-Cijmlz7q.js +0 -30
  279. package/web/dist/assets/index-DzroxqBG.css +0 -2
  280. package/web/dist/assets/lib-DPEZDBUN.js +0 -1
  281. package/web/dist/assets/lib-uHpeUPzK.js +0 -1
  282. package/web/dist/assets/mermaid-GHXKKRXX-CN2zQue0.js +0 -1
  283. package/web/dist/assets/open-mercato-toBr6SOa.svg +0 -11
  284. package/web/dist/assets/project-router-BMx4e8m-.js +0 -1
  285. package/web/dist/assets/repo-git-BusYfa3t.js +0 -1
  286. package/web/dist/assets/run-diff-wOJ3odA-.js +0 -3
  287. package/web/dist/assets/run-header-Cg5incil.js +0 -1
  288. package/web/dist/assets/skill-empty-hint-DTUSp6Vl.js +0 -1
  289. package/web/dist/assets/skills-PpP2owbM.js +0 -1
  290. package/web/dist/assets/tab-link-F3uQrdm2.js +0 -1
  291. package/web/dist/assets/task-changes-iD9kS79I.js +0 -1
  292. package/web/dist/assets/task-commits-COkLaUri.js +0 -1
  293. package/web/dist/assets/task-files-Ckttz7XH.js +0 -2
  294. package/web/dist/assets/task-thread-CQD91VPt.js +0 -9
  295. package/web/dist/assets/textarea-Bj_LCreg.js +0 -1
  296. package/web/dist/assets/use-desktop-DUh0STzq.js +0 -3
  297. package/web/dist/assets/utils-B8s5qIcK.js +0 -1
  298. package/web/dist/assets/workflows-BFk8C0co.js +0 -11
  299. package/web/dist/assets/zoomable-image-PEDOqyLQ.js +0 -1
  300. /package/web/{open-mercato.svg → dist/open-mercato.svg} +0 -0
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
- import { parseAskMarker, stripAskMarker } from '../core/ask.js';
5
+ import { parseAskMarkerResult, stripAskMarker, } from '../core/ask.js';
6
6
  import { onUsage, registerRunProcess, unregisterRunProcess } from '../core/process-usage.js';
7
7
  import { createRunner } from '../core/runner-factory.js';
8
8
  import { modelConflictsWithRunner } from '../core/model-presets.js';
@@ -14,7 +14,7 @@ import { materializeSkillDir } from '../skills-remote.js';
14
14
  import { seedAgentConfigLocalLayer } from '../agent-config/seed.js';
15
15
  import { loadConfig, resolveWorktreeRetention } from '../config.js';
16
16
  import { autosaveCommit, createWorktree, resolveBaseRef, worktreeDiff, worktreeShortstat } from '../git-worktree.js';
17
- import { getRepoInfo } from '../server/git.js';
17
+ import { getHeadCommit, getRepoInfo } from '../server/git.js';
18
18
  import { loadWorkflows } from './load.js';
19
19
  import { reclaimWorktrees, rematerializeReclaimedWorktree } from '../runs/retention.js';
20
20
  import { extractTaskRefs, refineTaskRefs, titleRefNumber } from '../runs/task-refs.js';
@@ -77,6 +77,18 @@ function emitAskRequested(sink, ask) {
77
77
  sink.handle({ type: 'ask.requested', requestId, questions: ask.questions });
78
78
  return requestId;
79
79
  }
80
+ /** A persisted, non-fatal explanation for protocol-shaped text that could not
81
+ * become an ask card. Never include the raw payload in this diagnostic. */
82
+ function askMarkerRejection(result) {
83
+ if (result.kind === 'invalid-json') {
84
+ return 'structured question ignored — CEZ:ASK payload is not valid JSON';
85
+ }
86
+ if (result.kind !== 'invalid-structure')
87
+ return undefined;
88
+ const issue = result.issues[0];
89
+ const location = issue?.path.length ? ` at ${issue.path.join('.')}` : '';
90
+ return `structured question ignored — CEZ:ASK payload failed validation${location}${issue ? `: ${issue.message}` : ''}`;
91
+ }
80
92
  /** Periodic "cezar autosave" commit in the task worktree (spec 006). */
81
93
  export const AUTOSAVE_INTERVAL_MS = 90_000;
82
94
  /** The periodic autosave timer is opt-in (#471): off, a task branch carries only the
@@ -89,6 +101,7 @@ export function periodicAutosaveEnabled(env = process.env) {
89
101
  /** Safety cap on autonomous auto-continues per run — stops a stuck agent from nudging forever. */
90
102
  const MAX_AUTO_CONTINUES = 40;
91
103
  const AUTONOMOUS_NUDGE = 'Continue working autonomously until the task is fully complete. Do not ask me for confirmation or clarification — make reasonable assumptions and proceed. When everything is done, end the session with your done signal.';
104
+ const MONITORING_WAKE_NUDGE = 'Re-check the downstream work you were monitoring. Continue toward the task goal; emit CEZ:MONITORING again only if it is still pending.';
92
105
  /**
93
106
  * The effective "extra" system prompt for a run (spec §protocol v2, R2 2.3):
94
107
  * the per-run override (`POST /api/runs` `systemPrompt`) REPLACES the
@@ -171,6 +184,7 @@ const VARIANT_HINTS = {
171
184
  B: 'Approach hint: prefer the minimal, surgical change.',
172
185
  C: 'Approach hint: prefer a thorough, structural approach.',
173
186
  };
187
+ const RESTART_CONTINUATION_PROMPT = 'The cezar process restarted while you were working on this task. Read the handoff file (CEZ_HANDOFF_FILE) to recover context, then continue the task from where you left off.';
174
188
  /**
175
189
  * The mini workflow engine: executes a `WorkflowDef` against a repo, one step
176
190
  * at a time, persisting every event to the RunStore (which the SSE endpoints
@@ -200,7 +214,13 @@ export class RunManager {
200
214
  // timeout already bounds how long a session can sit open. Invariant:
201
215
  // `waiting ⊆ active` — always cleared together via dropActive().
202
216
  waiting = new Set();
217
+ /** Durable monitoring subset. Only the configured number receives the waiting-slot exemption. */
218
+ monitoring = new Set();
203
219
  pendingJobs = new Map();
220
+ /** Interrupted agent turns recovered after a process restart. Unlike an
221
+ * explicit user Continue, these are bulk scheduler work and must re-enter
222
+ * through `pump()` so both workspace and per-project caps are honored. */
223
+ pendingContinuations = new Map();
204
224
  /** Per-run image counter behind `pasted-<n>` / `screenshot-<n>` (#472). Lives on
205
225
  * the manager rather than the `ActiveRun` so a *queued* run — which has no
206
226
  * `ActiveRun` at all — can persist attachments. Seeded lazily from disk. */
@@ -260,8 +280,9 @@ export class RunManager {
260
280
  dispose() {
261
281
  this.offUsage();
262
282
  this.offSemaphore();
263
- for (const state of this.active.values()) {
283
+ for (const [runId, state] of this.active) {
264
284
  this.clearIdleTimer(state);
285
+ this.clearMonitoringWakeTimer(state, runId);
265
286
  this.clearAutosaveTimer(state);
266
287
  state.releaseRepoRoot?.();
267
288
  state.releaseRepoRoot = undefined;
@@ -271,6 +292,7 @@ export class RunManager {
271
292
  this.starting.clear();
272
293
  this.queue.length = 0;
273
294
  this.pendingJobs.clear();
295
+ this.pendingContinuations.clear();
274
296
  this.memoryPausing.clear();
275
297
  this.lastNamerKey.clear();
276
298
  }
@@ -354,6 +376,9 @@ export class RunManager {
354
376
  // auto-nudge reads `input.autonomous` (`execute`), but the record is the
355
377
  // only source those after-the-fact consumers have.
356
378
  autonomous: input.autonomous === true,
379
+ // Persist the explicit opt-out so queued-run restart recovery and the
380
+ // session Git routes can distinguish it from a removed isolated worktree.
381
+ worktree: !group && input.worktree === false ? false : undefined,
357
382
  groupId: group?.groupId,
358
383
  variant: group?.variant,
359
384
  steps: workflow.steps.map((s) => ({ id: s.id, name: s.name ?? s.id, kind: stepKind(s) })),
@@ -405,7 +430,7 @@ export class RunManager {
405
430
  return VARIANT_LETTERS.slice(0, Math.min(Math.max(count, 1), VARIANT_LETTERS.length)).map((variant) => {
406
431
  const hint = VARIANT_HINTS[variant];
407
432
  const task = hint ? `${input.task}\n\n${hint}` : input.task;
408
- return this.startRun(workflow, { ...input, task }, { groupId, variant });
433
+ return this.startRun(workflow, { ...input, task, worktree: undefined }, { groupId, variant });
409
434
  });
410
435
  }
411
436
  /**
@@ -418,7 +443,9 @@ export class RunManager {
418
443
  * `maxParallel`, including when other projects saturate the cap.
419
444
  */
420
445
  busySlots() {
421
- return this.active.size + this.starting.size - this.waiting.size;
446
+ const ordinaryWaiting = this.waiting.size - this.monitoring.size;
447
+ const exemptMonitoring = Math.min(this.monitoring.size, this.semaphore.maxMonitoringSessions());
448
+ return this.active.size + this.starting.size - ordinaryWaiting - exemptMonitoring;
422
449
  }
423
450
  /** Epoch ms of this manager's oldest queued run (the semaphore's fairness
424
451
  * key when a freed slot is broadcast), or null when nothing is queued.
@@ -454,6 +481,7 @@ export class RunManager {
454
481
  * root (spec 006 degradation rule), which is always the tighter bound.
455
482
  */
456
483
  async pump() {
484
+ this.reconcileMonitoringWakeTimers();
457
485
  // A pump requested while one is in flight can't just be dropped: the
458
486
  // in-flight pass may already have read capacity (it awaits `getRepoInfo`
459
487
  // before the first check), so a slot freed in that window would be lost
@@ -483,10 +511,28 @@ export class RunManager {
483
511
  if (!runId)
484
512
  break;
485
513
  const job = this.pendingJobs.get(runId);
514
+ const continuation = this.pendingContinuations.get(runId);
486
515
  this.pendingJobs.delete(runId);
487
- if (!job)
516
+ this.pendingContinuations.delete(runId);
517
+ if (!job && !continuation)
488
518
  continue;
489
519
  this.starting.add(runId);
520
+ if (continuation) {
521
+ const hydrated = this.hydrateQueuedContinuation(runId, continuation);
522
+ void this.runContinuation(runId, hydrated.stepId, hydrated.sessionId, hydrated.backend, hydrated.prompt, hydrated.images, hydrated.persistedImages, hydrated.persistedAttachments).catch((err) => {
523
+ const message = err instanceof Error ? err.message : String(err);
524
+ this.store.updateRun(runId, {
525
+ status: 'failed',
526
+ error: `continue crashed: ${message}`,
527
+ finishedAt: new Date().toISOString(),
528
+ });
529
+ this.starting.delete(runId);
530
+ this.dropActive(runId);
531
+ });
532
+ continue;
533
+ }
534
+ if (!job)
535
+ continue;
490
536
  // Rebuild the prompt from the store at the last instant (#472), so an edit
491
537
  // or a stacked message that landed while the run waited is honored. Entered
492
538
  // in the same synchronous tick as the `pendingJobs.delete` above, so no
@@ -532,6 +578,35 @@ export class RunManager {
532
578
  .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
533
579
  for (const run of live) {
534
580
  if (run.status === 'queued') {
581
+ // A second restart can find a continuation that the first restart put
582
+ // behind a capacity gate. Its executable details are process-local,
583
+ // but the queued continue step and preceding provider session are
584
+ // durable, so reconstruct that job before considering workflow revival.
585
+ const queuedContinuation = [...run.steps]
586
+ .reverse()
587
+ .find((step) => step.status === 'pending' && step.id.startsWith('continue-'));
588
+ const sessionStep = queuedContinuation
589
+ ? [...run.steps]
590
+ .reverse()
591
+ .find((step) => step.id !== queuedContinuation.id && step.sessionId)
592
+ : undefined;
593
+ if (queuedContinuation && sessionStep?.sessionId) {
594
+ const backend = run.runner ?? 'claude';
595
+ const sessionBackend = sessionStep.backend ?? backend;
596
+ this.pendingContinuations.set(run.id, {
597
+ stepId: queuedContinuation.id,
598
+ sessionId: sessionBackend === backend ? sessionStep.sessionId : undefined,
599
+ backend,
600
+ prompt: RESTART_CONTINUATION_PROMPT,
601
+ images: [],
602
+ });
603
+ this.queue.push(run.id);
604
+ this.store.appendEvent(run.id, {
605
+ type: 'lifecycle',
606
+ message: 'cezar restarted — interrupted continuation re-queued',
607
+ });
608
+ continue;
609
+ }
535
610
  const workflow = await this.reviveWorkflow(run);
536
611
  if (workflow) {
537
612
  // Re-apply the inbox ceiling (#471). `execute()` gates again at spawn time, so the
@@ -559,6 +634,9 @@ export class RunManager {
559
634
  // recovered queued autonomous run would run non-autonomously (no
560
635
  // auto-nudge) and later wrongly park at `review`.
561
636
  autonomous: run.autonomous,
637
+ // Preserve an explicit worktree opt-out across a queued restart.
638
+ // Missing on older records means the default isolated mode.
639
+ worktree: run.worktree,
562
640
  }),
563
641
  });
564
642
  this.queue.push(run.id);
@@ -605,8 +683,8 @@ export class RunManager {
605
683
  currentStepId: undefined,
606
684
  });
607
685
  const resumed = this.continueRun(run.id, {
608
- text: 'The cezar process restarted while you were working on this task. Read the handoff file (CEZ_HANDOFF_FILE) to recover context, then continue the task from where you left off.',
609
- });
686
+ text: RESTART_CONTINUATION_PROMPT,
687
+ }, true);
610
688
  this.store.appendEvent(run.id, {
611
689
  type: 'lifecycle',
612
690
  message: resumed.ok
@@ -618,10 +696,12 @@ export class RunManager {
618
696
  }
619
697
  /** The persisted definition when it looks sane, else the catalog by name. */
620
698
  async reviveWorkflow(run) {
699
+ // "Looks sane" is the STORE's job now: it parses `workflowDef` against the definition schema
700
+ // and `.catch`es a def that no longer fits to `undefined`, so anything present here already
701
+ // has the `steps` array the old inline `Array.isArray` check was asking for.
621
702
  const def = run.workflowDef;
622
- if (def && Array.isArray(def.steps)) {
703
+ if (def)
623
704
  return def;
624
- }
625
705
  const { workflows } = await loadWorkflows(this.repoRoot);
626
706
  return workflows.find((w) => w.name === run.workflow) ?? null;
627
707
  }
@@ -632,6 +712,9 @@ export class RunManager {
632
712
  if (state)
633
713
  state.releaseRepoRoot = undefined;
634
714
  this.waiting.delete(runId);
715
+ this.monitoring.delete(runId);
716
+ if (state)
717
+ this.clearMonitoringWakeTimer(state, runId);
635
718
  this.active.delete(runId);
636
719
  this.memoryPausing.delete(runId);
637
720
  this.lastNamerKey.delete(runId);
@@ -723,6 +806,7 @@ export class RunManager {
723
806
  if (queuedAt >= 0) {
724
807
  this.queue.splice(queuedAt, 1);
725
808
  this.pendingJobs.delete(runId);
809
+ this.pendingContinuations.delete(runId);
726
810
  this.store.updateRun(runId, { status: 'cancelled', finishedAt: new Date().toISOString() });
727
811
  this.store.appendEvent(runId, { type: 'lifecycle', message: 'cancelled while queued' });
728
812
  return true;
@@ -763,35 +847,13 @@ export class RunManager {
763
847
  .map((part) => part.trim())
764
848
  .filter((part) => part.length > 0)
765
849
  .join('\n\n');
766
- const readImages = (urls, kind) => {
767
- const images = [];
768
- for (const url of urls) {
769
- const name = url.split('/').pop();
770
- if (!name || name.includes('..') || name.includes('/') || name.includes('\\'))
771
- continue;
772
- try {
773
- const data = readFileSync(join(this.dataDir, 'runs', `${runId}-images`, name));
774
- images.push({
775
- type: 'image',
776
- source: { type: 'base64', media_type: mediaTypeFor(name), data: data.toString('base64') },
777
- });
778
- }
779
- catch {
780
- // Degrade, never fail the boot (AGENTS.md): the user deleted `.ai/cezar/`
781
- // or the file is unreadable — start with the text and say which image went.
782
- this.store.appendEvent(runId, {
783
- type: 'note',
784
- message: `${kind} attachment ${name} could not be read — starting without it`,
785
- });
786
- }
787
- }
788
- return images;
789
- };
790
850
  // Keep the original in-memory blocks for a live process (including the
791
851
  // best-effort case where persistence failed). Recovery has no such copy,
792
852
  // so rebuild it from the durable task-image URLs.
793
- const images = input.images?.length ? input.images : readImages(run.taskImages ?? [], 'task');
794
- const stackedImages = readImages(stack.flatMap((m) => m.images ?? []), 'queued');
853
+ const images = input.images?.length
854
+ ? input.images
855
+ : this.readPersistedImages(runId, run.taskImages ?? [], 'task').blocks;
856
+ const stackedImages = this.readPersistedImages(runId, stack.flatMap((m) => m.images ?? []), 'queued').blocks;
795
857
  return {
796
858
  ...input,
797
859
  task,
@@ -799,15 +861,67 @@ export class RunManager {
799
861
  ...(stackedImages.length ? { stackedImages } : { stackedImages: undefined }),
800
862
  };
801
863
  }
864
+ /** Apply edits and messages made while a restart continuation waits for
865
+ * capacity. The durable record remains the source of truth, just as it is for
866
+ * an ordinary queued workflow (#472), so a second restart reconstructs and
867
+ * hydrates the same amendments instead of dropping them. */
868
+ hydrateQueuedContinuation(runId, continuation) {
869
+ const run = this.store.getRun(runId);
870
+ if (!run) {
871
+ return { ...continuation, persistedImages: [], persistedAttachments: [] };
872
+ }
873
+ const stack = run.queuedMessages ?? [];
874
+ const amendedTask = [run.task, ...stack.map((message) => message.text)]
875
+ .map((part) => part.trim())
876
+ .filter((part) => part.length > 0)
877
+ .join('\n\n');
878
+ const prompt = amendedTask
879
+ ? `${continuation.prompt}\n\nCurrent task and queued updates:\n\n${amendedTask}`
880
+ : continuation.prompt;
881
+ const persisted = this.readPersistedImages(runId, stack.flatMap((message) => message.images ?? []), 'queued');
882
+ return {
883
+ ...continuation,
884
+ prompt,
885
+ persistedImages: persisted.blocks,
886
+ persistedAttachments: persisted.attachments,
887
+ };
888
+ }
889
+ readPersistedImages(runId, urls, kind) {
890
+ const blocks = [];
891
+ const attachments = [];
892
+ for (const url of urls) {
893
+ const name = url.split('/').pop();
894
+ if (!name || name.includes('..') || name.includes('/') || name.includes('\\'))
895
+ continue;
896
+ const path = join(this.dataDir, 'runs', `${runId}-images`, name);
897
+ try {
898
+ const data = readFileSync(path);
899
+ blocks.push({
900
+ type: 'image',
901
+ source: { type: 'base64', media_type: mediaTypeFor(name), data: data.toString('base64') },
902
+ });
903
+ attachments.push({ name, url, path });
904
+ }
905
+ catch {
906
+ // Degrade, never fail the boot (AGENTS.md): the user deleted `.ai/cezar/`
907
+ // or the file is unreadable — start with the text and say which image went.
908
+ this.store.appendEvent(runId, {
909
+ type: 'note',
910
+ message: `${kind} attachment ${name} could not be read — starting without it`,
911
+ });
912
+ }
913
+ }
914
+ return { blocks, attachments };
915
+ }
802
916
  /**
803
917
  * Still waiting for a slot? Checked against the engine's own queue rather than
804
918
  * the record's `status` (#472): the record is written by `execute()` a tick
805
919
  * after `pump()` dequeues, so a status read can see `queued` for a run that has
806
- * already started. `pendingJobs` is deleted synchronously at dequeue, so it is
807
- * the authoritative answer for "can this prompt still be amended".
920
+ * already started. The pending maps are deleted synchronously at dequeue, so
921
+ * they are the authoritative answer for "can this prompt still be amended".
808
922
  */
809
923
  isQueued(runId) {
810
- return this.pendingJobs.has(runId);
924
+ return this.pendingJobs.has(runId) || this.pendingContinuations.has(runId);
811
925
  }
812
926
  /** Split `ContentBlock[]` into the persisted shape a stacked message holds. */
813
927
  toQueuedMessage(runId, content) {
@@ -987,6 +1101,18 @@ export class RunManager {
987
1101
  * then offers "Continue" instead.
988
1102
  */
989
1103
  sendMessage(runId, content) {
1104
+ const delivered = this.deliverMessage(runId, content, true);
1105
+ if (delivered) {
1106
+ const state = this.active.get(runId);
1107
+ if (state)
1108
+ state.monitoringWakeups = 0;
1109
+ this.store.updateRun(runId, { monitoringWakeCapReached: undefined });
1110
+ }
1111
+ return delivered;
1112
+ }
1113
+ /** Shared live-session delivery. Synthetic scheduler prompts reuse lifecycle
1114
+ * bookkeeping without masquerading as user-authored transcript messages. */
1115
+ deliverMessage(runId, content, userAuthored) {
990
1116
  const state = this.active.get(runId);
991
1117
  if (!state?.session?.open || state.cancelled)
992
1118
  return false;
@@ -997,27 +1123,32 @@ export class RunManager {
997
1123
  // Persist the attached images so the thread can render them (not just count them) — the same
998
1124
  // on-disk store + `/images/` route the agent's own screenshots use. `pasted` prefix marks
999
1125
  // these as user attachments (vs. agent tool screenshots) on disk (#357).
1000
- const persisted = content
1126
+ const persisted = userAuthored ? content
1001
1127
  .filter((b) => b.type === 'image')
1002
1128
  .map((b) => this.persistImage(runId, b.source.media_type, b.source.data, 'pasted'))
1003
- .filter((saved) => saved !== null);
1129
+ .filter((saved) => saved !== null) : [];
1004
1130
  const images = persisted.map((saved) => saved.url);
1005
- this.store.appendEvent(runId, {
1006
- type: 'user-message',
1007
- stepId: state.currentStepId,
1008
- text,
1009
- imageCount: content.filter((b) => b.type === 'image').length,
1010
- images,
1011
- });
1131
+ if (userAuthored) {
1132
+ this.store.appendEvent(runId, {
1133
+ type: 'user-message',
1134
+ stepId: state.currentStepId,
1135
+ text,
1136
+ imageCount: content.filter((b) => b.type === 'image').length,
1137
+ images,
1138
+ });
1139
+ }
1012
1140
  // Tell the agent where the pasted files live on disk (#357): the base64 blocks below still
1013
1141
  // ride along so the model can *view* them, but a real path is what lets it *operate* on them
1014
1142
  // (save, `cp`, attach to a GitHub issue/PR) — and it's the only usable reference on backends
1015
1143
  // (codex, opencode) that drop image blocks entirely before reaching the model.
1016
- const deliverable = persisted.length ? [...content, pastedAttachmentsNote(persisted)] : content;
1144
+ const expanded = userAuthored ? expandRegistrySlashSkill(content, state.skills ?? []) : content;
1145
+ const deliverable = persisted.length ? [...expanded, pastedAttachmentsNote(persisted)] : expanded;
1017
1146
  const delivered = state.session.sendMessage(deliverable);
1018
1147
  if (delivered) {
1019
1148
  this.clearIdleTimer(state);
1149
+ this.clearMonitoringWakeTimer(state, runId);
1020
1150
  this.waiting.delete(runId); // resumed — the run counts against slots again
1151
+ this.monitoring.delete(runId);
1021
1152
  // Clear any `monitoring` activity — the agent is actively working again
1022
1153
  // (spec 2026-07-18-subagent-monitoring-status, #490).
1023
1154
  this.store.updateRun(runId, { status: 'running', activity: undefined });
@@ -1054,7 +1185,10 @@ export class RunManager {
1054
1185
  * behaves exactly like an interactive step: `waiting` after each turn,
1055
1186
  * messages via sendMessage, closed by finish/idle/cancel.
1056
1187
  */
1057
- continueRun(runId, opts = {}) {
1188
+ continueRun(runId, opts = {},
1189
+ /** Restart recovery may discover several interrupted tasks at once. Those
1190
+ * continuations are queued; an explicit user Continue remains immediate. */
1191
+ deferForCapacity = false) {
1058
1192
  if (this.active.has(runId))
1059
1193
  return { ok: false, error: 'run is still active' };
1060
1194
  const run = this.store.getRun(runId);
@@ -1108,7 +1242,26 @@ export class RunManager {
1108
1242
  const continuations = run.steps.filter((s) => s.id.startsWith('continue-')).length;
1109
1243
  const stepId = `continue-${continuations + 1}`;
1110
1244
  this.store.addStep(runId, { id: stepId, name: 'Continue', kind: 'agent' });
1111
- void this.runContinuation(runId, stepId, resume ? sessionStep.sessionId : undefined, targetRunner, opts.text?.trim() || 'Continue.', opts.images ?? []).catch((err) => {
1245
+ const prompt = opts.text?.trim() || 'Continue.';
1246
+ const images = opts.images ?? [];
1247
+ if (deferForCapacity) {
1248
+ this.pendingContinuations.set(runId, {
1249
+ stepId,
1250
+ sessionId: resume ? sessionStep.sessionId : undefined,
1251
+ backend: targetRunner,
1252
+ prompt,
1253
+ images,
1254
+ });
1255
+ this.queue.push(runId);
1256
+ this.store.updateRun(runId, {
1257
+ status: 'queued',
1258
+ error: undefined,
1259
+ finishedAt: undefined,
1260
+ currentStepId: undefined,
1261
+ });
1262
+ return { ok: true };
1263
+ }
1264
+ void this.runContinuation(runId, stepId, resume ? sessionStep.sessionId : undefined, targetRunner, prompt, images).catch((err) => {
1112
1265
  const message = err instanceof Error ? err.message : String(err);
1113
1266
  this.store.updateRun(runId, {
1114
1267
  status: 'failed',
@@ -1123,7 +1276,11 @@ export class RunManager {
1123
1276
  /** Screenshots pasted into the follow-up composer — delivered with the
1124
1277
  * reopened session's opening message, exactly like a live-session
1125
1278
  * message's attachments. */
1126
- images = []) {
1279
+ images = [],
1280
+ /** Queued-message screenshots were persisted when they were enqueued and
1281
+ * reconstructed at dequeue. Keep them separate from fresh `images` so
1282
+ * opening a recovered continuation does not persist duplicate files. */
1283
+ persistedImages = [], persistedAttachments = []) {
1127
1284
  // Continuation runs in the task's worktree when it still exists (spec
1128
1285
  // 006) — the resumed session sees exactly what the original run left.
1129
1286
  // Retention (#483) may have reclaimed this run's worktree directory while
@@ -1141,6 +1298,7 @@ export class RunManager {
1141
1298
  : this.repoRoot;
1142
1299
  const state = { cancelled: false, interrupt: () => undefined, cwd };
1143
1300
  this.active.set(runId, state);
1301
+ this.starting.delete(runId);
1144
1302
  if (state.cwd === this.repoRoot) {
1145
1303
  this.store.appendEvent(runId, {
1146
1304
  type: 'note',
@@ -1180,15 +1338,17 @@ export class RunManager {
1180
1338
  // images rather than a bare count, and handed to the agent BOTH as base64 blocks (so it can
1181
1339
  // view them) and as absolute paths appended to the prompt (so it can operate on them — and
1182
1340
  // because codex/opencode drop image blocks before they reach the model).
1183
- const attachments = images
1341
+ const freshAttachments = images
1184
1342
  .filter((b) => b.type === 'image')
1185
1343
  .map((b) => this.persistImage(runId, b.source.media_type, b.source.data, 'pasted'))
1186
1344
  .filter((saved) => saved !== null);
1345
+ const openingImages = [...images, ...persistedImages];
1346
+ const attachments = [...freshAttachments, ...persistedAttachments];
1187
1347
  this.store.appendEvent(runId, {
1188
1348
  type: 'user-message',
1189
1349
  stepId,
1190
1350
  text: prompt,
1191
- imageCount: images.filter((b) => b.type === 'image').length,
1351
+ imageCount: openingImages.filter((b) => b.type === 'image').length,
1192
1352
  ...(attachments.length ? { images: attachments.map((saved) => saved.url) } : {}),
1193
1353
  });
1194
1354
  let stepCost = 0;
@@ -1229,9 +1389,13 @@ export class RunManager {
1229
1389
  const done = sessionOpen && DONE_MARKER_RE.test(turnText.trimEnd());
1230
1390
  // `CEZ:ASK` → the user is genuinely blocked; wins over `CEZ:MONITORING`
1231
1391
  // (a pending question is always attention), loses to `CEZ:DONE` (#473).
1232
- const ask = sessionOpen && !done ? parseAskMarker(turnText) : null;
1392
+ const askResult = sessionOpen && !done ? parseAskMarkerResult(turnText) : undefined;
1393
+ const ask = askResult?.kind === 'valid' ? askResult.request : null;
1394
+ const askRejection = askResult ? askMarkerRejection(askResult) : undefined;
1233
1395
  const monitoring = sessionOpen && !done && !ask && MONITORING_MARKER_RE.test(turnText.trimEnd());
1234
1396
  turnText = '';
1397
+ if (askRejection)
1398
+ this.store.appendEvent(runId, { type: 'note', message: askRejection, stepId });
1235
1399
  if (done) {
1236
1400
  // Goal achieved (agent contract, #347) — same as in runAgentStep.
1237
1401
  this.store.appendEvent(runId, { type: 'lifecycle', message: 'goal achieved — session closed' });
@@ -1267,13 +1431,19 @@ export class RunManager {
1267
1431
  if (monitoring) {
1268
1432
  this.store.updateRun(runId, { status: 'running', activity: 'monitoring' });
1269
1433
  this.store.updateStep(runId, stepId, { status: 'running' });
1434
+ this.monitoring.add(runId);
1435
+ this.clearIdleTimer(state);
1436
+ this.armMonitoringWakeTimer(runId, state);
1270
1437
  }
1271
1438
  else {
1272
1439
  this.store.updateRun(runId, { status: 'waiting', activity: undefined });
1273
1440
  this.store.updateStep(runId, stepId, { status: 'waiting' });
1441
+ this.monitoring.delete(runId);
1442
+ this.clearMonitoringWakeTimer(state, runId);
1274
1443
  }
1275
1444
  this.waiting.add(runId);
1276
- this.armIdleTimer(runId, state);
1445
+ if (!monitoring)
1446
+ this.armIdleTimer(runId, state);
1277
1447
  this.releaseSlot();
1278
1448
  }
1279
1449
  }
@@ -1321,13 +1491,15 @@ export class RunManager {
1321
1491
  return;
1322
1492
  }
1323
1493
  const runner = createRunner(continueBackend);
1494
+ state.currentStepId = stepId;
1495
+ this.beginUsageInvocation(runId, state, stepId);
1324
1496
  const session = runner.startSession({
1325
1497
  // The Continue step is a fresh agent session on the same run — the
1326
1498
  // run's extra system prompt (already resolved at execute time and
1327
1499
  // echoed on the record) rides along with the handoff contract.
1328
1500
  systemPrompt: composeSystemPrompt(record?.systemPrompt, generateFollowups ? HANDOFF_INSTRUCTIONS : HANDOFF_ONLY_INSTRUCTIONS),
1329
1501
  userPrompt: attachments.length ? `${prompt}\n\n${pastedAttachmentsText(attachments)}` : prompt,
1330
- ...(images.length ? { images } : {}),
1502
+ ...(openingImages.length ? { images: openingImages } : {}),
1331
1503
  cwd: state.cwd,
1332
1504
  allowedTools: DEFAULT_ALLOWED_TOOLS,
1333
1505
  additionalDirectories: [join(this.dataDir, 'runs')],
@@ -1340,7 +1512,6 @@ export class RunManager {
1340
1512
  state.session = session;
1341
1513
  state.sessionEverOpened = true;
1342
1514
  this.flushDeferred(runId);
1343
- state.currentStepId = stepId;
1344
1515
  state.interrupt = () => session.interrupt();
1345
1516
  if (session.pid !== undefined)
1346
1517
  registerRunProcess(runId, session.pid);
@@ -1429,11 +1600,20 @@ export class RunManager {
1429
1600
  // established; only explicit opt-out and non-Git modes run in place.
1430
1601
  const repo = await getRepoInfo(this.repoRoot);
1431
1602
  if (repo && input.worktree === false) {
1432
- // Composer opt-out: run in the repo working tree, no branch/worktree
1433
- // (read-only skills review, summarize that never need isolation).
1603
+ // Composer opt-out: run in the repo working tree, no branch/worktree. The
1604
+ // repository-root lease serializes these runs so workflows cannot overlap.
1605
+ // Pin the starting commit: the session's Changes and Commits views use it
1606
+ // as their stable lower bound while reading the current working copy.
1607
+ const startingCommit = await getHeadCommit(repo.root);
1608
+ if (startingCommit)
1609
+ this.store.updateRun(runId, { baseBranch: startingCommit });
1434
1610
  emit({ type: 'note', message: 'worktree off — running in the repo working tree' });
1435
1611
  }
1436
1612
  else if (repo) {
1613
+ emit({
1614
+ type: 'note',
1615
+ message: `worktree on — using an isolated task worktree (${input.worktree === true ? 'explicit request' : 'default'})`,
1616
+ });
1437
1617
  // Fork from the configured base branch (config.json `baseBranch`, e.g.
1438
1618
  // `develop`) — also the target of the eventual draft PR. Unresolvable
1439
1619
  // (typo, not fetched) → note + the currently checked-out branch.
@@ -1508,6 +1688,7 @@ export class RunManager {
1508
1688
  if (seeded)
1509
1689
  seedHandoffFile(this.dataDir, seeded);
1510
1690
  const skills = await discoverSkills(this.repoRoot);
1691
+ state.skills = skills;
1511
1692
  const retriesUsed = new Map();
1512
1693
  let checkFailure = null;
1513
1694
  let runError = null;
@@ -1723,13 +1904,17 @@ export class RunManager {
1723
1904
  const done = interactive && sessionOpen && DONE_MARKER_RE.test(turnText.trimEnd());
1724
1905
  // `CEZ:ASK` → the user is blocked; wins over `CEZ:MONITORING`, loses to
1725
1906
  // `CEZ:DONE` (#473).
1726
- const ask = interactive && sessionOpen && !done ? parseAskMarker(turnText) : null;
1907
+ const askResult = interactive && sessionOpen && !done ? parseAskMarkerResult(turnText) : undefined;
1908
+ const ask = askResult?.kind === 'valid' ? askResult.request : null;
1909
+ const askRejection = askResult ? askMarkerRejection(askResult) : undefined;
1727
1910
  const monitoring = interactive &&
1728
1911
  sessionOpen &&
1729
1912
  !done &&
1730
1913
  !ask &&
1731
1914
  MONITORING_MARKER_RE.test(turnText.trimEnd());
1732
1915
  turnText = '';
1916
+ if (askRejection)
1917
+ emit({ type: 'note', stepId: step.id, message: askRejection });
1733
1918
  if (done) {
1734
1919
  // Goal achieved (agent contract, #347): close the session instead
1735
1920
  // of parking at `waiting` — the run completes and frees its slot.
@@ -1752,13 +1937,19 @@ export class RunManager {
1752
1937
  if (monitoring) {
1753
1938
  this.store.updateRun(runId, { status: 'running', activity: 'monitoring' });
1754
1939
  this.store.updateStep(runId, step.id, { status: 'running' });
1940
+ this.monitoring.add(runId);
1941
+ this.clearIdleTimer(state);
1942
+ this.armMonitoringWakeTimer(runId, state);
1755
1943
  }
1756
1944
  else {
1757
1945
  this.store.updateRun(runId, { status: 'waiting', activity: undefined });
1758
1946
  this.store.updateStep(runId, step.id, { status: 'waiting' });
1947
+ this.monitoring.delete(runId);
1948
+ this.clearMonitoringWakeTimer(state, runId);
1759
1949
  }
1760
1950
  this.waiting.add(runId);
1761
- this.armIdleTimer(runId, state);
1951
+ if (!monitoring)
1952
+ this.armIdleTimer(runId, state);
1762
1953
  this.releaseSlot(); // the freed slot can start a queued run right away — in any project
1763
1954
  }
1764
1955
  // Cez's own heartbeat — the handoff stays current even when the
@@ -1790,6 +1981,8 @@ export class RunManager {
1790
1981
  }
1791
1982
  const runner = createRunner(stepBackend);
1792
1983
  let session;
1984
+ state.currentStepId = step.id;
1985
+ this.beginUsageInvocation(runId, state, step.id);
1793
1986
  try {
1794
1987
  session = runner.startSession({
1795
1988
  // Skill body, then the run's extra prompt (POST override or config
@@ -1815,6 +2008,7 @@ export class RunManager {
1815
2008
  });
1816
2009
  }
1817
2010
  catch (err) {
2011
+ state.currentStepId = undefined;
1818
2012
  return err instanceof Error ? err.message : String(err);
1819
2013
  }
1820
2014
  state.session = session;
@@ -1840,6 +2034,9 @@ export class RunManager {
1840
2034
  finally {
1841
2035
  this.recordUsagePeaks(runId);
1842
2036
  this.clearIdleTimer(state);
2037
+ this.monitoring.delete(runId);
2038
+ this.waiting.delete(runId);
2039
+ this.clearMonitoringWakeTimer(state, runId);
1843
2040
  state.session = undefined;
1844
2041
  state.currentStepId = undefined;
1845
2042
  state.interrupt = () => undefined;
@@ -1863,16 +2060,90 @@ export class RunManager {
1863
2060
  /** Native backend asks arrive before turn-end. Persist and park immediately
1864
2061
  * so the cockpit shows attention and the run releases its workspace slot. */
1865
2062
  handleRunnerUiEvent(runId, state, sink, event) {
2063
+ this.recordUsageUiEvent(runId, state, event);
1866
2064
  sink.handle(event);
1867
2065
  if (event.type !== 'ask.requested' || state.cancelled)
1868
2066
  return;
1869
2067
  this.clearIdleTimer(state);
2068
+ this.monitoring.delete(runId);
2069
+ this.clearMonitoringWakeTimer(state, runId);
1870
2070
  this.waiting.add(runId);
1871
2071
  this.store.updateRun(runId, { status: 'waiting', activity: undefined });
1872
2072
  if (state.currentStepId)
1873
2073
  this.store.updateStep(runId, state.currentStepId, { status: 'waiting' });
1874
2074
  this.releaseSlot();
1875
2075
  }
2076
+ /** Persist the invocation checkpoint before launching a runner. A throw or
2077
+ * process exit before `turn.started` therefore leaves a durable mismatch. */
2078
+ beginUsageInvocation(runId, state, stepId) {
2079
+ const step = this.store.getRun(runId)?.steps.find((candidate) => candidate.id === stepId);
2080
+ if (!step)
2081
+ return;
2082
+ const epoch = (step.usageInvocationEpoch ?? 0) + 1;
2083
+ this.persistUsageCheckpoint(runId, stepId, {
2084
+ usageInvocationEpoch: epoch,
2085
+ usageInvocationsStarted: (step.usageInvocationsStarted ?? 0) + 1,
2086
+ });
2087
+ state.usageInvocation = {
2088
+ stepId,
2089
+ epoch,
2090
+ observed: false,
2091
+ startedTurns: new Set(),
2092
+ recordedTurns: new Set(),
2093
+ };
2094
+ }
2095
+ /** Fold backend-neutral completed-turn usage into the current step exactly
2096
+ * once. Invocation/turn counters are written before the event reaches the
2097
+ * NDJSON sink so crashes cannot preserve a falsely complete subtotal. */
2098
+ recordUsageUiEvent(runId, state, event) {
2099
+ const invocation = state.usageInvocation;
2100
+ if (!invocation)
2101
+ return;
2102
+ const step = this.store.getRun(runId)?.steps.find((candidate) => candidate.id === invocation.stepId);
2103
+ if (!step)
2104
+ return;
2105
+ if (event.type === 'turn.started') {
2106
+ if (invocation.startedTurns.has(event.turnId))
2107
+ return;
2108
+ invocation.startedTurns.add(event.turnId);
2109
+ const firstObservedTurn = !invocation.observed;
2110
+ invocation.observed = true;
2111
+ this.persistUsageCheckpoint(runId, invocation.stepId, {
2112
+ usageTurnsStarted: (step.usageTurnsStarted ?? 0) + 1,
2113
+ ...(firstObservedTurn
2114
+ ? { usageInvocationsObserved: (step.usageInvocationsObserved ?? 0) + 1 }
2115
+ : {}),
2116
+ });
2117
+ return;
2118
+ }
2119
+ if (event.type !== 'turn.completed')
2120
+ return;
2121
+ if (!invocation.startedTurns.has(event.turnId) || invocation.recordedTurns.has(event.turnId))
2122
+ return;
2123
+ const input = event.usage?.input;
2124
+ const output = event.usage?.output;
2125
+ if (typeof input !== 'number' ||
2126
+ !Number.isFinite(input) ||
2127
+ input < 0 ||
2128
+ typeof output !== 'number' ||
2129
+ !Number.isFinite(output) ||
2130
+ output < 0) {
2131
+ return;
2132
+ }
2133
+ invocation.recordedTurns.add(event.turnId);
2134
+ this.persistUsageCheckpoint(runId, invocation.stepId, {
2135
+ inputTokens: (step.inputTokens ?? 0) + input,
2136
+ outputTokens: (step.outputTokens ?? 0) + output,
2137
+ usageTurnsRecorded: (step.usageTurnsRecorded ?? 0) + 1,
2138
+ });
2139
+ }
2140
+ /** Usage completeness is a crash boundary, unlike high-frequency token
2141
+ * snapshots: the checkpoint must reach `runs.json` before the runner starts
2142
+ * or the matching UI event is persisted and forwarded. */
2143
+ persistUsageCheckpoint(runId, stepId, patch) {
2144
+ this.store.updateStep(runId, stepId, patch);
2145
+ this.store.flush();
2146
+ }
1876
2147
  /**
1877
2148
  * Turn-end bookkeeping (#389), shared by `runAgentStep` and
1878
2149
  * `runContinuation` — called (fire-and-forget) from every `turn-end` event:
@@ -1939,7 +2210,12 @@ export class RunManager {
1939
2210
  // deliberately NEVER a title source; see maybeRefreshTitle below. The
1940
2211
  // one exception is an explicit CEZ:TITLE declaration (applied above).
1941
2212
  if (run.worktreePath && existsSync(run.worktreePath)) {
1942
- const stat = await worktreeShortstat(run.worktreePath, run.baseBranch ?? 'HEAD');
2213
+ // `taskBranch` is what keeps this number *this task's* (#751): a review/QA run
2214
+ // repoints the worktree onto the branch under review, and without the branch to
2215
+ // compare HEAD against, the stat would claim that whole branch's diff.
2216
+ const stat = await worktreeShortstat(run.worktreePath, run.baseBranch ?? 'HEAD', {
2217
+ taskBranch: run.branch,
2218
+ });
1943
2219
  if (stat)
1944
2220
  this.store.updateRun(runId, { diffStat: stat });
1945
2221
  else
@@ -2096,7 +2372,10 @@ export class RunManager {
2096
2372
  throw err;
2097
2373
  }
2098
2374
  this.queuedImageSeq.set(runId, seq);
2099
- return { name, url: `/api/runs/${runId}/images/${name}`, path };
2375
+ // Versioned, because that is the only surface served now. The cockpit still upgrades
2376
+ // the unversioned URLs sitting in OLD transcripts when it renders them
2377
+ // (`resolveApiUrl`), but a URL minted today must be fetchable as written.
2378
+ return { name, url: `/api/v1/runs/${runId}/images/${name}`, path };
2100
2379
  }
2101
2380
  return null;
2102
2381
  }
@@ -2123,6 +2402,68 @@ export class RunManager {
2123
2402
  state.idleTimer = undefined;
2124
2403
  }
2125
2404
  }
2405
+ reconcileMonitoringWakeTimers() {
2406
+ for (const runId of this.monitoring) {
2407
+ const state = this.active.get(runId);
2408
+ if (state)
2409
+ this.armMonitoringWakeTimer(runId, state);
2410
+ }
2411
+ }
2412
+ armMonitoringWakeTimer(runId, state) {
2413
+ const minutes = this.semaphore.monitoringWakeIntervalMinutes();
2414
+ if (minutes === null) {
2415
+ this.clearMonitoringWakeTimer(state, runId);
2416
+ return;
2417
+ }
2418
+ if ((state.monitoringWakeups ?? 0) >= MAX_AUTO_CONTINUES) {
2419
+ this.clearMonitoringWakeTimer(state, runId);
2420
+ if (!this.store.getRun(runId)?.monitoringWakeCapReached) {
2421
+ this.store.updateRun(runId, { monitoringWakeCapReached: true });
2422
+ this.store.appendEvent(runId, {
2423
+ type: 'note',
2424
+ message: `automatic monitoring wake-up cap reached (${MAX_AUTO_CONTINUES}); session remains parked`,
2425
+ });
2426
+ }
2427
+ return;
2428
+ }
2429
+ if (state.monitoringWakeTimer && state.monitoringWakeIntervalMinutes === minutes)
2430
+ return;
2431
+ this.clearMonitoringWakeTimer(state, runId);
2432
+ state.monitoringWakeIntervalMinutes = minutes;
2433
+ this.store.updateRun(runId, { monitoringWakeCapReached: undefined });
2434
+ const deadline = Date.now() + minutes * 60_000;
2435
+ this.store.updateRun(runId, { monitoringWakeAt: new Date(deadline).toISOString() });
2436
+ state.monitoringWakeTimer = setTimeout(() => {
2437
+ state.monitoringWakeTimer = undefined;
2438
+ this.store.updateRun(runId, { monitoringWakeAt: undefined });
2439
+ if (!this.monitoring.has(runId) || !state.session?.open || state.cancelled)
2440
+ return;
2441
+ const wakeups = state.monitoringWakeups ?? 0;
2442
+ if (wakeups >= MAX_AUTO_CONTINUES) {
2443
+ this.store.updateRun(runId, { monitoringWakeCapReached: true });
2444
+ this.store.appendEvent(runId, {
2445
+ type: 'note',
2446
+ message: `automatic monitoring wake-up cap reached (${MAX_AUTO_CONTINUES}); session remains parked`,
2447
+ });
2448
+ return;
2449
+ }
2450
+ state.monitoringWakeups = wakeups + 1;
2451
+ this.store.appendEvent(runId, {
2452
+ type: 'note',
2453
+ message: `automatic monitoring wake-up (${state.monitoringWakeups}/${MAX_AUTO_CONTINUES})`,
2454
+ });
2455
+ this.deliverMessage(runId, [{ type: 'text', text: MONITORING_WAKE_NUDGE }], false);
2456
+ }, Math.max(0, deadline - Date.now()));
2457
+ state.monitoringWakeTimer.unref?.();
2458
+ }
2459
+ clearMonitoringWakeTimer(state, runId) {
2460
+ if (state.monitoringWakeTimer)
2461
+ clearTimeout(state.monitoringWakeTimer);
2462
+ state.monitoringWakeTimer = undefined;
2463
+ state.monitoringWakeIntervalMinutes = undefined;
2464
+ if (runId)
2465
+ this.store.updateRun(runId, { monitoringWakeAt: undefined });
2466
+ }
2126
2467
  /** Autosave-commit the worktree every 90 s while the run lives (spec 006).
2127
2468
  * Opt-in via CEZ_AUTOSAVE=1 (#471) — see periodicAutosaveEnabled. */
2128
2469
  armAutosave(state) {
@@ -2244,4 +2585,35 @@ export function skillSystemPrompt(skill) {
2244
2585
  lines.push('', 'Skill instructions:', skill.body.trim());
2245
2586
  return lines.join('\n');
2246
2587
  }
2588
+ /**
2589
+ * Expand a registry-backed slash skill in a live chat message before it reaches
2590
+ * a backend. Claude otherwise intercepts an unknown leading slash command, and
2591
+ * Codex/OpenCode have no native slash-skill lookup at all (#676).
2592
+ *
2593
+ * Only the first text block is eligible, only at character zero, and unknown
2594
+ * commands pass through byte-for-byte. The caller persists the original user
2595
+ * content before applying this delivery-only rewrite.
2596
+ */
2597
+ export function expandRegistrySlashSkill(content, skills) {
2598
+ const textIndex = content.findIndex((block) => block.type === 'text');
2599
+ if (textIndex < 0)
2600
+ return content;
2601
+ const block = content[textIndex];
2602
+ if (!block || block.type !== 'text')
2603
+ return content;
2604
+ const match = /^\/([A-Za-z0-9][A-Za-z0-9._-]*)(?=\s|$)/.exec(block.text);
2605
+ if (!match)
2606
+ return content;
2607
+ const skill = skills.find((candidate) => candidate.name === match[1]);
2608
+ if (!skill)
2609
+ return content;
2610
+ const request = block.text.slice(match[0].length).trim();
2611
+ const replacement = {
2612
+ type: 'text',
2613
+ text: request ? `${skillSystemPrompt(skill)}\n\nUser request:\n${request}` : skillSystemPrompt(skill),
2614
+ };
2615
+ const expanded = [...content];
2616
+ expanded[textIndex] = replacement;
2617
+ return expanded;
2618
+ }
2247
2619
  //# sourceMappingURL=run.js.map