@wjarka/cezarion 0.16.0 → 0.17.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 (907) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +594 -207
  3. package/dist/agent-config/account-identity.d.ts +3 -19
  4. package/dist/agent-config/account-identity.js +8 -5
  5. package/dist/agent-config/account-identity.js.map +1 -1
  6. package/dist/agent-config/catalog.d.ts +3 -14
  7. package/dist/agent-config/catalog.js.map +1 -1
  8. package/dist/agent-config/files.d.ts +2 -6
  9. package/dist/agent-config/files.js +2 -6
  10. package/dist/agent-config/files.js.map +1 -1
  11. package/dist/agent-config/model-settings/types.d.ts +1 -1
  12. package/dist/agent-config/seed.d.ts +11 -3
  13. package/dist/agent-config/seed.js +52 -48
  14. package/dist/agent-config/seed.js.map +1 -1
  15. package/dist/agent-config/service.js +3 -2
  16. package/dist/agent-config/service.js.map +1 -1
  17. package/dist/agent-config/validate.d.ts +1 -4
  18. package/dist/agent-config/validate.js.map +1 -1
  19. package/dist/application-update/helper.d.ts +2 -2
  20. package/dist/application-update/helper.js +7 -12
  21. package/dist/application-update/helper.js.map +1 -1
  22. package/dist/application-update/launcher.d.ts +4 -0
  23. package/dist/application-update/launcher.js +18 -12
  24. package/dist/application-update/launcher.js.map +1 -1
  25. package/dist/application-update/service.d.ts +3 -3
  26. package/dist/application-update/service.js +1 -1
  27. package/dist/application-update/service.js.map +1 -1
  28. package/dist/archive-on-merge-setting.d.ts +15 -0
  29. package/dist/archive-on-merge-setting.js +14 -0
  30. package/dist/archive-on-merge-setting.js.map +1 -0
  31. package/dist/artifacts/cli.d.ts +8 -0
  32. package/dist/artifacts/cli.js +21 -2
  33. package/dist/artifacts/cli.js.map +1 -1
  34. package/dist/artifacts/resolve.js +2 -1
  35. package/dist/artifacts/resolve.js.map +1 -1
  36. package/dist/artifacts/store.js.map +1 -1
  37. package/dist/automations/automation-cli.d.ts +59 -0
  38. package/dist/automations/automation-cli.js +509 -0
  39. package/dist/automations/automation-cli.js.map +1 -0
  40. package/dist/automations/builtin-skill.d.ts +7 -0
  41. package/dist/automations/builtin-skill.js +14 -0
  42. package/dist/automations/builtin-skill.js.map +1 -0
  43. package/dist/automations/github-baseline.d.ts +3 -0
  44. package/dist/automations/github-baseline.js +17 -0
  45. package/dist/automations/github-baseline.js.map +1 -0
  46. package/dist/automations/github-poller.d.ts +23 -4
  47. package/dist/automations/github-poller.js +157 -88
  48. package/dist/automations/github-poller.js.map +1 -1
  49. package/dist/automations/prompts.d.ts +12 -0
  50. package/dist/automations/prompts.js +160 -0
  51. package/dist/automations/prompts.js.map +1 -0
  52. package/dist/automations/schedule-runner.d.ts +43 -24
  53. package/dist/automations/schedule-runner.js +207 -20
  54. package/dist/automations/schedule-runner.js.map +1 -1
  55. package/dist/automations/scheduler.d.ts +10 -2
  56. package/dist/automations/scheduler.js +200 -54
  57. package/dist/automations/scheduler.js.map +1 -1
  58. package/dist/automations/service.d.ts +777 -0
  59. package/dist/automations/service.js +444 -0
  60. package/dist/automations/service.js.map +1 -0
  61. package/dist/automations/store.d.ts +17 -7
  62. package/dist/automations/store.js +88 -17
  63. package/dist/automations/store.js.map +1 -1
  64. package/dist/automations/task-template.d.ts +7 -2
  65. package/dist/automations/task-template.js +93 -44
  66. package/dist/automations/task-template.js.map +1 -1
  67. package/dist/automations/tool-adapter.d.ts +6 -0
  68. package/dist/automations/tool-adapter.js +108 -0
  69. package/dist/automations/tool-adapter.js.map +1 -0
  70. package/dist/automations/types.d.ts +1 -405
  71. package/dist/automations/types.js +1 -218
  72. package/dist/automations/types.js.map +1 -1
  73. package/dist/autosave-cleanup.d.ts +26 -0
  74. package/dist/autosave-cleanup.js +87 -0
  75. package/dist/autosave-cleanup.js.map +1 -0
  76. package/dist/autosave-git.d.ts +3 -0
  77. package/dist/autosave-git.js +67 -35
  78. package/dist/autosave-git.js.map +1 -1
  79. package/dist/autosave-group.d.ts +8 -0
  80. package/dist/autosave-group.js +61 -0
  81. package/dist/autosave-group.js.map +1 -0
  82. package/dist/autosave-holders.d.ts +31 -4
  83. package/dist/autosave-holders.js +92 -7
  84. package/dist/autosave-holders.js.map +1 -1
  85. package/dist/autosave-windows.d.ts +14 -0
  86. package/dist/autosave-windows.js +36 -2
  87. package/dist/autosave-windows.js.map +1 -1
  88. package/dist/ci-wait/client.d.ts +20 -3
  89. package/dist/ci-wait/client.js +40 -44
  90. package/dist/ci-wait/client.js.map +1 -1
  91. package/dist/ci-wait/controller.d.ts +622 -36
  92. package/dist/ci-wait/controller.js +152 -20
  93. package/dist/ci-wait/controller.js.map +1 -1
  94. package/dist/ci-wait/errors.d.ts +19 -0
  95. package/dist/ci-wait/errors.js +180 -2
  96. package/dist/ci-wait/errors.js.map +1 -1
  97. package/dist/ci-wait/github.d.ts +25 -1
  98. package/dist/ci-wait/github.js +76 -9
  99. package/dist/ci-wait/github.js.map +1 -1
  100. package/dist/ci-wait/mcp.d.ts +0 -1
  101. package/dist/ci-wait/mcp.js +0 -2
  102. package/dist/ci-wait/mcp.js.map +1 -1
  103. package/dist/ci-wait/supervisor.d.ts +7 -0
  104. package/dist/ci-wait/supervisor.js +113 -17
  105. package/dist/ci-wait/supervisor.js.map +1 -1
  106. package/dist/ci-wait/tools.d.ts +536 -16
  107. package/dist/ci-wait/tools.js +165 -20
  108. package/dist/ci-wait/tools.js.map +1 -1
  109. package/dist/config.d.ts +3 -17
  110. package/dist/config.js +4 -60
  111. package/dist/config.js.map +1 -1
  112. package/dist/contract/active-provider.d.ts +12 -0
  113. package/dist/contract/agent-profiles.d.ts +2 -10
  114. package/dist/contract/ask-schema.d.ts +22 -4
  115. package/dist/contract/ask.d.ts +6 -4
  116. package/dist/contract/attachments.d.ts +35 -0
  117. package/dist/contract/attention.d.ts +46 -37
  118. package/dist/contract/automation-requests.d.ts +308 -0
  119. package/dist/contract/automation-schedule.d.ts +40 -11
  120. package/dist/contract/automation-storage.d.ts +512 -0
  121. package/dist/contract/automations.d.ts +421 -40
  122. package/dist/contract/ci-wait.d.ts +41 -4
  123. package/dist/contract/config-requests.d.ts +36 -0
  124. package/dist/contract/conversations.d.ts +24 -10
  125. package/dist/contract/defined.d.ts +8 -0
  126. package/dist/contract/delegation.d.ts +312 -8
  127. package/dist/contract/events.d.ts +24 -3
  128. package/dist/contract/github-requests.d.ts +26 -0
  129. package/dist/contract/github.d.ts +60 -27
  130. package/dist/contract/handoff.d.ts +43 -0
  131. package/dist/contract/health.d.ts +2 -2
  132. package/dist/contract/image-path.d.ts +6 -0
  133. package/dist/contract/index.d.ts +31 -0
  134. package/dist/contract/index.js +3756 -1525
  135. package/dist/contract/live-coordination.d.ts +158 -0
  136. package/dist/contract/live-limits.d.ts +8 -0
  137. package/dist/contract/live.d.ts +123 -0
  138. package/dist/contract/pending-ask.d.ts +58 -0
  139. package/dist/contract/permissions.d.ts +54 -0
  140. package/dist/contract/preview.d.ts +33 -0
  141. package/dist/contract/project-limits.d.ts +33 -0
  142. package/dist/contract/projects.d.ts +486 -20
  143. package/dist/contract/provider-requests.d.ts +77 -0
  144. package/dist/contract/query-requests.d.ts +52 -0
  145. package/dist/contract/repo-requests.d.ts +12 -0
  146. package/dist/contract/run-labels.d.ts +8 -0
  147. package/dist/contract/run-limits.d.ts +8 -0
  148. package/dist/contract/run-predicates.d.ts +32 -0
  149. package/dist/contract/run-requests.d.ts +33 -0
  150. package/dist/contract/run-search.d.ts +193 -0
  151. package/dist/contract/runs.d.ts +2232 -1894
  152. package/dist/contract/scheduled-tasks.d.ts +187 -0
  153. package/dist/contract/skills.d.ts +22 -772
  154. package/dist/contract/team-skill-ref.d.ts +10 -0
  155. package/dist/{core → contract}/tool-display.d.ts +4 -10
  156. package/dist/contract/ui-state-requests.d.ts +60 -0
  157. package/dist/contract/version.d.ts +3 -0
  158. package/dist/contract/voice.d.ts +213 -0
  159. package/dist/contract/waits.d.ts +1204 -0
  160. package/dist/contract/workflow-file.d.ts +37 -0
  161. package/dist/contract/workflow-requests.d.ts +7 -0
  162. package/dist/contract/workflows.d.ts +1 -2
  163. package/dist/contract/workspace.d.ts +34 -82
  164. package/dist/contract/zoned-time.d.ts +2 -11
  165. package/dist/core/agent-env.d.ts +4 -16
  166. package/dist/core/agent-env.js +14 -16
  167. package/dist/core/agent-env.js.map +1 -1
  168. package/dist/core/agent-profiles.d.ts +28 -40
  169. package/dist/core/agent-profiles.js +56 -51
  170. package/dist/core/agent-profiles.js.map +1 -1
  171. package/dist/core/agent-runner.d.ts +88 -44
  172. package/dist/core/agent-runner.js +17 -18
  173. package/dist/core/agent-runner.js.map +1 -1
  174. package/dist/core/ask.d.ts +3 -0
  175. package/dist/core/ask.js +12 -0
  176. package/dist/core/ask.js.map +1 -1
  177. package/dist/core/backend-detect.js +16 -2
  178. package/dist/core/backend-detect.js.map +1 -1
  179. package/dist/core/claude-cli-runner.d.ts +23 -4
  180. package/dist/core/claude-cli-runner.js +280 -76
  181. package/dist/core/claude-cli-runner.js.map +1 -1
  182. package/dist/core/claude-model-catalog.d.ts +2 -10
  183. package/dist/core/claude-model-catalog.js +2 -10
  184. package/dist/core/claude-model-catalog.js.map +1 -1
  185. package/dist/core/claude-ui-mapper.d.ts +2 -2
  186. package/dist/core/claude-ui-mapper.js +94 -2
  187. package/dist/core/claude-ui-mapper.js.map +1 -1
  188. package/dist/core/codex-app-server-runner.d.ts +3 -4
  189. package/dist/core/codex-app-server-runner.js +146 -51
  190. package/dist/core/codex-app-server-runner.js.map +1 -1
  191. package/dist/core/codex-app-server-transport.d.ts +4 -3
  192. package/dist/core/codex-app-server-transport.js +11 -7
  193. package/dist/core/codex-app-server-transport.js.map +1 -1
  194. package/dist/core/codex-approvals.d.ts +12 -0
  195. package/dist/core/codex-approvals.js +161 -0
  196. package/dist/core/codex-approvals.js.map +1 -0
  197. package/dist/core/codex-permissions.d.ts +1 -1
  198. package/dist/core/codex-ui-mapper.js +45 -1
  199. package/dist/core/codex-ui-mapper.js.map +1 -1
  200. package/dist/core/cursor-acp-runner.d.ts +1 -1
  201. package/dist/core/cursor-acp-runner.js +39 -15
  202. package/dist/core/cursor-acp-runner.js.map +1 -1
  203. package/dist/core/cursor-cli-flags.d.ts +19 -0
  204. package/dist/core/cursor-cli-flags.js +47 -0
  205. package/dist/core/cursor-cli-flags.js.map +1 -0
  206. package/dist/core/cursor-model-catalog.d.ts +22 -0
  207. package/dist/core/cursor-model-catalog.js +53 -24
  208. package/dist/core/cursor-model-catalog.js.map +1 -1
  209. package/dist/core/cursor-print-capabilities.d.ts +16 -0
  210. package/dist/core/cursor-print-capabilities.js +141 -0
  211. package/dist/core/cursor-print-capabilities.js.map +1 -0
  212. package/dist/core/cursor-print-process.d.ts +31 -0
  213. package/dist/core/cursor-print-process.js +117 -0
  214. package/dist/core/cursor-print-process.js.map +1 -0
  215. package/dist/core/cursor-print-runner.d.ts +31 -0
  216. package/dist/core/cursor-print-runner.js +555 -0
  217. package/dist/core/cursor-print-runner.js.map +1 -0
  218. package/dist/core/cursor-print-tool-catalog.d.ts +10 -0
  219. package/dist/core/cursor-print-tool-catalog.js +91 -0
  220. package/dist/core/cursor-print-tool-catalog.js.map +1 -0
  221. package/dist/core/cursor-print-ui-mapper.d.ts +47 -0
  222. package/dist/core/cursor-print-ui-mapper.js +553 -0
  223. package/dist/core/cursor-print-ui-mapper.js.map +1 -0
  224. package/dist/core/cursor-runner.d.ts +24 -0
  225. package/dist/core/cursor-runner.js +149 -0
  226. package/dist/core/cursor-runner.js.map +1 -0
  227. package/dist/core/cursor-ui-mapper.js +5 -2
  228. package/dist/core/cursor-ui-mapper.js.map +1 -1
  229. package/dist/core/defined.d.ts +1 -0
  230. package/dist/core/defined.js +2 -0
  231. package/dist/core/defined.js.map +1 -0
  232. package/dist/core/hook-items.d.ts +14 -0
  233. package/dist/core/hook-items.js +13 -0
  234. package/dist/core/hook-items.js.map +1 -0
  235. package/dist/core/host-model-catalog.d.ts +2 -9
  236. package/dist/core/host-model-catalog.js +2 -9
  237. package/dist/core/host-model-catalog.js.map +1 -1
  238. package/dist/core/managed-session.js +15 -3
  239. package/dist/core/managed-session.js.map +1 -1
  240. package/dist/core/model-identity.d.ts +4 -21
  241. package/dist/core/model-identity.js.map +1 -1
  242. package/dist/core/model-presets.d.ts +2 -13
  243. package/dist/core/model-presets.js +2 -13
  244. package/dist/core/model-presets.js.map +1 -1
  245. package/dist/core/omp-model-catalog.js.map +1 -1
  246. package/dist/core/omp-runner.d.ts +6 -7
  247. package/dist/core/omp-runner.js +92 -33
  248. package/dist/core/omp-runner.js.map +1 -1
  249. package/dist/core/omp-ui-mapper.js +1 -1
  250. package/dist/core/omp-ui-mapper.js.map +1 -1
  251. package/dist/core/opencode-model-catalog.js.map +1 -1
  252. package/dist/core/opencode-permissions.d.ts +52 -0
  253. package/dist/core/opencode-permissions.js +101 -0
  254. package/dist/core/opencode-permissions.js.map +1 -0
  255. package/dist/core/opencode-server-runner.d.ts +10 -9
  256. package/dist/core/opencode-server-runner.js +286 -28
  257. package/dist/core/opencode-server-runner.js.map +1 -1
  258. package/dist/core/opencode-session-error.d.ts +15 -7
  259. package/dist/core/opencode-session-error.js +25 -9
  260. package/dist/core/opencode-session-error.js.map +1 -1
  261. package/dist/core/opencode-ui-mapper.js +2 -2
  262. package/dist/core/opencode-ui-mapper.js.map +1 -1
  263. package/dist/core/pi-model-catalog.js.map +1 -1
  264. package/dist/core/pi-runner.d.ts +12 -4
  265. package/dist/core/pi-runner.js +171 -63
  266. package/dist/core/pi-runner.js.map +1 -1
  267. package/dist/core/pi-ui-mapper.js +1 -1
  268. package/dist/core/pi-ui-mapper.js.map +1 -1
  269. package/dist/core/process-usage.d.ts +5 -0
  270. package/dist/core/process-usage.js +24 -4
  271. package/dist/core/process-usage.js.map +1 -1
  272. package/dist/core/provider-auth.d.ts +16 -107
  273. package/dist/core/provider-auth.js +16 -132
  274. package/dist/core/provider-auth.js.map +1 -1
  275. package/dist/core/runner-factory.d.ts +4 -1
  276. package/dist/core/runner-factory.js +3 -3
  277. package/dist/core/runner-factory.js.map +1 -1
  278. package/dist/core/runner-runtime.d.ts +22 -0
  279. package/dist/core/runner-runtime.js +66 -5
  280. package/dist/core/runner-runtime.js.map +1 -1
  281. package/dist/core/runner-tool-grants.d.ts +21 -0
  282. package/dist/core/runner-tool-grants.js +64 -0
  283. package/dist/core/runner-tool-grants.js.map +1 -0
  284. package/dist/core/secret-redaction.d.ts +3 -20
  285. package/dist/core/secret-redaction.js +3 -20
  286. package/dist/core/secret-redaction.js.map +1 -1
  287. package/dist/core/session-process.d.ts +22 -2
  288. package/dist/core/session-process.js +170 -12
  289. package/dist/core/session-process.js.map +1 -1
  290. package/dist/core/shell-env.d.ts +2 -15
  291. package/dist/core/shell-env.js +2 -15
  292. package/dist/core/shell-env.js.map +1 -1
  293. package/dist/core/ui-events.d.ts +60 -47
  294. package/dist/core/ui-events.js +0 -23
  295. package/dist/core/ui-events.js.map +1 -1
  296. package/dist/core/usage-limit.js.map +1 -1
  297. package/dist/data-gitignore.js +3 -1
  298. package/dist/data-gitignore.js.map +1 -1
  299. package/dist/delegation/capacity.d.ts +4 -8
  300. package/dist/delegation/capacity.js +4 -8
  301. package/dist/delegation/capacity.js.map +1 -1
  302. package/dist/delegation/cli.js +123 -21
  303. package/dist/delegation/cli.js.map +1 -1
  304. package/dist/delegation/context.d.ts +19 -3
  305. package/dist/delegation/context.js +217 -22
  306. package/dist/delegation/context.js.map +1 -1
  307. package/dist/delegation/conversations.d.ts +4 -3
  308. package/dist/delegation/conversations.js +7 -5
  309. package/dist/delegation/conversations.js.map +1 -1
  310. package/dist/delegation/destroy-observation.d.ts +1 -1
  311. package/dist/delegation/destroy-observation.js.map +1 -1
  312. package/dist/delegation/effort.js.map +1 -1
  313. package/dist/delegation/execution-identity.d.ts +1 -1
  314. package/dist/delegation/execution-identity.js +41 -7
  315. package/dist/delegation/execution-identity.js.map +1 -1
  316. package/dist/delegation/idle-test-app.d.ts +4 -0
  317. package/dist/delegation/idle-test-app.js +174 -0
  318. package/dist/delegation/idle-test-app.js.map +1 -0
  319. package/dist/delegation/input.js.map +1 -1
  320. package/dist/delegation/park-notice.d.ts +42 -0
  321. package/dist/delegation/park-notice.js +71 -0
  322. package/dist/delegation/park-notice.js.map +1 -0
  323. package/dist/delegation/policy.d.ts +2 -0
  324. package/dist/delegation/policy.js +9 -1
  325. package/dist/delegation/policy.js.map +1 -1
  326. package/dist/delegation/process-liveness.d.ts +10 -15
  327. package/dist/delegation/process-liveness.js +11 -35
  328. package/dist/delegation/process-liveness.js.map +1 -1
  329. package/dist/delegation/provision.js +21 -19
  330. package/dist/delegation/provision.js.map +1 -1
  331. package/dist/delegation/questions.d.ts +3 -1
  332. package/dist/delegation/questions.js +10 -5
  333. package/dist/delegation/questions.js.map +1 -1
  334. package/dist/delegation/readiness.d.ts +2 -2
  335. package/dist/delegation/results.d.ts +4 -1
  336. package/dist/delegation/results.js +49 -18
  337. package/dist/delegation/results.js.map +1 -1
  338. package/dist/delegation/retry-backoff.d.ts +8 -0
  339. package/dist/delegation/retry-backoff.js +8 -0
  340. package/dist/delegation/retry-backoff.js.map +1 -1
  341. package/dist/delegation/routes.d.ts +114 -30
  342. package/dist/delegation/routes.js +4 -1
  343. package/dist/delegation/routes.js.map +1 -1
  344. package/dist/delegation/scratch-cleanup.d.ts +4 -5
  345. package/dist/delegation/scratch-cleanup.js +10 -8
  346. package/dist/delegation/scratch-cleanup.js.map +1 -1
  347. package/dist/delegation/service.d.ts +12 -4
  348. package/dist/delegation/service.js +76 -28
  349. package/dist/delegation/service.js.map +1 -1
  350. package/dist/delegation/transport.d.ts +154 -70
  351. package/dist/delegation/wait.d.ts +11 -0
  352. package/dist/delegation/wait.js +24 -0
  353. package/dist/delegation/wait.js.map +1 -1
  354. package/dist/delegation/windows-test-app.d.ts +5 -0
  355. package/dist/delegation/windows-test-app.js +63 -0
  356. package/dist/delegation/windows-test-app.js.map +1 -0
  357. package/dist/delegation/windows-workspace-identity.d.ts +3 -0
  358. package/dist/delegation/windows-workspace-identity.js +22 -0
  359. package/dist/delegation/windows-workspace-identity.js.map +1 -0
  360. package/dist/delegation/workspace.d.ts +5 -10
  361. package/dist/delegation/workspace.js +136 -75
  362. package/dist/delegation/workspace.js.map +1 -1
  363. package/dist/detached-process.d.ts +103 -0
  364. package/dist/detached-process.js +301 -0
  365. package/dist/detached-process.js.map +1 -0
  366. package/dist/discovery/catalog.d.ts +2 -1
  367. package/dist/discovery/catalog.js.map +1 -1
  368. package/dist/discovery/cli.js +3 -1
  369. package/dist/discovery/cli.js.map +1 -1
  370. package/dist/git-diff-base.d.ts +6 -61
  371. package/dist/git-diff-base.js +6 -70
  372. package/dist/git-diff-base.js.map +1 -1
  373. package/dist/git-index-copy.d.ts +4 -0
  374. package/dist/git-index-copy.js +10 -0
  375. package/dist/git-index-copy.js.map +1 -0
  376. package/dist/git-worktree-lock.d.ts +2 -4
  377. package/dist/git-worktree-lock.js +2 -4
  378. package/dist/git-worktree-lock.js.map +1 -1
  379. package/dist/git-worktree-release.d.ts +2 -2
  380. package/dist/git-worktree-release.js.map +1 -1
  381. package/dist/git-worktree.d.ts +47 -76
  382. package/dist/git-worktree.js +240 -94
  383. package/dist/git-worktree.js.map +1 -1
  384. package/dist/handoff.d.ts +51 -41
  385. package/dist/handoff.js +130 -78
  386. package/dist/handoff.js.map +1 -1
  387. package/dist/index.js +60 -39
  388. package/dist/index.js.map +1 -1
  389. package/dist/merge-observations.d.ts +13 -0
  390. package/dist/merge-observations.js +55 -0
  391. package/dist/merge-observations.js.map +1 -0
  392. package/dist/open-url.js +4 -14
  393. package/dist/open-url.js.map +1 -1
  394. package/dist/paths.d.ts +10 -13
  395. package/dist/paths.js +36 -17
  396. package/dist/paths.js.map +1 -1
  397. package/dist/planner.d.ts +2 -1
  398. package/dist/planner.js +9 -6
  399. package/dist/planner.js.map +1 -1
  400. package/dist/preview/agent-address.d.ts +34 -0
  401. package/dist/preview/agent-address.js +51 -0
  402. package/dist/preview/agent-address.js.map +1 -0
  403. package/dist/preview/agent-marker.d.ts +17 -0
  404. package/dist/preview/agent-marker.js +71 -0
  405. package/dist/preview/agent-marker.js.map +1 -0
  406. package/dist/preview/cdp-proxy.d.ts +54 -0
  407. package/dist/preview/cdp-proxy.js +151 -0
  408. package/dist/preview/cdp-proxy.js.map +1 -0
  409. package/dist/preview/cdp.d.ts +39 -3
  410. package/dist/preview/cdp.js +155 -57
  411. package/dist/preview/cdp.js.map +1 -1
  412. package/dist/preview/chromium.d.ts +18 -6
  413. package/dist/preview/chromium.js +112 -25
  414. package/dist/preview/chromium.js.map +1 -1
  415. package/dist/preview/dev-server.d.ts +1 -1
  416. package/dist/preview/dev-server.js +20 -44
  417. package/dist/preview/dev-server.js.map +1 -1
  418. package/dist/preview/host.d.ts +70 -3
  419. package/dist/preview/host.js +314 -28
  420. package/dist/preview/host.js.map +1 -1
  421. package/dist/preview/registration.d.ts +10 -5
  422. package/dist/preview/registration.js +2 -0
  423. package/dist/preview/registration.js.map +1 -1
  424. package/dist/preview/session.d.ts +2 -0
  425. package/dist/preview/session.js +10 -0
  426. package/dist/preview/session.js.map +1 -1
  427. package/dist/process-wait/exit-file.d.ts +29 -0
  428. package/dist/process-wait/exit-file.js +67 -0
  429. package/dist/process-wait/exit-file.js.map +1 -0
  430. package/dist/process-wait/supervisor.d.ts +30 -0
  431. package/dist/process-wait/supervisor.js +71 -0
  432. package/dist/process-wait/supervisor.js.map +1 -0
  433. package/dist/release/snapshot.d.ts +6 -6
  434. package/dist/runs/agent-tmpdir.d.ts +51 -36
  435. package/dist/runs/agent-tmpdir.js +389 -132
  436. package/dist/runs/agent-tmpdir.js.map +1 -1
  437. package/dist/runs/auto-name.d.ts +6 -6
  438. package/dist/runs/auto-name.js +1 -1
  439. package/dist/runs/auto-name.js.map +1 -1
  440. package/dist/runs/client-request.d.ts +3 -3
  441. package/dist/runs/client-request.js +3 -3
  442. package/dist/runs/client-request.js.map +1 -1
  443. package/dist/runs/delegation-state.d.ts +11 -2
  444. package/dist/runs/delegation-state.js.map +1 -1
  445. package/dist/runs/event-history-agent-context.d.ts +14 -0
  446. package/dist/runs/event-history-agent-context.js +105 -0
  447. package/dist/runs/event-history-agent-context.js.map +1 -0
  448. package/dist/runs/event-history-context.d.ts +15 -0
  449. package/dist/runs/event-history-context.js +109 -0
  450. package/dist/runs/event-history-context.js.map +1 -0
  451. package/dist/runs/event-history.d.ts +4 -1
  452. package/dist/runs/event-history.js +29 -130
  453. package/dist/runs/event-history.js.map +1 -1
  454. package/dist/runs/history-stream.d.ts +2 -0
  455. package/dist/runs/history-stream.js +62 -0
  456. package/dist/runs/history-stream.js.map +1 -0
  457. package/dist/runs/legacy-index.d.ts +6 -19
  458. package/dist/runs/legacy-index.js +6 -19
  459. package/dist/runs/legacy-index.js.map +1 -1
  460. package/dist/runs/live-item-snapshots.d.ts +13 -0
  461. package/dist/runs/live-item-snapshots.js +79 -0
  462. package/dist/runs/live-item-snapshots.js.map +1 -0
  463. package/dist/runs/loaded-run-healing.d.ts +3 -0
  464. package/dist/runs/loaded-run-healing.js +17 -0
  465. package/dist/runs/loaded-run-healing.js.map +1 -0
  466. package/dist/runs/outcome-commit.d.ts +98 -0
  467. package/dist/runs/outcome-commit.js +132 -0
  468. package/dist/runs/outcome-commit.js.map +1 -0
  469. package/dist/runs/pull-requests.d.ts +14 -0
  470. package/dist/runs/pull-requests.js +96 -0
  471. package/dist/runs/pull-requests.js.map +1 -0
  472. package/dist/runs/raw-record.d.ts +3 -15
  473. package/dist/runs/raw-record.js +3 -15
  474. package/dist/runs/raw-record.js.map +1 -1
  475. package/dist/runs/retention.d.ts +2 -2
  476. package/dist/runs/retention.js +7 -0
  477. package/dist/runs/retention.js.map +1 -1
  478. package/dist/runs/review-gate.d.ts +1 -1
  479. package/dist/runs/review-gate.js.map +1 -1
  480. package/dist/runs/run-claims.d.ts +2 -9
  481. package/dist/runs/run-claims.js.map +1 -1
  482. package/dist/runs/run-database.d.ts +29 -34
  483. package/dist/runs/run-database.js +124 -46
  484. package/dist/runs/run-database.js.map +1 -1
  485. package/dist/runs/run-index.d.ts +6 -25
  486. package/dist/runs/run-index.js +25 -38
  487. package/dist/runs/run-index.js.map +1 -1
  488. package/dist/runs/run-row.d.ts +5 -34
  489. package/dist/runs/run-row.js +13 -38
  490. package/dist/runs/run-row.js.map +1 -1
  491. package/dist/runs/store.d.ts +1597 -744
  492. package/dist/runs/store.js +1231 -739
  493. package/dist/runs/store.js.map +1 -1
  494. package/dist/runs/task-markers.d.ts +1 -0
  495. package/dist/runs/task-markers.js +5 -1
  496. package/dist/runs/task-markers.js.map +1 -1
  497. package/dist/runs/task-refs.d.ts +3 -3
  498. package/dist/runs/transcript-facts-fold.d.ts +64 -0
  499. package/dist/runs/transcript-facts-fold.js +109 -0
  500. package/dist/runs/transcript-facts-fold.js.map +1 -0
  501. package/dist/runs/transcript-facts-load.d.ts +11 -0
  502. package/dist/runs/transcript-facts-load.js +66 -0
  503. package/dist/runs/transcript-facts-load.js.map +1 -0
  504. package/dist/runs/transcript-facts-queue.d.ts +30 -0
  505. package/dist/runs/transcript-facts-queue.js +227 -0
  506. package/dist/runs/transcript-facts-queue.js.map +1 -0
  507. package/dist/runs/transcript-facts-worker.d.ts +15 -0
  508. package/dist/runs/transcript-facts-worker.js +26 -0
  509. package/dist/runs/transcript-facts-worker.js.map +1 -0
  510. package/dist/runs/transcript-facts.d.ts +13 -51
  511. package/dist/runs/transcript-facts.js +86 -202
  512. package/dist/runs/transcript-facts.js.map +1 -1
  513. package/dist/runs/ui-event-sink.d.ts +5 -30
  514. package/dist/runs/ui-event-sink.js +8 -30
  515. package/dist/runs/ui-event-sink.js.map +1 -1
  516. package/dist/runs/webhook.d.ts +104 -38
  517. package/dist/runs/webhook.js +180 -77
  518. package/dist/runs/webhook.js.map +1 -1
  519. package/dist/server/app-type.d.ts +2 -17
  520. package/dist/server/archive-on-merge.d.ts +35 -0
  521. package/dist/server/archive-on-merge.js +202 -0
  522. package/dist/server/archive-on-merge.js.map +1 -0
  523. package/dist/server/capabilities.d.ts +3 -15
  524. package/dist/server/capabilities.js +6 -19
  525. package/dist/server/capabilities.js.map +1 -1
  526. package/dist/server/checkout-progress.d.ts +10 -0
  527. package/dist/server/checkout-progress.js +27 -0
  528. package/dist/server/checkout-progress.js.map +1 -0
  529. package/dist/server/checkout.d.ts +10 -19
  530. package/dist/server/checkout.js +15 -45
  531. package/dist/server/checkout.js.map +1 -1
  532. package/dist/server/cockpit-ownership.d.ts +2 -0
  533. package/dist/server/cockpit-ownership.js +9 -0
  534. package/dist/server/cockpit-ownership.js.map +1 -1
  535. package/dist/server/cold-repo-handles.d.ts +2 -7
  536. package/dist/server/cold-repo-handles.js +2 -7
  537. package/dist/server/cold-repo-handles.js.map +1 -1
  538. package/dist/server/completion-actions.d.ts +30 -0
  539. package/dist/server/completion-actions.js +204 -0
  540. package/dist/server/completion-actions.js.map +1 -0
  541. package/dist/server/create-project.d.ts +71 -0
  542. package/dist/server/create-project.js +448 -0
  543. package/dist/server/create-project.js.map +1 -0
  544. package/dist/server/forge/github-filters.d.ts +7 -1
  545. package/dist/server/forge/github-filters.js +66 -51
  546. package/dist/server/forge/github-filters.js.map +1 -1
  547. package/dist/server/forge/github-merge-requirements.d.ts +18 -0
  548. package/dist/server/forge/github-merge-requirements.js +137 -0
  549. package/dist/server/forge/github-merge-requirements.js.map +1 -0
  550. package/dist/server/forge/github.d.ts +98 -111
  551. package/dist/server/forge/github.js +625 -306
  552. package/dist/server/forge/github.js.map +1 -1
  553. package/dist/server/forge/types.d.ts +25 -22
  554. package/dist/server/fs-browse.d.ts +3 -28
  555. package/dist/server/fs-browse.js.map +1 -1
  556. package/dist/server/git-changes.d.ts +5 -15
  557. package/dist/server/git-changes.js +3 -50
  558. package/dist/server/git-changes.js.map +1 -1
  559. package/dist/server/git.d.ts +3 -1
  560. package/dist/server/git.js +12 -0
  561. package/dist/server/git.js.map +1 -1
  562. package/dist/server/github.d.ts +1 -1
  563. package/dist/server/github.js +1 -1
  564. package/dist/server/github.js.map +1 -1
  565. package/dist/server/launch-key.d.ts +2 -4
  566. package/dist/server/launch-key.js +2 -4
  567. package/dist/server/launch-key.js.map +1 -1
  568. package/dist/server/live-routes.d.ts +68 -0
  569. package/dist/server/live-routes.js +147 -0
  570. package/dist/server/live-routes.js.map +1 -0
  571. package/dist/server/open-in-app.d.ts +4 -13
  572. package/dist/server/open-in-app.js +5 -41
  573. package/dist/server/open-in-app.js.map +1 -1
  574. package/dist/server/open-in-targets.d.ts +42 -0
  575. package/dist/server/open-in-targets.js +110 -0
  576. package/dist/server/open-in-targets.js.map +1 -0
  577. package/dist/server/open-in-terminal.d.ts +3 -0
  578. package/dist/server/open-in-terminal.js +6 -26
  579. package/dist/server/open-in-terminal.js.map +1 -1
  580. package/dist/server/preview-cdp-socket.d.ts +21 -0
  581. package/dist/server/preview-cdp-socket.js +112 -0
  582. package/dist/server/preview-cdp-socket.js.map +1 -0
  583. package/dist/server/preview-socket.d.ts +11 -11
  584. package/dist/server/preview-socket.js +22 -22
  585. package/dist/server/preview-socket.js.map +1 -1
  586. package/dist/server/preview-sockets.d.ts +14 -0
  587. package/dist/server/preview-sockets.js +18 -0
  588. package/dist/server/preview-sockets.js.map +1 -0
  589. package/dist/server/project-context.d.ts +54 -18
  590. package/dist/server/project-context.js +178 -5
  591. package/dist/server/project-context.js.map +1 -1
  592. package/dist/server/provider-action-gate.d.ts +0 -2
  593. package/dist/server/provider-action-gate.js +0 -21
  594. package/dist/server/provider-action-gate.js.map +1 -1
  595. package/dist/server/provider-auth-runtime.d.ts +3 -18
  596. package/dist/server/provider-auth-runtime.js +3 -18
  597. package/dist/server/provider-auth-runtime.js.map +1 -1
  598. package/dist/server/repo-branches.d.ts +3 -9
  599. package/dist/server/repo-branches.js +6 -22
  600. package/dist/server/repo-branches.js.map +1 -1
  601. package/dist/server/run-event-feed.d.ts +20 -0
  602. package/dist/server/run-event-feed.js +190 -0
  603. package/dist/server/run-event-feed.js.map +1 -0
  604. package/dist/server/server-deps.d.ts +37 -0
  605. package/dist/server/server-deps.js +114 -0
  606. package/dist/server/server-deps.js.map +1 -0
  607. package/dist/server/server.d.ts +14082 -11998
  608. package/dist/server/server.js +785 -1647
  609. package/dist/server/server.js.map +1 -1
  610. package/dist/server/start-run-options.d.ts +82 -0
  611. package/dist/server/start-run-options.js +111 -0
  612. package/dist/server/start-run-options.js.map +1 -0
  613. package/dist/server/validators.d.ts +3 -29
  614. package/dist/server/validators.js +1 -2
  615. package/dist/server/validators.js.map +1 -1
  616. package/dist/server/ws.d.ts +3 -23
  617. package/dist/server/ws.js +3 -23
  618. package/dist/server/ws.js.map +1 -1
  619. package/dist/server-install/engine.d.ts +6 -9
  620. package/dist/server-install/engine.js +2 -9
  621. package/dist/server-install/engine.js.map +1 -1
  622. package/dist/server-install/platforms/ubuntu-vps.d.ts +1 -1
  623. package/dist/server-install/state.d.ts +2 -4
  624. package/dist/server-install/state.js +2 -4
  625. package/dist/server-install/state.js.map +1 -1
  626. package/dist/server-install/steps.d.ts +2 -5
  627. package/dist/server-install/steps.js +3 -6
  628. package/dist/server-install/steps.js.map +1 -1
  629. package/dist/server-install/types.d.ts +3 -6
  630. package/dist/server-install/types.js +2 -5
  631. package/dist/server-install/types.js.map +1 -1
  632. package/dist/server-install/ui.d.ts +2 -8
  633. package/dist/server-install/ui.js +3 -3
  634. package/dist/server-install/ui.js.map +1 -1
  635. package/dist/session-context.d.ts +64 -0
  636. package/dist/session-context.js +264 -0
  637. package/dist/session-context.js.map +1 -0
  638. package/dist/skills-provision.d.ts +62 -0
  639. package/dist/skills-provision.js +325 -0
  640. package/dist/skills-provision.js.map +1 -0
  641. package/dist/skills-remote.d.ts +29 -27
  642. package/dist/skills-remote.js +236 -100
  643. package/dist/skills-remote.js.map +1 -1
  644. package/dist/skills.d.ts +25 -36
  645. package/dist/skills.js +43 -54
  646. package/dist/skills.js.map +1 -1
  647. package/dist/subcommands.d.ts +11 -0
  648. package/dist/subcommands.js +24 -0
  649. package/dist/subcommands.js.map +1 -0
  650. package/dist/task-cli/cli.js +78 -25
  651. package/dist/task-cli/cli.js.map +1 -1
  652. package/dist/task-cli/discovery.d.ts +4 -2
  653. package/dist/task-cli/discovery.js +14 -13
  654. package/dist/task-cli/discovery.js.map +1 -1
  655. package/dist/task-cli/hint.d.ts +20 -0
  656. package/dist/task-cli/hint.js +14 -0
  657. package/dist/task-cli/hint.js.map +1 -0
  658. package/dist/task-cli/http.d.ts +1 -0
  659. package/dist/task-cli/http.js +3 -1
  660. package/dist/task-cli/http.js.map +1 -1
  661. package/dist/task-cli/projections.d.ts +14 -11
  662. package/dist/task-cli/projections.js +15 -11
  663. package/dist/task-cli/projections.js.map +1 -1
  664. package/dist/task-cli/watch.d.ts +18 -10
  665. package/dist/task-cli/watch.js +128 -20
  666. package/dist/task-cli/watch.js.map +1 -1
  667. package/dist/ui-state.d.ts +17 -3
  668. package/dist/ui-state.js +56 -9
  669. package/dist/ui-state.js.map +1 -1
  670. package/dist/update-check.d.ts +0 -10
  671. package/dist/update-check.js +1 -16
  672. package/dist/update-check.js.map +1 -1
  673. package/dist/voice/decide.d.ts +32 -0
  674. package/dist/voice/decide.js +297 -0
  675. package/dist/voice/decide.js.map +1 -0
  676. package/dist/voice/decision-log.d.ts +52 -0
  677. package/dist/voice/decision-log.js +107 -0
  678. package/dist/voice/decision-log.js.map +1 -0
  679. package/dist/voice/engine.d.ts +119 -0
  680. package/dist/voice/engine.js +60 -0
  681. package/dist/voice/engine.js.map +1 -0
  682. package/dist/voice/fake-engine.d.ts +10 -0
  683. package/dist/voice/fake-engine.js +192 -0
  684. package/dist/voice/fake-engine.js.map +1 -0
  685. package/dist/voice/jev-engine.d.ts +17 -0
  686. package/dist/voice/jev-engine.js +110 -0
  687. package/dist/voice/jev-engine.js.map +1 -0
  688. package/dist/voice/pending-ask.d.ts +17 -0
  689. package/dist/voice/pending-ask.js +50 -0
  690. package/dist/voice/pending-ask.js.map +1 -0
  691. package/dist/voice/routes.d.ts +115 -0
  692. package/dist/voice/routes.js +91 -0
  693. package/dist/voice/routes.js.map +1 -0
  694. package/dist/voice/sentence.d.ts +21 -0
  695. package/dist/voice/sentence.js +61 -0
  696. package/dist/voice/sentence.js.map +1 -0
  697. package/dist/voice/thresholds.d.ts +28 -0
  698. package/dist/voice/thresholds.js +29 -0
  699. package/dist/voice/thresholds.js.map +1 -0
  700. package/dist/webhooks/cli.d.ts +18 -0
  701. package/dist/webhooks/cli.js +145 -0
  702. package/dist/webhooks/cli.js.map +1 -0
  703. package/dist/webhooks/prompt.d.ts +5 -0
  704. package/dist/webhooks/prompt.js +26 -0
  705. package/dist/webhooks/prompt.js.map +1 -0
  706. package/dist/workflows/active-run.d.ts +167 -0
  707. package/dist/workflows/active-run.js +21 -0
  708. package/dist/workflows/active-run.js.map +1 -0
  709. package/dist/workflows/continuation-admission.d.ts +913 -0
  710. package/dist/workflows/continuation-admission.js +265 -0
  711. package/dist/workflows/continuation-admission.js.map +1 -0
  712. package/dist/workflows/execution-account.d.ts +13 -0
  713. package/dist/workflows/execution-account.js +18 -0
  714. package/dist/workflows/execution-account.js.map +1 -0
  715. package/dist/workflows/input-acknowledgement.d.ts +152 -0
  716. package/dist/workflows/input-acknowledgement.js +38 -0
  717. package/dist/workflows/input-acknowledgement.js.map +1 -0
  718. package/dist/workflows/line-order.d.ts +5 -0
  719. package/dist/workflows/line-order.js +25 -0
  720. package/dist/workflows/line-order.js.map +1 -0
  721. package/dist/workflows/outcome-batches.d.ts +20 -0
  722. package/dist/workflows/outcome-batches.js +25 -0
  723. package/dist/workflows/outcome-batches.js.map +1 -0
  724. package/dist/workflows/question-routing.d.ts +8 -0
  725. package/dist/workflows/question-routing.js +18 -0
  726. package/dist/workflows/question-routing.js.map +1 -0
  727. package/dist/workflows/run-scheduling.d.ts +35 -0
  728. package/dist/workflows/run-scheduling.js +61 -0
  729. package/dist/workflows/run-scheduling.js.map +1 -0
  730. package/dist/workflows/run-states.d.ts +190 -0
  731. package/dist/workflows/run-states.js +295 -0
  732. package/dist/workflows/run-states.js.map +1 -0
  733. package/dist/workflows/run.d.ts +1296 -278
  734. package/dist/workflows/run.js +5730 -3265
  735. package/dist/workflows/run.js.map +1 -1
  736. package/dist/workflows/turn-end.d.ts +116 -0
  737. package/dist/workflows/turn-end.js +68 -0
  738. package/dist/workflows/turn-end.js.map +1 -0
  739. package/dist/workflows/types.d.ts +35 -130
  740. package/dist/workflows/types.js +47 -100
  741. package/dist/workflows/types.js.map +1 -1
  742. package/dist/workflows/waits.d.ts +74 -0
  743. package/dist/workflows/waits.js +197 -0
  744. package/dist/workflows/waits.js.map +1 -0
  745. package/dist/workspace/agent-accounts.d.ts +5 -38
  746. package/dist/workspace/agent-accounts.js +14 -40
  747. package/dist/workspace/agent-accounts.js.map +1 -1
  748. package/dist/workspace/agent-profiles.d.ts +40 -16
  749. package/dist/workspace/agent-profiles.js +77 -17
  750. package/dist/workspace/agent-profiles.js.map +1 -1
  751. package/dist/workspace/config.d.ts +48 -49
  752. package/dist/workspace/config.js +68 -72
  753. package/dist/workspace/config.js.map +1 -1
  754. package/dist/workspace/migrations.d.ts +3 -13
  755. package/dist/workspace/migrations.js.map +1 -1
  756. package/dist/workspace/projects-cli.d.ts +1 -1
  757. package/dist/workspace/projects-cli.js.map +1 -1
  758. package/dist/workspace/projects.d.ts +15 -18
  759. package/dist/workspace/projects.js +20 -16
  760. package/dist/workspace/projects.js.map +1 -1
  761. package/dist/workspace/semaphore.d.ts +11 -45
  762. package/dist/workspace/semaphore.js +6 -18
  763. package/dist/workspace/semaphore.js.map +1 -1
  764. package/dist/workspace/ui-state.d.ts +6 -4
  765. package/dist/workspace/ui-state.js +6 -4
  766. package/dist/workspace/ui-state.js.map +1 -1
  767. package/dist/workspace/webhooks.d.ts +40 -0
  768. package/dist/workspace/webhooks.js +146 -0
  769. package/dist/workspace/webhooks.js.map +1 -0
  770. package/dist/worktree-manifest.d.ts +165 -0
  771. package/dist/worktree-manifest.js +530 -0
  772. package/dist/worktree-manifest.js.map +1 -0
  773. package/dist/worktree-setup.js.map +1 -1
  774. package/package.json +6 -5
  775. package/scripts/benchmark-run-store-fixture.ts +7 -3
  776. package/scripts/check-pack.mjs +3 -1
  777. package/scripts/inline-contract.mjs +80 -27
  778. package/scripts/install-as-command.mjs +3 -1
  779. package/scripts/measure-transcript-facts-health.mjs +87 -0
  780. package/scripts/measure-transcript-facts-lag.mjs +68 -0
  781. package/scripts/mock-ci-tool.mjs +49 -6
  782. package/scripts/mock-claude-model-refusal.mjs +42 -0
  783. package/scripts/mock-claude.mjs +833 -591
  784. package/scripts/mock-codex-app-server.mjs +785 -418
  785. package/scripts/mock-cursor-acp.mjs +296 -125
  786. package/scripts/mock-cursor-print.mjs +256 -0
  787. package/scripts/mock-deferred-turn-start.mjs +16 -0
  788. package/scripts/mock-delivery-control.d.mts +23 -0
  789. package/scripts/mock-delivery-control.mjs +80 -0
  790. package/scripts/mock-env-record.mjs +11 -0
  791. package/scripts/mock-failure-control.d.mts +13 -0
  792. package/scripts/mock-failure-control.mjs +68 -0
  793. package/scripts/mock-multi-pr.mjs +7 -0
  794. package/scripts/mock-nudge.mjs +45 -0
  795. package/scripts/mock-omp-rpc.mjs +429 -255
  796. package/scripts/mock-opencode-serve.mjs +972 -557
  797. package/scripts/mock-pi-rpc.mjs +558 -331
  798. package/scripts/mock-runner-crash.mjs +18 -2
  799. package/scripts/mock-session-context.d.mts +5 -0
  800. package/scripts/mock-session-context.mjs +56 -0
  801. package/scripts/mock-silent-tail.mjs +10 -3
  802. package/scripts/pi-default-tools.mjs +22 -0
  803. package/scripts/pi-project-skills.mjs +59 -0
  804. package/scripts/probe-cursor-print.mjs +627 -0
  805. package/scripts/transcript-facts-fixture.mjs +53 -0
  806. package/scripts/voice-replay.ts +50 -0
  807. package/scripts/windows-workspace-identity.cs +208 -0
  808. package/scripts/windows-workspace-identity.ps1 +10 -0
  809. package/web/dist/assets/alert-dialog-DfctKUMw.js +1 -0
  810. package/web/dist/assets/button-CeupvoKG.js +41 -0
  811. package/web/dist/assets/centered-state-hDsyXrnH.js +1 -0
  812. package/web/dist/assets/cez-idle-D3aj_9lp.js +1 -0
  813. package/web/dist/assets/diff-BnIXZK3i2.js +3 -0
  814. package/web/dist/assets/diff-controls-GewpW9aF.js +1 -0
  815. package/web/dist/assets/diff-view-Byo7gpvW.js +11 -0
  816. package/web/dist/assets/dropdown-menu-COj_J5wd.js +6 -0
  817. package/web/dist/assets/eye-B8okNWXU.js +1 -0
  818. package/web/dist/assets/git-toolbar-DY0z0n4S.js +1 -0
  819. package/web/dist/assets/github-DwU6ALRK.js +1 -0
  820. package/web/dist/assets/{image-preview-FOEfqpZP.js → image-preview-C_t9q45f.js} +1 -1
  821. package/web/dist/assets/index-BOfE8pyD.js +34 -0
  822. package/web/dist/assets/index-D9HpsmdW.css +1 -0
  823. package/web/dist/assets/live-protocol-Uce2NwEX.js +64 -0
  824. package/web/dist/assets/live-worker-BizfZSoh.ts +33 -0
  825. package/web/dist/assets/markdown-BBDDg-4t.js +1 -0
  826. package/web/dist/assets/nav-row-styles-AoIhhIwf.js +1 -0
  827. package/web/dist/assets/popover-D56fhiXj.js +1 -0
  828. package/web/dist/assets/preview-pane-connected-BmVqfSU5.js +3 -0
  829. package/web/dist/assets/reference-chip-gQNk-4jc.js +1 -0
  830. package/web/dist/assets/repo-git-B5ECDUuv.js +1 -0
  831. package/web/dist/assets/{repo-git-CoKp-g9K.css → repo-git-DVXNOJDT.css} +1 -1
  832. package/web/dist/assets/run-header-C0Y1NvEM.js +2 -0
  833. package/web/dist/assets/run-header-CC7VUTg9.css +1 -0
  834. package/web/dist/assets/skills-DUeTkZ9z.js +1 -0
  835. package/web/dist/assets/status-dot-1PR0i69P.js +1 -0
  836. package/web/dist/assets/tab-link-DmWk4l5O.js +1 -0
  837. package/web/dist/assets/task-changes-DKZXuW89.js +1 -0
  838. package/web/dist/assets/task-commits-BZ6KxWdF.js +1 -0
  839. package/web/dist/assets/task-files-DlF7Apcn.js +2 -0
  840. package/web/dist/assets/task-github-item-CVPo3geZ.js +1 -0
  841. package/web/dist/assets/task-thread-DtQ6uGfQ.css +1 -0
  842. package/web/dist/assets/task-thread-mm4-ECyo.js +9 -0
  843. package/web/dist/assets/thread-state-CIw2BlhT.js +4 -0
  844. package/web/dist/assets/use-hand-to-agent-state-BCDjzHnG.js +1 -0
  845. package/web/dist/assets/use-hand-to-agent-state-C7esPJ-N.css +1 -0
  846. package/web/dist/assets/workflows-D8vYD-ql.js +7 -0
  847. package/web/dist/assets/zoomable-image-CxQSsfcn.js +1 -0
  848. package/web/dist/index.html +13 -17
  849. package/web/dist/live-worker.js +4 -0
  850. package/dist/core/tool-display.js +0 -179
  851. package/dist/core/tool-display.js.map +0 -1
  852. package/dist/runs/human-ask-summary.d.ts +0 -9
  853. package/dist/runs/human-ask-summary.js +0 -39
  854. package/dist/runs/human-ask-summary.js.map +0 -1
  855. package/dist/runs/run-search.d.ts +0 -29
  856. package/dist/runs/run-search.js +0 -63
  857. package/dist/runs/run-search.js.map +0 -1
  858. package/dist/skills-banner.d.ts +0 -22
  859. package/dist/skills-banner.js +0 -41
  860. package/dist/skills-banner.js.map +0 -1
  861. package/dist/skills-update.d.ts +0 -88
  862. package/dist/skills-update.js +0 -392
  863. package/dist/skills-update.js.map +0 -1
  864. package/dist/todos.d.ts +0 -48
  865. package/dist/todos.js +0 -201
  866. package/dist/todos.js.map +0 -1
  867. package/web/dist/assets/agent-accounts-CFOBMVHc.js +0 -1
  868. package/web/dist/assets/alert-dialog-BrpNUwMt.js +0 -1
  869. package/web/dist/assets/button-C_rd7CPp.js +0 -43
  870. package/web/dist/assets/compare-variants-D6WXhvXA.js +0 -1
  871. package/web/dist/assets/diff-DrEARXTV.js +0 -3
  872. package/web/dist/assets/diff-controls-B1-j7nFg.js +0 -1
  873. package/web/dist/assets/diff-view-DvGLjdTk.js +0 -6
  874. package/web/dist/assets/dropdown-menu-BegOZFrj.js +0 -1
  875. package/web/dist/assets/format-DpNe3S0A.js +0 -1
  876. package/web/dist/assets/git-toolbar-Cw4ljmhB.js +0 -1
  877. package/web/dist/assets/github-CfV5sqL0.js +0 -1
  878. package/web/dist/assets/index-DgWWlOLy.js +0 -13
  879. package/web/dist/assets/index-fBFnuvxu.css +0 -1
  880. package/web/dist/assets/nav-row-styles-vp2JbhuJ.js +0 -1
  881. package/web/dist/assets/popover-BohP6WTx.js +0 -1
  882. package/web/dist/assets/preview-pane-connected-qTA5KNoJ.js +0 -3
  883. package/web/dist/assets/preview-state-vuMviq6q.js +0 -1
  884. package/web/dist/assets/prompt-templates-Cubdut_C.js +0 -15
  885. package/web/dist/assets/repo-git-Cf4t5PD0.js +0 -1
  886. package/web/dist/assets/run-diff-BpkUW37Z.js +0 -3
  887. package/web/dist/assets/run-header-68WHf9wV.js +0 -1
  888. package/web/dist/assets/run-header-8GObNx6F.css +0 -1
  889. package/web/dist/assets/search-x-I9Q8pgUa.js +0 -1
  890. package/web/dist/assets/skills-DFBiAuJG.js +0 -1
  891. package/web/dist/assets/skills-DiVIgKeU.js +0 -1
  892. package/web/dist/assets/square-terminal-B6RYYieR.js +0 -1
  893. package/web/dist/assets/status-dot-CqTJHSP9.js +0 -1
  894. package/web/dist/assets/tab-link-B07Hu4QX.js +0 -1
  895. package/web/dist/assets/task-changes--lQF6EYf.js +0 -1
  896. package/web/dist/assets/task-commits-Cx0qy4DN.js +0 -1
  897. package/web/dist/assets/task-files-D50s-gKW.js +0 -2
  898. package/web/dist/assets/task-github-item-DaqmSwYT.js +0 -1
  899. package/web/dist/assets/task-thread-BPQ64lui.js +0 -7
  900. package/web/dist/assets/task-thread-zNE2Q9E-.css +0 -1
  901. package/web/dist/assets/textarea-D20O4Pw0.js +0 -1
  902. package/web/dist/assets/thread-state-DWcFMrvb.js +0 -4
  903. package/web/dist/assets/use-hand-to-agent-state-BLIGmwbd.css +0 -1
  904. package/web/dist/assets/use-hand-to-agent-state-Cy9eyyfL.js +0 -1
  905. package/web/dist/assets/utils-JsL3ZsJf.js +0 -64
  906. package/web/dist/assets/workflows-jwG7uxqi.js +0 -11
  907. package/web/dist/assets/zoomable-image-BJJ64KGc.js +0 -1
