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,10 +1,12 @@
1
1
  import type { OrchestrationHost } from "./host.ts";
2
2
  import { z } from "zod";
3
3
  import type { Budget } from "./budget.ts";
4
+ import type { Bars } from "./bars.ts";
4
5
  import { BILLING_KEYS, type BillingKey } from "./catalog.ts";
5
6
  import { BILLING_MODES, type BillingMode, DEFAULT_BILLING } from "./cost.ts";
6
7
  import { CatherdError } from "./errors.ts";
7
- import { ADAPTER_IDS, parseRung, tryParseRung } from "./ids.ts";
8
+ import { ADAPTER_IDS, EFFORT_ORDER, type EffortWord, effortRank, parseRung, tryParseRung } from "./ids.ts";
9
+ import { DIFFICULTIES } from "./lane.ts";
8
10
  import { ACCESS, type Access } from "./record.ts";
9
11
  import { DEFAULT_ACCESS, ROLES, type Role } from "./roles.ts";
10
12
  import { isPlain, slug } from "./util.ts";
@@ -46,6 +48,8 @@ const RoleDocSchema = z.looseObject({
46
48
  defaultRung: z.string().optional(),
47
49
  network: z.boolean().optional(),
48
50
  timeouts: z.looseObject({ idleMin: positive.optional(), wallMin: positive.optional() }).optional(),
51
+ // plan 33: unknown, so a hand edit never makes the file unreadable; resolveProfile reads what it can
52
+ bars: z.unknown().optional(),
49
53
  });
50
54
 
