catherd-cli 1.4.0 → 2.0.0-beta.20261022113309

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 (320) hide show
  1. package/README.md +188 -169
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -239
  5. package/package.json +6 -8
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/bin/catherd-mcp +1 -1
  9. package/plugin/commands/catherd.md +2 -2
  10. package/plugin/skills/catherd/SKILL.md +97 -356
  11. package/plugin/skills/catherd-setup/SKILL.md +21 -20
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +19 -4
  14. package/src/adapters/claude-code/index.ts +126 -14
  15. package/src/adapters/codex/events.ts +10 -0
  16. package/src/adapters/codex/index.ts +17 -5
  17. package/src/adapters/cursor/home.ts +1 -1
  18. package/src/adapters/cursor/index.ts +10 -4
  19. package/src/adapters/opencode/index.ts +38 -1
  20. package/src/cli.ts +27 -27
  21. package/src/domain/agents.ts +3 -1
  22. package/src/domain/bars.ts +33 -107
  23. package/src/domain/blocked.ts +72 -0
  24. package/src/domain/branch.ts +51 -0
  25. package/src/domain/brief.ts +106 -0
  26. package/src/domain/catalog.ts +69 -313
  27. package/src/domain/changes.ts +23 -1
  28. package/src/domain/cost.ts +19 -29
  29. package/src/domain/errors.ts +6 -2
  30. package/src/domain/fail-lines.ts +26 -0
  31. package/src/domain/failover.ts +37 -75
  32. package/src/domain/findings.ts +47 -0
  33. package/src/domain/gate-brief.ts +53 -0
  34. package/src/domain/hints.ts +13 -2
  35. package/src/domain/ids.ts +13 -2
  36. package/src/domain/jev.ts +47 -2
  37. package/src/domain/judge.ts +124 -0
  38. package/src/domain/lane.ts +118 -3
  39. package/src/domain/milestone-memory.ts +53 -0
  40. package/src/domain/notice.ts +32 -8
  41. package/src/domain/notify.ts +32 -0
  42. package/src/domain/plan-doc.ts +198 -0
  43. package/src/domain/plan-lint.ts +141 -0
  44. package/src/domain/profile-rules.ts +175 -138
  45. package/src/domain/profile.ts +140 -63
  46. package/src/domain/record.ts +68 -3
  47. package/src/domain/reply.ts +29 -0
  48. package/src/domain/role-prompts.ts +182 -56
  49. package/src/domain/role-scope.ts +76 -0
  50. package/src/domain/role-tools.ts +34 -0
  51. package/src/domain/roles.ts +2 -0
  52. package/src/domain/route.ts +90 -0
  53. package/src/domain/rules.ts +33 -0
  54. package/src/domain/run-events.ts +298 -0
  55. package/src/domain/run-machine.ts +668 -0
  56. package/src/domain/run-state.ts +1310 -0
  57. package/src/domain/scorecard.ts +197 -0
  58. package/src/domain/select.ts +229 -72
  59. package/src/domain/slopalytics.ts +72 -0
  60. package/src/domain/state.ts +5 -0
  61. package/src/domain/util.ts +22 -0
  62. package/src/domain/workspace.ts +130 -0
  63. package/src/entry/answer-command.ts +39 -0
  64. package/src/entry/catalog-command.ts +24 -165
  65. package/src/entry/check-bin.ts +17 -0
  66. package/src/entry/cli-kit.ts +18 -2
  67. package/src/entry/daemon-bin.ts +22 -0
  68. package/src/entry/deps.ts +2 -0
  69. package/src/entry/doctor-command.ts +20 -2
  70. package/src/entry/glyphs.ts +15 -2
  71. package/src/entry/host-arg.ts +28 -0
  72. package/src/entry/init-command.ts +20 -69
  73. package/src/entry/knowledge-command.ts +121 -5
  74. package/src/entry/lock-command.ts +103 -8
  75. package/src/entry/mcp/handshake.ts +6 -2
  76. package/src/entry/mcp/role-bin.ts +20 -0
  77. package/src/entry/mcp/role-server.ts +134 -0
  78. package/src/entry/mcp/server.ts +85 -21
  79. package/src/entry/mcp/session-tools.ts +149 -0
  80. package/src/entry/panel/openapi.ts +189 -0
  81. package/src/entry/panel/server.ts +410 -0
  82. package/src/entry/panel-command.ts +68 -0
  83. package/src/entry/pause-command.ts +67 -0
  84. package/src/entry/profile-command.ts +33 -18
  85. package/src/entry/role-cli-command.ts +159 -0
  86. package/src/entry/runs-command.ts +81 -27
  87. package/src/entry/scorecard-command.ts +187 -0
  88. package/src/entry/supervise-bin.ts +1 -1
  89. package/src/entry/supervise-command.ts +30 -3
  90. package/src/entry/workspace-command.ts +44 -0
  91. package/src/infra/boot-lock.ts +25 -0
  92. package/src/infra/delivery.ts +3 -0
  93. package/src/infra/dispatch-dir.ts +2 -0
  94. package/src/infra/env.ts +11 -3
  95. package/src/infra/git.ts +70 -0
  96. package/src/infra/heavy-lock.ts +133 -6
  97. package/src/infra/host-probe.ts +16 -0
  98. package/src/infra/launch.ts +63 -7
  99. package/src/infra/lock-activity.ts +88 -0
  100. package/src/infra/login-env.ts +48 -0
  101. package/src/infra/memory.ts +45 -0
  102. package/src/infra/ntfy.ts +40 -0
  103. package/src/infra/open-url.ts +42 -0
  104. package/src/infra/paths.ts +37 -2
  105. package/src/infra/role-mcp.ts +69 -0
  106. package/src/infra/slopalytics.ts +30 -0
  107. package/src/infra/store.ts +6 -1
  108. package/src/infra/supervisor.ts +107 -16
  109. package/src/services/admission.ts +133 -31
  110. package/src/services/backends.ts +4 -6
  111. package/src/services/budget.ts +26 -6
  112. package/src/services/capture.ts +1 -22
  113. package/src/services/catalog-service.ts +155 -254
  114. package/src/services/credentials.ts +15 -26
  115. package/src/services/daemon.ts +181 -0
  116. package/src/services/dispatch-service.ts +209 -17
  117. package/src/services/dispatches.ts +30 -2
  118. package/src/services/doctor-access.ts +8 -3
  119. package/src/services/doctor-backends.ts +1 -1
  120. package/src/services/doctor-checks.ts +47 -0
  121. package/src/services/doctor-docker.ts +221 -0
  122. package/src/services/doctor-role-mcp.ts +65 -0
  123. package/src/services/doctor.ts +72 -15
  124. package/src/services/end-push.ts +157 -0
  125. package/src/services/failover-rungs.ts +109 -0
  126. package/src/services/finalize.ts +24 -2
  127. package/src/services/gate-env.ts +217 -0
  128. package/src/services/gate-service.ts +231 -45
  129. package/src/services/global-install.ts +46 -13
  130. package/src/services/jev-service.ts +2 -4
  131. package/src/services/lane-edit.ts +154 -0
  132. package/src/services/lane-service.ts +291 -55
  133. package/src/services/milestone-check.ts +433 -0
  134. package/src/services/milestone-tree.ts +170 -0
  135. package/src/services/milestones.ts +167 -29
  136. package/src/services/notifier.ts +124 -4
  137. package/src/services/notify-service.ts +26 -0
  138. package/src/services/orchestrator-wait.ts +82 -0
  139. package/src/services/panel-setup.ts +116 -0
  140. package/src/services/pause.ts +161 -0
  141. package/src/services/peek.ts +79 -17
  142. package/src/services/plan-input.ts +151 -0
  143. package/src/services/ports.ts +60 -5
  144. package/src/services/preflight.ts +232 -24
  145. package/src/services/profile-service.ts +31 -3
  146. package/src/services/profile-store.ts +18 -6
  147. package/src/services/protocol.ts +123 -25
  148. package/src/services/provenance.ts +34 -89
  149. package/src/services/questions.ts +24 -0
  150. package/src/services/reconcile.ts +33 -1
  151. package/src/services/reentry.ts +7 -6
  152. package/src/services/role-access.ts +27 -0
  153. package/src/services/routing-service.ts +161 -42
  154. package/src/services/run-briefs.ts +477 -0
  155. package/src/services/run-client.ts +217 -0
  156. package/src/services/run-effects.ts +837 -0
  157. package/src/services/run-events-store.ts +174 -0
  158. package/src/services/run-evidence.ts +1 -1
  159. package/src/services/run-pin.ts +125 -0
  160. package/src/services/run-read.ts +221 -0
  161. package/src/services/run-report.ts +176 -0
  162. package/src/services/run-service.ts +204 -23
  163. package/src/services/run-store.ts +159 -18
  164. package/src/services/scorecard-metrics.ts +86 -0
  165. package/src/services/scorecard.ts +382 -0
  166. package/src/services/scratch.ts +60 -0
  167. package/src/services/sessions.ts +3 -1
  168. package/src/services/state.ts +21 -0
  169. package/src/services/summary.ts +76 -12
  170. package/src/services/tree-path.ts +27 -0
  171. package/src/services/verifier-brief.ts +23 -0
  172. package/src/services/verifier-step.ts +48 -0
  173. package/src/services/workspace-admission.ts +259 -0
  174. package/src/services/workspace-service.ts +259 -0
  175. package/src/services/workspace-store.ts +95 -0
  176. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  177. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  178. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  179. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  180. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  181. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  182. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  183. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  184. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  185. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  186. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  187. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  188. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  189. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  190. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  191. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  192. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  193. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  194. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  195. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  196. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  197. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  198. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  199. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  200. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  201. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  202. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  203. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  204. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  205. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  206. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  207. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  208. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  209. package/web/dist/assets/index-C5IbylvM.css +2 -0
  210. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  211. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  212. package/web/dist/assets/label-DFSYBc62.js +1 -0
  213. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  214. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  215. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  216. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  217. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  218. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  219. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  220. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  221. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  222. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  223. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  224. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  225. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  226. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  227. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  228. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  229. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  230. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  231. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  232. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  233. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  234. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  235. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  236. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  237. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  238. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  239. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  240. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  241. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  242. package/web/dist/assets/time--9kSQbbp.js +1 -0
  243. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  244. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  245. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  246. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  247. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  248. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  249. package/web/dist/index.html +15 -0
  250. package/catalog/ATTRIBUTION.md +0 -36
  251. package/catalog/scores.json +0 -3202
  252. package/catalog/sources.json +0 -97
  253. package/src/adapters/antigravity/events.ts +0 -131
  254. package/src/adapters/antigravity/home.ts +0 -46
  255. package/src/adapters/antigravity/index.ts +0 -320
  256. package/src/adapters/antigravity/models.ts +0 -37
  257. package/src/adapters/grok/events.ts +0 -112
  258. package/src/adapters/grok/home.ts +0 -132
  259. package/src/adapters/grok/index.ts +0 -339
  260. package/src/adapters/grok/models.ts +0 -48
  261. package/src/domain/calibration.ts +0 -125
  262. package/src/domain/sources.ts +0 -144
  263. package/src/entry/mcp/dispatch-tools.ts +0 -52
  264. package/src/entry/mcp/lane-tools.ts +0 -84
  265. package/src/entry/mcp/protocol-tools.ts +0 -57
  266. package/src/entry/mcp/run-tools.ts +0 -148
  267. package/src/entry/mcp/setup-tools.ts +0 -108
  268. package/src/entry/tui/commands.ts +0 -563
  269. package/src/entry/tui/effects.ts +0 -381
  270. package/src/entry/tui/fixtures.ts +0 -637
  271. package/src/entry/tui/profile-edits.ts +0 -146
  272. package/src/entry/tui/profile-tree.ts +0 -429
  273. package/src/entry/tui/providers/app.tsx +0 -162
  274. package/src/entry/tui/providers/boundary.tsx +0 -57
  275. package/src/entry/tui/providers/data.tsx +0 -160
  276. package/src/entry/tui/providers/keymap.tsx +0 -166
  277. package/src/entry/tui/providers/root.tsx +0 -45
  278. package/src/entry/tui/providers/theme.tsx +0 -17
  279. package/src/entry/tui/providers/toast.tsx +0 -77
  280. package/src/entry/tui/run.tsx +0 -114
  281. package/src/entry/tui/state.ts +0 -373
  282. package/src/entry/tui/stories.ts +0 -114
  283. package/src/entry/tui/text.ts +0 -130
  284. package/src/entry/tui/theme.ts +0 -140
  285. package/src/entry/tui/views/app.tsx +0 -208
  286. package/src/entry/tui/views/dialogs.tsx +0 -41
  287. package/src/entry/tui/views/profile-actions.ts +0 -331
  288. package/src/entry/tui/views/profiles.tsx +0 -520
  289. package/src/entry/tui/views/runs.tsx +0 -573
  290. package/src/entry/tui/views/save-dialog.tsx +0 -340
  291. package/src/entry/tui/views/status.tsx +0 -209
  292. package/src/entry/tui/widgets/chrome.tsx +0 -114
  293. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  294. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  295. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  296. package/src/entry/tui/widgets/dialog.tsx +0 -68
  297. package/src/entry/tui/widgets/line.tsx +0 -92
  298. package/src/entry/tui/widgets/list.tsx +0 -197
  299. package/src/entry/tui/widgets/spinner.tsx +0 -49
  300. package/src/entry/tui/widgets/toast.tsx +0 -62
  301. package/src/infra/sources/arena.ts +0 -87
  302. package/src/infra/sources/artificial-analysis.ts +0 -112
  303. package/src/infra/sources/cache.ts +0 -124
  304. package/src/infra/sources/csv.ts +0 -38
  305. package/src/infra/sources/epoch.ts +0 -82
  306. package/src/infra/sources/http.ts +0 -75
  307. package/src/infra/sources/litellm.ts +0 -58
  308. package/src/infra/sources/models-dev.ts +0 -59
  309. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  310. package/src/infra/sources/openrouter-models.ts +0 -60
  311. package/src/infra/sources/rows.ts +0 -38
  312. package/src/infra/sources/vectara.ts +0 -69
  313. package/src/infra/sources/zip.ts +0 -48
  314. package/src/services/catalog-refresh.ts +0 -201
  315. package/src/services/doctor-sources.ts +0 -76
  316. package/src/services/runs-page.ts +0 -255
  317. package/src/services/source-derive.ts +0 -374
  318. package/src/services/source-sync.ts +0 -305
  319. package/src/services/standins.ts +0 -204
  320. package/src/services/treat-likes.ts +0 -97
