catherd-cli 1.5.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 (275) hide show
  1. package/README.md +164 -208
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -247
  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 -421
  11. package/plugin/skills/catherd-setup/SKILL.md +20 -19
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +1 -2
  14. package/src/adapters/cursor/home.ts +1 -1
  15. package/src/adapters/cursor/index.ts +4 -4
  16. package/src/cli.ts +22 -27
  17. package/src/domain/bars.ts +33 -107
  18. package/src/domain/blocked.ts +72 -0
  19. package/src/domain/branch.ts +51 -0
  20. package/src/domain/brief.ts +21 -6
  21. package/src/domain/catalog.ts +69 -324
  22. package/src/domain/cost.ts +15 -30
  23. package/src/domain/errors.ts +2 -2
  24. package/src/domain/fail-lines.ts +26 -0
  25. package/src/domain/failover.ts +37 -87
  26. package/src/domain/findings.ts +47 -0
  27. package/src/domain/gate-brief.ts +1 -0
  28. package/src/domain/ids.ts +13 -2
  29. package/src/domain/jev.ts +47 -2
  30. package/src/domain/judge.ts +124 -0
  31. package/src/domain/lane.ts +22 -3
  32. package/src/domain/milestone-memory.ts +53 -0
  33. package/src/domain/notice.ts +25 -8
  34. package/src/domain/notify.ts +32 -0
  35. package/src/domain/plan-doc.ts +198 -0
  36. package/src/domain/plan-lint.ts +141 -0
  37. package/src/domain/profile-rules.ts +101 -160
  38. package/src/domain/profile.ts +116 -63
  39. package/src/domain/record.ts +53 -2
  40. package/src/domain/role-prompts.ts +131 -45
  41. package/src/domain/role-scope.ts +4 -34
  42. package/src/domain/role-tools.ts +2 -2
  43. package/src/domain/roles.ts +2 -0
  44. package/src/domain/route.ts +17 -0
  45. package/src/domain/rules.ts +33 -0
  46. package/src/domain/run-events.ts +298 -0
  47. package/src/domain/run-machine.ts +668 -0
  48. package/src/domain/run-state.ts +1310 -0
  49. package/src/domain/scorecard.ts +197 -0
  50. package/src/domain/select.ts +151 -146
  51. package/src/domain/slopalytics.ts +72 -0
  52. package/src/entry/answer-command.ts +39 -0
  53. package/src/entry/catalog-command.ts +25 -174
  54. package/src/entry/check-bin.ts +17 -0
  55. package/src/entry/cli-kit.ts +1 -1
  56. package/src/entry/daemon-bin.ts +22 -0
  57. package/src/entry/glyphs.ts +15 -2
  58. package/src/entry/init-command.ts +18 -68
  59. package/src/entry/knowledge-command.ts +45 -4
  60. package/src/entry/lock-command.ts +6 -7
  61. package/src/entry/mcp/role-server.ts +18 -0
  62. package/src/entry/mcp/server.ts +13 -40
  63. package/src/entry/mcp/session-tools.ts +149 -0
  64. package/src/entry/panel/openapi.ts +189 -0
  65. package/src/entry/panel/server.ts +410 -0
  66. package/src/entry/panel-command.ts +68 -0
  67. package/src/entry/profile-command.ts +10 -14
  68. package/src/entry/role-cli-command.ts +47 -3
  69. package/src/entry/runs-command.ts +3 -19
  70. package/src/entry/scorecard-command.ts +187 -0
  71. package/src/entry/supervise-bin.ts +1 -1
  72. package/src/infra/env.ts +0 -2
  73. package/src/infra/git.ts +64 -0
  74. package/src/infra/heavy-lock.ts +45 -7
  75. package/src/infra/launch.ts +63 -7
  76. package/src/infra/memory.ts +45 -0
  77. package/src/infra/ntfy.ts +40 -0
  78. package/src/infra/open-url.ts +42 -0
  79. package/src/infra/paths.ts +2 -2
  80. package/src/infra/slopalytics.ts +30 -0
  81. package/src/infra/store.ts +6 -1
  82. package/src/services/admission.ts +24 -9
  83. package/src/services/backends.ts +4 -6
  84. package/src/services/capture.ts +1 -22
  85. package/src/services/catalog-service.ts +155 -262
  86. package/src/services/credentials.ts +4 -20
  87. package/src/services/daemon.ts +181 -0
  88. package/src/services/dispatch-service.ts +5 -1
  89. package/src/services/doctor-backends.ts +1 -1
  90. package/src/services/doctor-checks.ts +47 -0
  91. package/src/services/doctor.ts +4 -9
  92. package/src/services/failover-rungs.ts +109 -0
  93. package/src/services/finalize.ts +12 -0
  94. package/src/services/gate-env.ts +1 -2
  95. package/src/services/gate-service.ts +21 -6
  96. package/src/services/jev-service.ts +2 -4
  97. package/src/services/lane-service.ts +34 -3
  98. package/src/services/milestone-check.ts +433 -0
  99. package/src/services/milestone-tree.ts +170 -0
  100. package/src/services/milestones.ts +35 -8
  101. package/src/services/notifier.ts +69 -2
  102. package/src/services/notify-service.ts +26 -0
  103. package/src/services/panel-setup.ts +116 -0
  104. package/src/services/plan-input.ts +151 -0
  105. package/src/services/ports.ts +30 -4
  106. package/src/services/preflight.ts +82 -8
  107. package/src/services/profile-service.ts +30 -3
  108. package/src/services/profile-store.ts +8 -3
  109. package/src/services/protocol.ts +4 -0
  110. package/src/services/provenance.ts +31 -90
  111. package/src/services/questions.ts +9 -0
  112. package/src/services/reconcile.ts +11 -0
  113. package/src/services/routing-service.ts +26 -15
  114. package/src/services/run-briefs.ts +477 -0
  115. package/src/services/run-client.ts +217 -0
  116. package/src/services/run-effects.ts +837 -0
  117. package/src/services/run-events-store.ts +174 -0
  118. package/src/services/run-evidence.ts +1 -1
  119. package/src/services/run-read.ts +221 -0
  120. package/src/services/run-report.ts +176 -0
  121. package/src/services/run-service.ts +55 -6
  122. package/src/services/run-store.ts +54 -7
  123. package/src/services/scorecard-metrics.ts +86 -0
  124. package/src/services/scorecard.ts +382 -0
  125. package/src/services/summary.ts +27 -3
  126. package/src/services/tree-path.ts +27 -0
  127. package/src/services/workspace-admission.ts +5 -13
  128. package/src/services/workspace-service.ts +5 -0
  129. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  130. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  131. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  132. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  133. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  134. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  135. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  136. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  137. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  138. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  139. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  140. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  141. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  142. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  143. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  144. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  145. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  146. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  147. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  148. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  149. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  150. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  151. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  152. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  153. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  154. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  155. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  156. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  157. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  158. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  159. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  160. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  161. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  162. package/web/dist/assets/index-C5IbylvM.css +2 -0
  163. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  164. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  165. package/web/dist/assets/label-DFSYBc62.js +1 -0
  166. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  167. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  168. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  169. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  170. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  171. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  172. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  173. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  174. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  175. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  176. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  177. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  178. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  179. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  180. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  181. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  182. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  183. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  184. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  185. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  186. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  187. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  188. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  189. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  190. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  191. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  192. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  193. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  194. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  195. package/web/dist/assets/time--9kSQbbp.js +1 -0
  196. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  197. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  198. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  199. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  200. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  201. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  202. package/web/dist/index.html +15 -0
  203. package/catalog/ATTRIBUTION.md +0 -44
  204. package/catalog/scores.json +0 -3202
  205. package/catalog/sources.json +0 -97
  206. package/src/adapters/antigravity/events.ts +0 -131
  207. package/src/adapters/antigravity/home.ts +0 -46
  208. package/src/adapters/antigravity/index.ts +0 -335
  209. package/src/adapters/antigravity/models.ts +0 -37
  210. package/src/adapters/grok/events.ts +0 -112
  211. package/src/adapters/grok/home.ts +0 -187
  212. package/src/adapters/grok/index.ts +0 -358
  213. package/src/adapters/grok/models.ts +0 -48
  214. package/src/domain/calibration.ts +0 -132
  215. package/src/domain/sources.ts +0 -144
  216. package/src/entry/mcp/dispatch-tools.ts +0 -52
  217. package/src/entry/mcp/lane-tools.ts +0 -131
  218. package/src/entry/mcp/protocol-tools.ts +0 -63
  219. package/src/entry/mcp/push-tools.ts +0 -16
  220. package/src/entry/mcp/run-tools.ts +0 -164
  221. package/src/entry/mcp/setup-tools.ts +0 -109
  222. package/src/entry/mcp/workspace-tools.ts +0 -142
  223. package/src/entry/tui/commands.ts +0 -563
  224. package/src/entry/tui/effects.ts +0 -385
  225. package/src/entry/tui/fixtures.ts +0 -637
  226. package/src/entry/tui/profile-edits.ts +0 -146
  227. package/src/entry/tui/profile-tree.ts +0 -429
  228. package/src/entry/tui/providers/app.tsx +0 -162
  229. package/src/entry/tui/providers/boundary.tsx +0 -57
  230. package/src/entry/tui/providers/data.tsx +0 -160
  231. package/src/entry/tui/providers/keymap.tsx +0 -166
  232. package/src/entry/tui/providers/root.tsx +0 -45
  233. package/src/entry/tui/providers/theme.tsx +0 -17
  234. package/src/entry/tui/providers/toast.tsx +0 -77
  235. package/src/entry/tui/run.tsx +0 -114
  236. package/src/entry/tui/state.ts +0 -400
  237. package/src/entry/tui/stories.ts +0 -114
  238. package/src/entry/tui/text.ts +0 -130
  239. package/src/entry/tui/theme.ts +0 -140
  240. package/src/entry/tui/views/app.tsx +0 -208
  241. package/src/entry/tui/views/dialogs.tsx +0 -41
  242. package/src/entry/tui/views/profile-actions.ts +0 -331
  243. package/src/entry/tui/views/profiles.tsx +0 -520
  244. package/src/entry/tui/views/runs.tsx +0 -619
  245. package/src/entry/tui/views/save-dialog.tsx +0 -344
  246. package/src/entry/tui/views/status.tsx +0 -213
  247. package/src/entry/tui/widgets/chrome.tsx +0 -114
  248. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  249. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  250. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  251. package/src/entry/tui/widgets/dialog.tsx +0 -68
  252. package/src/entry/tui/widgets/line.tsx +0 -92
  253. package/src/entry/tui/widgets/list.tsx +0 -197
  254. package/src/entry/tui/widgets/spinner.tsx +0 -49
  255. package/src/entry/tui/widgets/toast.tsx +0 -62
  256. package/src/infra/sources/arena.ts +0 -95
  257. package/src/infra/sources/artificial-analysis.ts +0 -119
  258. package/src/infra/sources/cache.ts +0 -126
  259. package/src/infra/sources/csv.ts +0 -38
  260. package/src/infra/sources/epoch.ts +0 -82
  261. package/src/infra/sources/http.ts +0 -75
  262. package/src/infra/sources/litellm.ts +0 -58
  263. package/src/infra/sources/models-dev.ts +0 -59
  264. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  265. package/src/infra/sources/openrouter-models.ts +0 -60
  266. package/src/infra/sources/rows.ts +0 -38
  267. package/src/infra/sources/vectara.ts +0 -69
  268. package/src/infra/sources/zip.ts +0 -48
  269. package/src/services/catalog-refresh.ts +0 -201
  270. package/src/services/doctor-sources.ts +0 -76
  271. package/src/services/runs-page.ts +0 -290
  272. package/src/services/source-derive.ts +0 -397
  273. package/src/services/source-sync.ts +0 -312
  274. package/src/services/standins.ts +0 -238
  275. package/src/services/treat-likes.ts +0 -113
