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,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
- }