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
@@ -1,4 +1,5 @@
1
1
  import { assertNativeHost } from "./backends.ts";
2
+ import { percentilesOf } from "../domain/bars.ts";
2
3
  import { BUDGET_CHEAP_AT } from "../domain/budget.ts";
3
4
  import { errorMessage } from "../domain/errors.ts";
4
5
  import {
@@ -13,27 +14,38 @@ import {
13
14
  import { type Difficulty, type Kind, parseLaneHeader } from "../domain/lane.ts";
14
15
  import type { Role } from "../domain/roles.ts";
15
16
  import type { RouteJev } from "../domain/route.ts";
17
+ import type { Catalog } from "../domain/catalog.ts";
16
18
  import {
17
19
  candidates,
18
20
  defaultDifficulty,
19
21
  defaultLadder,
20
22
  type Pick,
23
+ quotaUsage,
21
24
  type RoutingProfile,
25
+ rolesWithoutRung,
22
26
  select,
23
27
  } from "../domain/select.ts";
24
28
  import { log } from "../infra/log.ts";
25
- import { catalogQuery, freshenDiscovery, loadCatalog } from "./catalog-service.ts";
29
+ import { catalogQuery, freshenDiscovery, loadCatalog, startIndex } from "./catalog-service.ts";
26
30
  import { type Asked, askJev, type JevOpts, jevQuestions, logJev } from "./jev-service.ts";
27
31
  import type { ProfileView, RouteAnswer, RouteRequest, RoutingPort, Verdict } from "./ports.ts";
28
32
  import { provenanceOf } from "./provenance.ts";
29
- import { runEvidence } from "./run-evidence.ts";
33
+ import { type EvidenceTable, runEvidence } from "./run-evidence.ts";
30
34
 
31
- function routingProfile(v: ProfileView, role: Role, spentFraction: number): RoutingProfile {
35
+ function routingProfile(
36
+ v: ProfileView,
37
+ role: Role,
38
+ spentFraction: number,
39
+ usage: Record<string, number> = {},
40
+ ): RoutingProfile {
32
41
  const rc = v.roles[role];
33
42
  return {
43
+ usage,
34
44
  // spec §4.6: from 80 % of the budget on, start at the cheapest rung that clears the bar
35
45
  objective: spentFraction >= BUDGET_CHEAP_AT ? "cost" : v.objective,
36
46
  billing: v.billing,
47
+ maxEffort: v.maxEffort ?? {},
48
+ bars: { ...v.bars, ...rc?.bars },
37
49
  role: {
38
50
  enabled: rc?.enabled ?? false,
39
51
  rungs: rc?.rungs ?? [],
@@ -50,14 +62,60 @@ const answer = (
50
62
  asked: Asked | null,
51
63
  jev: RouteJev | null,
52
64
  ): RouteAnswer => ({
53
- ...pick,
65
+ rung: pick.rung,
66
+ ladder: pick.ladder,
54
67
  source,
55
68
  kind,
56
69
  difficulty,
57
70
  questionSet: asked?.answers ? asked.meta.questionSet : null,
58
71
  jev,
72
+ jevSaid: null,
73
+ why: "",
74
+ ...(pick.noClear ? { noClear: pick.noClear } : {}),
75
+ ...(pick.tie ? { tie: pick.tie } : {}),
59
76
  });
60
77
 
78
+ /** Spec 1.5 plan 24: what Jev said when it differs from the kind and difficulty the route used, else null. */
79
+ function jevSaid(
80
+ judged: ReturnType<typeof judgeRoute> | null,
81
+ kind: Kind | null,
82
+ difficulty: Difficulty | null,
83
+ ): string | null {
84
+ if (!judged) return null;
85
+ const k = judged.kind;
86
+ const d = judged.track ? judged.difficulty : null;
87
+ if ((k === null || k === kind) && (d === null || d === difficulty)) return null;
88
+ return `Jev said ${k ?? "no sure kind"}/${d ?? "no sure difficulty"}`;
89
+ }
90
+
91
+ /** Spec 1.5 plan 24: the route's one-line why, for the coordinator; the provenance goes to routes.jsonl. */
92
+ function whyOf(a: RouteAnswer, hasLane: boolean): string {
93
+ const what = `${a.kind}/${a.difficulty}`;
94
+ const head =
95
+ a.source === "lane"
96
+ ? `the lane's Kind/Difficulty, ${what}`
97
+ : a.source === "jev"
98
+ ? `Jev: ${what}`
99
+ : a.source === "jev-kind"
100
+ ? `Jev's kind with ${what}`
101
+ : hasLane
102
+ ? "the role's default rung: neither Jev nor the lane file gave a kind and difficulty"
103
+ : "the role's default rung";
104
+ const start =
105
+ a.noClear ?? (a.source === "default" ? null : "the first rung in objective order that clears it");
106
+ return [head, a.jevSaid, start, a.tie].filter((x): x is string => Boolean(x)).join("; ");
107
+ }
108
+
109
+ /** Spec 1.2 §8 evidence is display only: an unreadable run on this machine never fails a route. */
110
+ function evidenceOrNone(c: Catalog): EvidenceTable | null {
111
+ try {
112
+ return runEvidence(c);
113
+ } catch (e) {
114
+ log("debug", "route", { evidence: errorMessage(e) });
115
+ return null;
116
+ }
117
+ }
118
+
61
119
  /** How long `route` waits on the daily discovery refresh before routing on the cached listing. */
62
120
  const DISCOVERY_BUDGET_MS = 5_000;
63
121
 
@@ -79,53 +137,78 @@ async function freshenWithin(rungs: string[], repo: string, ms: number): Promise
79
137
  clearTimeout(timer);
80
138
  }
81
139
 
140
+ /** What every route of one call shares: the role's routing profile and the catalog. */
141
+ interface Prepared {
142
+ p: RoutingProfile;
143
+ c: Catalog;
144
+ }
145
+
146
+ async function prepare(req: RouteRequest, o: RoutingOpts): Promise<Prepared> {
147
+ const p = routingProfile(req.profile, req.role, req.spentFraction, req.usage);
148
+ await freshenWithin(p.role.rungs, req.repo, o.discoveryBudgetMs ?? DISCOVERY_BUDGET_MS);
149
+ return { p, c: loadCatalog({ repo: req.repo, runDir: req.runDir }) };
150
+ }
151
+
152
+ /** Jev's answer about a lane; null when the route reads no lane, empty when it asks nothing (Jev off, one usable rung). */
153
+ interface Judged {
154
+ asked: Asked | null;
155
+ judged: ReturnType<typeof judgeRoute> | null;
156
+ }
157
+
158
+ async function judge(req: RouteRequest, { p, c }: Prepared, o: RoutingOpts): Promise<Judged | null> {
159
+ if (req.laneText === null) return null;
160
+ // one usable rung: nothing for Jev to choose, but the lane's header still names its kind and difficulty
161
+ if (req.profile.jev.use === "off" || candidates(c, p, req.role).length <= 1)
162
+ return { asked: null, judged: null };
163
+ const asked = await askJev(req.runDir, "route-v2", laneState(req.laneText), o);
164
+ const judged = asked.answers ? judgeRoute(jevQuestions().sets["route-v2"].rule, asked.answers) : null;
165
+ return { asked, judged };
166
+ }
167
+
82
168
  /**
83
- * Spec §5.4: kind and difficulty from Jev (§5.5's rule), else the lane file's `Kind:`/`Difficulty:`,
84
- * else the role's default rung. A kind Jev is sure of survives a difficulty in the dead band: the
85
- * difficulty then comes from the lane, else the role's default difficulty (`jev-kind`). A role with one
86
- * usable rung never asks Jev.
169
+ * Spec §5.4: kind and difficulty from the lane file's `Kind:`/`Difficulty:` when it declares both (spec 1.5
170
+ * plan 24: a declared header wins, and `jevSaid` names where Jev disagreed), else from Jev (§5.5's rule), else
171
+ * the role's default rung. A kind Jev is sure of survives a difficulty in the dead band: the difficulty then
172
+ * comes from the lane, else the role's default difficulty (`jev-kind`). A role with one usable rung never asks
173
+ * Jev.
87
174
  */
88
- async function route(req: RouteRequest, o: RoutingOpts): Promise<RouteAnswer> {
89
- const p = routingProfile(req.profile, req.role, req.spentFraction);
90
- await freshenWithin(p.role.rungs, req.repo, o.discoveryBudgetMs ?? DISCOVERY_BUDGET_MS);
91
- const c = loadCatalog({ repo: req.repo });
175
+ function decide(req: RouteRequest, { p, c }: Prepared, j: Judged | null): RouteAnswer {
92
176
  const fallback = () => defaultLadder(c, p, req.role);
93
- if (req.laneText === null || candidates(c, p, req.role).length <= 1) {
94
- const out = answer(fallback(), "default", null, null, null, null);
95
- return { ...out, provenance: provenanceOf(c, out.rung, null, null, p.billing, runEvidence(c)) };
96
- }
177
+ const finish = (out: RouteAnswer): RouteAnswer => {
178
+ out.why = whyOf(out, req.laneText !== null);
179
+ out.provenance = provenanceOf(
180
+ c,
181
+ out.rung,
182
+ out.kind,
183
+ out.difficulty,
184
+ p.billing,
185
+ evidenceOrNone(c),
186
+ percentilesOf(p),
187
+ );
188
+ return out;
189
+ };
190
+ if (req.laneText === null || j === null)
191
+ return finish(answer(fallback(), "default", null, null, null, null));
97
192
  const lane = parseLaneHeader(req.laneText);
98
- let asked: Asked | null = null;
99
- let judged: ReturnType<typeof judgeRoute> | null = null;
100
- if (req.profile.jev.use !== "off") {
101
- asked = await askJev(req.runDir, "route-v2", laneState(req.laneText), o);
102
- if (asked.answers) judged = judgeRoute(jevQuestions().sets["route-v2"].rule, asked.answers);
103
- }
193
+ const { asked, judged } = j;
104
194
  const jev: RouteJev | null = judged
105
195
  ? { pKind: judged.pKind, pA: judged.pA, pB: judged.pB, nouls: judged.nouls }
106
196
  : null;
107
197
  let out: RouteAnswer;
108
- if (judged?.track && judged.difficulty) {
109
- const kind = judged.kind ?? lane.kind ?? "repo_code";
110
- out = answer(select(c, p, req.role, kind, judged.difficulty), "jev", kind, judged.difficulty, asked, jev);
111
- } else if (judged?.kind) {
112
- const difficulty = lane.difficulty ?? defaultDifficulty(c, p, req.role, judged.kind);
113
- out = answer(
114
- select(c, p, req.role, judged.kind, difficulty),
115
- "jev-kind",
116
- judged.kind,
117
- difficulty,
118
- asked,
119
- jev,
120
- );
198
+ if (lane.kind && lane.difficulty) {
199
+ out = answer(select(c, p, req.role, lane.difficulty), "lane", lane.kind, lane.difficulty, asked, jev);
200
+ } else if (judged?.track && judged.difficulty) {
201
+ const kind = lane.kind ?? judged.kind ?? "repo_code";
202
+ out = answer(select(c, p, req.role, judged.difficulty), "jev", kind, judged.difficulty, asked, jev);
203
+ } else if (judged?.kind || (judged && lane.kind)) {
204
+ const kind = (lane.kind ?? judged.kind) as Kind;
205
+ const difficulty = lane.difficulty ?? defaultDifficulty(c, p, req.role);
206
+ out = answer(select(c, p, req.role, difficulty), "jev-kind", kind, difficulty, asked, jev);
121
207
  } else {
122
- const kind = lane.kind;
123
- out =
124
- kind && lane.difficulty
125
- ? answer(select(c, p, req.role, kind, lane.difficulty), "lane", kind, lane.difficulty, asked, jev)
126
- : answer(fallback(), "default", null, null, asked, jev);
208
+ out = answer(fallback(), "default", null, null, asked, jev);
127
209
  }
128
- out.provenance = provenanceOf(c, out.rung, out.kind, out.difficulty, p.billing, runEvidence(c));
210
+ out.jevSaid = out.source === "default" ? null : jevSaid(judged, out.kind, out.difficulty);
211
+ finish(out);
129
212
  if (asked) {
130
213
  logJev(req.runDir, {
131
214
  ...asked.meta,
@@ -143,9 +226,34 @@ async function route(req: RouteRequest, o: RoutingOpts): Promise<RouteAnswer> {
143
226
  return out;
144
227
  }
145
228
 
229
+ async function route(req: RouteRequest, o: RoutingOpts): Promise<RouteAnswer> {
230
+ const prep = await prepare(req, o);
231
+ return decide(req, prep, await judge(req, prep, o));
232
+ }
233
+
234
+ /**
235
+ * Spec 1.5 plan 24, batch `route`: the lanes of one role (one run, one profile) share one discovery refresh and
236
+ * one catalog, and Jev is asked about all of them at once (Ruling 6: one request per lane, all in flight
237
+ * together, so each keeps its own cache key and jev.jsonl row). Then each lane is decided in order, every
238
+ * routed start counting as a use of its quota, so lanes that tie spread over the quotas.
239
+ */
240
+ async function routeMany(reqs: RouteRequest[], o: RoutingOpts): Promise<RouteAnswer[]> {
241
+ const first = reqs[0];
242
+ if (!first) return [];
243
+ const prep = await prepare(first, o);
244
+ const judged = await Promise.all(reqs.map((r) => judge(r, prep, o)));
245
+ const usage: Record<string, number> = { ...first.usage };
246
+ return reqs.map((req, i) => {
247
+ const out = decide(req, { ...prep, p: { ...prep.p, usage: { ...usage } } }, judged[i] ?? null);
248
+ const q = quotaUsage([out.rung]);
249
+ for (const [k, n] of Object.entries(q)) usage[k] = (usage[k] ?? 0) + n;
250
+ return out;
251
+ });
252
+ }
253
+
146
254
  async function verdict<T extends string>(
147
255
  runDir: string,
148
- set: Extract<SetName, "finding" | "same-defect">,
256
+ set: Extract<SetName, "finding" | "same-defect" | "stop">,
149
257
  state: Record<string, unknown>,
150
258
  options: readonly T[],
151
259
  use: "auto" | "off",
@@ -177,6 +285,11 @@ export function routingService(o: RoutingOpts = {}): RoutingPort {
177
285
  assertNativeHost(result.rung, req.host ?? "unknown");
178
286
  return result;
179
287
  },
288
+ routeMany: async (reqs) => {
289
+ const results = await routeMany(reqs, o);
290
+ results.forEach((r, i) => assertNativeHost(r.rung, reqs[i]?.host ?? "unknown"));
291
+ return results;
292
+ },
180
293
  finding: (runDir, laneText, finding, use) =>
181
294
  verdict(
182
295
  runDir,
@@ -195,6 +308,12 @@ export function routingService(o: RoutingOpts = {}): RoutingPort {
195
308
  use,
196
309
  o,
197
310
  ),
311
+ stopCause: (runDir, status, reply, use) =>
312
+ verdict(runDir, "stop", { status, reply: scrubFree(reply.slice(-4000)) }, VERDICT_OPTIONS.stop, use, o),
313
+ checkStart: async (view, repo) => {
314
+ const { catalog, index } = await startIndex(repo);
315
+ return { unusable: rolesWithoutRung(catalog, view), index };
316
+ },
198
317
  catalog: (filter, billing) => catalogQuery(filter, billing),
199
318
  };
200
319
  }