@@ -2,18 +2,24 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import type { RunRecord } from "../domain/record.ts";
4
4
  import type { RouteRow } from "../domain/route.ts";
5
+ import { parseLaneHeader } from "../domain/lane.ts";
5
6
  import { ensurePrivateDir, readJsonl, writeTextAtomic } from "../infra/store.ts";
6
7
  import { type Dispatch, listDispatches, liveDispatches } from "./dispatches.ts";
7
- import type { VerifierStep } from "./gate-service.ts";
8
+ import { RECHECK_COMMAND_MIN } from "../domain/gate-brief.ts";
9
+ import { failedItems, type VerifierStep } from "./gate-service.ts";
8
10
  import {
11
+ countFindings,
12
+ FINDING,
9
13
  landedMilestones,
10
14
  milestoneReviewer,
11
15
  milestoneVerifier,
12
16
  milestoneStart,
13
17
  namesMilestone,
18
+ openFindings,
19
+ partialReviewer,
14
20
  reviewerPassed,
15
- verifierPassed,
16
21
  } from "./milestones.ts";
22
+ import { openQuestions } from "./questions.ts";
17
23
  import { readAgentRuns, readRecords, readRoutes, type Run, runPaths } from "./run-store.ts";
18
24
 
19
25
  // Spec 1.1 §10: the protocol's next step, derived from the run's own files so a session that lost its
