@qodeca/xezar 0.13.0 → 0.14.0

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 (250) hide show
  1. package/README.md +72 -39
  2. package/dist/agent-config/account-identity.d.ts +2 -8
  3. package/dist/agent-config/account-identity.js +54 -10
  4. package/dist/agent-config/account-identity.js.map +1 -1
  5. package/dist/agent-config/catalog.d.ts +49 -7
  6. package/dist/agent-config/catalog.js +107 -0
  7. package/dist/agent-config/catalog.js.map +1 -1
  8. package/dist/agent-config/model-settings/pi.d.ts +12 -6
  9. package/dist/agent-config/model-settings/pi.js +12 -6
  10. package/dist/agent-config/model-settings/pi.js.map +1 -1
  11. package/dist/agent-config/seed.js +10 -0
  12. package/dist/agent-config/seed.js.map +1 -1
  13. package/dist/contract/github.d.ts +15 -0
  14. package/dist/contract/index.d.ts +9 -0
  15. package/dist/contract/index.js +1527 -895
  16. package/dist/contract/mcp-api-reference.d.ts +174 -0
  17. package/dist/contract/mcp-audit.d.ts +103 -0
  18. package/dist/contract/mcp-discovery.d.ts +257 -0
  19. package/dist/contract/mcp-event-catalog.d.ts +121 -0
  20. package/dist/contract/mcp-event-catalog.test.d.ts +1 -0
  21. package/dist/contract/mcp-idempotency.d.ts +200 -0
  22. package/dist/contract/mcp-journal.d.ts +268 -0
  23. package/dist/contract/mcp-leader.d.ts +88 -0
  24. package/dist/contract/mcp-ownership.d.ts +78 -0
  25. package/dist/contract/mcp-versioning.d.ts +139 -0
  26. package/dist/contract/runs.d.ts +81 -0
  27. package/dist/core/agent-profiles.d.ts +15 -4
  28. package/dist/core/agent-profiles.js +22 -8
  29. package/dist/core/agent-profiles.js.map +1 -1
  30. package/dist/core/opencode-server-runner.js +71 -11
  31. package/dist/core/opencode-server-runner.js.map +1 -1
  32. package/dist/core/secret-redaction.d.ts +7 -1
  33. package/dist/core/secret-redaction.js +44 -10
  34. package/dist/core/secret-redaction.js.map +1 -1
  35. package/dist/index.js +138 -27
  36. package/dist/index.js.map +1 -1
  37. package/dist/machine-identity.d.ts +66 -0
  38. package/dist/machine-identity.js +96 -0
  39. package/dist/machine-identity.js.map +1 -0
  40. package/dist/mcp/adapters/claude-code.d.ts +41 -0
  41. package/dist/mcp/adapters/claude-code.js +40 -0
  42. package/dist/mcp/adapters/claude-code.js.map +1 -0
  43. package/dist/mcp/adapters/codex.d.ts +140 -0
  44. package/dist/mcp/adapters/codex.js +323 -0
  45. package/dist/mcp/adapters/codex.js.map +1 -0
  46. package/dist/mcp/adapters/opencode.d.ts +148 -0
  47. package/dist/mcp/adapters/opencode.js +528 -0
  48. package/dist/mcp/adapters/opencode.js.map +1 -0
  49. package/dist/mcp/adapters/pi-link.d.ts +102 -0
  50. package/dist/mcp/adapters/pi-link.js +235 -0
  51. package/dist/mcp/adapters/pi-link.js.map +1 -0
  52. package/dist/mcp/adapters/pi.d.ts +188 -0
  53. package/dist/mcp/adapters/pi.js +418 -0
  54. package/dist/mcp/adapters/pi.js.map +1 -0
  55. package/dist/mcp/api-reference.d.ts +26 -0
  56. package/dist/mcp/api-reference.js +133 -0
  57. package/dist/mcp/api-reference.js.map +1 -0
  58. package/dist/mcp/audit-trail.d.ts +162 -0
  59. package/dist/mcp/audit-trail.js +306 -0
  60. package/dist/mcp/audit-trail.js.map +1 -0
  61. package/dist/mcp/bridge.d.ts +81 -0
  62. package/dist/mcp/bridge.js +407 -0
  63. package/dist/mcp/bridge.js.map +1 -0
  64. package/dist/mcp/connection-file.d.ts +49 -0
  65. package/dist/mcp/connection-file.js +63 -0
  66. package/dist/mcp/connection-file.js.map +1 -0
  67. package/dist/mcp/echo-guard.d.ts +94 -0
  68. package/dist/mcp/echo-guard.js +160 -0
  69. package/dist/mcp/echo-guard.js.map +1 -0
  70. package/dist/mcp/event-catalog.d.ts +120 -0
  71. package/dist/mcp/event-catalog.js +306 -0
  72. package/dist/mcp/event-catalog.js.map +1 -0
  73. package/dist/mcp/event-controller.d.ts +230 -0
  74. package/dist/mcp/event-controller.js +596 -0
  75. package/dist/mcp/event-controller.js.map +1 -0
  76. package/dist/mcp/event-journal.d.ts +57 -0
  77. package/dist/mcp/event-journal.js +423 -0
  78. package/dist/mcp/event-journal.js.map +1 -0
  79. package/dist/mcp/index.d.ts +63 -0
  80. package/dist/mcp/index.js +386 -0
  81. package/dist/mcp/index.js.map +1 -0
  82. package/dist/mcp/ipc.d.ts +202 -0
  83. package/dist/mcp/ipc.js +206 -0
  84. package/dist/mcp/ipc.js.map +1 -0
  85. package/dist/mcp/leader-delivery.d.ts +119 -0
  86. package/dist/mcp/leader-delivery.js +450 -0
  87. package/dist/mcp/leader-delivery.js.map +1 -0
  88. package/dist/mcp/operation-receipts.d.ts +210 -0
  89. package/dist/mcp/operation-receipts.js +723 -0
  90. package/dist/mcp/operation-receipts.js.map +1 -0
  91. package/dist/mcp/project-catalogs.d.ts +23 -0
  92. package/dist/mcp/project-catalogs.js +25 -0
  93. package/dist/mcp/project-catalogs.js.map +1 -0
  94. package/dist/mcp/project-leaders.d.ts +26 -0
  95. package/dist/mcp/project-leaders.js +17 -0
  96. package/dist/mcp/project-leaders.js.map +1 -0
  97. package/dist/mcp/protocol.d.ts +50 -0
  98. package/dist/mcp/protocol.js +47 -0
  99. package/dist/mcp/protocol.js.map +1 -0
  100. package/dist/mcp/reconnect.d.ts +297 -0
  101. package/dist/mcp/reconnect.js +465 -0
  102. package/dist/mcp/reconnect.js.map +1 -0
  103. package/dist/mcp/resource-ownership.d.ts +228 -0
  104. package/dist/mcp/resource-ownership.js +390 -0
  105. package/dist/mcp/resource-ownership.js.map +1 -0
  106. package/dist/mcp/service-adapter.d.ts +16488 -0
  107. package/dist/mcp/service-adapter.js +148 -0
  108. package/dist/mcp/service-adapter.js.map +1 -0
  109. package/dist/mcp/service.d.ts +83 -0
  110. package/dist/mcp/service.js +233 -0
  111. package/dist/mcp/service.js.map +1 -0
  112. package/dist/mcp/session-binding.d.ts +105 -0
  113. package/dist/mcp/session-binding.js +163 -0
  114. package/dist/mcp/session-binding.js.map +1 -0
  115. package/dist/mcp/stale-write.d.ts +145 -0
  116. package/dist/mcp/stale-write.js +219 -0
  117. package/dist/mcp/stale-write.js.map +1 -0
  118. package/dist/mcp/tool.d.ts +48 -0
  119. package/dist/mcp/tool.js +24 -0
  120. package/dist/mcp/tool.js.map +1 -0
  121. package/dist/mcp/tools/discovery.d.ts +40 -0
  122. package/dist/mcp/tools/discovery.js +232 -0
  123. package/dist/mcp/tools/discovery.js.map +1 -0
  124. package/dist/mcp/tools/execution-control.d.ts +171 -0
  125. package/dist/mcp/tools/execution-control.js +513 -0
  126. package/dist/mcp/tools/execution-control.js.map +1 -0
  127. package/dist/mcp/tools/handoff-git.d.ts +26 -0
  128. package/dist/mcp/tools/handoff-git.js +544 -0
  129. package/dist/mcp/tools/handoff-git.js.map +1 -0
  130. package/dist/mcp/tools/index.d.ts +2 -0
  131. package/dist/mcp/tools/index.js +23 -0
  132. package/dist/mcp/tools/index.js.map +1 -0
  133. package/dist/mcp/tools/leader-events.d.ts +46 -0
  134. package/dist/mcp/tools/leader-events.js +126 -0
  135. package/dist/mcp/tools/leader-events.js.map +1 -0
  136. package/dist/mcp/tools/local-handoff.d.ts +102 -0
  137. package/dist/mcp/tools/local-handoff.js +306 -0
  138. package/dist/mcp/tools/local-handoff.js.map +1 -0
  139. package/dist/mcp/tools/project-config.d.ts +340 -0
  140. package/dist/mcp/tools/project-config.js +1075 -0
  141. package/dist/mcp/tools/project-config.js.map +1 -0
  142. package/dist/mcp/tools/results-evidence.d.ts +112 -0
  143. package/dist/mcp/tools/results-evidence.js +1045 -0
  144. package/dist/mcp/tools/results-evidence.js.map +1 -0
  145. package/dist/mcp/tools/task-create.d.ts +70 -0
  146. package/dist/mcp/tools/task-create.js +473 -0
  147. package/dist/mcp/tools/task-create.js.map +1 -0
  148. package/dist/mcp/tools/task-reads.d.ts +102 -0
  149. package/dist/mcp/tools/task-reads.js +689 -0
  150. package/dist/mcp/tools/task-reads.js.map +1 -0
  151. package/dist/mcp/tools/work-organisation.d.ts +2 -0
  152. package/dist/mcp/tools/work-organisation.js +502 -0
  153. package/dist/mcp/tools/work-organisation.js.map +1 -0
  154. package/dist/paths.d.ts +17 -7
  155. package/dist/paths.js +18 -8
  156. package/dist/paths.js.map +1 -1
  157. package/dist/runs/project-writer.js +76 -11
  158. package/dist/runs/project-writer.js.map +1 -1
  159. package/dist/runs/retention.d.ts +25 -2
  160. package/dist/runs/retention.js +63 -2
  161. package/dist/runs/retention.js.map +1 -1
  162. package/dist/server/forge/github.js +44 -0
  163. package/dist/server/forge/github.js.map +1 -1
  164. package/dist/server/forge/types.d.ts +16 -0
  165. package/dist/server/project-context.d.ts +81 -8
  166. package/dist/server/project-context.js +143 -35
  167. package/dist/server/project-context.js.map +1 -1
  168. package/dist/server/server.d.ts +1050 -53
  169. package/dist/server/server.js +326 -120
  170. package/dist/server/server.js.map +1 -1
  171. package/dist/server/validators.d.ts +26 -6
  172. package/dist/server/validators.js +17 -0
  173. package/dist/server/validators.js.map +1 -1
  174. package/dist/skills-remote.js +64 -6
  175. package/dist/skills-remote.js.map +1 -1
  176. package/dist/workflows/run.d.ts +277 -12
  177. package/dist/workflows/run.js +599 -79
  178. package/dist/workflows/run.js.map +1 -1
  179. package/dist/workspace/agent-profiles.d.ts +30 -3
  180. package/dist/workspace/agent-profiles.js +39 -4
  181. package/dist/workspace/agent-profiles.js.map +1 -1
  182. package/dist/workspace/config.js +25 -4
  183. package/dist/workspace/config.js.map +1 -1
  184. package/dist/workspace/project-owner.d.ts +210 -0
  185. package/dist/workspace/project-owner.js +518 -0
  186. package/dist/workspace/project-owner.js.map +1 -0
  187. package/package.json +5 -3
  188. package/scripts/pi-leader-extension.ts +420 -0
  189. package/scripts/sync-readme.mjs +83 -2
  190. package/web/dist/assets/{alert-dialog-BVsVcbFy.js → alert-dialog-D6rP6d39.js} +1 -1
  191. package/web/dist/assets/arrow-down-DMOfFIzW.js +1 -0
  192. package/web/dist/assets/arrow-left-C4Ec2BUg.js +1 -0
  193. package/web/dist/assets/{centered-state-Djgly6Ni.js → centered-state-B0ZqrPaa.js} +12 -12
  194. package/web/dist/assets/chevron-right-mHJ-crvn.js +1 -0
  195. package/web/dist/assets/{collapsible-DZ4D-DHG.js → collapsible-DchEHgUM.js} +1 -1
  196. package/web/dist/assets/{commit-list-B-z1JL9G.js → commit-list-BpMim1Rv.js} +1 -1
  197. package/web/dist/assets/{compare-variants-CdS8r8E2.js → compare-variants-D5ItQbAq.js} +1 -1
  198. package/web/dist/assets/{diff-BKew9rQX.js → diff-HqqZ_WDR.js} +2 -2
  199. package/web/dist/assets/{diff-stat-D0mc5V6Z.js → diff-stat-NsU-3ecr.js} +1 -1
  200. package/web/dist/assets/{diff-view-xpIcuLIC.js → diff-view-B7c11Xux.js} +1 -1
  201. package/web/dist/assets/{dropdown-menu-CEQziH7X.js → dropdown-menu-CtTpnyod.js} +1 -1
  202. package/web/dist/assets/{editable-title-BCX3T2BY.js → editable-title-BXtuGQry.js} +1 -1
  203. package/web/dist/assets/{ellipsis-vertical-ByVdDc4z.js → ellipsis-vertical-MHtGZSH6.js} +1 -1
  204. package/web/dist/assets/{file-xBSB5hXO.js → file-DKLw5SOb.js} +1 -1
  205. package/web/dist/assets/{folder-CldgbZ5A.js → folder-Drw6-ufH.js} +1 -1
  206. package/web/dist/assets/{git-toolbar-BYlnf4qf.js → git-toolbar-BQH8bORV.js} +1 -1
  207. package/web/dist/assets/github-ynnpKFCJ.js +1 -0
  208. package/web/dist/assets/{image-preview-Cv5mZr0n.js → image-preview-DLhMN_R9.js} +1 -1
  209. package/web/dist/assets/index-BEsrNdXt.js +29 -0
  210. package/web/dist/assets/index-Da8DOBau.css +2 -0
  211. package/web/dist/assets/{markdown-CgEEE2ND.js → markdown-q365eN6Y.js} +1 -1
  212. package/web/dist/assets/{new-task-form-1r2bgsdD.js → new-task-form-CW-hNIWQ.js} +1 -1
  213. package/web/dist/assets/{pill-q-x22_ej.js → pill-B_TFulXa.js} +1 -1
  214. package/web/dist/assets/{project-router-VDN4jQM8.js → project-router-CLPc-ImZ.js} +1 -1
  215. package/web/dist/assets/{prompt-templates-BmZtCe-L.js → prompt-templates-CLc4RxyO.js} +1 -1
  216. package/web/dist/assets/{repo-git-C2On8SlI.js → repo-git-Is7R6-Tj.js} +1 -1
  217. package/web/dist/assets/{run-diff-vguEMJzh.js → run-diff-DHMvDlVo.js} +2 -2
  218. package/web/dist/assets/run-header-DvZJbQ8I.js +1 -0
  219. package/web/dist/assets/{search-x-QmhaAbg5.js → search-x-Ce2MMetu.js} +1 -1
  220. package/web/dist/assets/{skill-empty-hint-BrzUwByZ.js → skill-empty-hint-C_xCSvzB.js} +1 -1
  221. package/web/dist/assets/skills-CivqD4si.js +1 -0
  222. package/web/dist/assets/{sparkles-CP8QDOgX.js → sparkles-BCqPoj5g.js} +1 -1
  223. package/web/dist/assets/{square-terminal-CGypshSD.js → square-terminal-ikj5g-oK.js} +1 -1
  224. package/web/dist/assets/{tab-link-D4lFzzI1.js → tab-link-VsEjFV1Y.js} +1 -1
  225. package/web/dist/assets/{task-changes-6aopqAmV.js → task-changes-CH-E81-_.js} +1 -1
  226. package/web/dist/assets/{task-commits-u-dwaCyw.js → task-commits-DqfkMFxp.js} +1 -1
  227. package/web/dist/assets/{task-files-IMOZnIUK.js → task-files-D_nkHZ_a.js} +2 -2
  228. package/web/dist/assets/task-thread-bxQompxJ.js +9 -0
  229. package/web/dist/assets/{textarea-CyBCyKIk.js → textarea-xajuW1L_.js} +1 -1
  230. package/web/dist/assets/{thread-loading-Cuq2jchC.js → thread-loading-C3JQZbIE.js} +1 -1
  231. package/web/dist/assets/{trash-2-B6Dp3ZNs.js → trash-2-hJrBhpUD.js} +1 -1
  232. package/web/dist/assets/{triangle-alert-xBya8FUp.js → triangle-alert-D1u82y1a.js} +1 -1
  233. package/web/dist/assets/{upload-D6MXEB9p.js → upload-Bn6FBZ59.js} +1 -1
  234. package/web/dist/assets/{use-desktop-fLDKEJoB.js → use-desktop-DOIMfHfY.js} +1 -1
  235. package/web/dist/assets/{use-submit-shortcut-JTpNzU-u.js → use-submit-shortcut-Dj4DHTdE.js} +1 -1
  236. package/web/dist/assets/utils-YwBpOoRN.js +64 -0
  237. package/web/dist/assets/{workflows-BY3KTkFw.js → workflows-CTaAKz2m.js} +3 -3
  238. package/web/dist/assets/{zoomable-image-GTYwLtkS.js → zoomable-image-C1-6P3tg.js} +1 -1
  239. package/web/dist/index.html +23 -23
  240. package/web/dist/assets/arrow-down-CYgAlZtW.js +0 -1
  241. package/web/dist/assets/arrow-left-BOqE0a3k.js +0 -1
  242. package/web/dist/assets/chevron-right-Cc9BWnbN.js +0 -1
  243. package/web/dist/assets/github-CqGK62Lw.js +0 -1
  244. package/web/dist/assets/index-3nxVmdn5.css +0 -2
  245. package/web/dist/assets/index-qX7n11FX.js +0 -9
  246. package/web/dist/assets/refresh-cw-DcNCj2nu.js +0 -1
  247. package/web/dist/assets/run-header-DM7MiKj9.js +0 -1
  248. package/web/dist/assets/skills-Bj-uG_Hz.js +0 -1
  249. package/web/dist/assets/task-thread-CbMPmrWq.js +0 -9
  250. package/web/dist/assets/utils--z9hRtl3.js +0 -64
