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