51
55
  export const ProfileDocSchema = z.looseObject({
@@ -57,6 +61,9 @@ export const ProfileDocSchema = z.looseObject({
57
61
  roles: z.record(z.string(), RoleDocSchema).optional(),
58
62
  harness: z.record(z.string(), z.looseObject({ isolated: z.boolean().optional() })).optional(),
59
63
  failover: z.record(z.string(), z.string()).optional(),
64
+ // unknown values, like `bars`: an entry that is no effort word never makes the file unreadable
65
+ maxEffort: z.record(z.string(), z.unknown()).optional(),
66
+ bars: z.unknown().optional(),
60
67
  budget: z
61
68
  .looseObject({ minutes: positive.optional(), tokens: positive.optional(), usd: positive.optional() })
62
69
  .optional(),
@@ -66,9 +73,20 @@ export const ProfileDocSchema = z.looseObject({
66
73
  .looseObject({ heavy: z.union([z.number().int().positive(), z.literal("cpus/2")]).optional() })
67
74
  .optional(),
68
75
  notify: z.array(z.string()).optional(),
76
+ // plan 35: unknown, so a hand edit never makes the file unreadable; resolveProfile reads what it can
77
+ limits: z.unknown().optional(),
69
78
  });
70
79
  export type ProfileDoc = z.infer<typeof ProfileDocSchema>;
71
80
 
81
+ /** plan 35: the profile's memory numbers for starting milestones side by side (megabytes). */
82
+ const MEMORY_KEYS = ["reserveMb", "perMilestoneMb", "floorMb"] as const;
83
+ type MemoryKey = (typeof MEMORY_KEYS)[number];
84
+ const positiveInt = (v: unknown): v is number => typeof v === "number" && Number.isInteger(v) && v > 0;
85
+ const storedMemory = (doc: ProfileDoc): Record<string, unknown> => {
86
+ const memory = isPlain(doc.limits) ? doc.limits.memory : undefined;
87
+ return isPlain(memory) ? memory : {};
88
+ };
89
+
72
90
  const OBJECTIVES = ["cost", "speed"] as const;
73
91
  const JEV_USES = ["auto", "off"] as const;
74
92
  const known = <T extends string>(values: readonly T[], v: string | undefined): v is T =>
@@ -86,6 +104,16 @@ const STORED_FALLBACK = {
86
104
  billing: (key: string): BillingMode => DEFAULT_BILLING[key as BillingKey] ?? "metered",
87
105
  } as const;
88
106
 
107
+ const barOk = (k: string, v: unknown): boolean =>
108
+ (DIFFICULTIES as readonly string[]).includes(k) && typeof v === "number" && v >= 0 && v <= 100;
109
+
110
+ const isEffort = (v: unknown): v is EffortWord => typeof v === "string" && effortRank(v) >= 0;
111
+
112
+ /** The entries of a stored `bars` this catherd can read; anything else is not a bar. */
113
+ function readBars(v: unknown): Bars {
114
+ return isPlain(v) ? (Object.fromEntries(Object.entries(v).filter((e) => barOk(e[0], e[1]))) as Bars) : {};
115
+ }
116
+
89
117
  /** A stored value this catherd does not know, with what it is read as. */
90
118
  export interface UnknownValue {
91
119
  path: string;
@@ -103,9 +131,25 @@ export function unknownValues(doc: ProfileDoc): UnknownValue[] {
103
131
  check("jev.use", JEV_USES, doc.jev?.use, STORED_FALLBACK.jevUse);
104
132
  for (const [k, v] of Object.entries(doc.billing ?? {}))
105
133
  check(`billing.${k}`, BILLING_MODES, v, STORED_FALLBACK.billing(k));
134
+ for (const [k, v] of Object.entries(doc.maxEffort ?? {}))
135
+ if (!isEffort(v)) {
136
+ const value = typeof v === "string" ? v : JSON.stringify(v);
137
+ out.push({ path: `maxEffort.${k}`, value, readAs: "no ceiling" });
138
+ }
139
+ const checkBars = (path: string, v: unknown, readAs: string) => {
140
+ if (v === undefined) return;
141
+ if (!isPlain(v)) return void out.push({ path, value: JSON.stringify(v), readAs });
142
+ for (const [k, x] of Object.entries(v))
143
+ if (!barOk(k, x)) out.push({ path: `${path}.${k}`, value: String(x), readAs });
144
+ };
145
+ checkBars("bars", doc.bars, "the shipped percentile");
146
+ for (const role of ROLES) checkBars(`roles.${role}.bars`, doc.roles?.[role]?.bars, "the profile's bar");
106
147
  for (const role of ROLES)
107
148
  check(`roles.${role}.access`, ACCESS, doc.roles?.[role]?.access, STORED_FALLBACK.access);
108
149
  for (const n of doc.notify ?? []) check("notify", NOTIFY, n, "skipped");
150
+ for (const [k, v] of Object.entries(storedMemory(doc)))
151
+ if (!(MEMORY_KEYS as readonly string[]).includes(k) || !positiveInt(v))
152
+ out.push({ path: `limits.memory.${k}`, value: JSON.stringify(v), readAs: "the default" });
109
153
  return out;
110
154
  }
111
155
 
@@ -120,6 +164,8 @@ export type RoleConfig = {
120
164
  network?: false;
121
165
  /** plan 23: this role's own idle and wall timeouts, over the profile's `timeouts` (a long gate, a slow model) */
122
166
  timeouts?: { idleMin?: number; wallMin?: number };
167
+ /** plan 33: this role's own bar percentiles, over the profile's `bars` (decision 11) */
168
+ bars?: Bars;
123
169
  };
124
170
 
125
171
  /** The idle and wall timeouts a role runs under: its own `timeouts`, else the profile's. */
@@ -140,11 +186,17 @@ export interface Profile {
140
186
  roles: Record<Role, RoleConfig>;
141
187
  harness: Record<string, { isolated: boolean }>;
142
188
  failover: Record<string, string>;
189
+ /** plan 33: `<backend>:<model>` → the highest effort routing starts it at; a model not named has no ceiling */
190
+ maxEffort: Record<string, EffortWord>;
191
+ /** plan 33: bar percentiles per difficulty, over the shipped ones (`percentilesOf`) */
192
+ bars: Bars;
143
193
  budget: Budget;
144
194
  timeouts: { idleMin: number; wallMin: number };
145
195
  preflight: { confirm: boolean };
146
196
  lock: { heavy: number | "cpus/2" };
147
197
  notify: NotifyMoment[];
198
+ /** plan 35: only the memory numbers the profile sets; the defaults are `MEMORY_LIMITS` */
199
+ limits: { memory: Partial<Record<MemoryKey, number>> };
148
200
  }
149
201
 
150
202
  const LUNA_HIGH = "codex:gpt-6-luna#high";
@@ -168,14 +220,15 @@ export const BUILTIN_ROLES: Record<Role, RoleConfig> = {
168
220
  artist: { enabled: true, access: DEFAULT_ACCESS.artist, rungs: [SOL("medium")] },
169
221
  writer: { enabled: true, access: DEFAULT_ACCESS.writer, rungs: [LUNA_HIGH] },
170
222
  researcher: { enabled: true, access: DEFAULT_ACCESS.researcher, rungs: [LUNA_HIGH] },
223
+ judge: { enabled: true, access: DEFAULT_ACCESS.judge, rungs: ["claude-code:claude-opus-5-5#medium"] },
171
224
  };
172
225
 
173
226
  /**
174
- * Spec 1.1 §11: each shipped worker rung fails over to the best stand-in the shipped catalog offers
175
- * (`rankStandIns` over `catalogRungs`, pinned by test/domain/failover.test.ts): on another quota, paid
176
- * from a plan, and no downgrade on the dims the rung's bars use. Go serves GPT-6 Luna itself; Kimi K3
177
- * borrows Sol medium's scores (a shipped treat-like). Sol high and xhigh have no such stand-in on the
178
- * owner's plans, so they have none: a limit on them pauses the lane instead of dropping it a tier.
227
+ * Spec 1.1 §11: each shipped worker rung fails over to the best stand-in the Slopalytics index offers
228
+ * (`rankStandIns` over `catalogPairs`, pinned by test/domain/failover.test.ts): on another quota, paid from a
229
+ * plan, and at least as intelligent. Go serves GPT-6 Luna itself; Kimi K3 stands on its own index, above Sol
230
+ * medium. Sol high and xhigh have no such stand-in on the owner's plans, so they have none: a limit on them
231
+ * pauses the lane instead of dropping it a tier.
179
232
  */
180
233
  export const DEFAULT_FAILOVER: Record<string, string> = {
181
234
  [LUNA_HIGH]: "opencode:opencode-go/gpt-6-luna#high",
@@ -183,63 +236,22 @@ export const DEFAULT_FAILOVER: Record<string, string> = {
183
236
  };
184
237
 
185
238
  /**
186
- * Cursor's live slug and efforts for a paired family (`cursor-agent models`, 2026-10-01). Empty efforts means the
187
- * bare slug.
239
+ * Plan 33 Ruling 8: t3herd's effort caps under the shipped catalog's ids, on every backend that lists the model.
240
+ * The default profile writes them, so a failover to any backend can never start a `#max` Opus; a profile without
241
+ * the key has no ceiling.
188
242
  */
189
- const CURSOR_PAIR: Record<string, { id: string; efforts: readonly string[] }> = {
190
- "grok-4.7": { id: "grok-4.7", efforts: ["low", "medium", "high", "xhigh"] },
191
- "grok-4.6": { id: "cursor-grok-4.6", efforts: ["low", "medium", "high", "xhigh"] },
192
- "grok-4.5": { id: "cursor-grok-4.5", efforts: ["low", "medium", "high"] },
193
- "gemini-3.8-flash": { id: "gemini-3.8-flash", efforts: ["low", "medium", "high"] },
194
- "gemini-3.7-flash": { id: "gemini-3.7-flash", efforts: ["low", "medium", "high"] },
195
- "gemini-3.6-flash": { id: "gemini-3.6-flash", efforts: ["minimal", "low", "medium", "high"] },
196
- "gemini-3.1-pro": { id: "gemini-3.1-pro", efforts: [] },
243
+ export const DEFAULT_MAX_EFFORT: Readonly<Record<string, EffortWord>> = {
244
+ "claude:claude-opus-5-5": "high",
245
+ "claude-code:claude-opus-5-5": "high",
246
+ "opencode:opencode/claude-opus-5-5": "high",
247
+ "cursor:claude-opus-5-5": "high",
248
+ "claude:claude-sonnet-5-5": "high",
249
+ "claude-code:claude-sonnet-5-5": "high",
250
+ "opencode:opencode/claude-sonnet-5-5": "high",
251
+ "codex:gpt-6.1-sol": "xhigh",
197
252
  };
198
253
 
199
- /** The Cursor effort a paired rung can actually run: the same one when listed, else `high`, else the bare slug. */
200
- function cursorPairEffort(model: string, effort: string): string {
201
- const list = CURSOR_PAIR[model]?.efforts ?? [];
202
- if (list.length === 0) return "default";
203
- if (effort !== "default" && list.includes(effort)) return effort;
204
- return list.includes("high") ? "high" : (list[0] as string);
205
- }
206
-
207
- /**
208
- * Spec 1.3 §7.3: the same model through two backends bills two pools, so each stands in for the other on a
209
- * limit, at the same effort where both list it. Otherwise the stand-in runs at `high`: Grok 4.5 `xhigh` at Cursor
210
- * `high`, Cursor's Gemini 3.6 `minimal` at agy `high`; Gemini 3.1 Pro's bare Cursor slug pairs with agy `high`.
211
- * Grok and Gemini are paired with Cursor. Consulted after the profile's own failover and never written into a
212
- * profile, where a pair whose rung is on no ladder would warn "never runs"; never a stand-in for a shipped Codex,
213
- * Claude or opencode rung.
214
- */
215
- export const PAIRED_FAILOVER: Record<string, string> = Object.fromEntries(
216
- (
217
- [
218
- ["grok", ["grok-4.7", "grok-4.6", "grok-4.5"], ["low", "medium", "high", "xhigh"]],
219
- [
220
- "antigravity",
221
- ["gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash"],
222
- ["low", "medium", "high"],
223
- ],
224
- ["antigravity", ["gemini-3.1-pro"], ["low", "high"]],
225
- ] as const
226
- ).flatMap(([backend, models, efforts]) =>
227
- models.flatMap((m) => {
228
- const cursor = CURSOR_PAIR[m] as { id: string; efforts: readonly string[] };
229
- const back = ["default", ...efforts].map(
230
- (e) => [`${backend}:${m}#${e}`, `cursor:${cursor.id}#${cursorPairEffort(m, e)}`] as const,
231
- );
232
- // `#default` too, as on the other side: a Cursor rung with no effort flag fails over the same way
233
- const fore = [...new Set(["default", ...cursor.efforts])].map((e) => {
234
- const there = e !== "default" && (efforts as readonly string[]).includes(e) ? e : "high";
235
- return [`cursor:${cursor.id}#${e}`, `${backend}:${m}#${there}`] as const;
236
- });
237
- return [...back, ...fore];
238
- }),
239
- ),
240
- );
241
-
242
- /** The five billing keys spec §7.1 writes out; cursor and grok arrive with their backends. */
254
+ /** The five billing keys spec §7.1 writes out; the rest default. */
243
255
  const WRITTEN_BILLING: BillingKey[] = ["codex", "claude", "claude-code", "opencode-go", "opencode"];
244
256
 
245
257
  /** Stored defaults for init/new; architect/verifier rungs resolve from the current host. */
@@ -257,6 +269,7 @@ export function defaultProfileDoc(name = "default"): ProfileDoc {
257
269
  opencode: { isolated: false },
258
270
  },
259
271
  failover: { ...DEFAULT_FAILOVER },
272
+ maxEffort: { ...DEFAULT_MAX_EFFORT },
260
273
  budget: {},
261
274
  timeouts: { idleMin: 15, wallMin: 90 },
262
275
  preflight: { confirm: false },
@@ -265,10 +278,10 @@ export function defaultProfileDoc(name = "default"): ProfileDoc {
265
278
  };
266
279
  }
267
280
 
268
- /** Remove only the four architect/verifier execution choices, preserving other stored fields. */
281
+ /** Remove only the architect, verifier and judge execution choices, preserving other stored fields. */
269
282
  export function hostDefaultsDoc(doc: ProfileDoc): ProfileDoc {
270
283
  const next = structuredClone(doc);
271
- for (const role of ["architect", "verifier"] as const) {
284
+ for (const role of ["architect", "verifier", "judge"] as const) {
272
285
  const r = next.roles?.[role];
273
286
  if (r) {
274
287
  delete r.rungs;
@@ -297,6 +310,9 @@ export function resolveProfile(doc: ProfileDoc, name: string, host: Orchestratio
297
310
  ? []
298
311
  : b.rungs;
299
312
  }
313
+ // the judge is Opus by default; on a Codex host it follows the host like the architect and the verifier do
314
+ if (role === "judge" && d?.rungs === undefined && host === "codex")
315
+ omitted = ["codex:gpt-6.1-sol#medium"];
300
316
  // a role that lists its own rungs never inherits the built-in default rung, which may not be among them
301
317
  const defaultRung = d?.defaultRung ?? (d?.rungs === undefined ? b.defaultRung : undefined);
302
318
  roles[role] = {
@@ -318,6 +334,7 @@ export function resolveProfile(doc: ProfileDoc, name: string, host: Orchestratio
318
334
  },
319
335
  }
320
336
  : {}),
337
+ ...(Object.keys(readBars(d?.bars)).length > 0 ? { bars: readBars(d?.bars) } : {}),
321
338
  };
322
339
  }
323
340
  const harness: Profile["harness"] = {};
@@ -340,11 +357,23 @@ export function resolveProfile(doc: ProfileDoc, name: string, host: Orchestratio
340
357
  roles,
341
358
  harness,
342
359
  failover: { ...doc.failover },
360
+ maxEffort: Object.fromEntries(
361
+ Object.entries(doc.maxEffort ?? {}).filter((e): e is [string, EffortWord] => isEffort(e[1])),
362
+ ),
363
+ bars: readBars(doc.bars),
343
364
  budget,
344
365
  timeouts: { idleMin: doc.timeouts?.idleMin ?? 15, wallMin: doc.timeouts?.wallMin ?? 90 },
345
366
  preflight: { confirm: doc.preflight?.confirm ?? false },
346
367
  lock: { heavy: doc.lock?.heavy ?? "cpus/2" },
347
368
  notify: doc.notify === undefined ? [...NOTIFY] : doc.notify.filter((n) => known(NOTIFY, n)),
369
+ limits: {
370
+ memory: Object.fromEntries(
371
+ MEMORY_KEYS.flatMap((k) => {
372
+ const v = storedMemory(doc)[k];
373
+ return positiveInt(v) ? [[k, v]] : [];
374
+ }),
375
+ ),
376
+ },
348
377
  };
349
378
  }
350
379
 
@@ -357,6 +386,10 @@ export function agentName(profile: string, role: Role, rung: string): string {
357
386
  // Patches: what profile_set and `catherd profile set` accept. Strict at every level, so a misspelt key is
358
387
  // refused with E_INPUT_INVALID instead of dropped (plan-2 review m9). `null` removes a map entry or a nullable
359
388
  // field (a role's `network` among them: absent means granted).
389
+ const BarsPatchSchema = z
390
+ .partialRecord(z.enum(DIFFICULTIES), z.number().min(0).max(100).nullable())
391
+ .nullable();
392
+
360
393
  const RolePatchSchema = z
361
394
  .strictObject({
362
395
  enabled: z.boolean(),
@@ -368,6 +401,7 @@ const RolePatchSchema = z
368
401
  .strictObject({ idleMin: positive.nullable(), wallMin: positive.nullable() })
369
402
  .partial()
370
403
  .nullable(),
404
+ bars: BarsPatchSchema,
371
405
  })
372
406
  .partial();
373
407
 
@@ -378,6 +412,13 @@ export const ProfilePatchSchema = z.strictObject({
378
412
  roles: z.partialRecord(z.enum(ROLES), RolePatchSchema).optional(),
379
413
  harness: z.partialRecord(z.enum(HARNESS_KEYS), z.strictObject({ isolated: z.boolean() })).optional(),
380
414
  failover: z.record(RungSchema, RungSchema.nullable()).optional(),
415
+ maxEffort: z
416
+ .record(
417
+ z.string().refine((k) => tryParseRung(`${k}#x`) !== null, "a key is <backend>:<model>"),
418
+ z.enum(EFFORT_ORDER).nullable(),
419
+ )
420
+ .optional(),
421
+ bars: BarsPatchSchema.optional(),
381
422
  budget: z
382
423
  .strictObject({ minutes: positive.nullable(), tokens: positive.nullable(), usd: positive.nullable() })
383
424
  .partial()
@@ -386,6 +427,18 @@ export const ProfilePatchSchema = z.strictObject({
386
427
  preflight: z.strictObject({ confirm: z.boolean() }).optional(),
387
428
  lock: z.strictObject({ heavy: z.union([z.number().int().positive(), z.literal("cpus/2")]) }).optional(),
388
429
  notify: z.array(z.enum(NOTIFY)).optional(),
430
+ limits: z
431
+ .strictObject({
432
+ memory: z
433
+ .strictObject({
434
+ reserveMb: z.number().int().positive().nullable(),
435
+ perMilestoneMb: z.number().int().positive().nullable(),
436
+ floorMb: z.number().int().positive().nullable(),
437
+ })
438
+ .partial(),
439
+ })
440
+ .partial()
441
+ .optional(),
389
442
  });
390
443
  export type ProfilePatch = z.infer<typeof ProfilePatchSchema>;
391
444
 
@@ -406,7 +459,7 @@ export function applyPatch(doc: ProfileDoc, patch: ProfilePatch): ProfileDoc {
406
459
  }
407
460
 
408
461
  /** Map fields whose keys are rungs or billing keys, which may hold dots: the rest of the path is one key. */
409
- const MAP_FIELDS = new Set(["failover", "billing"]);
462
+ const MAP_FIELDS = new Set(["failover", "billing", "maxEffort"]);
410
463
  const LIST_FIELDS = (keys: string[]) =>
411
464
  (keys[0] === "roles" && keys[2] === "rungs") || (keys[0] === "notify" && keys.length === 1);
412
465
 
@@ -3,11 +3,11 @@ import { z } from "zod";
3
3
  export const ACCESS = ["read-only", "workspace-write", "full"] as const;
4
4
  export type Access = (typeof ACCESS)[number];
5
5
 
6
- const RUN_STATUSES = ["ok", "failed", "limit", "cli-too-old", "timeout", "cancelled"] as const;
6
+ export const RUN_STATUSES = ["ok", "failed", "limit", "cli-too-old", "timeout", "cancelled"] as const;
7
7
  export type RunStatus = (typeof RUN_STATUSES)[number];
8
8
 
9
9
  /** plan 23: `flaky`, a worker's word for a check that failed, then passed when run alone */
10
- const REPLY_STATUSES = ["complete", "partial", "blocked", "refused", "flaky"] as const;
10
+ export const REPLY_STATUSES = ["complete", "partial", "blocked", "refused", "flaky"] as const;
11
11
  export type ReplyStatus = (typeof REPLY_STATUSES)[number];
12
12
 
13
13
  export interface Tokens {
@@ -70,6 +70,17 @@ export const RunRecordSchema = z.looseObject({
70
70
  replyWhy: z.string().nullable(),
71
71
  /** plan 23: the reply's ENV: line, when the environment stopped the role; absent otherwise */
72
72
  environment: z.string().optional(),
73
+ /** a verifier's `HEAD: <sha>` line: the commit it checked (lower-case hex, 7-64 characters) */
74
+ head: z
75
+ .string()
76
+ .regex(/^[0-9a-f]{7,64}$/)
77
+ .optional(),
78
+ /** a worker's `EXTRA: <path> — <why>` lines: files outside its lane it had to change */
79
+ extras: z.array(z.object({ path: z.string(), why: z.string() })).optional(),
80
+ /** a fix-round worker's `REJECTED: <finding> — <why>` lines */
81
+ rejected: z.array(z.object({ finding: z.string(), why: z.string() })).optional(),
82
+ /** a worker's `DESIGN:` lines: where its plan task conflicts with the code */
83
+ design: z.array(z.string()).optional(),
73
84
  threadHeavy: z.boolean(),
74
85
  access: z.enum(ACCESS),
75
86
  isolated: z.boolean(),
@@ -101,3 +112,43 @@ export function parseReplyStatus(reply: string): { status: ReplyStatus | null; w
101
112
  const m = STATUS_LINE.exec(last);
102
113
  return m ? { status: m[1] as ReplyStatus, why: (m[2] ?? "").trim() } : { status: null, why: null };
103
114
  }
115
+
116
+ /** The last `HEAD: <hex>` line of a reply outside code fences, lower-cased: the commit a verifier says it checked. */
117
+ export function parseReplyHead(reply: string): string | null {
118
+ let fenced = false;
119
+ let head: string | null = null;
120
+ for (const line of reply.split("\n")) {
121
+ if (/^\s*(```|~~~)/.test(line)) fenced = !fenced;
122
+ else if (!fenced) {
123
+ const m = /^[ \t]*HEAD:[ \t]*([0-9a-fA-F]{7,64})[ \t]*$/.exec(line);
124
+ if (m) head = (m[1] as string).toLowerCase();
125
+ }
126
+ }
127
+ return head;
128
+ }
129
+
130
+ /** `<LABEL>: <a> — <b>` lines (optional bullet; the separator is `—`, `–` or `-` with whitespace around it). */
131
+ function labelled(reply: string, label: string): [string, string][] {
132
+ const re = new RegExp(
133
+ `^[ \\t]*(?:[-*][ \\t]*)?${label}:[ \\t]*(\\S.*?)(?:[ \\t]+(?:—|–|-)[ \\t]+(.*?))?[ \\t]*$`,
134
+ "gm",
135
+ );
136
+ return [...reply.matchAll(re)].map((m) => [m[1] as string, m[2] ?? ""]);
137
+ }
138
+
139
+ /** Every `EXTRA: <path> — <why>` line: a file outside the lane the worker had to change. */
140
+ export function parseReplyExtras(reply: string): { path: string; why: string }[] {
141
+ return labelled(reply, "EXTRA").map(([path, why]) => ({ path: path.replace(/`/g, ""), why }));
142
+ }
143
+
144
+ /** Every `REJECTED: <finding> — <why>` line of a fix round. */
145
+ export function parseReplyRejections(reply: string): { finding: string; why: string }[] {
146
+ return labelled(reply, "REJECTED").map(([finding, why]) => ({ finding, why }));
147
+ }
148
+
149
+ /** The trimmed value of every `DESIGN:` line: a conflict between a plan task and the code. */
150
+ export function parseReplyDesign(reply: string): string[] {
151
+ return [...reply.matchAll(/^[ \t]*(?:[-*][ \t]*)?DESIGN:[ \t]*(\S.*?)[ \t]*$/gm)].map(
152
+ (m) => m[1] as string,
153
+ );
154
+ }