@@ -14,6 +14,22 @@ import { type WorkflowDef } from './types.ts';
14
14
  * `DEFAULT_IDLE_TIMEOUT_MINUTES`.
15
15
  */
16
16
  export declare const IDLE_TIMEOUT_MS: number;
17
+ /**
18
+ * Why a NON-FINAL agent step may not be marked `done` (#317), or null when it may.
19
+ *
20
+ * Such a step runs one turn and its session closes by itself, so it has no way to wait for an
21
+ * answer — and until #317 nothing decided it was done except "the session ended without an
22
+ * error". Run b86c6066's implement step ended on an `XEZ:ASK` design question, was marked done,
23
+ * and readiness, gates and the seal all ran on a branch with no work in it.
24
+ *
25
+ * The decision now belongs to the agent contract every step is given (`HANDOFF_INSTRUCTIONS`):
26
+ * `XEZ:DONE` means "my goal is achieved", and a turn that ends without it is "waiting on the
27
+ * user". A turn that ends on anything else — a question, the monitoring marker, or plain prose —
28
+ * stops the workflow here instead of carrying on without an answer. Deliberately fail-closed on
29
+ * plain prose: that is the case no agent-side marker can report, and the one the BLOCKED file
30
+ * could not catch either. `turnText` is null when the session ended without finishing a turn.
31
+ */
32
+ export declare function unfinishedStepReason(turnText: string | null): string | null;
17
33
  /**
18
34
  * Preserve boundaries between complete assistant text blocks while a turn is
19
35
  * accumulated for marker parsing. The runners join these same v1 blocks with
@@ -62,6 +78,17 @@ export declare const MAX_AUTO_RESUMES = 12;
62
78
  * note instead of fired, so the only tasks a sweep can revive are ones someone is still waiting on.
63
79
  */