@@ -1,9 +1,15 @@
1
+ import { defined } from '../../core/defined.js';
2
+ import { activeWorkflowRunsArgs, parseActiveWorkflowRuns } from '../../ci-wait/github.js';
3
+ import { exactLineIndex, nextLineIndex } from '../../contract/index.js';
4
+ import { pullRequestKey } from '../../runs/pull-requests.js';
5
+ import { matchesGithubPullRequest, mergedPullRequestObservations } from '../../merge-observations.js';
1
6
  import { randomUUID } from 'node:crypto';
2
7
  import { execFile } from 'node:child_process';
3
8
  import { promisify } from 'node:util';
4
9
  import { z } from 'zod';
5
- import { GITHUB_SEARCH_MAX, REFERENCE_STATUS_MAX } from '../../contract/index.js';
6
- import { fetchIssueProjects, fetchViewerLogin } from './github-filters.js';
10
+ import { GITHUB_COMMENT_COUNTS_MAX, GITHUB_SEARCH_MAX, REFERENCE_STATUS_MAX } from '../../contract/index.js';
11
+ import { fetchIssueProjects, fetchRepositoryProjects, fetchViewerLogin } from './github-filters.js';
12
+ import { fetchGithubMergeRequirements } from './github-merge-requirements.js';
7
13
  import { autosaveCommit } from '../../git-worktree.js';