@@ -44,30 +50,52 @@ function laneIds(run: Run): string[] {
44
50
  .sort(byNumber);
45
51
  }
46
52
 
53
+ /** Plan 28: the milestones the run's lane files name, in number order. */
54
+ export const runMilestones = (run: Run): string[] =>
55
+ [...new Set(laneIds(run).map(milestoneOf))].sort(byNumber);
56
+
57
+ /** What laneDone reads of the run, read once for every lane of the milestone. */
58
+ interface LaneEvidence {
59
+ routes: RouteRow[];
60
+ live: Dispatch[];
61
+ records: Map<string, RunRecord>;
62
+ dispatches: Dispatch[];
63
+ agents: ReturnType<typeof readAgentRuns>;
64
+ }
65
+
66
+ function laneEvidence(run: Run, routes: RouteRow[], live: Dispatch[]): LaneEvidence {
67
+ return {
68
+ routes,
69
+ live,
70
+ records: new Map(readRecords(run).records.map((r) => [r.dispatchId, r])),
71
+ dispatches: listDispatches(run),
72
+ agents: readAgentRuns(run),
73
+ };
74
+ }
75
+
47
76
  /**
48
77
  * Whether `lane` needs no dispatch now: its latest dispatch or native agent run since its latest climb is
49
78
  * still running, or ended ok with no blocked or refused reply. A lane whose last try failed, hit a limit,
50
79
  * was blocked, or was climbed since goes back to dispatch; a plain re-route leaves an ok try standing.
51
80
  */