64
80
  export declare const AUTO_RESUME_MISSED_WINDOW_MS: number;
81
+ /**
82
+ * How long a resumed turn must stay live before it counts as proof that the limit lifted (#285).
83
+ *
84
+ * The in-flight hold exists for the window where a resume is TESTING the account: a limit that
85
+ * has not lifted refuses the turn at its first API call, and the measured doomed run lives about
86
+ * 200 ms. A turn still running minutes later got past that call, so holding every other task on
87
+ * the account until it COMPLETES — which a long turn may not do for hours — is the hold outliving
88
+ * its purpose. Not the first streamed event: a refused Claude turn still streams its session init
89
+ * and a synthetic assistant frame before the error, so "it said something" proves nothing.
90
+ */
91
+ export declare const AUTO_RESUME_PROOF_MS: number;
65
92
  /**
66
93
  * How often the queue checks that it is not wedged.
67
94
  *
@@ -244,6 +271,12 @@ export declare class RunManager {
244
271
  * from the record rather than losing the wait. Runs here are `failed` and therefore NOT in
245
272
  * `active`, which is why the timer cannot live on an `ActiveRun` like the monitoring one. */
246
273
  private readonly autoResumeTimers;
274
+ /** Wake-ups for the instant a live resume's proof window closes (#285). The hold lifts by
275
+ * derivation, and a derived release is not an event, so these are only the pump that notices;
276
+ * the hold itself stays on the records. */
277
+ private readonly resumeProofTimers;
278
+ /** `AUTO_RESUME_PROOF_MS`, overridable only so a test need not wait two minutes. */
279
+ private readonly resumeProofMs;
247
280
  private pumping;