@@ -1,4 +1,4 @@
1
- import { replyContract } from "./role-prompts.ts";
1
+ import { replyContract, roleSections } from "./role-prompts.ts";
2
2
  import { ROLE_MCP_SERVER, roleMcpTools } from "./role-tools.ts";
3
3
  import type { Role } from "./roles.ts";
4
4
 
@@ -13,6 +13,10 @@ export interface BriefContext {
13
13
  scratch: string | null;
14
14
  /** whether the role's harness lists the catherd_role tools */
15
15
  roleServer: boolean;
16
+ /** the catherd version whose `lock` a worker's text names */
17
+ version: string;
18
+ /** the plan's preamble, which applies to every task (plan 32); null when the run has none for this role */
19
+ context: string | null;
16
20
  }
17
21
 
18
22
  /** The CLI form of each role server tool, which any role can run from its shell. */
@@ -28,6 +32,8 @@ function cliForm(tool: string, run: string): string {
28
32
  return `catherd gate check ${run} --milestone <M> --item <item> --command <command> --paths <path,…>`;
29
33
  case "gate_pass":
30
34
  return `catherd gate pass ${run} --item <item> --command <command> --paths <path,…> --evidence <evidence>`;
35
+ case "gate_run":
36
+ return `catherd gate run ${run} --milestone <M> --command <command> [--at <base checkout>]`;
31
37
  default:
32
38
  return tool;
33
39
  }
