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,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,33 +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
- /** a sync's speed facts (spec 1.2 §4.1), `<source>.<field>` → value; they never carry a bar */
88
- speed: z.record(z.string(), z.number()).optional(),
89
40
  });
90
41
  export type Family = z.infer<typeof FamilySchema>;
91
42
 
92
- /** `sources` (where the data came from) stays in the file as documentation; nothing reads it. */
93
43
  export const ModelsFileSchema = z.looseObject({
94
44
  schema: z.literal(1),
95
45
  version: z.string(),
@@ -103,236 +53,62 @@ export const ModelsFileSchema = z.looseObject({
103
53
  });
104
54
  export type ModelsFile = z.infer<typeof ModelsFileSchema>;
105
55
 
106
- /** A canonical rung: `<canonical model id>#<effort>`, the key scores and treat-likes use. */
107
- export const CanonicalRung = z.string().regex(/^[^:#\s]+#[^#\s]+$/, "a canonical rung is model#effort");
108
-
109
- /** Spec 1.2 §4.2: `anchor = a·x + b`, fitted on `n` rungs both sources cover, with its R². */
110
- export const FitSchema = z.object({
111
- source: z.string(),
112
- field: z.string(),
113
- a: z.number(),
114
- b: z.number(),
115
- r2: z.number(),
116
- n: z.number().int(),
117
- });
118
- export type Fit = z.infer<typeof FitSchema>;
119
-
120
- export const ScoreSchema = z.object({
121
- rung: CanonicalRung,
122
- dim: z.enum(DIMS),
123
- value: z.number(),
124
- benchmark: z.string(),
125
- version: z.string(),
126
- url: z.url(),
127
- date: z.iso.date(),
128
- confidence: z.enum(CONFIDENCE),
129
- note: z.string().optional(),
130
- /** spec 1.2 §4.3: the source a synced value came from (absent: the shipped file or the user's override) */
131
- source: z.string().optional(),
132
- /** a calibrated value: the fit that mapped it onto the anchor */
133
- fit: FitSchema.optional(),
134
- /** the source named no effort, so the family's default effort was assumed (spec 1.2 §3.4) */
135
- effortAssumed: z.boolean().optional(),
136
- });
137
- export type Score = z.infer<typeof ScoreSchema>;
138
-
139
- const BarSchema = z.partialRecord(z.enum(DIMS), z.number());
140
- type Bars = Record<Kind, Record<Difficulty, Partial<Record<Dim, number>>>>;
141
- const BarsSchema = z.record(z.enum(KINDS), z.record(z.enum(DIFFICULTIES), BarSchema));
142
-
143
- /** Spec 1.2 §5.2: per dimension and difficulty, where the default threshold came from. */
144
- const BarsWhySchema = z.partialRecord(z.enum(DIMS), z.partialRecord(z.enum(DIFFICULTIES), z.string()));
145
-
146
- /**
147
- * `catalog/scores.json`: the hand-typed and keyless values (spec 1.2 §7), the shipped treat-likes, and the
148
- * default bars with the `barsWhy` line of each threshold (spec 1.2 §5.2).
149
- */
150
- export const ScoresFileSchema = z.looseObject({
151
- schema: z.literal(1),
152
- version: z.string(),
153
- benchmarks: z.record(z.enum(DIMS), z.object({ benchmark: z.string(), version: z.string() })),
154
- scores: z.array(ScoreSchema),
155
- treatLike: z.record(CanonicalRung, z.object({ like: CanonicalRung, note: z.string() })),
156
- bars: BarsSchema,
157
- barsWhy: BarsWhySchema.default({}),
158
- });
159
- export type ScoresFile = z.infer<typeof ScoresFileSchema>;
160
-
161
- /** An override's bar: per dimension a threshold, or `null` to remove the default's (spec 1.2 §5.2). */
162
- const OverrideBarSchema = z.partialRecord(z.enum(DIMS), z.number().nullable());
163
-
164
- /**
165
- * `<config>/catalog.override.json` (spec §3.5): the user's treat-likes, scores and bars. Its bars override
166
- * the default bars per dimension (spec 1.2 §5.2): a number sets that threshold, `null` removes it, and a
167
- * dimension it does not name keeps the default's.
168
- */
169
- export const OverrideSchema = z.looseObject({
170
- schema: z.literal(1).default(1),
171
- treatLike: z.record(CanonicalRung, CanonicalRung).default({}),
172
- scores: z.array(ScoreSchema).default([]),
173
- bars: z.partialRecord(z.enum(KINDS), z.partialRecord(z.enum(DIFFICULTIES), OverrideBarSchema)).default({}),
174
- });
175
- export type Override = z.infer<typeof OverrideSchema>;
176
-
177
- /** Spec 1.2 §6.1: the rung whose value a rung without one uses on a dimension, as `inferred`. */
178
- export interface InferredStandIn {
179
- like: string;
180
- /** spec 1.2 §6.3's distance between the two */
181
- distance: number;
182
- /** the features that distance rests on */
183
- features: string[];
184
- }
185
-
186
- interface Listed {
56
+ export interface Listed {
187
57
  id: string;
188
58
  efforts: string[];
189
59
  context: number | null;
190
60
  imageIn: boolean;
191
61
  }
192
62
 
193
- /** 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. */
194
72
  export interface Catalog {
195
73
  families: Family[];
196
74
  backends: ModelsFile["backends"];
197
- /** canonical rung → its best-sourced value per dimension (the override's win) */
198
- scores: Record<string, Partial<Record<Dim, Score>>>;
199
- treatLike: Record<string, { like: string; source: "shipped" | "user" }>;
200
- bars: Bars;
201
- /** where each default threshold came from (spec 1.2 §5.2), by dimension and difficulty */
202
- 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[];
203
81
  /** per backend id, the last `listModels()`; absent when never listed */
204
82
  listed: Record<string, { fetchedAt: string; models: Listed[] }>;
205
- /** `<canonical rung>|<kind or *>` → median seconds, only with ≥ 5 samples (spec §5.2) */
206
- secs: Record<string, number>;
207
- /** canonical rung → the Artificial Analysis stand-in features a sync read for it (spec 1.2 §6.3) */
208
- features: Record<string, Record<string, number>>;
209
- /**
210
- * canonical rung → per dimension it has no value for (of its own or through a treat-like), the stand-in
211
- * whose value it uses as `inferred` (spec 1.2 §6.1); filled by the stand-in ranking (services/standins.ts)
212
- */
213
- inferred: Record<string, Partial<Record<Dim, InferredStandIn>>>;
214
83
  }
215
84
 
216
- /**
217
- * Spec 1.2 §3.5: what a sync learned about a family. `price`, `capabilities` and the opencode backends'
218
- * `efforts` and `context` come from models.dev; `speed` holds facts that never carry a bar (spec 1.2 §4.1),
219
- * as `<source>.<field>` → value.
220
- */
221
- export const FamilyFactsSchema = z.object({
222
- price: z.object({ input: z.number(), cached: z.number(), output: z.number() }).optional(),
223
- capabilities: z.object({ toolUse: z.boolean(), imageIn: z.boolean(), reasoning: z.boolean() }).optional(),
224
- on: z
225
- .partialRecord(
226
- z.enum(MODEL_KEYS),
227
- z.object({ efforts: z.array(z.string()).optional(), context: z.number().int().positive().optional() }),
228
- )
229
- .default({}),
230
- releaseDate: z.string().optional(),
231
- speed: z.record(z.string(), z.number()).default({}),
232
- });
233
- export type FamilyFacts = z.infer<typeof FamilyFactsSchema>;
234
-
235
- /**
236
- * Spec 1.2 §3.5: the families with a sync's facts laid over them. The shipped file stays the floor: a fact
237
- * the sync lacks keeps the shipped one, and a backend keeps every effort it ships with.
238
- */
239
- export function applyFacts(families: Family[], facts: Record<string, FamilyFacts>): Family[] {
240
- return families.map((f) => {
241
- const x = facts[f.id];
242
- if (!x) return f;
243
- const on = { ...f.on };
244
- for (const key of MODEL_KEYS) {
245
- const cur = on[key];
246
- const got = x.on[key];
247
- if (!cur || !got) continue;
248
- on[key] = {
249
- ...cur,
250
- efforts: [...cur.efforts, ...(got.efforts ?? []).filter((e) => !cur.efforts.includes(e))],
251
- context: got.context ?? cur.context,
252
- };
253
- }
254
- // a sync may add a capability, never remove one the shipped file gives (the floor)
255
- const got = x.capabilities;
256
- const capabilities = got
257
- ? {
258
- toolUse: f.capabilities.toolUse || got.toolUse,
259
- imageIn: f.capabilities.imageIn || got.imageIn,
260
- reasoning: f.capabilities.reasoning || got.reasoning,
261
- }
262
- : f.capabilities;
263
- return {
264
- ...f,
265
- price: x.price ?? f.price,
266
- capabilities,
267
- on,
268
- ...(x.releaseDate || f.releaseDate ? { releaseDate: x.releaseDate ?? f.releaseDate } : {}),
269
- ...(Object.keys(x.speed).length ? { speed: x.speed } : {}),
270
- };
271
- });
272
- }
273
-
274
- /** Spec 1.2 §4.3: the level a value counts at: its own, one lower once it is older than STALE_DAYS. */
275
- export function effectiveRank(s: Score, now: number): number {
276
- const stale = now - Date.parse(s.date) > STALE_DAYS * DAY_MS;
277
- return Math.min(RANK[s.confidence] + (stale ? 1 : 0), CONFIDENCE.length - 1);
278
- }
279
-
280
- /** Spec 1.2 §4.3: `a` beats `b` at a better level, or at the same level with a newer date. */
281
- export function outranks(a: Score, b: Score, now: number): boolean {
282
- const ra = effectiveRank(a, now);
283
- const rb = effectiveRank(b, now);
284
- return ra < rb || (ra === rb && a.date > b.date);
285
- }
286
-
287
- /**
288
- * The catalog routing reads: the shipped files, the synced values (spec 1.2 §3), each backend's listing and
289
- * the user's override, which always wins. Between shipped and synced values the better level wins, then the
290
- * newer date (spec 1.2 §4.3); `now` dates the 90-day drop.
291
- */
292
85
  export function buildCatalog(o: {
293
86
  models: ModelsFile;
294
- scores: ScoresFile;
295
- synced?: Score[];
296
- facts?: Record<string, FamilyFacts>;
297
- override?: Override;
87
+ index: IndexFile | null;
298
88
  listed?: Catalog["listed"];
299
- secs?: Catalog["secs"];
300
- features?: Catalog["features"];
301
- now?: number;
302
89
  }): Catalog {
303
- const now = o.now ?? Date.now();
304
- const scores: Catalog["scores"] = {};
305
- const put = (s: Score, force: boolean) => {
306
- const cur = (scores[s.rung] ??= {});
307
- const had = cur[s.dim];
308
- if (force || !had || outranks(s, had, now)) cur[s.dim] = s;
309
- };
310
- for (const s of o.scores.scores) put(s, false);
311
- for (const s of o.synced ?? []) put(s, false);
312
- // the user's values say so whatever source they were copied with, for route's provenance (spec 1.2 §5.3)
313
- for (const s of o.override?.scores ?? []) put({ ...s, source: "override" }, true);
314
- const treatLike: Catalog["treatLike"] = {};
315
- for (const [rung, t] of Object.entries(o.scores.treatLike))
316
- treatLike[rung] = { like: t.like, source: "shipped" };
317
- for (const [rung, like] of Object.entries(o.override?.treatLike ?? {}))
318
- treatLike[rung] = { like, source: "user" };
319
- const bars = structuredClone(o.scores.bars) as Bars;
320
- for (const kind of KINDS)
321
- for (const d of DIFFICULTIES)
322
- for (const [dim, min] of Object.entries(o.override?.bars[kind]?.[d] ?? {}) as [Dim, number | null][])
323
- if (min === null) delete bars[kind][d][dim];
324
- else bars[kind][d][dim] = min;
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);
325
105
  return {
326
- families: o.facts ? applyFacts(o.models.families, o.facts) : o.models.families,
106
+ families: o.models.families,
327
107
  backends: o.models.backends,
328
- scores,
329
- treatLike,
330
- bars,
331
- barsWhy: o.scores.barsWhy,
108
+ index,
109
+ indexedAt: o.index?.fetchedAt ?? null,
110
+ pool,
332
111
  listed: o.listed ?? {},
333
- secs: o.secs ?? {},
334
- features: o.features ?? {},
335
- inferred: {},
336
112
  };
337
113
  }
338
114
 
@@ -342,12 +118,16 @@ export interface RungInfo {
342
118
  parsed: Rung;
343
119
  key: BillingKey;
344
120
  family: Family | null;
345
- /** `<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 */
346
122
  canonical: string;
123
+ /** the model's Slopalytics family key */
124
+ slop: string;
347
125
  efforts: string[];
348
126
  context: number | null;
349
127
  /** true when the backend's last listing has it, false when that listing lacks it, null with no listing */
350
128
  listed: boolean | null;
129
+ /** the rung's index entry; null when it is not indexed */
130
+ pair: Pair | null;
351
131
  }
352
132
 
353
133
  function familyOf(c: Catalog, r: Rung): Family | null {
@@ -358,67 +138,42 @@ function familyOf(c: Catalog, r: Rung): Family | null {
358
138
  /** The backend's listing; the native `claude` backend has none of its own: it runs claude-code's models. */
359
139
  const listingOf = (c: Catalog, backend: string) => c.listed[backend === "claude" ? "claude-code" : backend];
360
140
 
361
- /** 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
+ */
362
145
  export function rungInfo(c: Catalog, rung: string): RungInfo {
363
146
  const parsed = parseRung(rung);
364
147
  const key = billingKeyOf(parsed);
365
148
  const family = familyOf(c, parsed);
366
- const shipped = family?.on[modelKeyOf(key)];
149
+ const slop = slopKey(family?.id ?? parsed.model);
367
150
  const listing = listingOf(c, parsed.backend);
368
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);
369
156
  return {
370
157
  rung,
371
158
  parsed,
372
159
  key,
373
160
  family,
374
161
  canonical: `${family?.id ?? parsed.model}#${parsed.effort}`,
375
- efforts: found?.efforts ?? shipped?.efforts ?? [],
376
- context: found?.context ?? shipped?.context ?? null,
162
+ slop,
163
+ efforts: found?.efforts ?? Object.keys(indexed),
164
+ context: found?.context ?? pair?.context ?? null,
377
165
  listed: listing && listing.models.length > 0 ? found !== undefined : null,
166
+ pair,
378
167
  };
379
168
  }
380
169
 
381
- /**
382
- * The rung's scores: per dimension its own value, else that of the rung it is treated like (a sync may score
383
- * a rung on some dimensions only), else its inferred stand-in's (spec 1.2 §6.1). `via` names the treat-like
384
- * when it lends any value and `borrowed` the dimensions it lends; `inferred` the dimensions an inferred
385
- * stand-in fills and `standIns` whose values they are. null when it has no value at all.
386
- */
387
- export function scoresOf(
388
- c: Catalog,
389
- canonical: string,
390
- ): {
391
- values: Partial<Record<Dim, number>>;
392
- records: Partial<Record<Dim, Score>>;
393
- via: string | null;
394
- borrowed: Dim[];
395
- inferred: Dim[];
396
- standIns: Partial<Record<Dim, string>>;
397
- } | null {
398
- const own = c.scores[canonical] ?? {};
399
- const like = c.treatLike[canonical]?.like ?? null;
400
- const lent = like ? (c.scores[like] ?? {}) : {};
401
- const guessed = c.inferred[canonical] ?? {};
402
- const values: Partial<Record<Dim, number>> = {};
403
- const records: Partial<Record<Dim, Score>> = {};
404
- const borrowed: Dim[] = [];
405
- const inferred: Dim[] = [];
406
- const standIns: Partial<Record<Dim, string>> = {};
407
- for (const d of DIMS) {
408
- const stand = guessed[d];
409
- const r = own[d] ?? lent[d] ?? (stand ? c.scores[stand.like]?.[d] : undefined);
410
- if (!r) continue;
411
- records[d] = r;
412
- values[d] = r.value;
413
- if (own[d]) continue;
414
- if (lent[d]) borrowed.push(d);
415
- else if (stand) {
416
- inferred.push(d);
417
- standIns[d] = stand.like;
418
- }
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;
419
176
  }
420
- if (Object.keys(records).length === 0) return null;
421
- return { values, records, via: borrowed.length ? like : null, borrowed, inferred, standIns };
422
177
  }
423
178
 
424
179
  /** Spec §4 roles: what a rung must offer to be placed on a role. */
@@ -431,6 +186,7 @@ export const ROLE_NEEDS: Record<Role, { toolUse?: true; imageIn?: true; imageGen
431
186
  artist: { imageGen: true },
432
187
  writer: { toolUse: true },
433
188
  researcher: { toolUse: true },
189
+ judge: { toolUse: true },
434
190
  };
435
191
 
436
192
  /**
@@ -33,6 +33,28 @@ export function changedPaths(before: Snapshot, after: Snapshot): string[] {
33
33
  .sort();
34
34
  }
35
35
 
36
+ /** Spec 1.5 plan 21: a writer's implicit docs lane, when its brief names no Owns. */
37
+ export const DOCS_OWNS = ["docs/**", "*.md"];
38
+
39
+ /** The paths of an `Owns:` line in a brief (as a lane file writes it), or null when it has none. */
40
+ export function briefOwns(brief: string): string[] | null {
41
+ const line = /^Owns:\s*(.+)$/m.exec(brief)?.[1];
42
+ const paths = line
43
+ ?.split(",")
44
+ .map((p) => p.trim().replace(/^`|`$/g, ""))
45
+ .filter(Boolean);
46
+ return paths?.length ? paths : null;
47
+ }
48
+
49
+ /**
50
+ * Whether `path` is inside an Owns entry: a path covers what is under it, and the other way round (as lanes'
51
+ * Owns always did); `dir/**` covers what is under dir; `*.ext` (no slash) is any file with that extension.
52
+ */
53
+ export function ownsPath(path: string, entry: string): boolean {
54
+ if (entry.startsWith("*.") && !entry.includes("/")) return path.endsWith(entry.slice(1));
55
+ return overlaps([path], [entry.replace(/\/\*\*$/, "")]).length > 0;
56
+ }
57
+
36
58
  /**
37
59
  * Spec §4.4: `changedOwned` are the changes inside the lane's Owns; `violations` the ones outside it
38
60
  * that no overlapping dispatch owns either. `strict` is false for a laneless dispatch that may write
@@ -44,7 +66,7 @@ export function splitChanges(
44
66
  others: string[],
45
67
  strict: boolean,
46
68
  ): { changedOwned: string[]; violations: string[] } {
47
- const inside = (p: string, paths: string[]) => overlaps([p], paths).length > 0;
69
+ const inside = (p: string, paths: string[]) => paths.some((entry) => ownsPath(p, entry));
48
70
  return {
49
71
  changedOwned: changed.filter((p) => inside(p, owns)),
50
72
  violations: strict ? changed.filter((p) => !inside(p, owns) && !inside(p, others)) : [],
@@ -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
 
@@ -51,35 +49,27 @@ export const CHATGPT_UNIT_USD = 0.019;
51
49
  export interface Cost {
52
50
  /** 0: paid from a subscription; 1: metered. Every tier-0 rung ranks before any tier-1 rung. */
53
51
  tier: 0 | 1;
54
- /** comparable across billing modes, in list-price dollars per task; null when unknown */
52
+ /**
53
+ * comparable across billing modes, in list-price dollars per task; 0 for an unpriced rung paid from a plan or
54
+ * subscription (it spends nothing beyond the flat fee); null for an unpriced metered one
55
+ */
55
56
  value: number | null;
56
57
  mode: BillingMode;
57
58
  }
58
59
 
59
60
  /**
60
- * Spec §5.3. Every mode yields list-price dollars per task, so plans compare with each other:
61
- * chatgpt-plan scales the official quota weight (Luna 1, Sol 20, Astra 60) by the effort factor;
62
- * claude-plan uses API prices as the proxy, with Fable metered (catherd cannot tell Pro from Max);
63
- * 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.
64
66
  */
65
- export function costOf(family: Family | null, effort: string, mode: BillingMode): Cost {
66
- if (!family) return { tier: mode === "metered" ? 1 : 0, value: null, mode };
67
- const usd = taskUsd(family.price, effort);
68
- switch (mode) {
69
- case "chatgpt-plan":
70
- return {
71
- tier: 0,
72
- value:
73
- family.planWeight === undefined ? usd : family.planWeight * CHATGPT_UNIT_USD * effortFactor(effort),
74
- mode,
75
- };
76
- case "claude-plan":
77
- return { tier: family.meteredOnPlan ? 1 : 0, value: usd, mode };
78
- case "subscription":
79
- return { tier: 0, value: usd, mode };
80
- case "metered":
81
- return { tier: 1, value: usd, mode };
82
- }
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 };
83
73
  }
84
74
 
85
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"
@@ -17,9 +16,13 @@ export type ErrorCode =
17
16
  | "E_ADMIT_OVERLAP"
18
17
  | "E_ADMIT_ID"
19
18
  | "E_ADMIT_THREAD"
19
+ | "E_ADMIT_PAUSED"
20
+ | "E_ADMIT_ORDER"
20
21
  | "E_LANE_INVALID"
21
22
  | "E_LAND_GATE"
23
+ | "E_TREE"
22
24
  | "E_CLIMB_DESIGN"
25
+ | "E_CLIMB_ENV"
23
26
  | "E_JEV_KEY"
24
27
  | "E_JEV_NETWORK"
25
28
  | "E_JEV_RESPONSE"
@@ -28,10 +31,11 @@ export type ErrorCode =
28
31
  | "E_IO_PATH"
29
32
  | "E_IO_UNEXPECTED"
30
33
  | "E_INPUT_INVALID"
34
+ | "E_ROLE_SCOPE"
31
35
  | "E_RUNTIME_TOO_OLD";
32
36
 
33
37
  /**
34
- * 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
35
39
  * nothing that is installed (audit N12).
36
40
  */
37
41
  export const UPGRADE = "bun add -g catherd-cli@latest (or run bunx catherd-cli@latest <command>)";
@@ -0,0 +1,26 @@
1
+ const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
2
+
3
+ /**
4
+ * The lines of a check's output that name a failure, stripped of colour, timings and counters so two runs compare
5
+ * equal: at most 60 lines of at most 300 characters (t3herd `failLines`).
6
+ */
7
+ export function checkFailLines(out: string): string[] {
8
+ const seen = new Set<string>();
9
+ for (const raw of out.split("\n")) {
10
+ const line = raw
11
+ .replace(ANSI, "")
12
+ .replace(/\s*[[(]\s*\d+(\.\d+)?\s*(ms|s|m)\s*[\])]/g, "")
13
+ .replace(/\b\d+(\.\d+)?(ms|s)\b/g, "")
14
+ .trim();
15
+ const vuln = line.match(/^Vulnerability #\d+: (GO-\d{4}-\d+)/)?.[1];
16
+ if (vuln) seen.add(`vulnerability ${vuln}`);
17
+ else if (/(^|\s)(FAIL|FAILED|✗|✕|×)(\s|:|$)|^\(fail\)|^--- FAIL|^not ok\b|\bpanic:|^error:/.test(line))
18
+ seen.add(line.slice(0, 300));
19
+ if (seen.size >= 60) break;
20
+ }
21
+ return [...seen];
22
+ }
23
+
24
+ /** The lines of `fails` the base does not have; all of them when there is no base record. */
25
+ export const newFails = (fails: string[], base: string[] | null): string[] =>
26
+ fails.filter((f) => !base?.includes(f));