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,637 +0,0 @@
1
- import type { OrchestrationHost } from "../../domain/host.ts";
2
- import { sessionKey } from "../../domain/host.ts";
3
- import { isDeepStrictEqual } from "node:util";
4
- import {
5
- applyPatch,
6
- defaultProfileDoc,
7
- diffProfiles,
8
- type ProfileDoc,
9
- resolveProfile,
10
- } from "../../domain/profile.ts";
11
- import { repairs, validateProfile } from "../../domain/profile-rules.ts";
12
- import { catalogQuery, loadCatalog } from "../../services/catalog-service.ts";
13
- import type { RunRecord } from "../../domain/record.ts";
14
- import type { DoctorReport } from "../../services/doctor.ts";
15
- import type { RunSummary } from "../../services/summary.ts";
16
- import {
17
- CHANGED_ON_DISK,
18
- type Effects,
19
- type RoleDetail,
20
- type RoleRow,
21
- rowOf,
22
- rungDetailOf,
23
- type SessionRow,
24
- type SessionRun,
25
- suggestFor,
26
- } from "./effects.ts";
27
- import { withStaged } from "./profile-tree.ts";
28
-
29
- /**
30
- * Fixed data for the storybook (CATHERD_STORY=1), the frame snapshots and the view tests: a setup with
31
- * one backend missing and one not logged in, two runs (one live), and profiles kept in memory. Only
32
- * the catalog is the shipped one, read through the catalog service.
33
- */
34
- export const FIXTURE_REPORT: DoctorReport = {
35
- host: { host: "unknown", session: null, conflict: null },
36
- queue: null,
37
- push: null,
38
- ready: false,
39
- version: "1.0.0",
40
- checks: [
41
- { id: "bun", label: "Bun", state: "ok", word: "ready", detail: "1.4.2" },
42
- { id: "config", label: "config", state: "ok", word: "ready", detail: "active profile default" },
43
- { id: "profile", label: "profile default", state: "ok", word: "ready", detail: "valid" },
44
- {
45
- id: "backend:codex",
46
- label: "codex",
47
- state: "ok",
48
- word: "ready",
49
- detail: "0.156.1, logged in, 14 models",
50
- },
51
- {
52
- id: "backend:claude-code",
53
- label: "claude-code",
54
- state: "ok",
55
- word: "ready",
56
- detail: "2.1.282, logged in, 6 models",
57
- },
58
- {
59
- id: "backend:opencode",
60
- label: "opencode",
61
- state: "warn",
62
- word: "not logged in",
63
- detail: "2.0.16 (a failover stand-in uses it)",
64
- fix: "opencode auth login",
65
- },
66
- {
67
- id: "jev",
68
- label: "Jev",
69
- state: "skip",
70
- word: "no key",
71
- detail: "optional: routing uses the lane files",
72
- },
73
- {
74
- id: "plugin",
75
- label: "Claude Code plugin",
76
- state: "fail",
77
- word: "missing",
78
- detail: "not installed",
79
- fix: "claude plugin marketplace add 47vigen/catherd && claude plugin install catherd@catherd",
80
- },
81
- { id: "agents", label: "Claude agents", state: "ok", word: "ready", detail: "2 linked" },
82
- { id: "mcp", label: "MCP server", state: "ok", word: "ready", detail: "21 tools" },
83
- ],
84
- };
85
-
86
- const summary = (o: Partial<RunSummary> & Pick<RunSummary, "id" | "title">): RunSummary => ({
87
- delivery: [],
88
- repo: "/home/me/app",
89
- createdAt: "2026-09-26T11:48:00.000Z",
90
- session: null,
91
- continuedIn: null,
92
- stateTail: [],
93
- live: [],
94
- totals: {
95
- runs: 0,
96
- ok: 0,
97
- notOk: [],
98
- tokens: { input: 0, cached: 0, output: 0 },
99
- costUsd: 0,
100
- wallMinutes: 12,
101
- },
102
- agents: { runs: 0, totalTokens: 0, costUsd: 0 },
103
- jev: { decisions: 0, fallbacks: 0 },
104
- harness: [],
105
- budget: null,
106
- milestones: [],
107
- verifier: null,
108
- questions: [],
109
- warnings: [],
110
- ...o,
111
- });
112
-
113
- const JOBS = "20260926-114800-jobs-screen";
114
- const AUTH = "20260925-090000-auth-refactor";
115
- const PLAN4 = "20260924-080000-auth-plan-4";
116
-
117
- const FIXTURE_RUNS: RunSummary[] = [
118
- summary({
119
- id: JOBS,
120
- title: "Jobs screen",
121
- session: {
122
- host: "claude-code",
123
- sessionId: "s-jobs",
124
- hostSessionId: null,
125
- name: "jobs screen",
126
- live: true,
127
- },
128
- stateTail: ["M1 in review", "Next: fix round for M1.L2"],
129
- live: [
130
- {
131
- name: "worker-M1.L2",
132
- rung: "codex:gpt-6-sol#medium",
133
- state: "running",
134
- secs: 252,
135
- dispatchId: "d2",
136
- },
137
- { name: "reviewer-M1", rung: "codex:gpt-6-sol#high", state: "starting", secs: 4, dispatchId: "d3" },
138
- ],
139
- totals: {
140
- runs: 3,
141
- ok: 3,
142
- notOk: [],
143
- tokens: { input: 812_000, cached: 640_000, output: 41_000 },
144
- costUsd: 0,
145
- wallMinutes: 12,
146
- },
147
- jev: { decisions: 2, fallbacks: 0 },
148
- budget: { fraction: 0.52, minutes: { spent: 31, cap: 60 } },
149
- milestones: ["M0 | scaffold the jobs screen | 3f2a9c1 | 9 | bun test test/jobs"],
150
- }),
151
- summary({
152
- id: AUTH,
153
- title: "Auth refactor",
154
- repo: "/home/me/api",
155
- createdAt: "2026-09-25T09:00:00.000Z",
156
- session: {
157
- host: "claude-code",
158
- sessionId: "s-auth",
159
- hostSessionId: null,
160
- name: "auth build",
161
- live: false,
162
- },
163
- continuedIn: "kit follow-up",
164
- stateTail: ["Next: done"],
165
- totals: {
166
- runs: 9,
167
- ok: 8,
168
- notOk: ["worker-M2.L1 (limit)"],
169
- tokens: { input: 3_100_000, cached: 2_400_000, output: 160_000 },
170
- costUsd: 1.84,
171
- wallMinutes: 96,
172
- },
173
- milestones: ["M1 | tokens | a1 | 30 | ok", "M2 | sessions | b2 | 41 | ok", "M3 | cleanup | c3 | 25 | ok"],
174
- }),
175
- ];
176
-
177
- /** A role of the fixtures; `since` minutes before the fixed clock (2026-09-26 12:00 UTC) for a live one. */
178
- const role = (o: Partial<RoleRow> & Pick<RoleRow, "run" | "dispatchId" | "name" | "rung">): RoleRow => ({
179
- role: o.name.split("-")[0] ?? "worker",
180
- status: "ok",
181
- live: false,
182
- since: "2026-09-26T11:48:10.000Z",
183
- secs: null,
184
- lastEvent: null,
185
- replyStatus: "complete",
186
- ...o,
187
- });
188
-
189
- const auth = (continued: SessionRun["continued"]): SessionRun => ({
190
- id: AUTH,
191
- title: "Auth refactor",
192
- repo: "/home/me/api",
193
- createdAt: "2026-09-25T09:00:00.000Z",
194
- continued,
195
- continuedIn: continued === "elsewhere" ? "kit follow-up" : null,
196
- budget: null,
197
- milestones: [
198
- { name: "M1", landed: true, what: "tokens" },
199
- { name: "M2", landed: true, what: "sessions" },
200
- { name: "M3", landed: true, what: "cleanup" },
201
- ],
202
- roles: [
203
- role({ run: AUTH, dispatchId: "a3", name: "reviewer-M3", rung: "codex:gpt-6-sol#high", secs: 188 }),
204
- role({ run: AUTH, dispatchId: "a2", name: "worker-M3.L1", rung: "codex:gpt-6-luna#high", secs: 402 }),
205
- role({
206
- run: AUTH,
207
- dispatchId: "a1",
208
- name: "worker-M2.L1",
209
- rung: "codex:gpt-6-sol#medium",
210
- status: "limit",
211
- secs: 95,
212
- replyStatus: null,
213
- }),
214
- ],
215
- });
216
-
217
- /** The Runs tab's sessions: a live one with three live roles, one whose run another session continued, and it. */
218
- export const FIXTURE_SESSIONS: { row: SessionRow; runs: SessionRun[] }[] = [
219
- {
220
- row: {
221
- key: sessionKey({ host: "claude-code", sessionId: "s-jobs" }),
222
- name: "jobs screen",
223
- live: true,
224
- runs: 1,
225
- liveRoles: 3,
226
- landed: 1,
227
- lastActivity: "2026-09-26T11:59:40.000Z",
228
- },
229
- runs: [
230
- {
231
- id: JOBS,
232
- title: "Jobs screen",
233
- repo: "/home/me/app",
234
- createdAt: "2026-09-26T11:48:00.000Z",
235
- continued: null,
236
- continuedIn: null,
237
- budget: 0.52,
238
- milestones: [
239
- { name: "M0", landed: true, what: "scaffold the jobs screen" },
240
- { name: "M1", landed: false, what: "" },
241
- ],
242
- roles: [
243
- role({
244
- run: JOBS,
245
- dispatchId: "d2",
246
- name: "worker-M1.L2",
247
- rung: "codex:gpt-6-sol#medium",
248
- status: "running",
249
- live: true,
250
- since: "2026-09-26T11:55:48.000Z",
251
- lastEvent: "$ bun test test/jobs --bail",
252
- replyStatus: null,
253
- }),
254
- role({
255
- run: JOBS,
256
- dispatchId: "d4",
257
- name: "worker-M1.L3",
258
- rung: "codex:gpt-6-luna#high",
259
- status: "running",
260
- live: true,
261
- since: "2026-09-26T11:57:00.000Z",
262
- lastEvent: "edit src/jobs/list.tsx",
263
- replyStatus: null,
264
- }),
265
- role({
266
- run: JOBS,
267
- dispatchId: "d3",
268
- name: "reviewer-M1",
269
- rung: "codex:gpt-6-sol#high",
270
- status: "starting",
271
- live: true,
272
- since: "2026-09-26T11:59:56.000Z",
273
- replyStatus: null,
274
- }),
275
- role({
276
- run: JOBS,
277
- dispatchId: "d1",
278
- name: "worker-M1.L1",
279
- rung: "codex:gpt-6-luna#high",
280
- secs: 312,
281
- }),
282
- ],
283
- },
284
- ],
285
- },
286
- {
287
- row: {
288
- key: sessionKey({ host: "claude-code", sessionId: "s-kit" }),
289
- name: "kit follow-up",
290
- live: false,
291
- runs: 1,
292
- liveRoles: 0,
293
- landed: 3,
294
- lastActivity: "2026-09-25T11:20:00.000Z",
295
- },
296
- runs: [auth("here")],
297
- },
298
- {
299
- row: {
300
- key: sessionKey({ host: "claude-code", sessionId: "s-auth" }),
301
- name: "auth build",
302
- live: false,
303
- runs: 2,
304
- liveRoles: 0,
305
- landed: 5,
306
- lastActivity: "2026-09-25T10:36:00.000Z",
307
- },
308
- runs: [
309
- auth("elsewhere"),
310
- {
311
- id: PLAN4,
312
- title: "Auth plan 4",
313
- repo: "/home/me/api",
314
- createdAt: "2026-09-24T08:00:00.000Z",
315
- continued: null,
316
- continuedIn: null,
317
- budget: null,
318
- milestones: [
319
- { name: "M1", landed: true, what: "schema" },
320
- { name: "M2", landed: true, what: "handlers" },
321
- ],
322
- roles: [
323
- role({
324
- run: PLAN4,
325
- dispatchId: "p2",
326
- name: "reviewer-M2",
327
- rung: "codex:gpt-6-sol#high",
328
- secs: 140,
329
- }),
330
- role({
331
- run: PLAN4,
332
- dispatchId: "p1",
333
- name: "worker-M2.L1",
334
- rung: "codex:gpt-6-sol#medium",
335
- secs: 610,
336
- }),
337
- ],
338
- },
339
- ],
340
- },
341
- ];
342
-
343
- /** The role screen's text: the brief, the reply and the record behind worker-M1.L1 of the jobs screen. */
344
- const FIXTURE_ROLE: Omit<RoleDetail, "run" | "runTitle" | "dispatchId" | "name" | "role" | "rung" | "state"> =
345
- {
346
- brief:
347
- "Read /home/me/.local/share/catherd/runs/jobs-screen/lanes/M1.L1.md\nFast check: bun test test/jobs/list",
348
- reply:
349
- "Done: the list renders every job with its state.\nsrc/jobs/list.tsx:12 — the list\nsrc/jobs/list.test.tsx:4 — its test\nSTATUS: complete — list in place, fast check green",
350
- record: null,
351
- };
352
-
353
- /** The digest `land` writes for M0 of the jobs screen (spec 1.1 §10): what a landed milestone's screen shows. */
354
- export const FIXTURE_DIGEST = [
355
- "# M0 — scaffold the jobs screen",
356
- "",
357
- "Commit 3f2a9c1 · 9 min · landed 2026-09-26T11:57:00.000Z",
358
- "A-lines: A1 the jobs screen lists every job with its state",
359
- "",
360
- "Lanes:",
361
- "- M0.L1 · codex:gpt-6-luna#high",
362
- "- M0.L2 · codex:gpt-6-luna#medium → codex:gpt-6-sol#medium · climbs: fast check red twice",
363
- "",
364
- "Reviewer: reviewer-M0 · 1 finding(s): 0 BLOCKER, 0 BUG, 1 NIT",
365
- "Verifier: PASS (verifier-M0)",
366
- "Evidence: bun test test/jobs",
367
- "Tokens: 1204k in (960k cached) · 58k out · Claude subagents 31k (reported)",
368
- "",
369
- ].join("\n");
370
-
371
- /** The record a finished fixture role would have: what the role screen shows under RECORD. */
372
- function fixtureRecord(r: SessionRun, x: RoleRow): RunRecord {
373
- const start = Date.parse(x.since);
374
- return {
375
- schema: 1,
376
- runId: r.id,
377
- dispatchId: x.dispatchId,
378
- name: x.name,
379
- role: x.role,
380
- lane: x.name.includes("-M") && x.name.includes(".") ? x.name.slice(x.name.indexOf("-") + 1) : null,
381
- backend: x.rung.slice(0, x.rung.indexOf(":")),
382
- rung: x.rung,
383
- attempt: 1,
384
- failoverFrom: null,
385
- thread: "01a0d0d4-d0a6-71a1-983c-82a9169200b4",
386
- status: x.status as RunRecord["status"],
387
- startedAt: x.since,
388
- endedAt: new Date(start + (x.secs ?? 0) * 1000).toISOString(),
389
- secs: x.secs ?? 0,
390
- exitCode: 0,
391
- signal: null,
392
- cliVersion: "0.157.0",
393
- tokens: { input: 812_000, cached: 640_000, output: 41_000 },
394
- costUsd: null,
395
- changedOwned: ["src/jobs/list.tsx", "src/jobs/list.test.tsx"],
396
- violations: [],
397
- replyStatus: x.replyStatus as RunRecord["replyStatus"],
398
- replyWhy: x.replyStatus ? "list in place, fast check green" : null,
399
- threadHeavy: false,
400
- access: "workspace-write",
401
- isolated: false,
402
- images: [],
403
- error: null,
404
- replyPath: `roles/${x.name}/${x.dispatchId}/reply.md`,
405
- };
406
- }
407
-
408
- const BACKENDS = ["claude", "codex", "claude-code", "opencode"];
409
-
410
- /**
411
- * Effects over the fixtures; profile writes stay in memory and are recorded in `writes`. `repo` is the
412
- * directory the TUI stands in and `bindings` the repo bindings, as `liveEffects(repo)` reads them.
413
- */
414
- export interface FixtureOptions {
415
- host?: OrchestrationHost;
416
- runs?: RunSummary[];
417
- sessions?: { row: SessionRow; runs: SessionRun[] }[];
418
- report?: DoctorReport;
419
- repo?: string;
420
- bindings?: Record<string, string>;
421
- /** false: the run folders cannot be watched here, as on a platform without fs.watch */
422
- watchable?: boolean;
423
- }
424
-
425
- /** Spec 1.2 §9: the sources as the `r` dialog shows them, one failing. */
426
- export const FIXTURE_SOURCES: ReturnType<Effects["sources"]> = [
427
- { source: "models-dev", name: "models.dev", age: "2 h ago", error: null },
428
- { source: "arena", name: "Arena (LMArena)", age: "2 h ago", error: null },
429
- { source: "epoch", name: "Epoch AI benchmarks", age: "1 d ago", error: "network error" },
430
- { source: "artificial-analysis", name: "Artificial Analysis", age: "never fetched", error: null },
431
- ];
432
-
433
- export function fixtureEffects(o: FixtureOptions = {}): Effects & {
434
- writes: string[];
435
- /** what a change to a watched run file does: every open watch hears of it */
436
- touch(): void;
437
- /** the run folders watched now */
438
- watched: string[][];
439
- } {
440
- const host = o.host ?? "claude-code";
441
- const runs = o.runs ?? FIXTURE_RUNS;
442
- const sessions = o.sessions ?? FIXTURE_SESSIONS;
443
- const listeners = new Set<() => void>();
444
- const watched: string[][] = [];
445
- const docs = new Map<string, ProfileDoc>([["default", defaultProfileDoc()]]);
446
- let active = "default";
447
- const bindings = new Map(Object.entries(o.bindings ?? {}));
448
- const bound = () => (o.repo && bindings.has(o.repo) ? o.repo : null);
449
- const writes: string[] = [];
450
- /** what the ProfileService returns for writing `after` over `before` */
451
- const result = (
452
- name: string,
453
- before: ProfileDoc,
454
- after: ProfileDoc,
455
- staged: Record<string, string> = {},
456
- ) => {
457
- const p = resolveProfile(after, name, host);
458
- const c = withStaged(loadCatalog({ timings: false }), staged);
459
- const v = validateProfile(p, c, BACKENDS, undefined, host);
460
- // spec 1.2 §6.2, as the ProfileService rules: a save that repairs part of an invalid profile goes through
461
- const was = docs.has(name)
462
- ? validateProfile(resolveProfile(before, name, host), c, BACKENDS, undefined, host)
463
- : null;
464
- return {
465
- saved: repairs(was, v),
466
- ...v,
467
- diff: diffProfiles(resolveProfile(before, name, host), p),
468
- linked: [],
469
- pruned: [],
470
- newSessionNeededFor: [],
471
- };
472
- };
473
- return {
474
- writes,
475
- watched,
476
- touch: () => {
477
- for (const l of listeners) l();
478
- },
479
- host: { host, session: null, conflict: null },
480
- version: "1.0.0",
481
- doctor: async () => o.report ?? FIXTURE_REPORT,
482
- runs: () => ({ rows: runs.map(rowOf), warnings: [] }),
483
- sessions: () => ({ rows: sessions.map((x) => x.row), warnings: [] }),
484
- session(key) {
485
- const x = sessions.find((y) => y.row.key === key);
486
- if (!x) throw new Error(`no session "${key}"`);
487
- return { session: x.row, runs: x.runs, dirs: x.runs.map((r) => `/runs/${r.id}`) };
488
- },
489
- role(run, dispatchId) {
490
- for (const x of sessions)
491
- for (const r of x.runs) {
492
- const role = r.id === run ? r.roles.find((y) => y.dispatchId === dispatchId) : undefined;
493
- if (!role) continue;
494
- return {
495
- run,
496
- runTitle: r.title,
497
- dispatchId,
498
- name: role.name,
499
- role: role.role,
500
- rung: role.rung,
501
- state: role.live ? role.status : "finished",
502
- brief: FIXTURE_ROLE.brief,
503
- reply: role.live ? "" : FIXTURE_ROLE.reply,
504
- record: role.live ? null : fixtureRecord(r, role),
505
- };
506
- }
507
- throw new Error(`no dispatch ${dispatchId} in run ${run}`);
508
- },
509
- milestone(run, name) {
510
- for (const x of sessions)
511
- for (const r of x.runs) {
512
- const m = r.id === run ? r.milestones.find((y) => y.name === name) : undefined;
513
- if (!m) continue;
514
- return { run, runTitle: r.title, ...m, digest: m.landed ? FIXTURE_DIGEST : null };
515
- }
516
- throw new Error(`no milestone "${name}" in run ${run}`);
517
- },
518
- watch(dirs, onChange) {
519
- if (o.watchable === false) return null;
520
- watched.push(dirs);
521
- listeners.add(onChange);
522
- return () => {
523
- listeners.delete(onChange);
524
- watched.splice(watched.indexOf(dirs), 1);
525
- };
526
- },
527
- async cancel(run, name) {
528
- writes.push(`cancel ${run} ${name}`);
529
- return `${name} cancelled`;
530
- },
531
- profiles: () => ({
532
- names: [...docs.keys()].sort(),
533
- active,
534
- here: bindings.get(o.repo ?? "") ?? active,
535
- repo: bound(),
536
- }),
537
- readProfile(name) {
538
- const d = docs.get(name);
539
- if (!d) throw new Error(`no profile named "${name}"`);
540
- return d;
541
- },
542
- catalog: (billing) => ({
543
- catalog: loadCatalog({ timings: false }),
544
- models: catalogQuery({ scoredOnly: false, limit: Number.MAX_SAFE_INTEGER }, billing).models,
545
- }),
546
- validate: (p, c) => validateProfile(p, c, BACKENDS, undefined, host),
547
- enforcement: (rung) => (rung.startsWith("codex:") ? "enforced" : "advisory"),
548
- harnesses: ["codex", "claude-code", "opencode"],
549
- isolation: () => ({}),
550
- agents: (p) =>
551
- (["architect", "verifier"] as const)
552
- .filter((r) => p.roles[r].enabled)
553
- .flatMap((r) =>
554
- p.roles[r].rungs
555
- .filter((x) => x.startsWith("claude:"))
556
- .map((x) => `catherd-${p.name}-${r}-${x.slice(7).replace("#", "-")}`),
557
- ),
558
- async save(name, patch, treatLikes, shown) {
559
- const before = docs.get(name) ?? defaultProfileDoc(name);
560
- // the ProfileService's compare-and-swap: nothing is written over a profile the preview did not show
561
- if (shown !== undefined && !isDeepStrictEqual(before, shown))
562
- return {
563
- saved: false,
564
- errors: [
565
- {
566
- path: CHANGED_ON_DISK,
567
- message: `profile "${name}" changed on disk since it was shown`,
568
- fix: "check the changes and save again",
569
- },
570
- ],
571
- warnings: [],
572
- diff: [],
573
- linked: [],
574
- pruned: [],
575
- newSessionNeededFor: [],
576
- };
577
- const after = applyPatch(before, patch);
578
- const r = result(name, before, after, treatLikes);
579
- if (r.saved) {
580
- docs.set(name, after);
581
- const tl = Object.keys(treatLikes).length ? ` ${JSON.stringify(treatLikes)}` : "";
582
- writes.push(`save ${name} ${JSON.stringify(patch)}${tl}`);
583
- }
584
- return r;
585
- },
586
- activate(name, repo) {
587
- if (repo !== null) {
588
- bindings.set(repo, name);
589
- writes.push(`bind ${name} ${repo}`);
590
- } else {
591
- active = name;
592
- writes.push(`activate ${name}`);
593
- }
594
- return {
595
- linked: [],
596
- pruned: [],
597
- newSessionNeededFor: [`catherd-${name}-architect-claude-opus-5-5-high`],
598
- };
599
- },
600
- create(name, from) {
601
- const doc = { ...(from ? (docs.get(from) as ProfileDoc) : defaultProfileDoc(name)), name };
602
- const r = result(name, doc, doc);
603
- if (r.saved) {
604
- docs.set(name, doc);
605
- writes.push(`create ${name}${from ? ` from ${from}` : ""}`);
606
- }
607
- return r;
608
- },
609
- remove(name) {
610
- if (name === active) throw new Error(`"${name}" is the active profile`);
611
- const at = [...bindings].find(([, p]) => p === name);
612
- if (at) throw new Error(`"${name}" is bound to ${at[0]}`);
613
- docs.delete(name);
614
- writes.push(`remove ${name}`);
615
- return { linked: [], pruned: [], newSessionNeededFor: [] };
616
- },
617
- async refreshCatalog() {
618
- writes.push("refresh");
619
- return [{ backend: "codex", models: 14, fetchedAt: "2026-09-26T12:00:00.000Z" }];
620
- },
621
- async syncSources() {
622
- writes.push("sync");
623
- return {
624
- busy: false,
625
- sources: [],
626
- newlyScored: [],
627
- noLongerNeeded: [],
628
- failed: [],
629
- warnings: [],
630
- unmatched: {},
631
- };
632
- },
633
- sources: () => FIXTURE_SOURCES,
634
- suggest: suggestFor,
635
- rungDetail: rungDetailOf,
636
- };
637
- }