248
281
  /** A pump that arrived while one was in flight — replayed by `pump()`'s own
249
282
  * loop so a slot freed mid-sweep is never a lost wakeup. */
@@ -265,15 +298,77 @@ export declare class RunManager {
265
298
  private readonly offUsage;
266
299
  /** The stalled-queue watchdog (see `rescueStalledQueue`). */
267
300
  private readonly queueWatchdog;
268
- /** The rescue sweeps that have STARTED and not settled yet (#125). `clearInterval` cancels the
269
- * next tick; it cannot cancel the tick already running, and a sweep is async it awaits
270
- * `reviveWorkflow` before appending NDJSON. dispose() settles these so a torn-down manager is
271
- * provably finished writing before its caller removes the data root under it. */
272
- private readonly rescuesInFlight;
301
+ /**
302
+ * Every background WRITE this manager started on its own initiative and has not settled yet.
303
+ * Two producers, one set: the queue-watchdog rescue sweeps (#125) and the terminal-transition
304
+ * worktree retention sweeps (#200). Neither is cancellable and both cross an await before they
305
+ * touch disk — a rescue appends NDJSON after `reviveWorkflow`, a retention sweep spawns
306
+ * `git worktree remove` after `resolveWorktreeRetention` — so `clearInterval` and the
307
+ * `disposed` flag between them still leave work running.
308
+ *
309
+ * dispose() settles this set, which is what makes its promise mean "provably finished writing"
310
+ * before the caller removes the data root. Retention was the half that was NOT tracked, and
311
+ * the harm differs by caller — be precise about which, because only one of them deletes
312
+ * anything. In a TEST teardown a disposed manager could still be spawning git inside a
313
+ * directory the case was already `rmSync`-ing, which is the reported ENOTEMPTY. In a LIVE
314
+ * server nothing removes the project directory at all: the sweep instead stamps
315
+ * `worktreeReclaimedAt` onto a record, scheduling a debounced `runs.json` write from a store
316
+ * whose lifecycle has ended — so a re-add inside that window can see the stale in-memory index
317
+ * overwrite the fresh one. Every producer therefore enrols through a wrapper
318
+ * (`rescueStalledQueue`, `enforceRetention`), so nothing can start a background write teardown
319
+ * cannot see.
320
+ *
321
+ * The live-server half needed the OTHER end fixed too, and the promise is worthless without it:
322
+ * `ProjectContexts.dispose` used to drop this promise on the floor (`teardown()` was
323
+ * synchronous), so the guarantee held only for callers that awaited — which was tests. It now
324
+ * awaits the manager before `store.flush()` + `removeAllListeners()`, and `DELETE /projects/:id`
325
+ * awaits that in turn.
326
+ */
327
+ private readonly writesInFlight;
328
+ /**
329
+ * The run BODIES — `execute()` / `runContinuation()` — that have started and not settled yet,
330
+ * plus the turn-end bookkeeping (`recordTurnEnd`) that a body fires and does not await.
331
+ *
332
+ * Deliberately NOT `writesInFlight`. dispose() is documented as "the manager makes no further
333
+ * moves ON ITS OWN" and is explicitly not a run-stopper, so awaiting a live agent session in
334
+ * its promise would silently change what every existing caller gets. `quiesce()` is the opt-in
335
+ * that cancels first and then awaits this set; dispose()'s semantics are untouched.
336
+ */
337
+ private readonly runsInFlight;
338
+ /**
339
+ * Cancellations that arrived while a run was still MATERIALIZING — dequeued into `starting`,
340
+ * or floated by the direct Continue path — and had nowhere to land.
341
+ *
342
+ * `cancel()` stops an active run by writing `state.cancelled` and calling `state.interrupt()`,
343
+ * and neither exists until the body has built its `ActiveRun`. Before this set, `cancel()` for
344
+ * such a run simply returned false and delivered NOTHING, so the run went on to spawn an agent
345
+ * turn that no one had asked for and that `quiesce()` then waited out — up to the runner's
346
+ * 30-minute default (#200). The body consumes the request through `adoptActive`, at the first
347
+ * instant a cancellation has somewhere to go.
348
+ *
349
+ * `dropActive` clears the entry too, because a body can leave without ever reaching
350
+ * `adoptActive` (the continuation whose isolation is gone), and a stale entry would cancel the
351
+ * NEXT run to reuse that id — which, for a Continue, is the same id again.
352
+ */
353
+ private readonly cancelRequested;
273
354
  /** Set by dispose(): this manager makes no further moves. Re-checked at every await boundary a
274
355
  * rescue crosses, because a sweep that was already past its first check when dispose() landed
275
356
  * would otherwise write an event — and re-populate the queue dispose() had just emptied. */
276
357
  private disposed;
358
+ /**
359
+ * Set by `quiesce()` for the length of its drain: the scheduler starts nothing new.
360
+ *
361
+ * `disposed` cannot cover this window, because `quiesce()` disposes LAST — the whole point is
362
+ * to stop the runs while `cancel()` can still see them. During the drain the manager is
363
+ * otherwise fully live, and every settling run pumps the entire workspace on its way out
364
+ * (`dropActive` → `releaseSlot` → `semaphore.release()`, which awaits `pump()` on every
365
+ * registered participant, this one included). A pump reconciles auto-resumes from the RECORDS,
366
+ * and a record whose `autoResumeAt` has already passed arms at zero delay — so the drain could
367
+ * fire a resume, spawn a fresh agent turn into the very repo root the caller is about to
368
+ * delete, and then dutifully wait for it. Bounded by `MAX_AUTO_RESUMES`, and the exact opposite
369
+ * of what the method promises.
370
+ */
371
+ private quiescing;
277
372
  /** Set by the watchdog for exactly one sweep: ignore the usage-limit hold and make progress. */
278
373
  private forceNextPump;
279
374
  /** Runs the watchdog started despite the hold. The spawn-time gate (`requeueWhileHeld`) would
@@ -288,6 +383,7 @@ export declare class RunManager {
288
383
  private readonly offSemaphore;
289
384
  constructor(store: RunStore, repoRoot: string, options?: {
290
385
  semaphore?: WorkspaceSemaphore;
386
+ resumeProofMs?: number;
291
387
  });
292
388
  /**
293
389
  * Release everything this manager owns without touching run records
@@ -300,17 +396,110 @@ export declare class RunManager {
300
396
  * dispose only guarantees the manager makes no further moves on its own.
301
397
  *
302
398
  * Every side effect below is SYNCHRONOUS, so a caller that ignores the return value behaves
303
- * exactly as it did before #125. The returned promise settles the one thing dispose cannot do
304
- * synchronously: a queue-watchdog rescue that had already started. `clearInterval` stops the
305
- * next tick, never the running one the same discipline `AGENTS.md` records for the e2e
399
+ * exactly as it did before #125. The returned promise settles the things dispose cannot do
400
+ * synchronously: the background writes already in flight — a queue-watchdog rescue (#125) and
401
+ * a worktree retention sweep (#200), both tracked in `writesInFlight`. `clearInterval` stops
402
+ * the next tick, never the running one — the same discipline `AGENTS.md` records for the e2e
306
403
  * fixture servers, where `kill()` only delivers the signal and the helper awaits the exit.
307
404
  * Await it whenever the data root is about to be removed; a test that deletes its temp
308
405
  * directory without awaiting is the ENOENT in #125.
309
406
  *
407
+ * It does NOT wait for running runs, and that is the point of `quiesce()`: dispose clears
408
+ * `active`/`starting`/`queue` without stopping anything, so after it returns a leaked run is
409
+ * invisible to `cancel()` and can no longer be stopped at all. Reach for `quiesce()` whenever
410
+ * the runs are yours to end; reach for `dispose()` when they are not.
411
+ *
412
+ * ## The states a disposed manager leaves behind, and how they end
413
+ *
414
+ * - A run that was `active` keeps running to its own terminal status and writes it to the
415
+ * record; only the manager's registries forgot it. `cancel()` answers false for it from here
416
+ * on, which is the leak `quiesce()` exists to avoid.
417
+ * - A run that was `queued` — or one `startRun()` accepts AFTER dispose, which still writes a
418
+ * `queued` record and still floats a `pump()` — never starts in this process: `pump()` bails
419
+ * on `disposed`, so nothing dequeues it. Its only exit is the next process: `recover()`
420
+ * re-queues every `queued` record at boot, and `rescueStalledQueue` re-adopts one the engine
421
+ * is holding no work item for. That is deliberate — a disposed manager's project has been
422
+ * removed from the registry, and starting its work would be the surprise — but it does mean
423
+ * the record outlives the process as `queued` rather than `cancelled`.
424
+ *
310
425
  * It never rejects: a sweep that fails still belongs to whoever started it (the watchdog floats
311
426
  * it exactly as before), and teardown must not become a second place that error surfaces.
312
427
  */
313
428
  dispose(): Promise<void>;
429
+ /**
430
+ * Stop every run this manager owns, then dispose — "the data root is about to disappear".
431
+ *
432
+ * dispose() alone cannot deliver that: it is not a run-stopper by design, and because it
433
+ * clears `active`/`starting`/`queue` first, a run left behind is afterwards invisible to
434
+ * `cancel()` (which looks only in `queue` then `active`) and can never be stopped. The order
435
+ * here is the whole fix — cancel, drain, dispose — and it is the order four test files were
436
+ * already hand-rolling three different ways.
437
+ *
438
+ * ## Why the cancel is INSIDE the loop, and why termination needs `quiescing`
439
+ *
440
+ * Cancelling once up front is not enough, because the population is not fixed while the drain
441
+ * runs. `starting` promotes to `active` (the `ActiveRun` a `cancel()` needs did not exist a
442
+ * moment ago), a queued continuation crosses `rematerializeReclaimedWorktree` before it
443
+ * registers anywhere at all, and every settling body pumps the whole workspace on its way out.
444
+ * So each pass re-asks `activeRunIds()` and re-issues `cancel()`; `cancelRequested` carries a
445
+ * cancellation into the one window where neither registry can hold it.
446
+ *
447
+ * Termination then rests on `quiescing`, not on "the queue is empty". The manager has three
448
+ * self-starting entry points — `pump()`, `fireAutoResume()` and the queue watchdog's
449
+ * `rescueStalledQueue()` — and all three bail while it is set, so no pass can enrol a run the
450
+ * previous pass did not cancel. Ask "who fires this?" of any new one: the watchdog is an
451
+ * unref'd 60 s interval, so it is invisible in a short drain and lands squarely in a long one
452
+ * (an agent session that will not close is bounded only by the runner's 30-minute default),
453
+ * where it would re-adopt queued records — NDJSON, `pendingJobs`, a `queue` push — inside the
454
+ * window this method exists to make quiet.
455
+ *
456
+ * What a pass CAN still enrol is bounded bookkeeping: a body fires one `recordTurnEnd` per turn
457
+ * it finishes, and a cancelled run finishes no further turns (`autoContinueTurn` refuses a
458
+ * cancelled state). Each iteration therefore awaits a strictly older generation of work than the
459
+ * last, and the generations are finite.
460
+ *
461
+ * An external caller CAN still enrol during the drain — `startRun()` and `continueRun()` are
462
+ * not gated by `quiescing`, because refusing a user's request is the HTTP layer's decision and
463
+ * not a teardown helper's. The loop absorbs one: the next pass finds it, cancels it and waits
464
+ * for it, which is exactly what the "an agent turn enrolled after the drain took its snapshot"
465
+ * case pins. What is NOT bounded is an unbroken STREAM of them — a caller that keeps starting
466
+ * work into a project it is tearing down gets what it asked for.
467
+ *
468
+ * ## Each pass delivers its cancel ONCE — the bodies are what make that enough
469
+ *
470
+ * Re-issuing `cancel()` per pass reaches runs whose REGISTRY membership changed; it does not
471
+ * reach a run that was already in `active` when the pass ran, because the next re-issue waits
472
+ * on the same `Promise.allSettled` the stuck body is holding open. So the guarantee has to live
473
+ * in the body: every point at which a run body can park must consume a cancellation that
474
+ * arrived while it was elsewhere. Those points are `adoptActive` (the `starting` window),
475
+ * `acquireRepoRoot` (the lease wait, which races its own abort), the step loop's
476
+ * `if (state.cancelled) break`, `publishSession` (the gap between the loop's check and a live
477
+ * `state.interrupt`, which is what #199 hung on), and `state.interrupt()` itself once a session
478
+ * is up. Adding a new `await` inside a run body means asking which of those covers it.
479
+ *
480
+ * ## If a tracked promise never settles
481
+ *
482
+ * `quiesce()` has no deadline of its own and stays pending — the caller's timeout is the only
483
+ * bound. That is deliberate. Cancellation is delivered, an agent step's own wall clock still
484
+ * applies, and a session that refuses to close is a real writer: returning early would hand the
485
+ * caller a promise that says "nothing is writing any more" while something is, which is the
486
+ * exact lie this method exists to remove. An invented deadline would only move the ENOTEMPTY a
487
+ * few hundred milliseconds later — and, in the #199 hang, would have shipped a teardown that
488
+ * deleted a repository out from under a live agent CLI instead of failing loudly at 90 s.
489
+ *
490
+ * Never rejects, the same contract as dispose(): teardown must not become a second place a run's
491
+ * error surfaces.
492
+ */
493
+ quiesce(): Promise<void>;
494
+ /**
495
+ * Enrol a run body in `runsInFlight` so `quiesce()` can wait for it.
496
+ *
497
+ * The body arrives with its own `.catch(…)` already attached — the failure policy stays at the
498
+ * call site, where it can name the run — and the extra `.catch` here only keeps the tracked
499
+ * copy from ever rejecting, so a handler that itself throws cannot turn teardown into an
500
+ * unhandled rejection.
501
+ */
502
+ private trackRun;
314
503
  /**
315
504
  * Pause any active run whose whole process tree exceeds the WORKSPACE
316
505
  * `resources.memoryLimitMb`, freeing its slot so the queue advances
@@ -503,7 +692,7 @@ export declare class RunManager {
503
692
  *
504
693
  * Public so a test can drive the wedge directly instead of waiting out the interval.
505
694
  *
506
- * The sweep itself is `sweepStalledQueue`; this wrapper only publishes it to `rescuesInFlight`
695
+ * The sweep itself is `sweepStalledQueue`; this wrapper only publishes it to `writesInFlight`
507
696
  * so dispose() can settle it (#125). Every entry point goes through here — the interval and the
508
697
  * tests alike — so nothing can start a sweep that teardown cannot see. The schedule, the
509
698
  * conditions and the effects are untouched.
@@ -532,13 +721,48 @@ export declare class RunManager {
532
721
  * which is the route's 404.
533
722
  */
534
723
  cancelAutoResume(runId: string): boolean;
724
+ /**
725
+ * Pump the whole workspace once a live resume's proof window has closed (#285). The hold is
726
+ * workspace-wide — one account can be queued in several projects — so this is `release()`,
727
+ * not this manager's own pump. It carries no state: whether the hold has lifted is decided by
728
+ * `accountHolds()` from the record when the pump reads it, so a turn that failed or finished
729
+ * first just makes this a no-op pump. The slack keeps a timer that fires a millisecond early
730
+ * from reading a window that has not quite closed yet.
731
+ */
732
+ private armResumeProofPump;
535
733
  /** Retire a pending resume — timer, deadline and counter. The counter goes too because every
536
734
  * caller is a fresh epoch: a human Continue, or a resume that re-stamps its own count. */
537
735
  private clearAutoResume;
538
- /** Reclaim finished worktrees beyond the keep-limit (#483) — directory only,
539
- * `xez/<id8>` branch kept. Best-effort; a failure never affects run
540
- * lifecycle. `review`/live runs are excluded by the selector. */
736
+ /**
737
+ * Reclaim finished worktrees beyond the keep-limit (#483) directory only, `xez/<id8>` branch
738
+ * kept. Best-effort; a failure never affects run lifecycle. `review`/live runs are excluded by
739
+ * the selector.
740
+ *
741
+ * The sweep itself is `reclaimRetiredWorktrees`; this wrapper only publishes it to
742
+ * `writesInFlight` so dispose() can settle it, the same shape `rescueStalledQueue` already
743
+ * uses. Callers still float it — retention must never delay or throw into a terminal
744
+ * transition — but a floated promise nobody holds is one teardown cannot wait for, and this
745
+ * one spawns `git worktree remove` and `git worktree prune` inside `repoRoot` (#200).
746
+ */
541
747
  private enforceRetention;
748
+ /**
749
+ * The retention sweep proper. `disposed` is re-read after `resolveWorktreeRetention` — the one
750
+ * await before any git spawn — for the reason the flag exists: a sweep already past its first
751
+ * check when dispose() landed would otherwise start removing directories under a root its
752
+ * caller is deleting. `shouldStop` carries the same question INTO the loop, because a sweep
753
+ * removes one directory per iteration and a single check before the first one stops nothing on
754
+ * a five-worktree pass that straddles a dispose.
755
+ *
756
+ * Bailing there reclaims nothing, and nothing is lost by it: retention is count-based and
757
+ * re-enforced from scratch by the boot sweep (`index.ts`) and by the Settings → Worktrees
758
+ * reclaim route, so a skipped pass costs one over-limit directory until the next boot. The
759
+ * alternative — finishing the pass — spends two git spawns inside a directory whose owner has
760
+ * just said it is done with it, plus a `worktreeReclaimedAt` stamp through `store.updateRun`
761
+ * that schedules a debounced `runs.json` write from a store nobody owns any more. That is the
762
+ * whole of #200, and the same hazard `armRepoHandle` is guarded against in
763
+ * `server/project-context.ts`.
764
+ */
765
+ private reclaimRetiredWorktrees;
542
766
  /** Last live-refresh namer inputs per run — unchanged inputs skip the call. */
543
767
  private lastNamerKey;
544
768
  /**
@@ -561,7 +785,48 @@ export declare class RunManager {
561
785
  */
562
786
  private acquireRepoRoot;
563
787
  cancel(runId: string): boolean;
788
+ /**
789
+ * Publish a run's freshly built `ActiveRun` and adopt any cancellation that arrived while it
790
+ * was being built. Returns true when the caller must abandon the run.
791
+ *
792
+ * One helper for both bodies, because the two `active.set` sites are the two halves of the same
793
+ * moment and had already drifted once (`state.autonomous`, #141). Ordering inside is
794
+ * load-bearing: the flag is consumed BEFORE `active.set`, so a `cancel()` landing after this
795
+ * returns finds a state to write to and takes the ordinary path.
796
+ */
797
+ private adoptActive;
798
+ /**
799
+ * Publish a freshly started agent session on the run's `ActiveRun`, and deliver any
800
+ * cancellation that arrived while the session was being BUILT.
801
+ *
802
+ * The twin of `adoptActive`, one phase later and for the same reason. `cancel()` stops a live
803
+ * turn by calling `state.interrupt()`, and between the step loop's own `state.cancelled` check
804
+ * and this moment that function is still the `() => undefined` placeholder: everything in
805
+ * between — `configuredModelProvider`, `agentEnvForStep`, a team skill's `materializeSkillDir`
806
+ * — is `await`ed, and a cancel landing in any of those gaps set the flag and delivered nothing.
807
+ *
808
+ * What that cost is a run that never ends, not a turn that runs one step too long (#199/#200).
809
+ * The session spawns anyway; an interactive step passes `timeoutMs: 0`, so the runner has no
810
+ * wall clock at all; and the turn-end handler computes `sessionOpen` as
811
+ * `!state.cancelled && session.open`, so a cancelled run neither parks at `waiting` nor closes
812
+ * the session on `XEZ:DONE`. The body then sits in `await session.result` for as long as the
813
+ * agent CLI keeps stdin open — which for the bundled mock is forever. `quiesce()` issues its
814
+ * cancel once per drain generation and only re-issues it after `Promise.allSettled` resolves,
815
+ * so a single missed delivery is a permanent hang: measured as the 90 s teardown timeout on a
816
+ * 2-core CI runner, where the widened gap makes the window easy to land in.
817
+ *
818
+ * ONE helper for both construction sites — `runAgentStep` and `runContinuation` — because they
819
+ * are the same moment written twice and had already drifted (only one of them re-stamped
820
+ * `currentStepId`). A third session site must come through here too, or it re-opens the hole.
821
+ */
822
+ private publishSession;
564
823
  isActive(runId: string): boolean;
824
+ /** The id set behind `isActive` — `active ∪ starting ∪ queue`. `quiesce()` needs the whole
825
+ * population rather than one membership test, and a second spelling of that union is exactly
826
+ * how the two drift apart: `run-quiesce.test.ts` pins the observable half by putting one run
827
+ * in `queue` and one in `active`, asserting `isActive` for both before `quiesce()` and for
828
+ * neither after. A registry this set forgets would leave a live writer behind. */
829
+ private activeRunIds;
565
830
  /**
566
831
  * Fold a queued run's persisted prompt — `run.task` plus everything stacked
567
832
  * onto it (#472) — into the job input that is about to execute.