@@ -66,14 +72,21 @@ export function roleNotes(c: BriefContext): string {
66
72
  return lines.join("\n");
67
73
  }
68
74
 
75
+ const TASK_OPEN = "<task>\n";
76
+ const TASK_CLOSE = "\n</task>";
77
+
69
78
  /**
70
- * The brief admission writes: the orchestrator's text, the lane file, the role's notes, then its reply contract,
71
- * each once. A failover stand-in that reruns an earlier brief gets the lane file as it stands now and its own notes,
72
- * never two copies.
79
+ * The brief admission writes: the role's sections around its task (the daemon's text, the lane file and the
80
+ * role's notes), each once. A failover stand-in that reruns an earlier brief gets the lane file as it stands now and
81
+ * its own notes, never two copies.
73
82
  */
74
83
  export function composeBrief(brief: string, c: BriefContext): string {
75
84
  const contract = replyContract(c.role);
76
85
  let body = brief.trimEnd();
86
+ const open = body.indexOf(TASK_OPEN);
87
+ const close = body.lastIndexOf(TASK_CLOSE);
88
+ if (open >= 0 && close > open) body = body.slice(open + TASK_OPEN.length, close).trim();
89
+ // a 1.5-era brief rerun ends with its contract
77
90
  if (body.endsWith(contract)) body = body.slice(0, -contract.length).trimEnd();
78
91
  if (c.lane) {
79
92
  const open = LANE_OPEN(c.lane.id);
@@ -86,6 +99,8 @@ export function composeBrief(brief: string, c: BriefContext): string {
86
99
  }
87
100
  const notesStart = body.lastIndexOf("\n\ncatherd: you are ");
88
101
  if (notesStart >= 0) body = body.slice(0, notesStart).trimEnd();
89
- const parts = [body, ...(c.lane ? [laneBlock(c.lane.id, c.lane.text)] : []), roleNotes(c), contract];
90
- return `${parts.filter(Boolean).join("\n\n")}\n`;
102
+ const task = [body, ...(c.lane ? [laneBlock(c.lane.id, c.lane.text)] : []), roleNotes(c)]
103
+ .filter(Boolean)
104
+ .join("\n\n");
105
+ return `${roleSections(c.role, c.version, { context: c.context, task })}\n`;
91
106
  }
@@ -1,59 +1,17 @@
1
1
  import { z } from "zod";
2
2
  import { type Rung, parseRung } from "./ids.ts";
3
- import { DIFFICULTIES, type Difficulty, KINDS, type Kind } from "./lane.ts";
4
3
  import type { Role } from "./roles.ts";
5
-
6
- /**
7
- * Spec §5.2 and 1.2 §5.1: the scored dimensions, each in its anchor's unit. `agentic`, `steer` and `frontend`
8
- * (1.2) carry no shipped bar yet.
9
- */
10
- export const DIMS = ["repo_code", "terminal", "honesty", "agentic", "steer", "frontend"] as const;
11
- export type Dim = (typeof DIMS)[number];
12
-
13
- /**
14
- * Spec 1.2 §4.3: how a value was obtained, best first. The 1.0 levels (`verified`, `secondary`,
15
- * `inferred`) keep their names, so an older override file stays valid.
16
- */
17
- export const CONFIDENCE = [
18
- "verified",
19
- "measured",
20
- "calibrated",
21
- "adjacent",
22
- "secondary",
23
- "inferred",
24
- ] as const;
25
- export type Confidence = (typeof CONFIDENCE)[number];
26
- export const RANK = Object.fromEntries(CONFIDENCE.map((c, i) => [c, i])) as Record<Confidence, number>;
27
- /** Spec 1.2 §4.3: a value older than this many days counts one level lower. */
28
- export const STALE_DAYS = 90;
29
- const DAY_MS = 86_400_000;
4
+ import { type IndexFile, slopKey } from "./slopalytics.ts";
30
5
 
31
6
  /**
32
7
  * Spec §7.1 `billing` keys: a rung's backend, except that opencode's Go models (`opencode-go/…`) are billed
33
8
  * apart from its Zen models (`opencode/…`).
34
9
  */
35
- export const BILLING_KEYS = [
36
- "codex",
37
- "claude",
38
- "claude-code",
39
- "opencode-go",
40
- "opencode",
41
- "cursor",
42
- "grok",
43
- "antigravity",
44
- ] as const;
10
+ export const BILLING_KEYS = ["codex", "claude", "claude-code", "opencode-go", "opencode", "cursor"] as const;
45
11
  export type BillingKey = (typeof BILLING_KEYS)[number];
46
12
 
47
13
  /** The keys a family's `on` map uses; the native `claude` pseudo-backend runs claude-code's model ids. */
48
- const MODEL_KEYS = [
49
- "codex",
50
- "claude-code",
51
- "opencode-go",
52
- "opencode",
53
- "cursor",
54
- "grok",
55
- "antigravity",
56
- ] as const;
14
+ const MODEL_KEYS = ["codex", "claude-code", "opencode-go", "opencode", "cursor"] as const;
57
15
  type ModelKey = (typeof MODEL_KEYS)[number];
58
16
 
59
17
  export function billingKeyOf(r: Rung): BillingKey {
@@ -63,38 +21,25 @@ export function billingKeyOf(r: Rung): BillingKey {
63
21
 
64
22
  const modelKeyOf = (k: BillingKey): ModelKey => (k === "claude" ? "claude-code" : k);
65
23
 
66
- const BackendModelSchema = z.object({
67
- id: z.string().min(1),
68
- efforts: z.array(z.string().min(1)),
69
- context: z.number().int().positive(),
70
- });
24
+ const BackendModelSchema = z.object({ id: z.string().min(1) });
71
25
 
72
- /** `vendor` and `status` (current or legacy) stay in the data as documentation; nothing reads them. */
26
+ /**
27
+ * What catherd knows of a model that Slopalytics does not: its capabilities, how a plan weighs it and the
28
+ * model id each backend runs it under. `vendor` and `status` stay in the data as documentation.
29
+ */
73
30
  const FamilySchema = z.looseObject({
74
31
  id: z.string().min(1),
75
32
  name: z.string(),
76
33
  capabilities: z.object({ toolUse: z.boolean(), imageIn: z.boolean(), reasoning: z.boolean() }),
77
- /** API list price, dollars per million tokens */
78
- price: z.object({ input: z.number(), cached: z.number(), output: z.number() }),
79
34
  /** chatgpt-plan: messages per 5 hours relative to GPT-6 Luna (spec §5.3) */
80
35
  planWeight: z.number().positive().optional(),
81
36
  /** claude-plan: billed as usage credits, not from the plan's limits */
82
37
  meteredOnPlan: z.boolean().default(false),
83
38
  on: z.partialRecord(z.enum(MODEL_KEYS), BackendModelSchema),
84
39
  notes: z.record(z.string(), z.string()).default({}),
85
- /** the day the vendor released it (models.dev), a stand-in feature (spec 1.2 §6.3) */
86
- releaseDate: z.iso.date().optional(),
87
- /**
88
- * spec 1.5 plan 24: the family this one succeeds in the same line (GPT-6.1 Sol after GPT-6 Sol): until a
89
- * value of its own arrives, a rung takes at least its predecessor's value at the same effort
90
- */
91
- predecessor: z.string().min(1).optional(),
92
- /** a sync's speed facts (spec 1.2 §4.1), `<source>.<field>` → value; they never carry a bar */
93
- speed: z.record(z.string(), z.number()).optional(),
94
40
  });
95
41
  export type Family = z.infer<typeof FamilySchema>;
96
42
 
97
- /** `sources` (where the data came from) stays in the file as documentation; nothing reads it. */
98
43
  export const ModelsFileSchema = z.looseObject({
99
44
  schema: z.literal(1),
100
45
  version: z.string(),
@@ -108,242 +53,62 @@ export const ModelsFileSchema = z.looseObject({
108
53
  });
109
54
  export type ModelsFile = z.infer<typeof ModelsFileSchema>;
110
55
 
111
- /** A canonical rung: `<canonical model id>#<effort>`, the key scores and treat-likes use. */
112
- export const CanonicalRung = z.string().regex(/^[^:#\s]+#[^#\s]+$/, "a canonical rung is model#effort");
113
-
114
- /** Spec 1.2 §4.2: `anchor = a·x + b`, fitted on `n` rungs both sources cover, with its R². */
115
- export const FitSchema = z.object({
116
- source: z.string(),
117
- field: z.string(),
118
- a: z.number(),
119
- b: z.number(),
120
- r2: z.number(),
121
- n: z.number().int(),
122
- });
123
- export type Fit = z.infer<typeof FitSchema>;
124
-
125
- export const ScoreSchema = z.object({
126
- rung: CanonicalRung,
127
- dim: z.enum(DIMS),
128
- value: z.number(),
129
- benchmark: z.string(),
130
- version: z.string(),
131
- url: z.url(),
132
- date: z.iso.date(),
133
- confidence: z.enum(CONFIDENCE),
134
- note: z.string().optional(),
135
- /** spec 1.2 §4.3: the source a synced value came from (absent: the shipped file or the user's override) */
136
- source: z.string().optional(),
137
- /** a calibrated value: the fit that mapped it onto the anchor */
138
- fit: FitSchema.optional(),
139
- /** the source named no effort, so the family's default effort was assumed (spec 1.2 §3.4) */
140
- effortAssumed: z.boolean().optional(),
141
- });
142
- export type Score = z.infer<typeof ScoreSchema>;
143
-
144
- const BarSchema = z.partialRecord(z.enum(DIMS), z.number());
145
- type Bars = Record<Kind, Record<Difficulty, Partial<Record<Dim, number>>>>;
146
- const BarsSchema = z.record(z.enum(KINDS), z.record(z.enum(DIFFICULTIES), BarSchema));
147
-
148
- /** Spec 1.2 §5.2: per dimension and difficulty, where the default threshold came from. */
149
- const BarsWhySchema = z.partialRecord(z.enum(DIMS), z.partialRecord(z.enum(DIFFICULTIES), z.string()));
150
-
151
- /**
152
- * `catalog/scores.json`: the hand-typed and keyless values (spec 1.2 §7), the shipped treat-likes, and the
153
- * default bars with the `barsWhy` line of each threshold (spec 1.2 §5.2).
154
- */
155
- export const ScoresFileSchema = z.looseObject({
156
- schema: z.literal(1),
157
- version: z.string(),
158
- benchmarks: z.record(z.enum(DIMS), z.object({ benchmark: z.string(), version: z.string() })),
159
- scores: z.array(ScoreSchema),
160
- treatLike: z.record(CanonicalRung, z.object({ like: CanonicalRung, note: z.string() })),
161
- bars: BarsSchema,
162
- barsWhy: BarsWhySchema.default({}),
163
- });
164
- export type ScoresFile = z.infer<typeof ScoresFileSchema>;
165
-
166
- /** An override's bar: per dimension a threshold, or `null` to remove the default's (spec 1.2 §5.2). */
167
- const OverrideBarSchema = z.partialRecord(z.enum(DIMS), z.number().nullable());
168
-
169
- /**
170
- * `<config>/catalog.override.json` (spec §3.5): the user's treat-likes, scores and bars. Its bars override
171
- * the default bars per dimension (spec 1.2 §5.2): a number sets that threshold, `null` removes it, and a
172
- * dimension it does not name keeps the default's.
173
- */
174
- export const OverrideSchema = z.looseObject({
175
- schema: z.literal(1).default(1),
176
- treatLike: z.record(CanonicalRung, CanonicalRung).default({}),
177
- scores: z.array(ScoreSchema).default([]),
178
- bars: z.partialRecord(z.enum(KINDS), z.partialRecord(z.enum(DIFFICULTIES), OverrideBarSchema)).default({}),
179
- });
180
- export type Override = z.infer<typeof OverrideSchema>;
181
-
182
- /** Spec 1.2 §6.1: the rung whose value a rung without one uses on a dimension, as `inferred`. */
183
- export interface InferredStandIn {
184
- like: string;
185
- /** spec 1.2 §6.3's distance between the two */
186
- distance: number;
187
- /** the features that distance rests on */
188
- features: string[];
189
- }
190
-
191
- interface Listed {
56
+ export interface Listed {
192
57
  id: string;
193
58
  efforts: string[];
194
59
  context: number | null;
195
60
  imageIn: boolean;
196
61
  }
197
62
 
198
- /** Everything routing reads, merged from the three layers plus the user's override and own timings. */
63
+ /** One model at one effort on the Slopalytics index: the one number routing ranks on, and what it costs and does. */
64
+ export interface Pair {
65
+ intelligence: number;
66
+ price: { input: number; output: number } | null;
67
+ speed: number | null;
68
+ context: number | null;
69
+ }
70
+
71
+ /** Everything routing reads: the shipped families, the Slopalytics index, and each backend's listing. */
199
72
  export interface Catalog {
200
73
  families: Family[];
201
74
  backends: ModelsFile["backends"];
202
- /** canonical rung → its best-sourced value per dimension (the override's win) */
203
- scores: Record<string, Partial<Record<Dim, Score>>>;
204
- treatLike: Record<string, { like: string; source: "shipped" | "user" }>;
205
- bars: Bars;
206
- /** where each default threshold came from (spec 1.2 §5.2), by dimension and difficulty */
207
- barsWhy: ScoresFile["barsWhy"];
75
+ /** Slopalytics family (`slopKey`) → effort → pair */
76
+ index: Record<string, Record<string, Pair>>;
77
+ /** when the proxy fetched the index; null with no index */
78
+ indexedAt: string | null;
79
+ /** ascending intelligences of every indexed pair of the shipped families: what the bars are percentiles of */
80
+ pool: number[];
208
81
  /** per backend id, the last `listModels()`; absent when never listed */
209
82
  listed: Record<string, { fetchedAt: string; models: Listed[] }>;
210
- /** `<canonical rung>|<kind or *>` → median seconds, only with ≥ 5 samples (spec §5.2) */
211
- secs: Record<string, number>;
212
- /** canonical rung → the Artificial Analysis stand-in features a sync read for it (spec 1.2 §6.3) */
213
- features: Record<string, Record<string, number>>;
214
- /**
215
- * canonical rung → per dimension it has no value for (of its own or through a treat-like), the stand-in
216
- * whose value it uses as `inferred` (spec 1.2 §6.1); filled by the stand-in ranking (services/standins.ts)
217
- */
218
- inferred: Record<string, Partial<Record<Dim, InferredStandIn>>>;
219
- /** the thresholds the user's override set or removed, as `<kind>/<difficulty>/<dim>` (route says "your override") */
220
- userBars?: string[];
221
83
  }
222
84
 
223
- /**
224
- * Spec 1.2 §3.5: what a sync learned about a family. `price`, `capabilities` and the opencode backends'
225
- * `efforts` and `context` come from models.dev; `speed` holds facts that never carry a bar (spec 1.2 §4.1),
226
- * as `<source>.<field>` → value.
227
- */
228
- export const FamilyFactsSchema = z.object({
229
- price: z.object({ input: z.number(), cached: z.number(), output: z.number() }).optional(),
230
- capabilities: z.object({ toolUse: z.boolean(), imageIn: z.boolean(), reasoning: z.boolean() }).optional(),
231
- on: z
232
- .partialRecord(
233
- z.enum(MODEL_KEYS),
234
- z.object({ efforts: z.array(z.string()).optional(), context: z.number().int().positive().optional() }),
235
- )
236
- .default({}),
237
- releaseDate: z.string().optional(),
238
- speed: z.record(z.string(), z.number()).default({}),
239
- });
240
- export type FamilyFacts = z.infer<typeof FamilyFactsSchema>;
241
-
242
- /**
243
- * Spec 1.2 §3.5: the families with a sync's facts laid over them. The shipped file stays the floor: a fact
244
- * the sync lacks keeps the shipped one, and a backend keeps every effort it ships with.
245
- */
246
- export function applyFacts(families: Family[], facts: Record<string, FamilyFacts>): Family[] {
247
- return families.map((f) => {
248
- const x = facts[f.id];
249
- if (!x) return f;
250
- const on = { ...f.on };
251
- for (const key of MODEL_KEYS) {
252
- const cur = on[key];
253
- const got = x.on[key];
254
- if (!cur || !got) continue;
255
- on[key] = {
256
- ...cur,
257
- efforts: [...cur.efforts, ...(got.efforts ?? []).filter((e) => !cur.efforts.includes(e))],
258
- context: got.context ?? cur.context,
259
- };
260
- }
261
- // a sync may add a capability, never remove one the shipped file gives (the floor)
262
- const got = x.capabilities;
263
- const capabilities = got
264
- ? {
265
- toolUse: f.capabilities.toolUse || got.toolUse,
266
- imageIn: f.capabilities.imageIn || got.imageIn,
267
- reasoning: f.capabilities.reasoning || got.reasoning,
268
- }
269
- : f.capabilities;
270
- return {
271
- ...f,
272
- price: x.price ?? f.price,
273
- capabilities,
274
- on,
275
- ...(x.releaseDate || f.releaseDate ? { releaseDate: x.releaseDate ?? f.releaseDate } : {}),
276
- ...(Object.keys(x.speed).length ? { speed: x.speed } : {}),
277
- };
278
- });
279
- }
280
-
281
- /** Spec 1.2 §4.3: the level a value counts at: its own, one lower once it is older than STALE_DAYS. */
282
- export function effectiveRank(s: Score, now: number): number {
283
- const stale = now - Date.parse(s.date) > STALE_DAYS * DAY_MS;
284
- return Math.min(RANK[s.confidence] + (stale ? 1 : 0), CONFIDENCE.length - 1);
285
- }
286
-
287
- /** Spec 1.2 §4.3: `a` beats `b` at a better level, or at the same level with a newer date. */
288
- export function outranks(a: Score, b: Score, now: number): boolean {
289
- const ra = effectiveRank(a, now);
290
- const rb = effectiveRank(b, now);
291
- return ra < rb || (ra === rb && a.date > b.date);
292
- }
293
-
294
- /**
295
- * The catalog routing reads: the shipped files, the synced values (spec 1.2 §3), each backend's listing and
296
- * the user's override, which always wins. Between shipped and synced values the better level wins, then the
297
- * newer date (spec 1.2 §4.3); `now` dates the 90-day drop.
298
- */
299
85
  export function buildCatalog(o: {
300
86
  models: ModelsFile;
301
- scores: ScoresFile;
302
- synced?: Score[];
303
- facts?: Record<string, FamilyFacts>;
304
- override?: Override;
87
+ index: IndexFile | null;
305
88
  listed?: Catalog["listed"];
306
- secs?: Catalog["secs"];
307
- features?: Catalog["features"];
308
- now?: number;
309
89
  }): Catalog {
310
- const now = o.now ?? Date.now();
311
- const scores: Catalog["scores"] = {};
312
- const put = (s: Score, force: boolean) => {
313
- const cur = (scores[s.rung] ??= {});
314
- const had = cur[s.dim];
315
- if (force || !had || outranks(s, had, now)) cur[s.dim] = s;
316
- };
317
- for (const s of o.scores.scores) put(s, false);
318
- for (const s of o.synced ?? []) put(s, false);
319
- // the user's values say so whatever source they were copied with, for route's provenance (spec 1.2 §5.3)
320
- for (const s of o.override?.scores ?? []) put({ ...s, source: "override" }, true);
321
- const treatLike: Catalog["treatLike"] = {};
322
- for (const [rung, t] of Object.entries(o.scores.treatLike))
323
- treatLike[rung] = { like: t.like, source: "shipped" };
324
- for (const [rung, like] of Object.entries(o.override?.treatLike ?? {}))
325
- treatLike[rung] = { like, source: "user" };
326
- const bars = structuredClone(o.scores.bars) as Bars;
327
- const userBars: string[] = [];
328
- for (const kind of KINDS)
329
- for (const d of DIFFICULTIES)
330
- for (const [dim, min] of Object.entries(o.override?.bars[kind]?.[d] ?? {}) as [Dim, number | null][]) {
331
- userBars.push(`${kind}/${d}/${dim}`);
332
- if (min === null) delete bars[kind][d][dim];
333
- else bars[kind][d][dim] = min;
334
- }
90
+ const index: Catalog["index"] = {};
91
+ for (const r of o.index?.models ?? [])
92
+ (index[slopKey(r.family)] ??= {})[r.effort] = {
93
+ intelligence: r.intelligence,
94
+ price:
95
+ r.inputPrice !== null && r.outputPrice !== null
96
+ ? { input: r.inputPrice, output: r.outputPrice }
97
+ : null,
98
+ speed: r.speed,
99
+ context: r.context,
100
+ };
101
+ const shippedKeys = new Set(o.models.families.map((f) => slopKey(f.id)));
102
+ const pool = [...shippedKeys]
103
+ .flatMap((k) => Object.values(index[k] ?? {}).map((p) => p.intelligence))
104
+ .sort((a, b) => a - b);
335
105
  return {
336
- families: o.facts ? applyFacts(o.models.families, o.facts) : o.models.families,
106
+ families: o.models.families,
337
107
  backends: o.models.backends,
338
- scores,
339
- treatLike,
340
- bars,
341
- barsWhy: o.scores.barsWhy,
108
+ index,
109
+ indexedAt: o.index?.fetchedAt ?? null,
110
+ pool,
342
111
  listed: o.listed ?? {},
343
- secs: o.secs ?? {},
344
- features: o.features ?? {},
345
- inferred: {},
346
- userBars,
347
112
  };
348
113
  }
349
114
 
@@ -353,12 +118,16 @@ export interface RungInfo {
353
118
  parsed: Rung;
354
119
  key: BillingKey;
355
120
  family: Family | null;
356
- /** `<family id or backend model id>#<effort>`: the key scores, treat-likes and timings use */
121
+ /** `<family id or backend model id>#<effort>`: the key run evidence uses */
357
122
  canonical: string;
123
+ /** the model's Slopalytics family key */
124
+ slop: string;
358
125
  efforts: string[];
359
126
  context: number | null;
360
127
  /** true when the backend's last listing has it, false when that listing lacks it, null with no listing */
361
128
  listed: boolean | null;
129
+ /** the rung's index entry; null when it is not indexed */
130
+ pair: Pair | null;
362
131
  }
363
132
 
364
133
  function familyOf(c: Catalog, r: Rung): Family | null {
@@ -369,67 +138,42 @@ function familyOf(c: Catalog, r: Rung): Family | null {
369
138
  /** The backend's listing; the native `claude` backend has none of its own: it runs claude-code's models. */
370
139
  const listingOf = (c: Catalog, backend: string) => c.listed[backend === "claude" ? "claude-code" : backend];
371
140
 
372
- /** A rung's catalog facts; `rung` must parse (E_ADMIT_RUNG otherwise). */
141
+ /**
142
+ * A rung's catalog facts; `rung` must parse (E_ADMIT_RUNG otherwise). Its pair is the index's record for the
143
+ * model's Slopalytics family at its effort; `#default` takes the family's `default` record, else its only one.
144
+ */
373
145
  export function rungInfo(c: Catalog, rung: string): RungInfo {
374
146
  const parsed = parseRung(rung);
375
147
  const key = billingKeyOf(parsed);
376
148
  const family = familyOf(c, parsed);
377
- const shipped = family?.on[modelKeyOf(key)];
149
+ const slop = slopKey(family?.id ?? parsed.model);
378
150
  const listing = listingOf(c, parsed.backend);
379
151
  const found = listing?.models.find((m) => m.id === parsed.model);
152
+ const indexed = c.index[slop] ?? {};
153
+ const only = Object.values(indexed);
154
+ const pair =
155
+ indexed[parsed.effort] ?? (parsed.effort === "default" && only.length === 1 ? (only[0] as Pair) : null);
380
156
  return {
381
157
  rung,
382
158
  parsed,
383
159
  key,
384
160
  family,
385
161
  canonical: `${family?.id ?? parsed.model}#${parsed.effort}`,
386
- efforts: found?.efforts ?? shipped?.efforts ?? [],
387
- context: found?.context ?? shipped?.context ?? null,
162
+ slop,
163
+ efforts: found?.efforts ?? Object.keys(indexed),
164
+ context: found?.context ?? pair?.context ?? null,
388
165
  listed: listing && listing.models.length > 0 ? found !== undefined : null,
166
+ pair,
389
167
  };
390
168
  }
391
169
 
392
- /**
393
- * The rung's scores: per dimension its own value, else that of the rung it is treated like (a sync may score
394
- * a rung on some dimensions only), else its inferred stand-in's (spec 1.2 §6.1). `via` names the treat-like
395
- * when it lends any value and `borrowed` the dimensions it lends; `inferred` the dimensions an inferred
396
- * stand-in fills and `standIns` whose values they are. null when it has no value at all.
397
- */
398
- export function scoresOf(
399
- c: Catalog,
400
- canonical: string,
401
- ): {
402
- values: Partial<Record<Dim, number>>;
403
- records: Partial<Record<Dim, Score>>;
404
- via: string | null;
405
- borrowed: Dim[];
406
- inferred: Dim[];
407
- standIns: Partial<Record<Dim, string>>;
408
- } | null {
409
- const own = c.scores[canonical] ?? {};
410
- const like = c.treatLike[canonical]?.like ?? null;
411
- const lent = like ? (c.scores[like] ?? {}) : {};
412
- const guessed = c.inferred[canonical] ?? {};
413
- const values: Partial<Record<Dim, number>> = {};
414
- const records: Partial<Record<Dim, Score>> = {};
415
- const borrowed: Dim[] = [];
416
- const inferred: Dim[] = [];
417
- const standIns: Partial<Record<Dim, string>> = {};
418
- for (const d of DIMS) {
419
- const stand = guessed[d];
420
- const r = own[d] ?? lent[d] ?? (stand ? c.scores[stand.like]?.[d] : undefined);
421
- if (!r) continue;
422
- records[d] = r;
423
- values[d] = r.value;
424
- if (own[d]) continue;
425
- if (lent[d]) borrowed.push(d);
426
- else if (stand) {
427
- inferred.push(d);
428
- standIns[d] = stand.like;
429
- }
170
+ /** The rung's index entry; null when it is not a rung or not indexed. */
171
+ export function pairOf(c: Catalog, rung: string): Pair | null {
172
+ try {
173
+ return rungInfo(c, rung).pair;
174
+ } catch {
175
+ return null;
430
176
  }
431
- if (Object.keys(records).length === 0) return null;
432
- return { values, records, via: borrowed.length ? like : null, borrowed, inferred, standIns };
433
177
  }
434
178
 
435
179
  /** Spec §4 roles: what a rung must offer to be placed on a role. */
@@ -442,6 +186,7 @@ export const ROLE_NEEDS: Record<Role, { toolUse?: true; imageIn?: true; imageGen
442
186
  artist: { imageGen: true },
443
187
  writer: { toolUse: true },
444
188
  researcher: { toolUse: true },
189
+ judge: { toolUse: true },
445
190
  };
446
191
 
447
192
  /**
@@ -1,4 +1,4 @@
1
- import type { BillingKey, Family } from "./catalog.ts";
1
+ import type { BillingKey, Family, Pair } from "./catalog.ts";
2
2
 
3
3
  /** Spec §5.3: how the user pays for each billing key. */
4
4
  export const BILLING_MODES = ["chatgpt-plan", "claude-plan", "subscription", "metered"] as const;
@@ -12,8 +12,6 @@ export const DEFAULT_BILLING: Record<BillingKey, BillingMode> = {
12
12
  "opencode-go": "subscription",
13
13
  opencode: "metered",
14
14
  cursor: "metered",
15
- grok: "metered",
16
- antigravity: "metered",
17
15
  };
18
16
 
19
17
  /**
@@ -38,10 +36,10 @@ const REF_TASK = { input: 60_000, cached: 400_000, output: 18_000 };
38
36
 
39
37
  export const effortFactor = (effort: string): number => EFFORT_FACTOR[effort] ?? 1;
40
38
 
41
- /** Estimated list-price dollars for one task at `effort`. */
42
- export function taskUsd(price: Family["price"], effort: string): number {
39
+ /** Estimated list-price dollars for one task at `effort`; cached input costs a tenth of the input price. */
40
+ export function taskUsd(price: { input: number; output: number }, effort: string): number {
43
41
  const perTask =
44
- REF_TASK.input * price.input + REF_TASK.cached * price.cached + REF_TASK.output * price.output;
42
+ REF_TASK.input * price.input + REF_TASK.cached * (price.input / 10) + REF_TASK.output * price.output;
45
43
  return (perTask / 1_000_000) * effortFactor(effort);
46
44
  }
47
45
 
@@ -60,31 +58,18 @@ export interface Cost {
60
58
  }
61
59
 
62
60
  /**
63
- * Spec §5.3. Every mode yields list-price dollars per task, so plans compare with each other. Spec 1.5 plan 24:
64
- * a rung catherd has no price for costs 0 within its tier when a plan or subscription pays for it, so it can
65
- * start a lane (the profile's ladder order then breaks the tie); a metered one stays unknown, last in its tier.
66
- * chatgpt-plan scales the official quota weight (Luna 1, Sol 20, Astra 60) by the effort factor;
67
- * claude-plan uses API prices as the proxy, with Fable metered (catherd cannot tell Pro from Max);
68
- * Go's share of its monthly dollar limit is dollars / limit, which orders the same as dollars.
61
+ * Spec §5.3. Every mode yields list-price dollars per task, so plans compare with each other. chatgpt-plan scales
62
+ * the official quota weight (Luna 1, Sol 20, Astra 60) by the effort factor; claude-plan uses API prices as the
63
+ * proxy, with Fable metered (catherd cannot tell Pro from Max); Go's share of its monthly dollar limit is
64
+ * dollars / limit, which orders the same as dollars. A pair with no price costs 0 within its tier when a plan or
65
+ * subscription pays for it, so it can start a lane; a metered one stays unknown, last in its tier.
69
66
  */
70
- export function costOf(family: Family | null, effort: string, mode: BillingMode): Cost {
71
- if (!family) return mode === "metered" ? { tier: 1, value: null, mode } : { tier: 0, value: 0, mode };
72
- const usd = taskUsd(family.price, effort);
73
- switch (mode) {
74
- case "chatgpt-plan":
75
- return {
76
- tier: 0,
77
- value:
78
- family.planWeight === undefined ? usd : family.planWeight * CHATGPT_UNIT_USD * effortFactor(effort),
79
- mode,
80
- };
81
- case "claude-plan":
82
- return { tier: family.meteredOnPlan ? 1 : 0, value: usd, mode };
83
- case "subscription":
84
- return { tier: 0, value: usd, mode };
85
- case "metered":
86
- return { tier: 1, value: usd, mode };
87
- }
67
+ export function costOf(family: Family | null, pair: Pair | null, effort: string, mode: BillingMode): Cost {
68
+ if (mode === "chatgpt-plan" && family?.planWeight !== undefined)
69
+ return { tier: 0, value: family.planWeight * CHATGPT_UNIT_USD * effortFactor(effort), mode };
70
+ const metered = mode === "metered" || (mode === "claude-plan" && family?.meteredOnPlan === true);
71
+ if (!pair?.price) return metered ? { tier: 1, value: null, mode } : { tier: 0, value: 0, mode };
72
+ return { tier: metered ? 1 : 0, value: taskUsd(pair.price, effort), mode };
88
73
  }
89
74
 
90
75
  /** Cheaper first: tier, then value, an unknown value last. */
@@ -1,7 +1,6 @@
1
1
  export type ErrorCode =
2
2
  | "E_CONFIG_INVALID"
3
3
  | "E_CONFIG_NEWER_SCHEMA"
4
- | "E_CONFIG_KEYBIND"
5
4
  | "E_BACKEND_MISSING"
6
5
  | "E_BACKEND_TOO_OLD"
7
6
  | "E_BACKEND_NOT_LOGGED_IN"
@@ -21,6 +20,7 @@ export type ErrorCode =
21
20
  | "E_ADMIT_ORDER"
22
21
  | "E_LANE_INVALID"
23
22
  | "E_LAND_GATE"
23
+ | "E_TREE"
24
24
  | "E_CLIMB_DESIGN"
25
25
  | "E_CLIMB_ENV"
26
26
  | "E_JEV_KEY"
@@ -35,7 +35,7 @@ export type ErrorCode =
35
35
  | "E_RUNTIME_TOO_OLD";
36
36
 
37
37
  /**
38
- * How to get a newer catherd. `bunx catherd-cli@latest` alone opens the newest one's dashboard and upgrades
38
+ * How to get a newer catherd. `bunx catherd-cli@latest` alone prints the newest one's help and upgrades
39
39
  * nothing that is installed (audit N12).
40
40
  */
41
41
  export const UPGRADE = "bun add -g catherd-cli@latest (or run bunx catherd-cli@latest <command>)";