52
- function laneDone(run: Run, lane: string, routes: RouteRow[], live: Dispatch[]): boolean {
81
+ function laneDone(lane: string, e: LaneEvidence): boolean {
53
82
  const from = Math.max(
54
- ...routes.filter((r) => r.lane === lane && r.source === "climb").map((r) => Date.parse(r.at)),
83
+ ...e.routes.filter((r) => r.lane === lane && r.source === "climb").map((r) => Date.parse(r.at)),
55
84
  );
56
85
  // a dispatch routes its lane first, so a try at the route's own time counts as after it
57
86
  const after = (t: string) => !(Date.parse(t) < from);
58
- const records = new Map(readRecords(run).records.map((r) => [r.dispatchId, r]));
59
- const running = new Set(live.map((d) => d.admit.dispatchId));
87
+ const running = new Set(e.live.map((d) => d.admit.dispatchId));
60
88
  const tries = [
61
- ...listDispatches(run)
89
+ ...e.dispatches
62
90
  .filter((d) => d.admit.lane === lane && after(d.admit.admittedAt))
63
91
  .map((d) => {
64
- const r = records.get(d.admit.dispatchId);
92
+ const r = e.records.get(d.admit.dispatchId);
65
93
  const ok = r
66
94
  ? r.status === "ok" && r.replyStatus !== "blocked" && r.replyStatus !== "refused"
67
95
  : running.has(d.admit.dispatchId);
68
96
  return { at: Date.parse(d.admit.admittedAt), ok };
69
97
  }),
70
- ...readAgentRuns(run)
98
+ ...e.agents
71
99
  .filter((a) => a.lane === lane && after(a.at))
72
100
  .map((a) => ({ at: Date.parse(a.at), ok: a.status === "ok" })),
73
101
  ];
@@ -78,6 +106,27 @@ function laneDone(run: Run, lane: string, routes: RouteRow[], live: Dispatch[]):
78
106
  return latest?.ok ?? false;
79
107
  }
80
108
 
109
+ /** The lane's After: lanes, from its file; none when it has none or cannot be read. */
110
+ function afterOf(run: Run, lane: string): string[] {
111
+ try {
112
+ return parseLaneHeader(readFileSync(join(runPaths(run.dir).lanes, `${lane}.md`), "utf8")).after;
113
+ } catch {
114
+ return [];
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Spec 1.5 "Lane editing": the lanes `lane`'s After: line names that have not finished: their milestone has
120
+ * not landed and their latest try since the latest climb is still running, or did not end ok.
121
+ */
122
+ export function unfinishedAfter(run: Run, lane: string, routes: RouteRow[] = readRoutes(run)): string[] {
123
+ const landed = new Set(landedMilestones(run));
124
+ const after = afterOf(run, lane).filter((a) => !landed.has(milestoneOf(a)));
125
+ if (after.length === 0) return [];
126
+ const evidence = laneEvidence(run, routes, []);
127
+ return after.filter((a) => !laneDone(a, evidence));
128
+ }
129
+
81
130
  /**
82
131
  * The step the milestone loop is at: the first milestone neither landed nor parked, and within it the
83
132
  * first of route, dispatch, collect, reviewer, verifier and land that is still to do.
@@ -98,20 +147,49 @@ export function protocolNext(run: Run, parked: string[], now = Date.now()): stri
98
147
  const routed = new Set(routes.map((r) => r.lane));
99
148
  if (mine.some((l) => !routed.has(l))) return `route and preflight ${m}'s lanes`;
100
149
  const live = liveDispatches(run, now);
101
- const notDone = mine.filter((l) => !laneDone(run, l, routes, live));
150
+ const evidence = laneEvidence(run, routes, live);
151
+ const notDone = mine.filter((l) => !laneDone(l, evidence));
102
152
  // spec 1.1 §9: a climb past the top rung (from === rung) leaves nothing to climb to; that is a decision
103
153
  const spent = notDone.filter((l) => {
104
154
  const last = routes.findLast((r) => r.lane === l);
105
155
  return last?.source === "climb" && last.from === last.rung;
106
156
  });
107
157
  const waiting = notDone.filter((l) => !spent.includes(l));
108
- if (waiting.length) return `dispatch ${waiting.join(", ")}`;
158
+ // an After: line keeps a lane back until the lanes it names have finished
159
+ const held = new Map(waiting.map((l) => [l, unfinishedAfter(run, l, routes)]));
160
+ const ready = waiting.filter((l) => held.get(l)?.length === 0);
161
+ const order = waiting
162
+ .filter((l) => !ready.includes(l))
163
+ .map((l) => `${l} after ${held.get(l)?.join(", ")}`)
164
+ .join("; ");
165
+ if (ready.length) return `dispatch ${ready.join(", ")}${order ? `; then ${order}` : ""}`;
109
166
  if (spent.length) return `${spent.join(", ")}: out of rungs — ask finding, then the architect or park ${m}`;
110
167
  const running = live.filter((d) => d.admit.lane !== null && mine.includes(d.admit.lane));
111
- if (running.length) return `${m}: lanes running (${running.map((d) => d.admit.name).join(", ")})`;
168
+ if (running.length)
169
+ return `${m}: lanes running (${running.map((d) => d.admit.name).join(", ")})${order ? `; then ${order}` : ""}`;
170
+ if (order) return `${m}: ${order} (After:), not done: finish those lanes first`;
112
171
  const start = milestoneStart(run, m);
113
- if (!reviewerPassed(run, m, start)) return `${m}: reviewer`;
114
- if (!verifierPassed(run, m, start)) return `${m}: verifier`;
172
+ if (!reviewerPassed(run, m, start)) {
173
+ // plan 23: a partial review is not the review: a second pass, scoped to what it did not read
174
+ const partial = partialReviewer(run, m, start);
175
+ return partial
176
+ ? `${m}: ${partial.name} stopped partial: a scoped second pass (reviewer-${m}-2) over the files it did not read`
177
+ : `${m}: reviewer`;
178
+ }
179
+ // plan 23: open BLOCKER and BUG lines go to the fix round before the verifier
180
+ const open = openFindings(run, m, start);
181
+ if (open)
182
+ return `${m}: fix round for ${open.name}'s findings (${open.blocker} BLOCKER, ${open.bug} BUG), then the verifier`;
183
+ const verdict = milestoneVerifier(run, m, start);
184
+ if (!verdict) return `${m}: verifier`;
185
+ // plan 23: the machine, not the work: the owner hears of it, and no fix round runs
186
+ if (verdict.blocked !== null)
187
+ return `${m}: ${verdict.name} is blocked by the environment${verdict.blocked ? ` (${verdict.blocked})` : ""}: surface it to the owner (park ${m}), not a fix round`;
188
+ if (!verdict.passed) {
189
+ // plan 23: a resumed verifier can hang; a re-check goes to a fresh one, told what failed
190
+ const failed = failedItems(run, m);
191
+ return `${m}: ${verdict.name} failed: the owning lanes fix it, then a fresh verifier (not a resume) re-checks ${failed.length ? failed.join(", ") : "the failed items"}, each command capped at ${RECHECK_COMMAND_MIN} min`;
192
+ }
115
193
  return `land ${m}`;
116
194
  }
117
195
 
@@ -121,21 +199,24 @@ export const protocolView = (run: Run, parked: string[], now?: number) => ({
121
199
  checklist: PROTOCOL_CHECKLIST,
122
200
  });
123
201
 
124
- const FINDING = /^\s*(?:[-*]\s*)?(BLOCKER|BUG|NIT)\b/;
125
-
126
202
  function findingCounts(run: Run, r: RunRecord | undefined): string {
127
203
  if (!r?.replyPath) return "no reply";
128
- const file = join(run.dir, r.replyPath);
129
- const text = existsSync(file) ? readFileSync(file, "utf8") : "";
130
- const n = { BLOCKER: 0, BUG: 0, NIT: 0 };
131
- for (const line of text.split("\n")) {
132
- const k = FINDING.exec(line)?.[1] as keyof typeof n | undefined;
133
- if (k) n[k]++;
134
- }
204
+ const n = countFindings(run, r) ?? { BLOCKER: 0, BUG: 0, NIT: 0 };
135
205
  const total = n.BLOCKER + n.BUG + n.NIT;
136
206
  return `${total} finding(s): ${n.BLOCKER} BLOCKER, ${n.BUG} BUG, ${n.NIT} NIT`;
137
207
  }
138
208
 
209
+ /** The reviewer's BLOCKER and BUG lines, as it wrote them: what the digest lists as findings. */
210
+ function findingLines(run: Run, r: RunRecord | null | undefined): string[] {
211
+ if (!r?.replyPath) return [];
212
+ const file = join(run.dir, r.replyPath);
213
+ const text = existsSync(file) ? readFileSync(file, "utf8") : "";
214
+ return text
215
+ .split("\n")
216
+ .filter((l) => /^(BLOCKER|BUG)$/.test(FINDING.exec(l)?.[1] ?? ""))
217
+ .map((l) => l.trim().replace(/^[-*]\s*/, ""));
218
+ }
219
+
139
220
  const k = (x: number) => (x >= 1000 ? `${Math.round(x / 1000)}k` : String(x));
140
221
 
141
222
  /**
@@ -145,7 +226,18 @@ const k = (x: number) => (x >= 1000 ? `${Math.round(x / 1000)}k` : String(x));
145
226
  */
146
227
  export function writeDigest(
147
228
  run: Run,
148
- i: { milestone: string; what: string; commit: string; evidence: string; minutes: number; at: string },
229
+ i: {
230
+ milestone: string;
231
+ what: string;
232
+ commit: string;
233
+ evidence: string;
234
+ minutes: number;
235
+ at: string;
236
+ /** spec 1.5: the commits the milestone landed, `<hash> <subject>`, oldest first */
237
+ commits?: string[];
238
+ /** spec 1.5: the parked and paused minutes `minutes` leaves out */
239
+ pausedMinutes?: number;
240
+ },
149
241
  ): string {
150
242
  const m = i.milestone;
151
243
  const start = milestoneStart(run, m);
@@ -163,6 +255,9 @@ export function writeDigest(
163
255
  const records = readRecords(run).records;
164
256
  // the reviewer the gate counted: since the milestone's lanes started, a dispatch or a native subagent
165
257
  const reviewer = milestoneReviewer(run, m, start);
258
+ // spec 1.5: the review's BLOCKER and BUG lines, and the owner questions still open in the run
259
+ const findings = findingLines(run, reviewer?.record);
260
+ const open = openQuestions(run);
166
261
  const agents = readAgentRuns(run);
167
262
  // the verdict the gate counted: the latest attempt, native or headless, a FAIL shown as such
168
263
  const verdict = milestoneVerifier(run, m, start);
@@ -193,7 +288,8 @@ export function writeDigest(
193
288
  const text = [
194
289
  `# ${m} — ${i.what}`,
195
290
  "",
196
- `Commit ${i.commit} · ${i.minutes} min · landed ${i.at}`,
291
+ `Commit ${i.commit} · ${i.minutes} min${i.pausedMinutes ? ` (${i.pausedMinutes} min parked or paused left out)` : ""} · landed ${i.at}`,
292
+ ...(i.commits?.length ? [`Commits: ${i.commits.join("; ")}`] : []),
197
293
  `A-lines: ${aLines.length ? aLines.join("; ") : "none named in what or evidence"}`,
198
294
  "",
199
295
  "Lanes:",
@@ -201,6 +297,8 @@ export function writeDigest(
201
297
  "",
202
298
  `Reviewer: ${reviewer ? `${reviewer.name} · ${reviewer.record ? findingCounts(run, reviewer.record) : "a Claude subagent (findings in its reply)"}` : "none"}`,
203
299
  `Verifier: ${verdict ? `${verdict.passed ? "PASS" : `FAIL: ${verdict.verdict}`} (${verdict.name}${verdict.headless ? ", headless" : ""})` : "none"}${steps.length ? ` · carried: ${steps.map((s) => `${s.item}${s.commit ? ` from ${s.commit}` : ""}`).join(", ")}` : ""}`,
300
+ `Findings: ${findings.length ? findings.join(" · ") : "no BLOCKER or BUG lines"}`,
301
+ `Open: ${open.length ? open.map((q) => `${q.milestone} parked: ${q.question}`).join(" · ") : "none"}`,
204
302
  `Evidence: ${i.evidence}`,
205
303
  `Tokens: ${k(tokens.input)} in (${k(tokens.cached)} cached) · ${k(tokens.output)} out · Claude subagents ${k(reported)} (reported)`,
206
304
  "",
@@ -1,115 +1,60 @@
1
- import { type Catalog, type Confidence, DIMS, type Dim, rungInfo, scoresOf } from "../domain/catalog.ts";
1
+ import { barOf, BAR_PERCENTILE } from "../domain/bars.ts";
2
+ import { type Catalog, type Pair, rungInfo } from "../domain/catalog.ts";
2
3
  import { type BillingMode, type Cost, costOf, DEFAULT_BILLING } from "../domain/cost.ts";
3
4
  import type { Difficulty, Kind } from "../domain/lane.ts";
4
5
  import { type EvidenceTable, evidenceLine, evidenceOf } from "./run-evidence.ts";
5
6
 
6
- /** Spec 1.2 §5.3: one value routing uses for a rung, with where it came from. */
7
- export interface ValueUsed {
8
- dim: Dim;
9
- value: number;
10
- confidence: Confidence;
11
- /** the source a synced value came from; `shipped` for a hand-typed one, `override` for the user's */
12
- source: string;
13
- benchmark: string;
14
- date: string;
15
- url: string;
16
- /** catherd's guess for this rung: lent by a treat-like or an inferred stand-in (spec 1.2 §6.1) */
17
- inferred: boolean;
18
- /** the rung the value belongs to, when it is not this one */
19
- from: string | null;
20
- }
21
-
22
- /** Spec 1.2 §5.3: one threshold of the lane's bar, the value used against it, and whether it clears. */
23
- export interface Threshold {
24
- dim: Dim;
25
- min: number;
26
- used: ValueUsed | null;
27
- clears: boolean;
28
- /** where the default threshold came from (`barsWhy`) */
29
- why: string | null;
30
- }
31
-
32
- /** Spec 1.2 §5.3, §8: what `route` reports about the rung it chose. */
7
+ /** What `route` reports about the rung it chose: the one number it ranked on, the bar, and what it costs. */
33
8
  export interface Provenance {
34
9
  rung: string;
35
10
  canonical: string;
36
- /** the lane's bar, threshold by threshold; empty when the route read no bar (the default rung) */
37
- thresholds: Threshold[];
38
- /** every value the rung has */
39
- values: ValueUsed[];
40
- /** facts that never carry a bar (spec 1.2 §4.1): `<source>.<field>` → value */
41
- speed: Record<string, number>;
11
+ /** the rung's Slopalytics intelligence index; null when it is not indexed */
12
+ intelligence: number | null;
13
+ /** the difficulty's bar, with the percentile it sits at; null when the route read no bar (the default rung) */
14
+ bar: { difficulty: Difficulty; percentile: number; min: number; clears: boolean } | null;
15
+ price: Pair["price"];
16
+ /** tokens per second; null when Slopalytics has none */
17
+ speed: number | null;
42
18
  cost: Cost;
43
- /** spec 1.2 §8: its runs for the lane's kind (or every kind) and over every kind; never used for routing */
44
- evidence: { kind: string | null; all: string | null };
19
+ /** when the proxy fetched the index the route read; null with no index */
20
+ indexedAt: string | null;
21
+ /** spec 1.2 §8: its runs for the lane's kind (or every kind) and over every kind; null when they could not be read */
22
+ evidence: { kind: string | null; all: string | null } | null;
45
23
  }
46
24
 
47
- /** Spec 1.2 §5.3: each value a canonical rung has, with its confidence and source, borrowed ones marked. */
48
- export function valuesUsed(c: Catalog, canonical: string): ValueUsed[] {
49
- const s = scoresOf(c, canonical);
50
- if (!s) return [];
51
- return DIMS.flatMap((dim) => {
52
- const r = s.records[dim];
53
- if (!r) return [];
54
- const from = s.borrowed.includes(dim) ? s.via : (s.standIns[dim] ?? null);
55
- return [
56
- {
57
- dim,
58
- value: r.value,
59
- confidence: r.confidence,
60
- source: r.source ?? "shipped",
61
- benchmark: `${r.benchmark} ${r.version}`,
62
- date: r.date,
63
- url: r.url,
64
- inferred: from !== null,
65
- from,
66
- },
67
- ];
68
- });
69
- }
70
-
71
- /** Spec 1.2 §5.3: the chosen rung's thresholds for the lane's kind and difficulty, its values and facts. */
25
+ /** The chosen rung's intelligence, the difficulty's bar and whether it clears it, its price, speed and cost. */
72
26
  export function provenanceOf(
73
27
  c: Catalog,
74
28
  rung: string,
75
29
  kind: Kind | null,
76
30
  difficulty: Difficulty | null,
77
31
  billing: Partial<Record<string, BillingMode>>,
78
- evidence: EvidenceTable,
32
+ evidence: EvidenceTable | null,
33
+ at: Record<Difficulty, number> = BAR_PERCENTILE,
79
34
  ): Provenance {
80
35
  const info = rungInfo(c, rung);
81
- const values = valuesUsed(c, info.canonical);
82
- const bar = kind && difficulty ? c.bars[kind][difficulty] : {};
83
- const thresholds: Threshold[] = DIMS.flatMap((dim) => {
84
- const min = bar[dim];
85
- if (min === undefined || !kind || !difficulty) return [];
86
- const used = values.find((v) => v.dim === dim) ?? null;
87
- return [
88
- {
89
- dim,
90
- min,
91
- used,
92
- clears: used !== null && used.value >= min,
93
- why: c.barsWhy[dim]?.[difficulty] ?? null,
94
- },
95
- ];
96
- });
36
+ const intelligence = info.pair?.intelligence ?? null;
37
+ const min = difficulty ? barOf(c, difficulty, at) : null;
97
38
  return {
98
39
  rung,
99
40
  canonical: info.canonical,
100
- thresholds,
101
- values,
102
- speed: info.family?.speed ?? {},
103
- cost: costOf(info.family, info.parsed.effort, billing[info.key] ?? DEFAULT_BILLING[info.key]),
104
- evidence: {
41
+ intelligence,
42
+ bar:
43
+ difficulty && min !== null
44
+ ? {
45
+ difficulty,
46
+ percentile: at[difficulty],
47
+ min,
48
+ clears: intelligence !== null && intelligence >= min,
49
+ }
50
+ : null,
51
+ price: info.pair?.price ?? null,
52
+ speed: info.pair?.speed ?? null,
53
+ cost: costOf(info.family, info.pair, info.parsed.effort, billing[info.key] ?? DEFAULT_BILLING[info.key]),
54
+ indexedAt: c.indexedAt,
55
+ evidence: evidence && {
105
56
  kind: evidenceLine(evidenceOf(evidence, info.canonical, kind)),
106
57
  all: evidenceLine(evidenceOf(evidence, info.canonical)),
107
58
  },
108
59
  };
109
60
  }
110
-
111
- /** A value as one line: `repo_code 66.6 (adjacent, shipped DeepSWE 1.1, 2026-09-22)`, marking a guess. */
112
- export function valueWords(v: ValueUsed): string {
113
- const lent = v.inferred ? `, inferred from ${v.from}` : "";
114
- return `${v.dim} ${v.value} (${v.confidence}, ${v.source} ${v.benchmark}, ${v.date}${lent})`;
115
- }
@@ -1,6 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { CatherdError } from "../domain/errors.ts";
3
3
  import { assertId } from "../domain/ids.ts";
4
+ import type { Span } from "../domain/util.ts";
4
5
  import { appendJsonl, ensureJsonlHeader, readJsonl } from "../infra/store.ts";
5
6
  import type { Deps } from "./ports.ts";
6
7
  import { findRun, type Run } from "./run-store.ts";
@@ -28,6 +29,15 @@ const rows = (run: Run): QuestionRow[] =>
28
29
  (r) => typeof r?.milestone === "string" && typeof r.text === "string",
29
30
  );
30
31
 
32
+ /** Plan 28: how many questions the run parked on and how many were answered. */
33
+ export const questionCounts = (run: Run): { questions: number; answers: number } => {
34
+ const all = rows(run);
35
+ return {
36
+ questions: all.filter((r) => r.kind === "question").length,
37
+ answers: all.filter((r) => r.kind === "answer").length,
38
+ };
39
+ };
40
+
31
41
  /** The questions not answered yet, oldest first; a milestone answered after its question is no longer open. */
32
42
  export function openQuestions(run: Run): OpenQuestion[] {
33
43
  const open = new Map<string, OpenQuestion>();
@@ -38,6 +48,20 @@ export function openQuestions(run: Run): OpenQuestion[] {
38
48
  return [...open.values()];
39
49
  }
40
50
 
51
+ /** Spec 1.5 "The ledger": when `milestone` was parked, question to answer; the last one open while unanswered. */
52
+ export function parkedSpans(run: Run, milestone: string): Span[] {
53
+ const out: Span[] = [];
54
+ for (const r of rows(run)) {
55
+ if (r.milestone !== milestone) continue;
56
+ const at = Date.parse(r.at);
57
+ if (Number.isNaN(at)) continue;
58
+ const open = out.at(-1);
59
+ if (r.kind === "question" && (!open || open.to !== null)) out.push({ from: at, to: null });
60
+ else if (r.kind === "answer" && open && open.to === null) open.to = at;
61
+ }
62
+ return out;
63
+ }
64
+
41
65
  function append(run: Run, row: QuestionRow): void {
42
66
  const file = questionsFile(run);
43
67
  ensureJsonlHeader(file, "questions");
@@ -3,10 +3,12 @@ import { errorMessage } from "../domain/errors.ts";
3
3
  import { dispatchPaths } from "../infra/dispatch-dir.ts";
4
4
  import { log } from "../infra/log.ts";
5
5
  import { writeJsonAtomic } from "../infra/store.ts";
6
- import { settle, stallPoll, unsettledLimits, watching } from "./dispatch-service.ts";
6
+ import { ensureDaemon } from "./daemon.ts";
7
+ import { recoverOwned, settle, stallPoll, unsettledLimits, watching } from "./dispatch-service.ts";
7
8
  import { type Dispatch, listDispatches, pendingDispatches } from "./dispatches.ts";
8
9
  import { finalizeDispatch, waitForFinish } from "./finalize.ts";
9
10
  import type { Deps } from "./ports.ts";
11
+ import { isDaemonRun } from "./run-events-store.ts";
10
12
  import { listRuns, type Run } from "./run-store.ts";
11
13
  import { ownsRun } from "./sessions.ts";
12
14
 
@@ -57,6 +59,27 @@ async function watchAndFinalize(deps: Deps, run: Run, d: Dispatch): Promise<void
57
59
  }
58
60
  }
59
61
 
62
+ /**
63
+ * Plan 22, final-review I2: a server that does not lead the boot still watches the live roles of the runs its
64
+ * own session owns, so a coordinator's server that restarts mid-run (an /mcp reconnect, a crash) while another
65
+ * session's server holds the boot lock still finalizes and announces them. It also records and announces the
66
+ * roles of those runs that finished while it was away (no live process, no record: PR #47 P1), as a claim
67
+ * does. A second watcher of a dispatch the lead also watches is harmless: finalize and failover run once, and
68
+ * a role another live process is finalizing is left to it. A run that cannot be read is skipped, logged.
69
+ */
70
+ export function adoptOwned(deps: Deps): void {
71
+ for (const run of listRuns().runs) {
72
+ try {
73
+ if (!ownsRun(deps, run)) continue;
74
+ recoverOwned(deps, run, { background: true }).catch((e: unknown) =>
75
+ log("warn", "reconcile", { run: run.id, error: errorMessage(e) }),
76
+ );
77
+ } catch (e) {
78
+ log("warn", "reconcile", { run: run.id, error: errorMessage(e) });
79
+ }
80
+ }
81
+ }
82
+
60
83
  /**
61
84
  * Spec §4.7: on server start, finalize and settle each finished dispatch that has no record, settle each
62
85
  * unread usage limit that was never failed over (its server died between the two), and watch each live one
@@ -79,6 +102,15 @@ export async function reconcileAll(deps: Deps): Promise<ReconcileReport> {
79
102
  if (!report.warnings.includes(w)) report.warnings.push(w);
80
103
  };
81
104
  for (const run of runs) {
105
+ // a 2.0 run is its daemon's: an unfinished one gets its daemon back, and nothing here settles its roles
106
+ if (isDaemonRun(run)) {
107
+ try {
108
+ ensureDaemon(run);
109
+ } catch (e) {
110
+ warn(run, e);
111
+ }
112
+ continue;
113
+ }
82
114
  let pending;
83
115
  try {
84
116
  const scrubbed = scrubOldSpecs(run);
@@ -1,8 +1,8 @@
1
- import { latestVerifierStep, type VerifierStep } from "./gate-service.ts";
2
1
  import { protocolView } from "./protocol.ts";
3
2
  import { type OpenQuestion, openQuestions } from "./questions.ts";
4
3
  import type { Run } from "./run-store.ts";
5
- import { readNotes } from "./state.ts";
4
+ import { type Notes, readNotes } from "./state.ts";
5
+ import { verifierStepView, type VerifierStepView } from "./verifier-step.ts";
6
6
 
7
7
  // Spec 1.1 §8, §7 and §10: what a session re-entering a run needs first, as peek returns it: the owner questions still open, the protocol's next step with its checklist,
8
8
  // and the verifier's latest step.
@@ -10,13 +10,14 @@ import { readNotes } from "./state.ts";
10
10
  export interface Reentry {
11
11
  questions: OpenQuestion[];
12
12
  protocol: { next: string; checklist: string[] };
13
- verifier: VerifierStep | null;
13
+ verifier: VerifierStepView | null;
14
14
  }
15
15
 
16
- export function reentry(run: Run, now?: number): Reentry {
16
+ /** `notes`: state.json as the caller already read it (peek), so it is read once. */
17
+ export function reentry(run: Run, now?: number, notes: Notes = readNotes(run)): Reentry {
17
18
  return {
18
19
  questions: openQuestions(run),
19
- protocol: protocolView(run, readNotes(run).parked ?? [], now),
20
- verifier: latestVerifierStep(run),
20
+ protocol: protocolView(run, notes.parked ?? [], now),
21
+ verifier: verifierStepView(run, now ?? Date.now()),
21
22
  };
22
23
  }
@@ -0,0 +1,27 @@
1
+ import { CatherdError } from "../domain/errors.ts";
2
+ import type { RoleScope } from "../domain/role-scope.ts";
3
+ import { roleMcpTools } from "../domain/role-tools.ts";
4
+ import { latestDispatch } from "./dispatches.ts";
5
+ import { findRun } from "./run-store.ts";
6
+
7
+ /**
8
+ * Spec 1.5 plan 21: what the role server lets a role do, for the CLI forms any role can run
9
+ * (`catherd run-file`, `catherd gate`). Outside a role (`scope` null: the user's own terminal) everything is
10
+ * allowed. In a role, only its own run, and only the operations its role server would list (`tool`).
11
+ */
12
+ export function assertRoleMay(scope: RoleScope | null, run: string, tool: string): void {
13
+ if (!scope) return;
14
+ if (scope.run !== run)
15
+ throw new CatherdError(
16
+ "E_ROLE_SCOPE",
17
+ `this process runs ${scope.name} of run ${scope.run}, not of run ${run}`,
18
+ { fix: `use run ${scope.run}` },
19
+ );
20
+ const role = latestDispatch(findRun(run), scope.name)?.admit.role;
21
+ if (!role || !roleMcpTools(role).includes(tool))
22
+ throw new CatherdError(
23
+ "E_ROLE_SCOPE",
24
+ `${scope.name} ${role ? `is a ${role}, which has no ${tool}` : `is no dispatch of run ${run}`}`,
25
+ { fix: "report it in your reply; the orchestrator does it" },
26
+ );
27
+ }