8
14
  /**
9
15
  * The GitHub forge driver — all `gh`-CLI logic in one place, moved here from
@@ -168,11 +174,14 @@ const ghPrViewSchema = z.object({
168
174
  number: z.number(),
169
175
  url: z.string(),
170
176
  state: z.string().default('OPEN'),
177
+ mergedAt: z.unknown().optional(),
171
178
  isDraft: z.boolean().default(false),
172
179
  statusCheckRollup: ghStatusCheckRollup,
173
180
  });
174
181
  const mergeCheckSchema = z.object({
175
- name: z.string().default('Check'),
182
+ name: z.string().optional(),
183
+ context: z.string().optional(),
184
+ targetUrl: z.string().nullish(),
176
185
  state: z.string().nullish(),
177
186
  status: z.string().nullish(),
178
187
  conclusion: z.string().nullish(),
@@ -183,6 +192,7 @@ const mergePrSchema = z.object({
183
192
  title: z.string(),
184
193
  url: z.string(),
185
194
  state: z.string(),
195
+ mergedAt: z.unknown().optional(),
186
196
  isDraft: z.boolean().default(false),
187
197
  headRefName: z.string(),
188
198
  baseRefName: z.string(),
@@ -232,112 +242,183 @@ async function gh(repoRoot, args, timeout = 15_000, signal, context) {
232
242
  });
233
243
  return stdout;
234
244
  }
235
- /** GraphQL's max page size; pagination is capped at 10 pages/kind (1000 rows — the GUI's full
236
- * background shot). Rows past the window keep `comments: 0`, which the UI reads as "no badge". */
237
- export const GH_COUNTS_MAX_PAGES = 10;
238
- const countsQuery = (root) => `
239
- query ($owner: String!, $name: String!, $endCursor: String) {
245
+ /** `gh api graphql` argv. `-f` sends every value as a String, and GitHub rejects a String where
246
+ * the query declares `Int!` ("Could not coerce value to Int", open-mercato/cezar#1366), so a
247
+ * number goes through `-F`. A string never does: `-F` would read `@file` and coerce
248
+ * `true`/numerals, which is not what an opaque cursor or a login means. */
249
+ export function ghGraphqlArgs(query, variables = {}) {
250
+ const args = ['api', 'graphql', '-f', `query=${query}`];
251
+ for (const [key, value] of Object.entries(variables)) {
252
+ args.push(typeof value === 'number' ? '-F' : '-f', `${key}=${value}`);
253
+ }
254
+ return args;
255
+ }
256
+ /** `owner/name` → `{ owner, name }`, or null when the handle isn't a clean two-part slug. */
257
+ export function parseOwnerName(nameWithOwner) {
258
+ const [owner, name, ...rest] = nameWithOwner.trim().split('/');
259
+ return owner && name && rest.length === 0 ? { owner, name } : null;
260
+ }
261
+ // Paged open lists defer comment counts; hydrate them through the counts route after paint.
262
+ /** GitHub GraphQL rejects `first` above 100 (EXCESSIVE_PAGINATION); larger limits walk pages. */
263
+ export const GH_GRAPHQL_PAGE_MAX = 100;
264
+ // Labels up to 100, which is what `gh issue list --json labels` read before the GraphQL page.
265
+ const LIST_NODE_FIELDS = 'number title author { login } createdAt url body labels(first: 100) { nodes { name color } }';
266
+ const listQuery = (side) => `
267
+ query ($owner: String!, $name: String!, $first: Int!, $after: String) {
240
268
  repository(owner: $owner, name: $name) {
241
- ${root}(first: 100, after: $endCursor, states: OPEN,
242
- orderBy: {field: CREATED_AT, direction: DESC}) {
243
- nodes { number comments { totalCount } }
269
+ ${side}(first: $first, after: $after, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) {
270
+ totalCount
244
271
  pageInfo { hasNextPage endCursor }
272
+ nodes { ${LIST_NODE_FIELDS} ${side === 'issues' ? 'assignees(first: 20) { nodes { login } }' : 'isDraft additions deletions'} }
245
273
  }
246
274
  }
247
275
  }`;
248
- const ghCountsPageSchema = z.object({
249
- nodes: z.array(z.object({ number: z.number(), comments: z.object({ totalCount: z.number() }) })),
276
+ /** A GraphQL connection (`{ nodes }`) read as the flat array `gh … --json` used to return. */
277
+ const connectionOf = (item) => z.object({ nodes: z.array(item.nullable()).default([]) }).nullish()
278
+ .transform((connection) => (connection?.nodes ?? []).filter((node) => node != null));
279
+ const ghListNodeSchema = z.object({
280
+ number: z.number(),
281
+ title: z.string(),
282
+ author: ghAuthor,
283
+ createdAt: z.string(),
284
+ url: z.string(),
285
+ body: z.string().nullish(),
286
+ labels: connectionOf(ghLabel),
287
+ assignees: connectionOf(z.object({ login: z.string() })),
288
+ isDraft: z.boolean().nullish(),
289
+ additions: z.number().nullish(),
290
+ deletions: z.number().nullish(),
291
+ });
292
+ const ghListConnectionSchema = z.object({
293
+ totalCount: z.number().int().nonnegative(),
250
294
  pageInfo: z.object({ hasNextPage: z.boolean(), endCursor: z.string().nullish() }),
295
+ nodes: z.array(ghListNodeSchema.nullable()),
251
296
  });
252
- /** Validate + flatten one gh GraphQL counts response into a `number → count` map plus the page
253
- * cursor. Exported for unit tests (the zod boundary + shape). Throws on a malformed envelope. */
254
- export function parseCountsPage(out, root) {
297
+ function parseListConnection(out, side) {
255
298
  const parsed = JSON.parse(out);
256
- const page = ghCountsPageSchema.parse(parsed?.data?.repository?.[root]);
257
- const counts = {};
258
- for (const node of page.nodes)
259
- counts[node.number] = node.comments.totalCount;
260
- return { counts, hasNextPage: page.pageInfo.hasNextPage, endCursor: page.pageInfo.endCursor ?? null };
299
+ const message = parsed.errors?.map((e) => e.message).filter(Boolean).join(' ');
300
+ if (message)
301
+ throw new Error(message);
302
+ if (!parsed.data?.repository)
303
+ throw new Error('GitHub did not resolve this repository');
304
+ return ghListConnectionSchema.parse(parsed.data.repository[side]);
261
305
  }
262
- async function paginateCounts(runGraphql, owner, name, root, maxPages) {
263
- const counts = {};
264
- let cursor = null;
265
- for (let page = 0; page < maxPages; page++) {
266
- const variables = { owner, name };
267
- if (cursor)
268
- variables.endCursor = cursor;
269
- const res = parseCountsPage(await runGraphql(countsQuery(root), variables), root);
270
- Object.assign(counts, res.counts);
271
- if (!res.hasNextPage || !res.endCursor)
272
- break;
273
- cursor = res.endCursor;
274
- }
275
- return counts;
276
- }
277
- /** Comment counts for open issues and PRs as `number → count` maps. Two independent paginated
278
- * queries (issues and PRs need separate cursors) run in parallel; any failure degrades the whole
279
- * thing to empty maps so the tab is never held up by counts. Exported for unit tests. */
280
- export async function fetchCommentCounts(runGraphql, owner, name, maxPages = GH_COUNTS_MAX_PAGES) {
281
- try {
282
- const [issues, prs] = await Promise.all([
283
- paginateCounts(runGraphql, owner, name, 'issues', maxPages),
284
- paginateCounts(runGraphql, owner, name, 'pullRequests', maxPages),
285
- ]);
286
- return { issues, prs };
287
- }
288
- catch {
289
- return { issues: {}, prs: {} };
306
+ /**
307
+ * One side of the open list: up to `limit` rows from `cursor`, in requests of at most
308
+ * `GH_GRAPHQL_PAGE_MAX`. `nextCursor` is where the rows stop, or `null` once the side is
309
+ * exhausted. `done` asks for `first: 0`, which still answers `totalCount`, so an exhausted side
310
+ * keeps a fresh total without ever restarting from its first page (open-mercato/cezar#1366
311
+ * restarted it while the other side paged).
312
+ */
313
+ export async function fetchListConnection(run, owner, name, side, limit, cursor, done = false) {
314
+ const query = listQuery(side);
315
+ if (done) {
316
+ const page = parseListConnection(await run(query, { owner, name, first: 0 }), side);
317
+ return { totalCount: page.totalCount, nodes: [], nextCursor: null };
318
+ }
319
+ const nodes = [];
320
+ let after = cursor;
321
+ for (;;) {
322
+ const first = Math.min(GH_GRAPHQL_PAGE_MAX, limit - nodes.length);
323
+ const page = parseListConnection(await run(query, { owner, name, first, ...(after ? { after } : {}) }), side);
324
+ const rows = page.nodes.filter((node) => node !== null);
325
+ nodes.push(...rows);
326
+ const next = page.pageInfo.hasNextPage ? page.pageInfo.endCursor ?? null : null;
327
+ // An empty page that claims more would loop forever; read it as the end of the side.
328
+ if (next === null || rows.length === 0)
329
+ return { totalCount: page.totalCount, nodes, nextCursor: null };
330
+ if (next === after)
331
+ throw new Error('GitHub list cursor did not advance');
332
+ // A short page with more behind it still ends this request: the cursor resumes it exactly.
333
+ if (nodes.length >= limit || rows.length < first)
334
+ return { totalCount: page.totalCount, nodes, nextCursor: next };
335
+ after = next;
290
336
  }
291
337
  }
292
- /** `owner/name` → `{ owner, name }`, or null when the handle isn't a clean two-part slug. */
293
- export function parseOwnerName(nameWithOwner) {
294
- const [owner, name, ...rest] = nameWithOwner.trim().split('/');
295
- return owner && name && rest.length === 0 ? { owner, name } : null;
296
- }
297
338
  /* Reads degrade to `available: false` with a hint — never an error (plan rule
298
339
  7): no `gh`, no remote, offline all land on the same quiet path. A short
299
340
  cache keeps tab switches from hammering the GitHub API; a cached fetch with
300
341
  a bigger limit than asked serves fine (it's a superset). Keyed by repository/host/account context
301
- (multi-project workspace, step 2.6): one project's — possibly private —
302
- issues/PRs must never be served under another project's scope. Bounded like
303
- `commentsCache` so an unbounded workspace can't grow it without limit. */
342
+ (multi-project workspace, step 2.6) plus the page asked for: one project's — possibly private —
343
+ issues/PRs must never be served under another project's scope, and page 2 is never page 1.
344
+ Bounded like `commentsCache` so an unbounded workspace can't grow it without limit. */
304
345
  let listRequestOrder = 0;
346
+ /** Per context, the order of the newest page-1 refresh. A refresh drops the context's later
347
+ * pages (their cursors were cut from the old first page), and a later page that started before
348
+ * it may answer its caller but never re-enters the cache. Bounded with `listCache`. */
349
+ const listRefreshOrder = new Map();
305
350
  const listCache = new Map();
306
351
  const LIST_CACHE_MAX = 50;
307
352
  const CACHE_MS = 60_000;
353
+ /** Which page key holds a projects generation, so `fetchGithubProjects` never scans the cache.
354
+ * Installed and evicted in step with `listCache`, so it shares its bound. */
355
+ const projectGenerations = new Map();
356
+ /** Board discovery (the `projectsV2` walk) per context: page 2 pays for membership only. A
357
+ * failed discovery is dropped, so the next page or refresh asks again. */
358
+ const repositoryProjectsCache = new Map();
359
+ function dropListEntry(key) {
360
+ const generation = listCache.get(key)?.data.projectsGeneration;
361
+ if (generation && projectGenerations.get(generation) === key)
362
+ projectGenerations.delete(generation);
363
+ listCache.delete(key);
364
+ }
365
+ function discoverRepositoryProjects(contextKey, refresh, start) {
366
+ const hit = repositoryProjectsCache.get(contextKey);
367
+ if (!refresh && hit && Date.now() - hit.at < CACHE_MS)
368
+ return hit.projects;
369
+ const projects = start();
370
+ const entry = { at: Date.now(), projects };
371
+ repositoryProjectsCache.delete(contextKey);
372
+ repositoryProjectsCache.set(contextKey, entry);
373
+ projects.catch(() => {
374
+ if (repositoryProjectsCache.get(contextKey) === entry)
375
+ repositoryProjectsCache.delete(contextKey);
376
+ });
377
+ while (repositoryProjectsCache.size > LIST_CACHE_MAX) {
378
+ const oldest = repositoryProjectsCache.keys().next().value;
379
+ if (oldest === undefined)
380
+ break;
381
+ repositoryProjectsCache.delete(oldest);
382
+ }
383
+ return projects;
384
+ }
308
385
  export const GH_MAX_LIMIT = 1000;
309
- export async function fetchGithub(repoRoot, refresh = false, limit = 30) {
386
+ function listPageKey(contextKey, page) {
387
+ return JSON.stringify([contextKey, page.issuesCursor ?? null, page.prsCursor ?? null, !!page.issuesDone, !!page.prsDone]);
388
+ }
389
+ export async function fetchGithub(repoRoot, refresh = false, limit = 30, page = {}) {
310
390
  if (process.env.CEZ_DRY_RUN === '1')
311
- return mockGithub();
391
+ return mockGithubPage(limit, page);
312
392
  const context = githubContext(repoRoot);
313
393
  const capped = Math.min(Math.max(limit, 1), GH_MAX_LIMIT);
314
- const hit = listCache.get(context.key);
394
+ const key = listPageKey(context.key, page);
395
+ const hit = listCache.get(key);
315
396
  if (!refresh && hit && Date.now() - hit.at < CACHE_MS && hit.limit >= capped) {
316
397
  return hit.data;
317
398
  }
318
399
  const order = ++listRequestOrder;
400
+ if (refresh && !page.issuesCursor && !page.prsCursor && !page.issuesDone && !page.prsDone) {
401
+ listRefreshOrder.delete(context.key);
402
+ listRefreshOrder.set(context.key, order);
403
+ while (listRefreshOrder.size > LIST_CACHE_MAX) {
404
+ const oldest = listRefreshOrder.keys().next().value;
405
+ if (oldest === undefined)
406
+ break;
407
+ listRefreshOrder.delete(oldest);
408
+ }
409
+ for (const [cached, entry] of [...listCache]) {
410
+ if (entry.contextKey === context.key && cached !== key)
411
+ dropListEntry(cached);
412
+ }
413
+ }
319
414
  try {
320
- // No `comments` field — `gh … --json comments` ships full comment bodies.
321
- // No `statusCheckRollup` either (#664): the CI rollup for every open PR was the
322
- // dominant cost — it forced the 60 s budget below — and is now hydrated lazily per
323
- // on-screen PR row via `fetchGithubChecks`. A big list still gets a little more wall
324
- // clock than the default 30, but nothing like the old rollup walk.
415
+ // No `statusCheckRollup` (#664): it is hydrated lazily per on-screen PR row via
416
+ // `fetchGithubChecks`. A big aggregate list still gets a little more wall clock.
325
417
  const timeout = capped > 100 ? 30_000 : 15_000;
326
- const fields = 'number,title,author,createdAt,labels,body,url';
327
- // The repo handle first (cheap) so the counts GraphQL query — which needs owner/name —
328
- // can run parallel to the two expensive list calls below.
329
418
  const ownerName = await resolveRepoHandleStrict(repoRoot, context);
330
- const runGraphql = (query, variables) => {
331
- const args = ['api', 'graphql', '-f', `query=${query}`];
332
- for (const [key, value] of Object.entries(variables))
333
- args.push('-f', `${key}=${value}`);
334
- return gh(repoRoot, args, timeout, undefined, context);
335
- };
336
- // Bound the counts pagination to the rows actually being fetched: a page is 100, so
337
- // `ceil(capped / 100)` pages (still capped at GH_COUNTS_MAX_PAGES) cover exactly the visible
338
- // window and no more — the default 30-item load pays ONE counts round-trip, not ten. Rows
339
- // beyond the window keep `comments: 0`, which the UI reads as "no badge" (same as before).
340
- const countsMaxPages = Math.min(GH_COUNTS_MAX_PAGES, Math.max(1, Math.ceil(capped / 100)));
419
+ if (!ownerName)
420
+ return { available: false, reason: 'Could not resolve this repository on GitHub.', issues: [], prs: [] };
421
+ const runGraphql = (query, variables) => gh(repoRoot, ghGraphqlArgs(query, variables), timeout, undefined, context);
341
422
  // Optional project queries have one shared wall-clock budget. A missing scope, a cap,
342
423
  // or malformed metadata must never turn an otherwise working list into unavailable.
343
424
  const deadline = Date.now() + 15_000;
@@ -345,24 +426,18 @@ export async function fetchGithub(repoRoot, refresh = false, limit = 30) {
345
426
  const remaining = deadline - Date.now();
346
427
  if (remaining <= 0)
347
428
  return Promise.reject(new Error('Project lookup timed out'));
348
- const args = ['api', 'graphql', '-f', `query=${query}`];
349
- for (const [key, value] of Object.entries(variables))
350
- args.push('-f', `${key}=${value}`);
351
- return gh(repoRoot, args, Math.min(remaining, 8_000), undefined, context);
429
+ return gh(repoRoot, ghGraphqlArgs(query, variables), Math.min(remaining, 8_000), undefined, context);
352
430
  };
353
- const issueList = gh(repoRoot, ['issue', 'list', '--limit', String(capped), '--json', `${fields},assignees`], timeout, undefined, context);
354
- const projectLookup = ownerName
355
- ? fetchIssueProjects(projectGraphql, ownerName.owner, ownerName.name, async () => z.array(ghIssueSchema).parse(JSON.parse(await issueList)).map(i => i.number))
356
- : Promise.resolve({ projectsReason: 'Project boards unavailable for this repository.' });
357
- const [issuesOut, prsOut, counts, viewerLogin] = await Promise.all([
358
- issueList,
359
- gh(repoRoot, ['pr', 'list', '--limit', String(capped), '--json', `${fields},isDraft,additions,deletions`], timeout, undefined, context),
360
- // Real comment counts (#499). Degrades to empty maps on its own — a failure here leaves
361
- // every count at 0, never fails the tab. Skipped entirely if the handle isn't parseable.
362
- ownerName
363
- ? fetchCommentCounts(runGraphql, ownerName.owner, ownerName.name, countsMaxPages)
364
- : Promise.resolve({ issues: {}, prs: {} }),
365
- fetchViewerLogin((query) => gh(repoRoot, ['api', 'graphql', '-f', `query=${query}`], 8_000, undefined, context)),
431
+ const firstPage = !page.issuesCursor && !page.prsCursor && !page.issuesDone && !page.prsDone;
432
+ const issueSide = fetchListConnection(runGraphql, ownerName.owner, ownerName.name, 'issues', capped, page.issuesCursor, page.issuesDone);
433
+ const projectLookup = fetchIssueProjects(projectGraphql, ownerName.owner, ownerName.name, async () => (await issueSide).nodes.map((node) => node.number), () => discoverRepositoryProjects(context.key, refresh, () => fetchRepositoryProjects(projectGraphql, ownerName.owner, ownerName.name)));
434
+ const [issuesPage, prsPage, viewerLogin] = await Promise.all([
435
+ issueSide,
436
+ fetchListConnection(runGraphql, ownerName.owner, ownerName.name, 'pullRequests', capped, page.prsCursor, page.prsDone),
437
+ // The viewer is the same on every page; only the first one asks.
438
+ firstPage
439
+ ? fetchViewerLogin((query) => gh(repoRoot, ghGraphqlArgs(query), 8_000, undefined, context))
440
+ : Promise.resolve(undefined),
366
441
  ]);
367
442
  // One repo-wide label→color map, filled as we flatten each item's labels.
368
443
  const labelColors = {};
@@ -370,7 +445,7 @@ export async function fetchGithub(repoRoot, refresh = false, limit = 30) {
370
445
  if (l.color && !labelColors[l.name])
371
446
  labelColors[l.name] = l.color;
372
447
  };
373
- const issues = z.array(ghIssueSchema).parse(JSON.parse(issuesOut)).map((i) => {
448
+ const issues = issuesPage.nodes.map((i) => {
374
449
  i.labels.forEach(recordColor);
375
450
  return {
376
451
  kind: 'issue',
@@ -381,32 +456,33 @@ export async function fetchGithub(repoRoot, refresh = false, limit = 30) {
381
456
  labels: i.labels.map((l) => l.name),
382
457
  body: (i.body ?? '').slice(0, 8_000),
383
458
  url: i.url,
384
- comments: counts.issues[i.number] ?? 0,
459
+ comments: 0,
385
460
  assignees: i.assignees.map(a => a.login),
386
461
  };
387
462
  });
388
- const prs = z.array(ghPrSchema).parse(JSON.parse(prsOut)).map((p) => {
463
+ const prs = prsPage.nodes.map((p) => {
389
464
  p.labels.forEach(recordColor);
465
+ const isDraft = p.isDraft ?? false;
390
466
  return {
391
467
  kind: 'pr',
392
468
  number: p.number,
393
469
  title: p.title,
394
470
  author: p.author?.login ?? '?',
395
471
  createdAt: p.createdAt,
396
- labels: [...p.labels.map((l) => l.name), ...(p.isDraft ? ['draft'] : [])],
472
+ labels: [...p.labels.map((l) => l.name), ...(isDraft ? ['draft'] : [])],
397
473
  body: (p.body ?? '').slice(0, 8_000),
398
474
  url: p.url,
399
- comments: counts.prs[p.number] ?? 0,
400
- isDraft: p.isDraft,
401
- additions: p.additions,
402
- deletions: p.deletions,
475
+ comments: 0,
476
+ isDraft,
477
+ additions: p.additions ?? 0,
478
+ deletions: p.deletions ?? 0,
403
479
  // Hydrated lazily by `fetchGithubChecks` for on-screen rows (#664) — the list no
404
480
  // longer pays for the CI rollup of every open PR.
405
481
  checks: null,
406
482
  };
407
483
  });
408
484
  // Only verified values from the same repository may survive a refresh.
409
- const previous = ownerName && hit?.data.repo === `${ownerName.owner}/${ownerName.name}` ? hit.data : undefined;
485
+ const previous = hit?.data.repo === `${ownerName.owner}/${ownerName.name}` ? hit.data : undefined;
410
486
  for (const issue of issues) {
411
487
  const known = previous?.issues.find(row => row.number === issue.number)?.projectIds;
412
488
  if (known !== undefined)
@@ -415,7 +491,7 @@ export async function fetchGithub(repoRoot, refresh = false, limit = 30) {
415
491
  const generation = randomUUID();
416
492
  const data = {
417
493
  available: true,
418
- ...(ownerName ? { repo: `${ownerName.owner}/${ownerName.name}` } : {}),
494
+ repo: `${ownerName.owner}/${ownerName.name}`,
419
495
  syncedAt: new Date().toISOString(),
420
496
  issues,
421
497
  prs,
@@ -424,12 +500,17 @@ export async function fetchGithub(repoRoot, refresh = false, limit = 30) {
424
500
  projectsState: 'refreshing',
425
501
  projectsGeneration: generation,
426
502
  ...(previous?.projects ? { projects: previous.projects } : {}),
503
+ issuesTotal: issuesPage.totalCount,
504
+ prsTotal: prsPage.totalCount,
505
+ issuesNextCursor: issuesPage.nextCursor,
506
+ prsNextCursor: prsPage.nextCursor,
507
+ commentsDeferred: true,
427
508
  };
428
509
  const projects = projectLookup.then((result) => {
429
510
  const metadata = 'membership' in result
430
511
  ? { generation, state: 'ready', projects: result.projects, membership: result.membership }
431
512
  : { generation, state: 'unavailable', reason: result.projectsReason };
432
- const current = listCache.get(context.key);
513
+ const current = listCache.get(key);
433
514
  if (current?.data.projectsGeneration === generation) {
434
515
  current.data = {
435
516
  ...current.data,
@@ -444,30 +525,50 @@ export async function fetchGithub(repoRoot, refresh = false, limit = 30) {
444
525
  });
445
526
  // Concurrent refreshes may finish in reverse order. An older list must not evict
446
527
  // the newer generation (and invalidate the newer client's metadata follow-up).
447
- if ((listCache.get(context.key)?.order ?? -1) < order) {
448
- listCache.delete(context.key); // re-insert so this key becomes the newest
449
- listCache.set(context.key, { order, at: Date.now(), limit: capped, data, projects });
528
+ if ((listCache.get(key)?.order ?? -1) < order && order >= (listRefreshOrder.get(context.key) ?? 0)) {
529
+ dropListEntry(key); // re-insert so this key becomes the newest
530
+ listCache.set(key, { contextKey: context.key, order, at: Date.now(), limit: capped, data, projects });
531
+ projectGenerations.set(generation, key);
450
532
  }
451
533
  while (listCache.size > LIST_CACHE_MAX) {
452
534
  const oldest = listCache.keys().next().value;
453
535
  if (oldest === undefined)
454
536
  break;
455
- listCache.delete(oldest);
537
+ dropListEntry(oldest);
456
538
  }
457
539
  return data;
458
540
  }
459
541
  catch (err) {
460
- const message = err instanceof Error ? err.message : String(err);
461
- const reason = /ENOENT/.test(message)
462
- ? 'gh CLI not found — install it and run `gh auth login`'
463
- : firstLine(message);
464
- return { available: false, reason, issues: [], prs: [] };
465
- }
542
+ return { available: false, reason: ghFailureReason(err), issues: [], prs: [] };
543
+ }
544
+ }
545
+ /** A `gh` failure as one readable line. GitHub's own words come on stderr; the Node message
546
+ * starts with the whole argv, and a GraphQL argv is a multi-line query, so its first line is
547
+ * a fragment of that query rather than the reason (open-mercato/cezar#1366). */
548
+ export function ghFailureReason(err) {
549
+ const message = err instanceof Error ? err.message : String(err);
550
+ if (/ENOENT/.test(message))
551
+ return 'gh CLI not found — install it and run `gh auth login`';
552
+ const failure = (err ?? {});
553
+ const stderr = typeof failure.stderr === 'string' ? failure.stderr : '';
554
+ if (stderr.trim()) {
555
+ const lines = stderr.split('\n').map((line) => line.trim()).filter(Boolean);
556
+ return lines.find((line) => !/^Command failed:/i.test(line)) ?? lines[0];
557
+ }
558
+ if (failure.killed === true || typeof failure.signal === 'string')
559
+ return 'GitHub did not answer in time. Refresh to try again.';
560
+ // Without stderr, everything after "Command failed:" is the argv, which is query text.
561
+ if (/^Command failed:/i.test(message))
562
+ return 'GitHub request failed. Refresh to try again.';
563
+ return message.split('\n').map((line) => line.trim()).find(Boolean) ?? 'gh failed';
466
564
  }
467
565
  /** Bounded lookup already started by the list. Never launch work for an expired generation. */
468
566
  export async function fetchGithubProjects(repoRoot, generation) {
469
- const entry = listCache.get(githubContext(repoRoot).key);
470
- if (!entry || entry.data.projectsGeneration !== generation) {
567
+ const key = projectGenerations.get(generation);
568
+ const entry = key === undefined ? undefined : listCache.get(key);
569
+ // A generation from another repository, host or account never resolves here, even when
570
+ // its UUID is known: the context is part of what the generation was issued for.
571
+ if (!entry || entry.contextKey !== githubContext(repoRoot).key || entry.data.projectsGeneration !== generation) {
471
572
  return { generation, state: 'unavailable', reason: 'Project metadata expired. Refresh to try again.' };
472
573
  }
473
574
  return entry.projects;
@@ -475,14 +576,7 @@ export async function fetchGithubProjects(repoRoot, generation) {
475
576
  function firstLine(s) {
476
577
  return s.split('\n').find((l) => l.trim().length > 0)?.trim() ?? 'gh failed';
477
578
  }
478
- // ---- search across states (#730) -------------------------------------------
479
- // `fetchGithub` above lists the OPEN set only (`gh issue/pr list` defaults to `--state open`),
480
- // and the tab's search is an in-memory filter over exactly that payload — so a closed or merged
481
- // item is not "past the window", it was never fetched at all and no amount of scrolling reaches
482
- // it. This tier asks GitHub instead of re-filtering what we already hold. It is deliberately a
483
- // SECOND, on-demand path rather than widening the list to `--state all`: the list is the tab's
484
- // hot path, and making every load pay for hundreds of closed rows to serve an occasional lookup
485
- // would trade a search bug for a performance one.
579
+ // On-demand cross-state search stays separate from the hot open list.
486
580
  /** Hits per search. `gh search`'s own default is 30; 50 matches the page size the incremental-
487
581
  * loading spec settled on and keeps one search to a single `gh` round-trip. */
488
582
  export const GH_SEARCH_MAX = GITHUB_SEARCH_MAX;
@@ -513,6 +607,8 @@ const ghSearchHitSchema = ghIssueSchema.extend({
513
607
  /** `gh {issue,pr} view <n> --json …` — the exact-number path. Shares `ghIssueSchema`'s core;
514
608
  * `isDraft`/`additions`/`deletions` are PR-only and default harmlessly for issues. */
515
609
  const ghViewHitSchema = ghIssueSchema.extend({
610
+ state: z.string().optional(),
611
+ mergedAt: z.unknown().optional(),
516
612
  assignees: z.array(z.object({ login: z.string() })).optional(),
517
613
  isDraft: z.boolean().default(false),
518
614
  additions: z.number().default(0),
@@ -565,9 +661,15 @@ async function viewGithubHit(repoRoot, kind, number) {
565
661
  'view',
566
662
  String(number),
567
663
  '--json',
568
- kind === 'pr' ? `${SEARCH_FIELDS},isDraft,additions,deletions` : `${SEARCH_FIELDS},assignees`,
664
+ kind === 'pr' ? `${SEARCH_FIELDS},isDraft,additions,deletions,state,mergedAt` : `${SEARCH_FIELDS},assignees`,
569
665
  ]);
570
- return ghViewHitSchema.parse(JSON.parse(out));
666
+ const hit = ghViewHitSchema.parse(JSON.parse(out));
667
+ if (kind === 'pr' && hit.state === 'MERGED') {
668
+ const handle = await resolveRepoHandle(repoRoot);
669
+ if (handle)
670
+ mergedPullRequestObservations.observe({ owner: handle.owner, repo: handle.name, number }, hit);
671
+ }
672
+ return hit;
571
673
  }
572
674
  catch (err) {
573
675
  if (MISSING_ITEM_RE.test(err instanceof Error ? err.message : String(err)))
@@ -591,22 +693,8 @@ export async function viewGithubItem(repoRoot, kind, number, labelColors = {}) {
591
693
  return hit ? finishViewHit(repoRoot, kind, number, hit, labelColors) : null;
592
694
  }
593
695
  /**
594
- * Find issues/PRs in ANY state (#730).
595
- *
596
- * Two query shapes, because they want different GitHub calls:
597
- * - **A bare number** (`4507`, `#4507`) is a lookup, not a search: `gh {pr,issue} view <n>` is
598
- * one cheap, state-agnostic call that finds a PR merged two years ago as readily as one opened
599
- * today. GitHub's search index cannot be relied on to surface an exact number at all.
600
- * - **Anything else** is a text search: `gh search {prs,issues} <query> --repo owner/name`, with
601
- * `--state` deliberately OMITTED — the flag only accepts `open|closed`, and leaving it off is
602
- * what searches every state.
603
- *
604
- * A numeric query that resolves to nothing (wrong repo, deleted, or simply a number the user meant
605
- * as text) falls through to the text path rather than reporting "not found" — `4507` should still
606
- * find a PR whose title contains it.
607
- *
608
- * Never throws: every failure lands on `{available: false, reason}`, the same quiet degrade the
609
- * rest of the tab uses. Exported for unit tests.
696
+ * Search any state: exact numbers try a direct lookup, then text fallback; text search omits
697
+ * state restriction. Return availability and a reason on failure instead of throwing.
610
698
  */
611
699
  export async function searchGithubItems(repoRoot, kind, query, limit = GH_SEARCH_MAX) {
612
700
  const trimmed = query.trim();
@@ -721,9 +809,7 @@ async function hydrateSearchProjects(repoRoot, kind, data) {
721
809
  const remaining = deadline - Date.now();
722
810
  if (remaining <= 0)
723
811
  return Promise.reject(new Error('Project lookup timed out'));
724
- const args = ['api', 'graphql', '-f', `query=${query}`];
725
- for (const [key, value] of Object.entries(variables))
726
- args.push('-f', `${key}=${value}`);
812
+ const args = ghGraphqlArgs(query, variables);
727
813
  return gh(repoRoot, args, Math.min(remaining, 8_000));
728
814
  };
729
815
  const projects = await fetchIssueProjects(run, handle.owner, handle.name, data.items.map(item => item.number));
@@ -731,6 +817,32 @@ async function hydrateSearchProjects(repoRoot, kind, data) {
731
817
  return { ...data, projectsReason: projects.projectsReason };
732
818
  return { ...data, items: data.items.map(item => ({ ...item, projectIds: projects.membership[item.number] ?? [] })) };
733
819
  }
820
+ /** The dry-run catalog, paged like the real list: numeric offset cursors per side, done flags
821
+ * honored, exact totals, and counts deferred to the counts route (hearsay-tools/cezarion#1183). */
822
+ function mockGithubPage(limit, page) {
823
+ const full = mockGithub();
824
+ const side = (rows, cursor, done) => {
825
+ const start = done ? rows.length : Number(cursor ?? 0);
826
+ const slice = rows.slice(start, start + Math.max(limit, 1));
827
+ const end = start + slice.length;
828
+ return { rows: slice.map((row) => ({ ...row, comments: 0 })), next: end < rows.length ? String(end) : null };
829
+ };
830
+ const issues = side(full.issues, page.issuesCursor, page.issuesDone);
831
+ const prs = side(full.prs, page.prsCursor, page.prsDone);
832
+ const firstPage = !page.issuesCursor && !page.prsCursor && !page.issuesDone && !page.prsDone;
833
+ const { viewerLogin, ...rest } = full;
834
+ return {
835
+ ...rest,
836
+ ...(firstPage && viewerLogin ? { viewerLogin } : {}),
837
+ issues: issues.rows,
838
+ prs: prs.rows,
839
+ issuesTotal: full.issues.length,
840
+ prsTotal: full.prs.length,
841
+ issuesNextCursor: issues.next,
842
+ prsNextCursor: prs.next,
843
+ commentsDeferred: true,
844
+ };
845
+ }
734
846
  /** CEZ_DRY_RUN=1 — a small fixed catalog so the GitHub tab is demoable offline. */
735
847
  function mockGithub() {
736
848
  const mk = (over) => ({
@@ -879,7 +991,7 @@ export const THREAD_ENTRY_CAP = 200;
879
991
  const COMMENT_BODY_CAP = 8_000;
880
992
  /** `gh api …/issues/{n}/comments` JSON → `ForgeComment[]`. Exported for unit tests. */
881
993
  export function normalizeComments(raw) {
882
- return z.array(ghIssueCommentSchema).parse(raw).map((c) => ({
994
+ return z.array(ghIssueCommentSchema).parse(raw).map((c) => defined({
883
995
  id: c.id,
884
996
  author: c.user?.login ?? '?',
885
997
  avatarUrl: c.user?.avatar_url ?? undefined,
@@ -901,7 +1013,7 @@ export function normalizeReviews(raw) {
901
1013
  const emptyBody = (r.body ?? '').trim().length === 0;
902
1014
  return !(emptyBody && (state === 'COMMENTED' || state === 'PENDING'));
903
1015
  })
904
- .map((r) => ({
1016
+ .map((r) => defined({
905
1017
  id: r.id,
906
1018
  author: r.user?.login ?? '?',
907
1019
  avatarUrl: r.user?.avatar_url ?? undefined,
@@ -1367,6 +1479,101 @@ export async function fetchPrChecks(runGraphql, owner, name, numbers, chunkSize
1367
1479
  }
1368
1480
  return out;
1369
1481
  }
1482
+ const itemDetailSchema = z.object({
1483
+ body: z.string().nullish(),
1484
+ additions: z.number().int().nonnegative().optional(),
1485
+ deletions: z.number().int().nonnegative().optional(),
1486
+ });
1487
+ /** One item's body (and a PR's diffstat), or `undefined` on any failure. */
1488
+ async function fetchItemDetail(repoRoot, kind, number) {
1489
+ try {
1490
+ const out = await gh(repoRoot, [kind, 'view', String(number), '--json', kind === 'pr' ? 'body,additions,deletions' : 'body']);
1491
+ const parsed = itemDetailSchema.parse(JSON.parse(out));
1492
+ return {
1493
+ body: (parsed.body ?? '').slice(0, COMMENT_BODY_CAP),
1494
+ ...(kind === 'pr' && parsed.additions !== undefined ? { additions: parsed.additions } : {}),
1495
+ ...(kind === 'pr' && parsed.deletions !== undefined ? { deletions: parsed.deletions } : {}),
1496
+ };
1497
+ }
1498
+ catch {
1499
+ return undefined;
1500
+ }
1501
+ }
1502
+ // Per-item comment counts (hearsay-tools/cezarion#1183): the list defers them, and the cockpit
1503
+ // asks for its loaded rows after paint. Keyed `context␀kind␀number`, same TTL and bound as checks.
1504
+ const commentCountsCache = new Map();
1505
+ const commentCountsSchema = z.object({
1506
+ data: z.object({
1507
+ repository: z.record(z.string(), z.object({ comments: z.object({ totalCount: z.number().int().nonnegative() }) }).nullable()),
1508
+ }),
1509
+ });
1510
+ /**
1511
+ * Comment counts for the given issue or PR numbers, in one aliased GraphQL query. A number GitHub
1512
+ * does not know is absent (unknown), never 0. Degrades to `{ available: false, reason }` — never a
1513
+ * throw. Numbers are de-duplicated, validated and capped at `GITHUB_COMMENT_COUNTS_MAX`.
1514
+ */
1515
+ export async function fetchGithubCommentCounts(repoRoot, kind, numbers, refresh = false) {
1516
+ if (process.env.CEZ_DRY_RUN === '1')
1517
+ return mockGithubCommentCounts(kind, numbers);
1518
+ const context = githubContext(repoRoot);
1519
+ const wanted = [...new Set(numbers)].filter((n) => Number.isSafeInteger(n) && n > 0).slice(0, GITHUB_COMMENT_COUNTS_MAX);
1520
+ const counts = {};
1521
+ const misses = [];
1522
+ const now = Date.now();
1523
+ const keyOf = (n) => `${context.key}\0${kind}\0${n}`;
1524
+ for (const n of wanted) {
1525
+ const hit = commentCountsCache.get(keyOf(n));
1526
+ if (!refresh && hit && now - hit.at < CACHE_MS)
1527
+ counts[n] = hit.count;
1528
+ else
1529
+ misses.push(n);
1530
+ }
1531
+ if (misses.length === 0)
1532
+ return { available: true, counts };
1533
+ try {
1534
+ const handle = await resolveRepoHandleStrict(repoRoot, context);
1535
+ if (!handle)
1536
+ return { available: false, reason: 'Could not resolve this repository on GitHub.' };
1537
+ // Integers only in the aliases; owner and name travel as variables.
1538
+ const fields = misses.map((n) => `n${n}: issueOrPullRequest(number: ${n}) {
1539
+ ... on Issue { comments { totalCount } }
1540
+ ... on PullRequest { comments { totalCount } }
1541
+ }`).join('\n');
1542
+ // A number GitHub does not know comes back NOT_FOUND and makes `gh` exit 1 with the rest of
1543
+ // the batch on stdout; `refStatusGraphql` keeps that partial answer.
1544
+ const raw = await refStatusGraphql(repoRoot, context)(`query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { ${fields} } }`, { owner: handle.owner, name: handle.name });
1545
+ const rows = commentCountsSchema.parse(JSON.parse(raw)).data.repository;
1546
+ for (const n of misses) {
1547
+ const count = rows[`n${n}`]?.comments.totalCount;
1548
+ if (count === undefined)
1549
+ continue; // unknown number: absent, and asked again next time
1550
+ counts[n] = count;
1551
+ commentCountsCache.delete(keyOf(n));
1552
+ commentCountsCache.set(keyOf(n), { at: now, count });
1553
+ }
1554
+ while (commentCountsCache.size > CHECKS_CACHE_MAX) {
1555
+ const oldest = commentCountsCache.keys().next().value;
1556
+ if (oldest === undefined)
1557
+ break;
1558
+ commentCountsCache.delete(oldest);
1559
+ }
1560
+ return { available: true, counts };
1561
+ }
1562
+ catch (err) {
1563
+ return { available: false, reason: ghFailureReason(err) };
1564
+ }
1565
+ }
1566
+ /** CEZ_DRY_RUN=1 — the catalog's own counts, so the offline list shows its badges. */
1567
+ function mockGithubCommentCounts(kind, numbers) {
1568
+ const rows = kind === 'pr' ? mockGithub().prs : mockGithub().issues;
1569
+ const counts = {};
1570
+ for (const n of numbers) {
1571
+ const row = rows.find((item) => item.number === n);
1572
+ if (row)
1573
+ counts[n] = row.comments;
1574
+ }
1575
+ return { available: true, counts };
1576
+ }
1370
1577
  // Per-PR checks cache: keyed `repoRoot␀number`, same 60 s TTL as the list cache but BOUNDED. Lets
1371
1578
  // a repeated visible-window hydration within a minute serve cached glyphs instead of re-querying.
1372
1579
  const checksCache = new Map();
@@ -1403,9 +1610,7 @@ export async function fetchGithubChecks(repoRoot, numbers) {
1403
1610
  if (!ownerName)
1404
1611
  return { available: false, reason: 'repository handle unavailable' };
1405
1612
  const runGraphql = (query, variables) => {
1406
- const args = ['api', 'graphql', '-f', `query=${query}`];
1407
- for (const [key, value] of Object.entries(variables))
1408
- args.push('-f', `${key}=${value}`);
1613
+ const args = ghGraphqlArgs(query, variables);
1409
1614
  return gh(repoRoot, args);
1410
1615
  };
1411
1616
  const fetched = await fetchPrChecks(runGraphql, ownerName.owner, ownerName.name, misses);
@@ -1464,6 +1669,8 @@ function reviewerKey(ref) {
1464
1669
  const ghRefStatusPrSchema = z
1465
1670
  .object({
1466
1671
  state: z.string(),
1672
+ url: z.unknown().optional(),
1673
+ mergedAt: z.unknown().optional(),
1467
1674
  isDraft: z.boolean().nullish(),
1468
1675
  reviewDecision: z.string().nullish(),
1469
1676
  /** `MERGEABLE` | `CONFLICTING` | `UNKNOWN` — GitHub computes it in the background, so
@@ -1546,85 +1753,19 @@ const ghRefStatusIssueSchema = z.object({ state: z.string(), stateReason: z.stri
1546
1753
  // `commits` silently removed.
1547
1754
  const ghRefStatusSchema = z.record(z.string(), z.unknown());
1548
1755
  /**
1549
- * One alias per NUMBER, asking `issueOrPullRequest` rather than `pullRequest`/`issue`.
1550
- *
1551
- * Issues and pull requests share one numbering space in a repository, so a number is exactly one
1552
- * of the two and asking which is a question the forge can answer — where asking `issue(number:)`
1553
- * about a pull request is a question with no answer, and GitHub says so with a NOT_FOUND *error*
1554
- * rather than a null. That mattered far more than it looks: `gh api graphql` exits non-zero the
1555
- * moment a response carries an `errors` array, so one mis-guessed kind used to fail the whole
1556
- * batch and report every reference in it as "not found" (see `refStatusGraphql`).
1557
- *
1558
- * It also fixes the reference whose kind the cockpit guessed wrong — `taskReferences` infers it
1559
- * from which field carried the number, and a bare `#774` can land in either — because the answer
1560
- * carries `__typename` and is filed under what the number REALLY is.
1561
- *
1562
- * `committedDate`, the head commit's `parents.totalCount` (`first: 0` — the COUNT is the whole
1563
- * question, so no parent is fetched) and the last CHANGES_REQUESTED review's `submittedAt` cost
1564
- * nothing extra, riding the same node, and are what let the precedence tell a review the author has
1565
- * already responded to from one still about the code on screen. The last `ReviewRequestedEvent` is
1566
- * asked for the same reason: `reviewRequests` says only THAT someone is on the hook, never since
1567
- * when, and the difference between a request made before the review and one made after it is the
1568
- * difference between a reviewer who has not looked yet and an author who has answered.
1569
- *
1570
- * `mergeable` rides it too, and is the reason the batch can answer "this one conflicts" without
1571
- * the per-PR probe `prMergeState` runs for the merge box. It is NOT folded into the status: see
1572
- * `conflicts` in the contract for why the two stay separate axes.
1756
+ * Query issueOrPullRequest once per number and use __typename for the actual kind. Include
1757
+ * branch-work and review timestamps for freshness decisions.
1573
1758
  */
1574
1759
  function refStatusQuery(numbers) {
1575
1760
  const aliases = numbers
1576
- .map((n, i) => ` r${i}: issueOrPullRequest(number: ${n}) { __typename ... on PullRequest { state isDraft reviewDecision mergeable commits(last: 1) { nodes { commit { committedDate parents(first: 1) { totalCount nodes { committedDate } } statusCheckRollup { state } } } } reviews(last: 1, states: CHANGES_REQUESTED) { nodes { submittedAt } } reviewRequests(first: 20) { totalCount nodes { requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { slug } } } } timelineItems(last: 20, itemTypes: [REVIEW_REQUESTED_EVENT]) { nodes { ... on ReviewRequestedEvent { createdAt requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { slug } } } } } } ... on Issue { state stateReason } }`)
1761
+ .map((n, i) => ` r${i}: issueOrPullRequest(number: ${n}) { __typename ... on PullRequest { url mergedAt state isDraft reviewDecision mergeable commits(last: 1) { nodes { commit { committedDate parents(first: 1) { totalCount nodes { committedDate } } statusCheckRollup { state } } } } reviews(last: 1, states: CHANGES_REQUESTED) { nodes { submittedAt } } reviewRequests(first: 20) { totalCount nodes { requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { slug } } } } timelineItems(last: 20, itemTypes: [REVIEW_REQUESTED_EVENT]) { nodes { ... on ReviewRequestedEvent { createdAt requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { slug } } } } } } ... on Issue { state stateReason } }`)
1577
1762
  .join('\n');
1578
- return `query ($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\n${aliases}\n }\n}`;
1763
+ return `query ($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\nnameWithOwner\n${aliases}\n }\n}`;
1579
1764
  }
1580
1765
  /**
1581
- * The one place a PR's signals collapse into a single word.
1582
- *
1583
- * The question the chip answers is "what is this waiting on RIGHT NOW", so the ranking is by
1584
- * freshness of the signal, not by how heavy a blocker it is:
1585
- *
1586
- * Read the ranking as **whose move is it**, which is the question a table is scanned for:
1587
- *
1588
- * 1. `merged` / `closed` — nobody's. Terminal, whatever checks or reviews say.
1589
- * 2. `draft` — the author's, and they have said so themselves.
1590
- * 3. `checks-pending` — the machine's. CI running means a commit was JUST pushed, the newest
1591
- * thing that has happened to this PR, so it outranks a requested change unconditionally.
1592
- * 4. `changes-requested` — the AUTHOR's, and only while that is still true: the review must be
1593
- * about the code that is there now, with no re-review already asked for.
1594
- * 5. `checks-failing` — the author's again. A reviewer cannot approve a red PR anyway.
1595
- * 6. `review-required` — the REVIEWER's: they have been asked, or the author has answered and
1596
- * the merge is now blocked on someone coming back to look.
1597
- * 7. `ready` — open, not a draft, nothing failing, nothing running, nobody waited on.
1598
- *
1599
- * The subtle one is (4) → (6), and it is not a preference but a data finding. `reviewDecision`
1600
- * stays `CHANGES_REQUESTED` after the author has responded — GitHub does not clear it until a
1601
- * reviewer submits again — so on its own it points at the author forever. Two signals say the ball
1602
- * has moved back:
1603
- *
1604
- * - **a review request made AFTER the review**, which is the author clicking re-request.
1605
- * Authoritative, and observed live alongside a stale `CHANGES_REQUESTED` and an EMPTY
1606
- * `latestReviews` — the case that has no other tell.
1607
- * - **a non-merge commit newer than the review**, the fallback for an author who pushed without
1608
- * clicking anything. Merges are excluded because GitHub's "Update branch" button writes one,
1609
- * dated now, that answers nothing — the reflexive click on a stale PR must not clear a
1610
- * rejection.
1611
- *
1612
- * The "after" in the first one is load-bearing, and its absence was a reported bug. A request that
1613
- * PREDATES the review is a reviewer who has not looked yet, and on a PR where several people were
1614
- * asked and one of them rejected it the others stay listed forever — so a bare
1615
- * `reviewRequests.totalCount > 0` reads a live "changes requested" as answered and hides it behind
1616
- * "waiting for review" (observed live: three reviewers asked at 10:28, changes requested
1617
- * the next day, one reviewer still pending).
1618
- *
1619
- * Either way the words change from "you owe edits" to "they owe a look", and so does the colour:
1620
- * danger is the author's move, info is the reviewer's.
1621
- *
1622
- * Both timestamps are optional and their ABSENCE is conservative: with no dates to compare and no
1623
- * re-request, a review counts as current, and the chip keeps pointing at the author.
1624
- *
1625
- * `reviewDecision` is null on a repo with no review policy, which is exactly why `ready` is not
1626
- * spelled "approved": on such a repo a green PR IS ready, and no approval will ever arrive — but a
1627
- * requested reviewer still moves it to `review-required`, because someone was explicitly asked.
1766
+ * Derive whose action is pending: terminal, draft, pending checks, current requested changes,
1767
+ * failing checks, review, then ready. Use dated review/work evidence; mergeability remains a
1768
+ * separate axis.
1628
1769
  */
1629
1770
  export function derivePrReferenceStatus(pr) {
1630
1771
  const state = pr.state.toUpperCase();
@@ -1734,6 +1875,10 @@ export async function fetchRefStatuses(runGraphql, owner, name, numbers, chunkSi
1734
1875
  const pr = ghRefStatusPrSchema.parse(node);
1735
1876
  if (!pr)
1736
1877
  return;
1878
+ if ((!raw.errors || (Array.isArray(raw.errors) && raw.errors.length === 0)) &&
1879
+ typeof repository.nameWithOwner === 'string' && repository.nameWithOwner.toLowerCase() === `${owner}/${name}`.toLowerCase()) {
1880
+ mergedPullRequestObservations.observe({ owner, repo: name, number }, { ...pr, number });
1881
+ }
1737
1882
  const head = pr.commits?.nodes[0]?.commit;
1738
1883
  const rollup = head?.statusCheckRollup;
1739
1884
  const mergeability = mergeabilityOf(pr.state, pr.mergeable);
@@ -1805,19 +1950,8 @@ export function __seedRefStatusCacheForTests(repoRoot, entries) {
1805
1950
  }
1806
1951
  }
1807
1952
  /**
1808
- * Forget what we knew about one reference, so the next read asks GitHub again.
1809
- *
1810
- * Called where cezar itself CHANGES a pull request — it merges one, it opens one — because those
1811
- * are the only forge changes this process can know about without asking. Everything else has to
1812
- * be polled (GitHub cannot push to a cockpit with no public endpoint), but waiting out a TTL to
1813
- * notice our own merge is a self-inflicted staleness: for up to a minute every chip would keep
1814
- * showing the pre-merge status of a pull request the user watched this server merge.
1815
- *
1816
- * Deleting rather than overwriting with a guessed `merged`: the forge is the authority on what a
1817
- * reference is, and a mutation that reports success is still not the same as having read the
1818
- * result. The next reader pays one query and gets the truth — after which the answer is `merged`,
1819
- * `recheckAfterMs` goes null, and the cockpit stops polling that batch entirely. Invalidating here
1820
- * therefore REDUCES long-run traffic rather than adding to it.
1953
+ * Invalidate one reference after a known mutation. Let the next forge read establish status
1954
+ * rather than caching a guessed result.
1821
1955
  */
1822
1956
  export function forgetRefStatus(repoRoot, number) {
1823
1957
  refStatusCache.delete(refStatusKey(repoRoot, number));
@@ -1848,7 +1982,7 @@ export function readCachedRefStatuses(repoRoot, numbers) {
1848
1982
  return out;
1849
1983
  }
1850
1984
  // The `#N` in a forge URL. Defined beside the run search matcher, which reads it too (#864).
1851
- export { refNumberFromUrl } from '../../runs/run-search.js';
1985
+ export { refNumberFromUrl } from '../../contract/index.js';
1852
1986
  /** A closed issue or an abandoned PR can be REOPENED, so this is long rather than forever — but
1853
1987
  * it is the rare event, and re-asking a hundred settled references every minute to catch it is
1854
1988
  * the wrong trade. */
@@ -1929,28 +2063,12 @@ const MERGEABILITY_UNKNOWN_WINDOW_MS = 60_000;
1929
2063
  * thing. Five minutes is the same order as the cockpit's own reconnect cadence. */
1930
2064
  const REF_STATUS_RETRY_MS = 5 * 60_000;
1931
2065
  /**
1932
- * GraphQL through `gh`, tolerating a PARTIALLY failed response.
1933
- *
1934
- * `gh api graphql` exits non-zero whenever the reply carries an `errors` array — even when `data`
1935
- * is fully populated for every alias that DID resolve. With one alias per reference that is not an
1936
- * edge case, it is the normal case: a single number that no longer exists (or never did, having
1937
- * been scraped from a transcript that named another repository) makes GitHub answer
1938
- * `{data: {...everything else...}, errors: [NOT_FOUND]}` and `gh` exit 1.
1939
- *
1940
- * `execFile` rejects on a non-zero exit, so that used to throw away a whole batch's worth of
1941
- * perfectly good statuses and report every reference in it as "not found" — including, in the bug
1942
- * that produced this function, an open pull request in the project's own repository.
1943
- *
1944
- * So: a non-zero exit whose stdout still carries a usable `data.repository` is a partial success
1945
- * and is used as-is. Anything else — no stdout, unparseable stdout, or a null `repository`, which
1946
- * means the repo handle itself did not resolve — rethrows, so a real failure still degrades to
1947
- * `{available: false}` instead of masquerading as "none of these exist".
2066
+ * Use failed gh GraphQL stdout only when it contains usable data.repository. Preserve successful
2067
+ * aliases in partial responses; rethrow malformed or unresolved-repository failures.
1948
2068
  */
1949
2069
  function refStatusGraphql(repoRoot, context = githubContext(repoRoot)) {
1950
2070
  return async (query, variables) => {
1951
- const args = ['api', 'graphql', '-f', `query=${query}`];
1952
- for (const [key, value] of Object.entries(variables))
1953
- args.push('-f', `${key}=${value}`);
2071
+ const args = ghGraphqlArgs(query, variables);
1954
2072
  try {
1955
2073
  return await gh(repoRoot, args, 15_000, undefined, context);
1956
2074
  }
@@ -2170,7 +2288,7 @@ function mockGithubRefStatus(prs, issues) {
2170
2288
  */
2171
2289
  export async function fetchGithubComments(repoRoot, kind, number, refresh = false) {
2172
2290
  if (process.env.CEZ_DRY_RUN === '1')
2173
- return mockGithubComments(kind);
2291
+ return mockGithubComments(kind, number);
2174
2292
  // NUL separator: cannot appear in a filesystem path, so roots can never alias.
2175
2293
  const key = `${repoRoot}\0${kind}#${number}`;
2176
2294
  const hit = commentsCache.get(key);
@@ -2178,6 +2296,10 @@ export async function fetchGithubComments(repoRoot, kind, number, refresh = fals
2178
2296
  return hit.data;
2179
2297
  // PR conversation comments live on the issues endpoint too — always use it for the body thread.
2180
2298
  const legacyComments = () => gh(repoRoot, ['api', `repos/{owner}/{repo}/issues/${number}/comments`, '--paginate']);
2299
+ // The detail tier (hearsay-tools/cezarion#1183): the list no longer has to carry a body, so
2300
+ // the pane asks for it here, in parallel with the thread. It is attached only to a thread
2301
+ // that loaded, and a failed view never costs the thread.
2302
+ const detail = fetchItemDetail(repoRoot, kind, number);
2181
2303
  try {
2182
2304
  let commentRows = [];
2183
2305
  let events;
@@ -2245,17 +2367,17 @@ export async function fetchGithubComments(repoRoot, kind, number, refresh = fals
2245
2367
  const handle = await resolveRepoHandle(repoRoot);
2246
2368
  if (handle) {
2247
2369
  const runGraphql = (query, variables) => {
2248
- const args = ['api', 'graphql', '-f', `query=${query}`];
2249
- for (const [k, v] of Object.entries(variables))
2250
- args.push('-f', `${k}=${v}`);
2370
+ const args = ghGraphqlArgs(query, variables);
2251
2371
  return gh(repoRoot, args);
2252
2372
  };
2253
2373
  const checks = await fetchCommitChecks(runGraphql, handle.owner, handle.name, commitShas);
2254
2374
  for (const event of events ?? []) {
2255
2375
  if (event.kind !== 'committed' || !event.sha)
2256
2376
  continue;
2257
- if (event.sha in checks)
2258
- event.checks = checks[event.sha];
2377
+ if (event.sha in checks) {
2378
+ delete event.checks;
2379
+ Object.assign(event, defined({ checks: checks[event.sha] }));
2380
+ }
2259
2381
  }
2260
2382
  }
2261
2383
  }
@@ -2265,15 +2387,18 @@ export async function fetchGithubComments(repoRoot, kind, number, refresh = fals
2265
2387
  parts.push(normalizeReviews(JSON.parse(reviewsOut)));
2266
2388
  }
2267
2389
  const { comments, truncated } = mergeThread(parts);
2268
- const data = {
2390
+ const data = defined({
2269
2391
  available: true,
2270
2392
  comments,
2271
2393
  // OR-folded exactly as before, so the pre-existing >200-comments trigger is preserved
2272
2394
  // rather than replaced.
2273
2395
  truncated: truncated || eventsTruncated || stoppedShort || undefined,
2274
- };
2396
+ });
2275
2397
  if (events)
2276
2398
  data.events = events;
2399
+ const itemDetail = await detail;
2400
+ if (itemDetail)
2401
+ data.detail = itemDetail;
2277
2402
  cacheComments(key, data);
2278
2403
  return data;
2279
2404
  }
@@ -2289,7 +2414,7 @@ export async function fetchGithubComments(repoRoot, kind, number, refresh = fals
2289
2414
  }
2290
2415
  /** CEZ_DRY_RUN=1 — a small fixed thread (one image-bearing comment, plus a review for PRs) so
2291
2416
  * the whole feature is demoable and e2e-testable offline. */
2292
- function mockGithubComments(kind) {
2417
+ function mockGithubComments(kind, number) {
2293
2418
  const base = Date.now() - 3_600_000;
2294
2419
  const at = (offset) => new Date(base + offset).toISOString();
2295
2420
  const comments = [
@@ -2396,7 +2521,14 @@ function mockGithubComments(kind) {
2396
2521
  createdAt: at(1_800_000),
2397
2522
  });
2398
2523
  }
2399
- return { available: true, comments, events };
2524
+ // The detail tier (hearsay-tools/cezarion#1183): the catalog row's own body and diffstat.
2525
+ const row = number === undefined ? undefined
2526
+ : (kind === 'pr' ? mockGithub().prs : mockGithub().issues).find((item) => item.number === number);
2527
+ const detail = row ? {
2528
+ body: row.body ?? '',
2529
+ ...(kind === 'pr' ? { additions: row.additions ?? 0, deletions: row.deletions ?? 0 } : {}),
2530
+ } : undefined;
2531
+ return { available: true, comments, events, ...(detail ? { detail } : {}) };
2400
2532
  }
2401
2533
  // ---- draft-PR creation (review gate, spec 009) ------------------------------
2402
2534
  // Final autosave-commit → `git push -u origin cez/<id8>` → `gh pr create
@@ -2470,23 +2602,20 @@ export async function createDraftPr(input) {
2470
2602
  */
2471
2603
  export function buildPrBody(handoffText, task) {
2472
2604
  const goal = section(handoffText, '## Goal') || task.trim();
2473
- const progress = section(handoffText, '## Progress log')
2474
- .split('\n')
2475
- .filter((l) => l.trim())
2476
- .slice(0, PROGRESS_LINES_MAX)
2477
- .join('\n');
2605
+ const progress = section(handoffText, '## Progress log').split('\n').filter((l) => l.trim()).slice(0, PROGRESS_LINES_MAX).join('\n');
2478
2606
  const parts = ['## Goal', '', goal];
2479
2607
  if (progress)
2480
2608
  parts.push('', '## Progress log', '', progress);
2481
2609
  parts.push('', '---', '', '🤖 made with cezar');
2482
2610
  return parts.join('\n');
2483
2611
  }
2484
- /** Text of one `## Header` section, up to the next `## ` header. */
2612
+ /** Text of one `## Header` section, up to the next `## ` header, with LF line endings. The header
2613
+ * is a whole line (hearsay-tools/cezarion#1147), so a Goal that quotes one is never taken for it. */
2485
2614
  function section(text, header) {
2486
- const start = text.indexOf(`${header}\n`);
2615
+ const start = exactLineIndex(text, header);
2487
2616
  if (start < 0)
2488
2617
  return '';
2489
- const rest = text.slice(start + header.length + 1);
2618
+ const rest = text.slice(nextLineIndex(text, start)).replaceAll('\r\n', '\n');
2490
2619
  const next = rest.indexOf('\n## ');
2491
2620
  return (next >= 0 ? rest.slice(0, next) : rest).trim();
2492
2621
  }
@@ -2616,21 +2745,32 @@ export function normalizeMergeState(raw, policyRaw, requirements = {
2616
2745
  ? 'changes-requested'
2617
2746
  : pr.reviewDecision?.toUpperCase() === 'REVIEW_REQUIRED'
2618
2747
  ? 'review-required'
2619
- : 'unknown';
2620
- const checks = (pr.statusCheckRollup ?? []).map((check) => ({
2621
- name: check.name,
2622
- state: mergeCheckState(check),
2623
- required: requirements.readable ? requirements.requiredChecks.includes(check.name) : null,
2624
- ...(check.detailsUrl?.startsWith('https://') || check.detailsUrl?.startsWith('http://')
2625
- ? { url: check.detailsUrl }
2626
- : {}),
2627
- }));
2748
+ : !pr.reviewDecision?.trim() && requirements.readable && requirements.requiredApprovingReviews === 0
2749
+ ? 'not-required'
2750
+ : 'unknown';
2751
+ const checks = (pr.statusCheckRollup ?? []).map((check) => {
2752
+ const name = check.name ?? check.context ?? 'Check';
2753
+ const url = check.detailsUrl ?? check.targetUrl;
2754
+ return {
2755
+ name,
2756
+ state: mergeCheckState(check),
2757
+ required: requirements.readable ? requirements.requiredChecks.includes(name) : null,
2758
+ ...(url?.startsWith('https://') || url?.startsWith('http://') ? { url } : {}),
2759
+ };
2760
+ });
2761
+ if (requirements.readable) {
2762
+ for (const name of requirements.requiredChecks) {
2763
+ if (!checks.some((check) => check.name === name))
2764
+ checks.push({ name, state: 'pending', required: true });
2765
+ }
2766
+ }
2628
2767
  const methods = [];
2629
- if (policy.allow_squash_merge)
2768
+ const methodAllowed = (method) => requirements.allowedMergeMethods?.includes(method) ?? true;
2769
+ if (policy.allow_squash_merge && methodAllowed('squash'))
2630
2770
  methods.push('squash');
2631
- if (policy.allow_merge_commit)
2771
+ if (policy.allow_merge_commit && methodAllowed('merge'))
2632
2772
  methods.push('merge');
2633
- if (policy.allow_rebase_merge)
2773
+ if (policy.allow_rebase_merge && methodAllowed('rebase'))
2634
2774
  methods.push('rebase');
2635
2775
  const defaultMethod = policy.squash_merge_commit_title && methods.includes('squash')
2636
2776
  ? 'squash'
@@ -2659,7 +2799,13 @@ export function normalizeMergeState(raw, policyRaw, requirements = {
2659
2799
  eligibility = 'blocked';
2660
2800
  blockers.push({ code: 'reviews', message: reviewDecision === 'changes-requested' ? 'Changes were requested.' : 'A required review is missing.' });
2661
2801
  }
2662
- else if (reviewDecision === 'unknown' || !requirements.readable) {
2802
+ else if ((requirements.unresolvedThreads ?? 0) > 0) {
2803
+ eligibility = 'blocked';
2804
+ blockers.push({ code: 'review-threads', message: 'Required review threads must be resolved before merging.' });
2805
+ }
2806
+ else if (reviewDecision === 'unknown' || !requirements.readable ||
2807
+ (requirements.requiresThreadResolution && requirements.unresolvedThreads == null) ||
2808
+ checks.some((check) => check.required === true && check.state === 'unknown')) {
2663
2809
  eligibility = 'unknown';
2664
2810
  blockers.push({
2665
2811
  code: 'rules-unknown',
@@ -2669,6 +2815,7 @@ export function normalizeMergeState(raw, policyRaw, requirements = {
2669
2815
  else if (checks.some((check) => check.state === 'pending') || pr.mergeStateStatus?.toUpperCase() === 'UNSTABLE') {
2670
2816
  eligibility = 'pending';
2671
2817
  blockers.push({ code: 'pending', message: 'Checks or GitHub mergeability are still pending.' });
2818
+ // GitHub's aggregate still enforces other rules and required check provider identities.
2672
2819
  }
2673
2820
  else if (mergeable !== 'mergeable' ||
2674
2821
  !['CLEAN', 'HAS_HOOKS'].includes(pr.mergeStateStatus?.toUpperCase() ?? '') ||
@@ -2731,23 +2878,19 @@ async function fetchPrMergeState(repoRoot, repoRef, number, refresh = false) {
2731
2878
  try {
2732
2879
  const prOut = await gh(repoRoot, [
2733
2880
  'pr', 'view', String(number), '--json',
2734
- 'number,title,url,state,isDraft,headRefName,baseRefName,headRefOid,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup',
2881
+ 'number,title,url,state,isDraft,headRefName,baseRefName,headRefOid,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,mergedAt',
2735
2882
  ]);
2736
2883
  const parsedPr = mergePrSchema.parse(JSON.parse(prOut));
2737
- const [policyOut, requiredChecks] = await Promise.all([
2884
+ if (!matchesGithubPullRequest({ ...repoRef, number }, parsedPr))
2885
+ return { available: false, reason: 'GitHub returned a different pull request.' };
2886
+ mergedPullRequestObservations.observe({ ...repoRef, number }, parsedPr);
2887
+ const [policyOut, requirements] = await Promise.all([
2738
2888
  gh(repoRoot, ['api', `repos/${repoRef.owner}/${repoRef.repo}`]),
2739
- gh(repoRoot, [
2740
- 'api',
2741
- `repos/${repoRef.owner}/${repoRef.repo}/branches/${encodeURIComponent(parsedPr.baseRefName)}/protection/required_status_checks`,
2742
- '--jq',
2743
- '[.contexts[]?, .checks[]?.context] | unique',
2744
- ])
2745
- .then((output) => ({ readable: true, requiredChecks: z.array(z.string()).parse(JSON.parse(output)) }))
2746
- .catch(() => ({ readable: false, requiredChecks: [] })),
2889
+ fetchGithubMergeRequirements((args) => gh(repoRoot, args), repoRef.owner, repoRef.repo, parsedPr.baseRefName, number),
2747
2890
  ]);
2748
2891
  const value = {
2749
2892
  available: true,
2750
- mergeState: normalizeMergeState(parsedPr, JSON.parse(policyOut), requiredChecks),
2893
+ mergeState: normalizeMergeState(parsedPr, JSON.parse(policyOut), requirements),
2751
2894
  };
2752
2895
  mergeStateCache.set(key, { at: Date.now(), value });
2753
2896
  return value;
@@ -2757,7 +2900,16 @@ async function fetchPrMergeState(repoRoot, repoRef, number, refresh = false) {
2757
2900
  }
2758
2901
  }
2759
2902
  export function evictGithubProjectCaches(repoRoot) {
2760
- listCache.delete(githubContext(repoRoot).key);
2903
+ const contextKey = githubContext(repoRoot).key;
2904
+ listCache.forEach((entry, key) => {
2905
+ if (entry.contextKey === contextKey)
2906
+ dropListEntry(key);
2907
+ });
2908
+ repositoryProjectsCache.delete(contextKey);
2909
+ commentCountsCache.forEach((_value, key) => {
2910
+ if (key.startsWith(`${contextKey}\0`))
2911
+ commentCountsCache.delete(key);
2912
+ });
2761
2913
  mergeStateCache.forEach((_value, key) => {
2762
2914
  if (key.startsWith(`${repoRoot}:`))
2763
2915
  mergeStateCache.delete(key);
@@ -2777,29 +2929,53 @@ async function mergePullRequest(repoRoot, repoRef, number, input) {
2777
2929
  return { merged: false, status: 409, error: 'A merge is already in progress.', code: 'concurrent' };
2778
2930
  mergeInflight.add(key);
2779
2931
  try {
2932
+ if (input.canProceed?.() === false)
2933
+ return { merged: false, status: 409, error: 'Automatic merging was cancelled.', code: 'cancelled' };
2780
2934
  const fresh = await fetchPrMergeState(repoRoot, repoRef, number, true);
2935
+ if (input.canProceed?.() === false)
2936
+ return { merged: false, status: 409, error: 'Automatic merging was cancelled.', code: 'cancelled' };
2781
2937
  if (!fresh.available)
2782
2938
  return { merged: false, status: 502, error: fresh.reason };
2783
2939
  const current = fresh.mergeState;
2784
2940
  if (current.headSha !== input.expectedHeadSha) {
2785
2941
  return { merged: false, status: 409, error: 'The pull request head changed. Review the new commits before merging.', code: 'stale-head', current };
2786
2942
  }
2943
+ if (input.expectedHeadRef !== undefined && current.headRef !== input.expectedHeadRef) {
2944
+ return { merged: false, status: 409, error: 'The pull request branch changed.', code: 'stale-branch', current };
2945
+ }
2946
+ if (input.requirePassingChecks && (current.checks.length === 0 || current.checks.some(check => check.state !== 'passing'))) {
2947
+ return { merged: false, status: 409, error: 'Automatic merging requires at least one check and every current-head check passing.', code: 'checks-not-passing', current };
2948
+ }
2787
2949
  if (!current.methods.includes(input.method)) {
2788
2950
  return { merged: false, status: 409, error: 'That merge method is no longer enabled.', code: 'disabled-method', current };
2789
2951
  }
2790
- if (!mergePreflightAllowed(current, input.overrideRules)) {
2952
+ if (!mergePreflightAllowed(current, !input.requirePassingChecks && input.overrideRules)) {
2791
2953
  return { merged: false, status: 409, error: current.blockers[0]?.message ?? 'The pull request is not eligible to merge.', code: current.eligibility, current };
2792
2954
  }
2955
+ if (input.requirePassingChecks && process.env.CEZ_DRY_RUN !== '1') {
2956
+ if (!repoRef)
2957
+ return { merged: false, status: 404, error: 'GitHub repository not found.' };
2958
+ const active = parseActiveWorkflowRuns(await gh(repoRoot, activeWorkflowRunsArgs({
2959
+ repository: `${repoRef.owner}/${repoRef.repo}`, headSha: current.headSha,
2960
+ })));
2961
+ if (input.canProceed?.() === false)
2962
+ return { merged: false, status: 409, error: 'Automatic merging was cancelled.', code: 'cancelled' };
2963
+ if (active)
2964
+ return { merged: false, status: 409, error: 'Waiting for active CI workflows on the current PR head.', code: 'checks-active', current };
2965
+ }
2793
2966
  if (process.env.CEZ_DRY_RUN === '1') {
2794
2967
  evictGithubProjectCaches(repoRoot);
2795
2968
  return { merged: true, number, url: current.url, method: input.method, mergeCommitSha: 'abcdef0123456789abcdef0123456789abcdef01' };
2796
2969
  }
2797
2970
  if (!repoRef)
2798
2971
  return { merged: false, status: 404, error: 'GitHub repository not found.' };
2972
+ if (input.canProceed?.() === false)
2973
+ return { merged: false, status: 409, error: 'Automatic merging was cancelled.', code: 'cancelled' };
2799
2974
  const out = await gh(repoRoot, [
2800
2975
  'api', '--method', 'PUT', `repos/${repoRef.owner}/${repoRef.repo}/pulls/${number}/merge`,
2801
2976
  '-f', `merge_method=${input.method}`, '-f', `sha=${input.expectedHeadSha}`,
2802
2977
  ]);
2978
+ // Once GitHub accepted the mutation, report its real outcome even if intent changed in flight.
2803
2979
  const result = ghMergeResultSchema.parse(JSON.parse(out));
2804
2980
  if (!result.merged)
2805
2981
  return { merged: false, status: 409, error: result.message ?? 'GitHub refused the merge.', code: 'github-blocked', current };
@@ -2818,6 +2994,61 @@ async function mergePullRequest(repoRoot, repoRef, number, input) {
2818
2994
  export function mergePreflightAllowed(current, overrideRules = false) {
2819
2995
  return current.canMerge || (overrideRules && current.canOverride);
2820
2996
  }
2997
+ /** gh has no compare-and-swap ready operation. Check ownership immediately before marking ready,
2998
+ * then refresh again; the later merge has its own head guard and GitHub's atomic SHA check. */
2999
+ async function readyPullRequest(repoRoot, repoRef, number, input) {
3000
+ if (!Number.isSafeInteger(number) || number < 1 || !/^[0-9a-f]{40}$/.test(input.expectedHeadSha) || !input.expectedHeadRef) {
3001
+ return { available: false, reason: 'A valid pull request, head SHA and task branch are required.' };
3002
+ }
3003
+ const key = `${repoRoot}:${number}`;
3004
+ if (mergeInflight.has(key))
3005
+ return { available: false, reason: 'A pull request mutation is already in progress.' };
3006
+ mergeInflight.add(key);
3007
+ try {
3008
+ if (input.canProceed?.() === false)
3009
+ return { available: false, reason: 'Marking the pull request ready was cancelled.' };
3010
+ const dryRun = process.env.CEZ_DRY_RUN === '1';
3011
+ if (!dryRun) {
3012
+ if (!repoRef)
3013
+ return { available: false, reason: 'GitHub remote could not be resolved' };
3014
+ const pr = mergePrSchema.pick({ number: true, url: true, state: true, isDraft: true, headRefName: true, headRefOid: true }).parse(JSON.parse(await gh(repoRoot, [
3015
+ 'pr', 'view', String(number), '--json', 'number,url,state,isDraft,headRefName,headRefOid',
3016
+ ])));
3017
+ if (input.canProceed?.() === false)
3018
+ return { available: false, reason: 'Marking the pull request ready was cancelled.' };
3019
+ if (!matchesGithubPullRequest({ ...repoRef, number }, pr))
3020
+ return { available: false, reason: 'GitHub returned a different pull request.' };
3021
+ if (pr.headRefOid !== input.expectedHeadSha)
3022
+ return { available: false, reason: 'The pull request head changed.' };
3023
+ if (pr.headRefName !== input.expectedHeadRef)
3024
+ return { available: false, reason: 'The pull request branch changed.' };
3025
+ if (pr.state !== 'OPEN')
3026
+ return { available: false, reason: 'The pull request is no longer open.' };
3027
+ if (pr.isDraft) {
3028
+ if (input.canProceed?.() === false)
3029
+ return { available: false, reason: 'Marking the pull request ready was cancelled.' };
3030
+ await gh(repoRoot, ['pr', 'ready', String(number), '--repo', `github.com/${repoRef.owner}/${repoRef.repo}`]);
3031
+ evictGithubProjectCaches(repoRoot);
3032
+ if (input.canProceed?.() === false)
3033
+ return { available: false, reason: 'Marking the pull request ready was cancelled.' };
3034
+ }
3035
+ }
3036
+ const fresh = await fetchPrMergeState(repoRoot, repoRef, number, true);
3037
+ if (input.canProceed?.() === false)
3038
+ return { available: false, reason: 'Marking the pull request ready was cancelled.' };
3039
+ if (fresh.available && fresh.mergeState.headSha !== input.expectedHeadSha)
3040
+ return { available: false, reason: 'The pull request head changed.' };
3041
+ if (fresh.available && fresh.mergeState.headRef !== input.expectedHeadRef)
3042
+ return { available: false, reason: 'The pull request branch changed.' };
3043
+ return fresh;
3044
+ }
3045
+ catch {
3046
+ return { available: false, reason: 'GitHub could not mark the pull request ready for review.' };
3047
+ }
3048
+ finally {
3049
+ mergeInflight.delete(key);
3050
+ }
3051
+ }
2821
3052
  const GH_PR_STATES = {
2822
3053
  MERGED: 'merged',
2823
3054
  CLOSED: 'closed',
@@ -2827,8 +3058,8 @@ export function createGithubDriver(repoRoot, repoRef) {
2827
3058
  kind: 'github',
2828
3059
  detect: () => detectGithub(repoRoot),
2829
3060
  detectCached: () => detectGithubCached(repoRoot),
2830
- listIssues: async (opts) => (await fetchGithub(repoRoot, opts?.refresh, opts?.limit)).issues,
2831
- listPRs: async (opts) => (await fetchGithub(repoRoot, opts?.refresh, opts?.limit)).prs,
3061
+ listIssues: async (opts) => (await fetchGithub(repoRoot, opts?.refresh, opts?.limit, opts)).issues,
3062
+ listPRs: async (opts) => (await fetchGithub(repoRoot, opts?.refresh, opts?.limit, opts)).prs,
2832
3063
  // The open-only list tier's escape hatch (#730) — this is the only path that can reach a
2833
3064
  // closed or merged item.
2834
3065
  searchItems: (kind, query, opts) => searchGithubItems(repoRoot, kind, query, opts?.limit),
@@ -2841,8 +3072,10 @@ export function createGithubDriver(repoRoot, repoRef) {
2841
3072
  if (process.env.CEZ_DRY_RUN === '1')
2842
3073
  return null;
2843
3074
  try {
2844
- const out = await gh(repoRoot, ['pr', 'view', branch, '--json', 'number,url,state,isDraft,statusCheckRollup']);
3075
+ const out = await gh(repoRoot, ['pr', 'view', branch, '--json', 'number,url,state,isDraft,statusCheckRollup,mergedAt']);
2845
3076
  const pr = ghPrViewSchema.parse(JSON.parse(out));
3077
+ if (repoRef)
3078
+ mergedPullRequestObservations.observe({ ...repoRef, number: pr.number }, pr);
2846
3079
  return {
2847
3080
  number: pr.number,
2848
3081
  url: pr.url,
@@ -2856,6 +3089,7 @@ export function createGithubDriver(repoRoot, repoRef) {
2856
3089
  }
2857
3090
  },
2858
3091
  prMergeState: (number, opts) => fetchPrMergeState(repoRoot, repoRef, number, opts?.refresh),
3092
+ readyPR: (number, input) => readyPullRequest(repoRoot, repoRef, number, input),
2859
3093
  mergePR: (number, input) => mergePullRequest(repoRoot, repoRef, number, input),
2860
3094
  viewUrl: (kind, ref) => {
2861
3095
  if (!repoRef)
@@ -2878,4 +3112,89 @@ export function createGithubDriver(repoRoot, repoRef) {
2878
3112
  },
2879
3113
  };
2880
3114
  }
3115
+ /** A missing alias must not poison unrelated families. Discard partial data and requery only
3116
+ * unaffected aliases once; archive authority still comes from an error-free response. */
3117
+ async function mergedPullRequestChunk(runGraphql, initial) {
3118
+ let chunk = initial;
3119
+ for (let attempt = 0; attempt < 2 && chunk.length; attempt++) {
3120
+ const query = `query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { nameWithOwner ${chunk.map(pr => `pr${pr.number}: pullRequest(number: ${pr.number}) { number url state mergedAt }`).join(' ')} } }`;
3121
+ let response;
3122
+ let failure;
3123
+ try {
3124
+ response = JSON.parse(await runGraphql(query, { owner: chunk[0].owner, name: chunk[0].repo }));
3125
+ }
3126
+ catch (error) {
3127
+ // gh exits 1 on GraphQL errors, with the structured envelope on stdout. Neither stderr
3128
+ // nor timeout/abort output is evidence that one particular alias was missing.
3129
+ const rejected = z.object({ code: z.literal(1), stdout: z.string() }).safeParse(error);
3130
+ if (!rejected.success)
3131
+ throw error;
3132
+ try {
3133
+ response = JSON.parse(rejected.data.stdout);
3134
+ }
3135
+ catch {
3136
+ throw error;
3137
+ }
3138
+ failure = error;
3139
+ }
3140
+ const partial = z.object({
3141
+ errors: z.array(z.object({ type: z.literal('NOT_FOUND'), path: z.tuple([z.literal('repository'), z.string()]) })).nonempty(),
3142
+ data: z.object({ repository: z.object({ nameWithOwner: z.string() }) }),
3143
+ }).safeParse(response);
3144
+ const aliases = new Set(chunk.map(pr => `pr${pr.number}`));
3145
+ if (attempt === 0 && partial.success &&
3146
+ partial.data.data.repository.nameWithOwner.toLowerCase() === `${chunk[0].owner}/${chunk[0].repo}`.toLowerCase() &&
3147
+ partial.data.errors.every(error => aliases.has(error.path[1]))) {
3148
+ const missing = new Set(partial.data.errors.map(error => error.path[1]));
3149
+ chunk = chunk.filter(pr => !missing.has(`pr${pr.number}`));
3150
+ continue;
3151
+ }
3152
+ if (failure)
3153
+ throw failure;
3154
+ return response;
3155
+ }
3156
+ return undefined;
3157
+ }
3158
+ /** Strict archive authority: no partial GraphQL success, redirects, guessed identity or date. */
3159
+ export async function fetchMergedPullRequests(runGraphql, identities) {
3160
+ const repositories = new Map();
3161
+ for (const identity of new Map(identities.map(pr => [pullRequestKey(pr), pr])).values()) {
3162
+ const key = `${identity.owner}/${identity.repo}`;
3163
+ repositories.set(key, [...(repositories.get(key) ?? []), identity]);
3164
+ }
3165
+ const merged = [];
3166
+ for (const [repository, prs] of repositories) {
3167
+ for (let offset = 0; offset < prs.length; offset += 25) {
3168
+ const chunk = prs.slice(offset, offset + 25);
3169
+ const response = await mergedPullRequestChunk(runGraphql, chunk);
3170
+ const parsed = z.object({ errors: z.array(z.unknown()).optional(), data: z.object({ repository: z.object({ nameWithOwner: z.string() }).passthrough().nullable() }) }).safeParse(response);
3171
+ if (!parsed.success || parsed.data.errors?.length || parsed.data.data.repository?.nameWithOwner.toLowerCase() !== repository.toLowerCase())
3172
+ continue;
3173
+ for (const pr of chunk) {
3174
+ const fact = mergedPullRequestObservations.observe(pr, parsed.data.data.repository[`pr${pr.number}`]);
3175
+ if (fact)
3176
+ merged.push(fact);
3177
+ }
3178
+ }
3179
+ }
3180
+ return merged;
3181
+ }
3182
+ export async function lookupMergedPullRequests(repoRoot, identities, signal) {
3183
+ signal.throwIfAborted();
3184
+ if (process.env.CEZ_DRY_RUN === '1')
3185
+ return [];
3186
+ const unique = [...new Map(identities.map(pr => [pullRequestKey(pr), pr])).values()];
3187
+ const known = unique.flatMap(pr => mergedPullRequestObservations.get(pr) ?? []);
3188
+ const missing = unique.filter(pr => !mergedPullRequestObservations.get(pr));
3189
+ if (missing.length === 0)
3190
+ return known;
3191
+ const context = githubContext(repoRoot);
3192
+ const fetched = await fetchMergedPullRequests(async (query, variables) => {
3193
+ const args = ['api', 'graphql', '--hostname', 'github.com', '-f', `query=${query}`];
3194
+ for (const [key, value] of Object.entries(variables))
3195
+ args.push('-f', `${key}=${value}`);
3196
+ return gh(repoRoot, args, 15_000, signal, context);
3197
+ }, missing);
3198
+ return [...known, ...fetched];
3199
+ }
2881
3200
  //# sourceMappingURL=github.js.map