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,146 +0,0 @@
1
- import type { Suggestion } from "../../services/standins.ts";
2
- import { type Catalog, rungInfo, scoresOf } from "../../domain/catalog.ts";
3
- import { parseRung, tryParseRung } from "../../domain/ids.ts";
4
- import { NOTIFY, type Profile, type ProfilePatch } from "../../domain/profile.ts";
5
- import { inferredScores, quotaOf } from "../../domain/profile-rules.ts";
6
- import { ACCESS } from "../../domain/record.ts";
7
- import type { Role } from "../../domain/roles.ts";
8
- import type { CatalogModel } from "../../services/catalog-service.ts";
9
- import type { RowAction } from "./profile-tree.ts";
10
- import type { NumberPath, SelectOption } from "./state.ts";
11
- import { shortRung } from "./text.ts";
12
-
13
- // What the Profiles tab's rows change: the patch a toggle or cycle makes, number fields, and the options
14
- // each picker dialog offers. profile-tree.ts builds the rows themselves.
15
-
16
- const next = <T>(list: readonly T[], cur: T): T => list[(list.indexOf(cur) + 1) % list.length] as T;
17
-
18
- /**
19
- * The patch a toggle (space) or a cycle (enter) makes on the draft; null for rows that open a dialog
20
- * or expand instead. Space on a model row never changes a rung (research K5).
21
- */
22
- export function patchFor(p: Profile, a: RowAction): ProfilePatch | null {
23
- switch (a.type) {
24
- case "role":
25
- return { roles: { [a.role]: { enabled: !p.roles[a.role].enabled } } };
26
- case "access":
27
- return { roles: { [a.role]: { access: next(ACCESS, p.roles[a.role].access) } } };
28
- case "rung": {
29
- const rc = p.roles[a.role];
30
- if (!rc.rungs.includes(a.rung)) return { roles: { [a.role]: { rungs: [...rc.rungs, a.rung] } } };
31
- return {
32
- roles: {
33
- [a.role]: {
34
- rungs: rc.rungs.filter((r) => r !== a.rung),
35
- ...(rc.defaultRung === a.rung ? { defaultRung: null } : {}),
36
- },
37
- },
38
- };
39
- }
40
- case "objective":
41
- return { objective: p.objective === "cost" ? "speed" : "cost" };
42
- case "jev":
43
- return { jev: { use: p.jev.use === "auto" ? "off" : "auto" } };
44
- case "isolated":
45
- return { harness: { [a.harness]: { isolated: !(p.harness[a.harness]?.isolated ?? false) } } };
46
- case "notify":
47
- return {
48
- notify: p.notify.includes(a.moment)
49
- ? p.notify.filter((m) => m !== a.moment)
50
- : NOTIFY.filter((m) => m === a.moment || p.notify.includes(m)),
51
- };
52
- default:
53
- return null;
54
- }
55
- }
56
-
57
- /** A number field set, or cleared with null (a budget cap); timeouts cannot be cleared. */
58
- export function numberPatch(path: NumberPath, value: number | null): ProfilePatch {
59
- const [head, leaf] = path.split(".") as ["budget" | "timeouts", string];
60
- return { [head]: { [leaf]: value } } as ProfilePatch;
61
- }
62
-
63
- /** Parses a value editor's text: a positive number, or empty for no cap where the field allows one. */
64
- export function parseNumber(path: NumberPath, text: string): { value: number | null } | { error: string } {
65
- const t = text.trim();
66
- if (t === "")
67
- return path.startsWith("budget.") ? { value: null } : { error: "enter a number of minutes above 0" };
68
- const n = Number(t);
69
- if (!Number.isFinite(n) || n <= 0) return { error: `"${t}" is not a number above 0` };
70
- if (path === "budget.tokens" && !Number.isInteger(n)) return { error: "tokens are a whole number" };
71
- return { value: n };
72
- }
73
-
74
- export function numberValue(p: Profile, path: NumberPath): number | undefined {
75
- const [head, leaf] = path.split(".") as ["budget" | "timeouts", string];
76
- return (p[head] as Record<string, number | undefined>)[leaf];
77
- }
78
-
79
- /** The default-rung picker: the role's rungs, and none. */
80
- export function startOptions(p: Profile, role: Role): SelectOption[] {
81
- const rc = p.roles[role];
82
- return [
83
- {
84
- value: "",
85
- title: "none: the cheapest rung that clears the bar",
86
- current: rc.defaultRung === undefined,
87
- },
88
- ...rc.rungs.map((r) => ({ value: r, title: shortRung(r), detail: r, current: r === rc.defaultRung })),
89
- ];
90
- }
91
-
92
- /** The failover picker: every usable rung on another quota (Ruling 3 of plan 5), and none. */
93
- export function failoverOptions(
94
- p: Profile,
95
- models: CatalogModel[],
96
- c: Catalog,
97
- rung: string,
98
- ): SelectOption[] {
99
- const parsed = tryParseRung(rung);
100
- if (!parsed) return [];
101
- const quota = quotaOf(parsed);
102
- const cur = p.failover[rung];
103
- const out: SelectOption[] = [{ value: "", title: "none", current: cur === undefined }];
104
- // usable: scored in catalog_query, or through a treat-like staged in `c` (`withStaged`)
105
- const usable = (x: string) => {
106
- try {
107
- return scoresOf(c, rungInfo(c, x).canonical) !== null;
108
- } catch {
109
- return false;
110
- }
111
- };
112
- for (const m of models)
113
- for (const r of m.rungs) {
114
- if (!(r.enabled || usable(r.rung)) || r.rung.startsWith("claude:")) continue;
115
- const q = quotaOf(parseRung(r.rung));
116
- if (q === quota) continue;
117
- const note = inferredScores(c, rungInfo(c, r.rung)).note;
118
- out.push({
119
- value: r.rung,
120
- title: shortRung(r.rung),
121
- group: m.billing,
122
- detail: note ?? "",
123
- current: r.rung === cur,
124
- });
125
- }
126
- return out;
127
- }
128
-
129
- /**
130
- * The treat-like picker: every rung with scores of its own, by model; spec 1.2 §6.4: the suggested stand-ins
131
- * (`suggestions`, nearest first) say their distance and what they would lend.
132
- */
133
- export function treatLikeOptions(c: Catalog, suggestions: Suggestion[] = []): SelectOption[] {
134
- return Object.keys(c.scores)
135
- .filter((canonical) => scoresOf(c, canonical)?.via === null)
136
- .sort()
137
- .map((canonical) => {
138
- const s = suggestions.find((x) => x.like === canonical);
139
- return {
140
- value: canonical,
141
- title: canonical,
142
- group: canonical.slice(0, canonical.lastIndexOf("#")),
143
- ...(s ? { detail: `distance ${s.distance.toFixed(2)} · lends ${s.lends.join(", ")}` } : {}),
144
- };
145
- });
146
- }
@@ -1,429 +0,0 @@
1
- import { billingKeyOf, type Catalog, rungInfo } from "../../domain/catalog.ts";
2
- import { tryParseRung } from "../../domain/ids.ts";
3
- import { NOTIFY, type NotifyMoment, type Profile } from "../../domain/profile.ts";
4
- import { type Issue, inferredScores, routingProfileOf, type Validation } from "../../domain/profile-rules.ts";
5
- import type { Access } from "../../domain/record.ts";
6
- import { ROLES, type Role } from "../../domain/roles.ts";
7
- import { candidates } from "../../domain/select.ts";
8
- import type { CatalogModel } from "../../services/catalog-service.ts";
9
- import type { NumberPath } from "./state.ts";
10
- import { shortRung } from "./text.ts";
11
-
12
- export type RowAction =
13
- | { type: "section" }
14
- | { type: "role"; role: Role }
15
- | { type: "access"; role: Role }
16
- | { type: "start"; role: Role }
17
- | { type: "group" }
18
- | { type: "model" }
19
- | { type: "rung"; role: Role; rung: string; scored: boolean }
20
- | { type: "objective" }
21
- | { type: "jev" }
22
- | { type: "isolated"; harness: string }
23
- | { type: "number"; path: NumberPath }
24
- | { type: "failover"; rung: string }
25
- | { type: "notify"; moment: NotifyMoment };
26
-
27
- /** One line of the Profiles tree (spec §9.1: roles → access + backend → model → efforts, then settings). */
28
- export interface Row {
29
- key: string;
30
- parent: string | null;
31
- depth: number;
32
- label: string;
33
- value: string;
34
- /** a checkbox's state; null for a row that is not one */
35
- check: boolean | null;
36
- expandable: boolean;
37
- expanded: boolean;
38
- /** headings are skipped by the cursor */
39
- selectable: boolean;
40
- /** drawn muted: an unusable rung, the children of a role that is off */
41
- dim: boolean;
42
- issue: (Issue & { level: "error" | "warning" }) | null;
43
- action: RowAction;
44
- /** what the filter matches: the labels of the row's parents, its own label and its value */
45
- search: string;
46
- }
47
-
48
- export interface TreeInput {
49
- /** the draft, resolved */
50
- profile: Profile;
51
- models: CatalogModel[];
52
- /** the catalog with the draft's staged treat-likes merged in (`withStaged`) */
53
- catalog: Catalog;
54
- staged: Record<string, string>;
55
- expanded: ReadonlySet<string>;
56
- /** the harnesses catherd can isolate: the registered adapters */
57
- harnesses: readonly string[];
58
- enforcement: (rung: string, access: Access) => "enforced" | "advisory";
59
- validation: Validation;
60
- /** every expandable row open, for filtering */
61
- expandAll?: boolean;
62
- }
63
-
64
- /** The catalog as it will be once the staged treat-likes are saved. */
65
- export function withStaged(c: Catalog, staged: Record<string, string>): Catalog {
66
- if (Object.keys(staged).length === 0) return c;
67
- const treatLike = { ...c.treatLike };
68
- for (const [rung, like] of Object.entries(staged)) {
69
- try {
70
- treatLike[rungInfo(c, rung).canonical] = { like, source: "user" };
71
- } catch {
72
- // a staged rung that does not parse is never offered
73
- }
74
- }
75
- return { ...c, treatLike };
76
- }
77
-
78
- const GROUP_ORDER = [
79
- "claude",
80
- "claude-code",
81
- "codex",
82
- "opencode-go",
83
- "opencode",
84
- "cursor",
85
- "grok",
86
- "antigravity",
87
- ];
88
- const GROUP_NOTE: Record<string, string> = { claude: "native subagent", "claude-code": "headless" };
89
-
90
- interface ModelEntry {
91
- backend: string;
92
- model: string;
93
- key: string;
94
- efforts: string[];
95
- listed: boolean | null;
96
- /** efforts catalog_query scores (their own or through a saved treat-like) */
97
- scored: Set<string>;
98
- inferred: Set<string>;
99
- }
100
-
101
- function issueFor(v: Validation, match: (path: string) => boolean): Row["issue"] {
102
- const e = v.errors.find((i) => match(i.path));
103
- if (e) return { ...e, level: "error" };
104
- const w = v.warnings.find((i) => match(i.path));
105
- return w ? { ...w, level: "warning" } : null;
106
- }
107
-
108
- /** A role's models: those catalog_query says can fill it, plus any rung the profile holds that it lacks. */
109
- function modelsFor(models: CatalogModel[], role: Role, rungs: string[]): ModelEntry[] {
110
- const out: ModelEntry[] = [];
111
- const find = (backend: string, model: string) =>
112
- out.find((m) => m.backend === backend && m.model === model);
113
- for (const m of models) {
114
- if (!m.roles.includes(role)) continue;
115
- const scored = new Set<string>();
116
- const inferred = new Set<string>();
117
- for (const r of m.rungs) {
118
- const effort = r.rung.slice(r.rung.lastIndexOf("#") + 1);
119
- if (r.enabled) scored.add(effort);
120
- if (r.treatLike || Object.values(r.scores).some((s) => s.confidence === "inferred"))
121
- inferred.add(effort);
122
- }
123
- out.push({
124
- backend: m.backend,
125
- model: m.model,
126
- key: m.billing,
127
- efforts: m.efforts.length ? [...m.efforts] : ["default"],
128
- listed: m.listed,
129
- scored,
130
- inferred,
131
- });
132
- }
133
- for (const rung of rungs) {
134
- const r = tryParseRung(rung);
135
- if (!r) continue;
136
- const m = find(r.backend, r.model);
137
- if (m) {
138
- if (!m.efforts.includes(r.effort)) m.efforts.push(r.effort);
139
- continue;
140
- }
141
- out.push({
142
- backend: r.backend,
143
- model: r.model,
144
- key: billingKeyOf(r),
145
- efforts: [r.effort],
146
- listed: null,
147
- scored: new Set(),
148
- inferred: new Set(),
149
- });
150
- }
151
- return out;
152
- }
153
-
154
- const rungOf = (m: ModelEntry, effort: string) => `${m.backend}:${m.model}#${effort}`;
155
-
156
- /** The ladder routing would climb, cheapest first, as short rungs. */
157
- function ladderOf(i: TreeInput, role: Role): string {
158
- const rc = i.profile.roles[role];
159
- if (!rc.enabled) return "off";
160
- const ladder = candidates(i.catalog, routingProfileOf(i.profile, role), role).map((c) => shortRung(c.rung));
161
- return ladder.length ? ladder.join(" → ") : "no usable rung";
162
- }
163
-
164
- /** Spec §9.1's Profiles tree for the draft: one row per line, in order. */
165
- export function buildRows(i: TreeInput): Row[] {
166
- const rows: Row[] = [];
167
- const labels = new Map<string, string>();
168
- const open = (key: string) => i.expandAll === true || i.expanded.has(key);
169
- const add = (
170
- r: Omit<Row, "search" | "dim" | "issue" | "expandable" | "expanded" | "selectable" | "check"> &
171
- Partial<Pick<Row, "dim" | "issue" | "expandable" | "selectable" | "check">>,
172
- ) => {
173
- const path = `${r.parent ? (labels.get(r.parent) ?? "") : ""} ${r.label}`;
174
- labels.set(r.key, path);
175
- const row: Row = {
176
- check: null,
177
- dim: false,
178
- issue: null,
179
- expandable: false,
180
- selectable: true,
181
- ...r,
182
- expanded: r.expandable === true && open(r.key),
183
- search: `${path} ${r.value}`.toLowerCase(),
184
- };
185
- rows.push(row);
186
- return row;
187
- };
188
- const section = (key: string, label: string) =>
189
- add({ key, parent: null, depth: 0, label, value: "", selectable: false, action: { type: "section" } });
190
-
191
- section("section:roles", "ROLES");
192
- for (const role of ROLES) {
193
- const rc = i.profile.roles[role];
194
- const roleKey = `role:${role}`;
195
- const roleRow = add({
196
- key: roleKey,
197
- parent: "section:roles",
198
- depth: 1,
199
- label: role,
200
- value: ladderOf(i, role),
201
- check: rc.enabled,
202
- expandable: true,
203
- issue: issueFor(
204
- i.validation,
205
- (p) =>
206
- p === `roles.${role}.enabled` || p === `roles.${role}.rungs` || p === `roles.${role}.defaultRung`,
207
- ),
208
- action: { type: "role", role },
209
- });
210
- if (!roleRow.expanded) continue;
211
- const dim = !rc.enabled;
212
- const weakest = rc.rungs.some((r) => i.enforcement(r, rc.access) === "advisory")
213
- ? "advisory"
214
- : "enforced";
215
- add({
216
- key: `access:${role}`,
217
- parent: roleKey,
218
- depth: 2,
219
- label: "access",
220
- value: `${rc.access} · ${rc.rungs.length ? weakest : "no rung"}`,
221
- dim,
222
- issue: issueFor(i.validation, (p) => p === `roles.${role}.access`),
223
- action: { type: "access", role },
224
- });
225
- add({
226
- key: `start:${role}`,
227
- parent: roleKey,
228
- depth: 2,
229
- label: "default rung",
230
- value: rc.defaultRung ? shortRung(rc.defaultRung) : "the cheapest that clears the bar",
231
- dim,
232
- action: { type: "start", role },
233
- });
234
- const entries = modelsFor(i.models, role, rc.rungs);
235
- const keys = [...new Set(entries.map((m) => m.key))].sort(
236
- (a, b) => GROUP_ORDER.indexOf(a) - GROUP_ORDER.indexOf(b) || a.localeCompare(b),
237
- );
238
- for (const key of keys) {
239
- const groupKey = `group:${role}:${key}`;
240
- const note = GROUP_NOTE[key];
241
- add({
242
- key: groupKey,
243
- parent: roleKey,
244
- depth: 2,
245
- label: note ? `${key} (${note})` : key,
246
- value: i.profile.billing[key] ?? "",
247
- selectable: false,
248
- dim: true,
249
- action: { type: "group" },
250
- });
251
- for (const m of entries.filter((e) => e.key === key)) {
252
- const modelKey = `model:${role}:${m.backend}:${m.model}`;
253
- const ticked = m.efforts.filter((e) => rc.rungs.includes(rungOf(m, e))).length;
254
- const usable = m.efforts.some((e) => m.scored.has(e) || i.staged[rungOf(m, e)]);
255
- const flags = [m.listed === false ? "not listed" : "", usable ? "" : "unscored"].filter(Boolean);
256
- const modelRow = add({
257
- key: modelKey,
258
- parent: groupKey,
259
- depth: 3,
260
- label: m.model,
261
- value: [`${ticked} of ${m.efforts.length}`, ...flags].join(" · "),
262
- expandable: true,
263
- dim: dim || !usable,
264
- action: { type: "model" },
265
- });
266
- if (!modelRow.expanded) continue;
267
- for (const effort of m.efforts) {
268
- const rung = rungOf(m, effort);
269
- const staged = i.staged[rung];
270
- const scored = m.scored.has(effort) || staged !== undefined;
271
- const value = staged
272
- ? `treated like ${staged} · unsaved`
273
- : !scored
274
- ? "unscored · enter: treat like"
275
- : m.inferred.has(effort)
276
- ? "inferred"
277
- : "";
278
- add({
279
- key: `rung:${role}:${rung}`,
280
- parent: modelKey,
281
- depth: 4,
282
- label: effort,
283
- value,
284
- check: rc.rungs.includes(rung),
285
- dim: dim || !scored,
286
- action: { type: "rung", role, rung, scored },
287
- });
288
- }
289
- }
290
- }
291
- }
292
-
293
- section("section:routing", "ROUTING");
294
- add({
295
- key: "objective",
296
- parent: "section:routing",
297
- depth: 1,
298
- label: "objective",
299
- value: i.profile.objective,
300
- action: { type: "objective" },
301
- });
302
- add({
303
- key: "jev",
304
- parent: "section:routing",
305
- depth: 1,
306
- label: "Jev",
307
- value: i.profile.jev.use,
308
- action: { type: "jev" },
309
- });
310
-
311
- section("section:harness", "HARNESS");
312
- for (const h of i.harnesses) {
313
- const isolated = i.profile.harness[h]?.isolated ?? false;
314
- add({
315
- key: `harness:${h}`,
316
- parent: "section:harness",
317
- depth: 1,
318
- label: h,
319
- value: isolated ? "isolated" : "your own config",
320
- check: isolated,
321
- action: { type: "isolated", harness: h },
322
- });
323
- }
324
-
325
- section("section:budget", "BUDGET");
326
- const b = i.profile.budget;
327
- for (const [path, label, v] of [
328
- ["budget.minutes", "minutes", b.minutes],
329
- ["budget.tokens", "tokens", b.tokens],
330
- ["budget.usd", "usd", b.usd],
331
- ] as const)
332
- add({
333
- key: path,
334
- parent: "section:budget",
335
- depth: 1,
336
- label,
337
- value: v === undefined ? "no cap" : String(v),
338
- action: { type: "number", path },
339
- });
340
-
341
- section("section:failover", "FAILOVER");
342
- const onLadders = ROLES.filter((r) => i.profile.roles[r].enabled).flatMap((r) => i.profile.roles[r].rungs);
343
- const fromRungs = [
344
- ...new Set([...onLadders.filter((r) => !r.startsWith("claude:")), ...Object.keys(i.profile.failover)]),
345
- ].sort();
346
- if (fromRungs.length === 0)
347
- add({
348
- key: "failover:none",
349
- parent: "section:failover",
350
- depth: 1,
351
- label: "no rung to stand in for",
352
- value: "",
353
- selectable: false,
354
- dim: true,
355
- action: { type: "section" },
356
- });
357
- for (const rung of fromRungs) {
358
- const to = i.profile.failover[rung];
359
- let mark = "";
360
- if (to) {
361
- try {
362
- const note = inferredScores(i.catalog, rungInfo(i.catalog, to)).note;
363
- mark = note ? ` (${note})` : "";
364
- } catch {
365
- mark = "";
366
- }
367
- }
368
- add({
369
- key: `failover:${rung}`,
370
- parent: "section:failover",
371
- depth: 1,
372
- label: shortRung(rung),
373
- value: to ? `→ ${shortRung(to)}${mark}` : "none",
374
- issue: issueFor(i.validation, (p) => p === `failover.${rung}`),
375
- action: { type: "failover", rung },
376
- });
377
- }
378
-
379
- section("section:timeouts", "TIMEOUTS");
380
- add({
381
- key: "timeouts.idleMin",
382
- parent: "section:timeouts",
383
- depth: 1,
384
- label: "idle minutes",
385
- value: String(i.profile.timeouts.idleMin),
386
- action: { type: "number", path: "timeouts.idleMin" },
387
- });
388
- add({
389
- key: "timeouts.wallMin",
390
- parent: "section:timeouts",
391
- depth: 1,
392
- label: "wall minutes",
393
- value: String(i.profile.timeouts.wallMin),
394
- action: { type: "number", path: "timeouts.wallMin" },
395
- });
396
-
397
- section("section:notify", "NOTIFY");
398
- for (const moment of NOTIFY)
399
- add({
400
- key: `notify:${moment}`,
401
- parent: "section:notify",
402
- depth: 1,
403
- label: moment,
404
- value: "",
405
- check: i.profile.notify.includes(moment),
406
- action: { type: "notify", moment },
407
- });
408
- return rows;
409
- }
410
-
411
- /** Rows matching every word of `text`, with their parents so each keeps its place in the tree. */
412
- export function filterRows(rows: Row[], text: string): Row[] {
413
- const words = text.toLowerCase().split(/\s+/).filter(Boolean);
414
- if (words.length === 0) return rows;
415
- const keep = new Set<string>();
416
- const byKey = new Map(rows.map((r) => [r.key, r]));
417
- for (const r of rows)
418
- if (r.selectable && words.every((w) => r.search.includes(w)))
419
- for (let k: string | null = r.key; k && !keep.has(k); k = byKey.get(k)?.parent ?? null) keep.add(k);
420
- return rows.filter((r) => keep.has(r.key));
421
- }
422
-
423
- /** The first row the filter matches itself (not only as a parent of a match): where the cursor goes. */
424
- export function firstMatch(rows: Row[], text: string): Row | null {
425
- const words = text.toLowerCase().split(/\s+/).filter(Boolean);
426
- return (
427
- rows.find((r) => r.selectable && words.length > 0 && words.every((w) => r.search.includes(w))) ?? null
428
- );
429
- }