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
@@ -2,7 +2,15 @@ import { resolve } from "node:path";
2
2
  import { defineCommand } from "citty";
3
3
  import { CatherdError } from "../domain/errors.ts";
4
4
  import { gateEnvLines, readGateEnv, removeGateEnv, setGateEnv } from "../services/gate-env.ts";
5
- import { addKnowledge, knowledgeLines, knowledgePath, readKnowledge } from "../services/run-service.ts";
5
+ import { ROLES, type Role } from "../domain/roles.ts";
6
+ import {
7
+ addKnowledge,
8
+ knowledgeLines,
9
+ knowledgePath,
10
+ readKnowledge,
11
+ removeKnowledge,
12
+ standingRules,
13
+ } from "../services/run-service.ts";
6
14
  import { JSON_ARG, printJson, refuseInRole } from "./cli-kit.ts";
7
15
 
8
16
  /** `--repo`, else the repository the command runs in, as run_start and read_knowledge resolve it. */
@@ -13,8 +21,26 @@ const repoOf = (repo: string | undefined): string => resolve(repo ?? process.cwd
13
21
 
14
22
  const show = defineCommand({
15
23
  meta: { name: "show", description: "Print what catherd knows about the repo (its knowledge.md)" },
16
- args: { ...REPO_ARG, ...JSON_ARG },
24
+ args: {
25
+ ...REPO_ARG,
26
+ role: { type: "string", description: "only the rules that reach this role" },
27
+ ...JSON_ARG,
28
+ },
17
29
  async run({ args }) {
30
+ if (args.role !== undefined) {
31
+ if (!(ROLES as readonly string[]).includes(args.role))
32
+ throw new CatherdError("E_INPUT_INVALID", `"${args.role}" is not a role`, {
33
+ fix: `pass one of ${ROLES.join(", ")}`,
34
+ });
35
+ const rules = await standingRules(repoOf(args.repo), args.role as Role);
36
+ if (args.json) {
37
+ const at = await knowledgePath(repoOf(args.repo));
38
+ return printJson({ ...at, role: args.role, rules });
39
+ }
40
+ const out = rules || "catherd: no knowledge recorded yet for this repo";
41
+ process.stdout.write(`${out}\n`);
42
+ return;
43
+ }
18
44
  if (args.json) return printJson(await knowledgeLines(repoOf(args.repo)));
19
45
  const text = await readKnowledge(repoOf(args.repo));
20
46
  process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
@@ -36,6 +62,21 @@ const add = defineCommand({
36
62
  },
37
63
  });
38
64
 
65
+ const rm = defineCommand({
66
+ meta: {
67
+ name: "rm",
68
+ description: "Remove the one line of the repo's knowledge.md that contains the text",
69
+ },
70
+ args: {
71
+ text: { type: "positional", required: true, description: "text of the one line to remove" },
72
+ ...REPO_ARG,
73
+ },
74
+ async run({ args }) {
75
+ refuseInRole(process.env, "catherd knowledge rm");
76
+ console.log(await removeKnowledge(repoOf(args.repo), args.text));
77
+ },
78
+ });
79
+
39
80
  const path = defineCommand({
40
81
  meta: { name: "path", description: "Print the path of the repo's knowledge.md (it may not exist yet)" },
41
82
  args: { ...REPO_ARG, ...JSON_ARG },
@@ -119,7 +160,7 @@ const env = defineCommand({
119
160
  export const knowledgeCommand = defineCommand({
120
161
  meta: {
121
162
  name: "knowledge",
122
- description: "What catherd knows about a repo: show, add or path, and its gate environment (env)",
163
+ description: "What catherd knows about a repo: show, add, rm or path, and its gate environment (env)",
123
164
  },
124
- subCommands: { show, add, path, env },
165
+ subCommands: { show, add, rm, path, env },
125
166
  });
@@ -3,7 +3,7 @@ import { defineCommand } from "citty";
3
3
  import { CatherdError } from "../domain/errors.ts";
4
4
  import { scrubSecrets } from "../infra/env.ts";
5
5
  import { gitToplevel } from "../infra/git.ts";
6
- import { heavySlots, withHeavySlot, withRoleLock } from "../infra/heavy-lock.ts";
6
+ import { heavySlots, LOCK_HELD_ENV, withHeavySlot, withRoleLock } from "../infra/heavy-lock.ts";
7
7
  import { activityReporter, DISPATCH_ID_ENV } from "../infra/lock-activity.ts";
8
8
  import { killGroup } from "../infra/proc.ts";
9
9
  import { activeName, readProfileDoc } from "../services/profile-store.ts";
@@ -138,8 +138,7 @@ export function roleLockOwnerOf(run: string | undefined, env: NodeJS.ProcessEnv)
138
138
  return fromRole ? `run:${fromRole}` : `process:${process.pid}`;
139
139
  }
140
140
 
141
- /** Set in a locked command's env: a `catherd lock` inside it runs at once, holding nothing more. */
142
- export const LOCK_HELD_ENV = "CATHERD_LOCK_HELD";
141
+ export { LOCK_HELD_ENV };
143
142
 
144
143
  export const lockCommand = defineCommand({
145
144
  meta: {
@@ -194,9 +193,9 @@ export const lockCommand = defineCommand({
194
193
  // killed before its first command
195
194
  const activity = activityReporter(process.env[DISPATCH_ID_ENV]);
196
195
  const waiting = activity ? { onWait: activity.tick } : {};
197
- const run = () =>
196
+ const run = (_slot: number, tmp: string) =>
198
197
  runForwarding(argv, {
199
- extraEnv: { [LOCK_HELD_ENV]: "1" },
198
+ extraEnv: { [LOCK_HELD_ENV]: "1", TMPDIR: tmp, GOTMPDIR: tmp },
200
199
  ...(activity ? { onOutput: activity.tick } : {}),
201
200
  });
202
201
  const role = args.role;
@@ -213,10 +212,10 @@ export const lockCommand = defineCommand({
213
212
  ? await withRoleLock(
214
213
  role,
215
214
  roleLockOwnerOf(args.run, process.env),
216
- () => withHeavySlot(slots, run, waiting),
215
+ () => withHeavySlot(slots, run, { ...waiting, tmpBase: process.env.TMPDIR }),
217
216
  waiting,
218
217
  )
219
- : await withHeavySlot(slots, run, waiting);
218
+ : await withHeavySlot(slots, run, { ...waiting, tmpBase: process.env.TMPDIR });
220
219
  } finally {
221
220
  activity?.done();
222
221
  }
@@ -6,6 +6,7 @@ import { ID_PATTERN } from "../../domain/ids.ts";
6
6
  import { ROLE_MCP_SERVER, type RoleMcpContext, roleMcpTools } from "../../domain/role-tools.ts";
7
7
  import { log } from "../../infra/log.ts";
8
8
  import { gateCheckOrList, gatePass } from "../../services/gate-service.ts";
9
+ import { runMilestoneCheck } from "../../services/milestone-check.ts";
9
10
  import type { Deps } from "../../services/ports.ts";
10
11
  import { readKnowledge, readRunFile, writeRunFile } from "../../services/run-service.ts";
11
12
  import { findRun } from "../../services/run-store.ts";
@@ -103,6 +104,23 @@ export function buildRoleServer(context: RoleMcpContext, deps: Deps = defaultDep
103
104
  (a) => call("gate_pass", () => gatePass(deps, a)),
104
105
  );
105
106
  }
107
+ if (tools.has("gate_run"))
108
+ server.registerTool(
109
+ "gate_run",
110
+ {
111
+ description:
112
+ "The verifier's full check of a milestone, run by catherd on the committed HEAD in a detached worker under the verifier lock and a heavy slot: { running: false, record (exit, failing lines, log, secrets, rerunOf), missing, audit } once done; land reads that record. The same run, milestone, command and HEAD answers the recorded result (force: true runs it again). Refused while tracked files are uncommitted. Past 10 minutes it answers { running: true }: call it again to wait for the same run. at: a checkout of the run's base you prepared, to record the base's own failures, which then do not block.",
113
+ inputSchema: {
114
+ run: boundRun,
115
+ milestone: z.string().regex(ID_PATTERN),
116
+ command: z.string().min(1),
117
+ at: z.string().min(1).optional(),
118
+ force: z.boolean().optional(),
119
+ },
120
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
121
+ },
122
+ (a) => call("gate_run", () => runMilestoneCheck(a)),
123
+ );
106
124
  return server;
107
125
  }
108
126
 
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
5
5
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { sessionKey, type HostContext } from "../../domain/host.ts";
7
- import { type CatherdError, errorMessage, isCatherdError } from "../../domain/errors.ts";
7
+ import { type CatherdError, errorMessage } from "../../domain/errors.ts";
8
8
  import {
9
9
  isCoordinatorTool,
10
10
  refusedForRole,
@@ -14,21 +14,14 @@ import {
14
14
  import { takeBootLock } from "../../infra/boot-lock.ts";
15
15
  import { resolveHost } from "../../infra/host-context.ts";
16
16
  import { log } from "../../infra/log.ts";
17
- import { assertRoleMay } from "../../services/role-access.ts";
18
17
  import { currentSession } from "../../services/sessions.ts";
19
18
  import { startNotifier } from "../../services/notifier.ts";
20
19
  import type { Deps } from "../../services/ports.ts";
21
20
  import { adoptOwned, reconcileAll } from "../../services/reconcile.ts";
22
- import { backgroundSync } from "../../services/source-sync.ts";
21
+ import { refreshIndex } from "../../services/catalog-service.ts";
23
22
  import { defaultDeps } from "../deps.ts";
24
- import { registerDispatchTools } from "./dispatch-tools.ts";
25
- import { registerLaneTools } from "./lane-tools.ts";
26
- import { registerProtocolTools } from "./protocol-tools.ts";
27
- import { registerPushTools } from "./push-tools.ts";
28
23
  import { handle, sdkToolError, toolOf } from "./result.ts";
29
- import { registerRunTools } from "./run-tools.ts";
30
- import { registerSetupTools } from "./setup-tools.ts";
31
- import { registerWorkspaceTools } from "./workspace-tools.ts";
24
+ import { registerSessionTools } from "./session-tools.ts";
32
25
 
33
26
  type ObserveSession = (context: Deps) => (() => void) | undefined;
34
27
 
@@ -45,26 +38,12 @@ type RequestHandler = (
45
38
  extra: unknown,
46
39
  ) => unknown;
47
40
 
48
- /** The role server's tools that act on a run: in a role they are bound as their CLI forms are (assertRoleMay). */
49
- const RUN_BOUND_TOOLS = new Set(["read_run_file", "write_run_file", "gate_check", "gate_pass"]);
41
+ /** How often a session looks for the new reports of the 2.0 runs it owns. */
42
+ const REPORT_POLL_MS = 3_000;
50
43
 
51
- /**
52
- * Why a role process may not make this call on the full server, else null (plan 21 review, finding 3): a
53
- * coordinator tool; `record_agent_run`, which could fabricate a verifier's ok; or a run-bound tool on another
54
- * run or outside its role's tool set, as `catherd run-file` and `catherd gate` refuse it.
55
- */
56
- function roleRefusal(name: string, args: unknown, role: RoleScope): CatherdError | null {
57
- if (refusedForRole(name, args, role) || name === "record_agent_run") return roleScopeError(name, role);
58
- if (!RUN_BOUND_TOOLS.has(name)) return null;
59
- const run = (args as { run?: unknown } | undefined)?.run;
60
- try {
61
- assertRoleMay(role, typeof run === "string" ? run : "", name);
62
- return null;
63
- } catch (e) {
64
- if (isCatherdError(e)) return e;
65
- throw e;
66
- }
67
- }
44
+ /** Why a role process may not make this call on the full server (plan 21 review, finding 3), else null: a coordinator tool. */
45
+ const roleRefusal = (name: string, args: unknown, role: RoleScope): CatherdError | null =>
46
+ refusedForRole(name, args, role) ? roleScopeError(name, role) : null;
68
47
 
69
48
  /**
70
49
  * Spec 1.5 plan 21: in a role's process (`deps.role`) a coordinator tool call is refused with E_ROLE_SCOPE
@@ -168,13 +147,7 @@ export function buildServer(deps: Deps = defaultDeps(), observe?: ObserveSession
168
147
  log("warn", "tool", { tool: toolOf(message), ok: false, code });
169
148
  return r;
170
149
  };
171
- registerRunTools(server, scoped);
172
- registerWorkspaceTools(server, scoped);
173
- registerLaneTools(server, scoped);
174
- registerDispatchTools(server, scoped);
175
- registerSetupTools(server, scoped);
176
- registerProtocolTools(server, scoped);
177
- registerPushTools(server, scoped);
150
+ registerSessionTools(server, scoped);
178
151
  refuseCoordinatorTools(server, deps);
179
152
  return server;
180
153
  }
@@ -197,7 +170,7 @@ export async function startMcpServer(
197
170
  const key = sessionKey(target);
198
171
  let active = observed.get(key);
199
172
  if (!active) {
200
- active = startNotifier(context);
173
+ active = startNotifier(context, { reportPollMs: REPORT_POLL_MS });
201
174
  observed.set(key, active);
202
175
  }
203
176
  const n = active;
@@ -243,15 +216,15 @@ export async function startMcpServer(
243
216
  const epoch = generation;
244
217
  recovery = { ...deps };
245
218
  const context = recovery;
246
- notifier = startNotifier(context);
219
+ notifier = startNotifier(context, { reportPollMs: REPORT_POLL_MS });
247
220
  const active = notifier;
248
221
  const target = currentSession(context);
249
222
  if (target) observed.set(sessionKey(target), active);
250
223
  void Promise.resolve()
251
224
  .then(() => {
252
- if (lead && epoch === generation) return (o.sync ?? (() => backgroundSync()))();
225
+ if (lead && epoch === generation) return (o.sync ?? (() => refreshIndex({ auto: true })))();
253
226
  })
254
- .catch((e: unknown) => log("debug", "sources", { error: errorMessage(e) }));
227
+ .catch((e: unknown) => log("debug", "catalog", { error: errorMessage(e) }));
255
228
  void (async () => {
256
229
  try {
257
230
  if (epoch !== generation) return;
@@ -0,0 +1,149 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { CatherdError } from "../../domain/errors.ts";
4
+ import { BaseBranchSchema } from "../../domain/branch.ts";
5
+ import { ID_PATTERN } from "../../domain/ids.ts";
6
+ import { PROFILE_NAME, ProfilePatchSchema } from "../../domain/profile.ts";
7
+ import { knownQueueCapability } from "../../infra/codex-queue.ts";
8
+ import { gitToplevel } from "../../infra/git.ts";
9
+ import { redact } from "../../infra/log.ts";
10
+ import { registerSavedSecrets } from "../../services/jev-service.ts";
11
+ import { pausesOver } from "../../services/pause.ts";
12
+ import type { Deps } from "../../services/ports.ts";
13
+ import { inspectionHost } from "../../services/run-debug.ts";
14
+ import { answerUnit, cancelRun, profileTool, runResult, runStart } from "../../services/run-client.ts";
15
+ import { listRunViews, type RunView, runView } from "../../services/run-read.ts";
16
+ import { findRun, listRuns } from "../../services/run-store.ts";
17
+ import { handle } from "./result.ts";
18
+
19
+ const RUN = z.string().min(1);
20
+ const PROFILE = z.string().regex(PROFILE_NAME).optional();
21
+
22
+ /**
23
+ * The git toplevel of `repo` (default: this server's directory, null outside a repository). A `repo` given
24
+ * outside a repository is refused: never the global profile in its stead.
25
+ */
26
+ const toplevel = async (repo: string | undefined): Promise<string | null> => {
27
+ const top = await gitToplevel(repo ?? process.cwd());
28
+ if (top === null && repo !== undefined)
29
+ throw new CatherdError("E_IO_PATH", `${repo} is not inside a git repository`, {
30
+ fix: "pass the path of the repository, or leave repo out for the active profile",
31
+ });
32
+ return top;
33
+ };
34
+
35
+ /** The runs `status` shows without a name: every unfinished one, else the newest. */
36
+ const unfinished = (v: RunView): boolean => (v.phase === null ? v.live.length > 0 : v.phase !== "ended");
37
+
38
+ /** The six tools of a session: a run is started, watched, answered, cancelled and read; the profile is its own. */
39
+ export function registerSessionTools(server: McpServer, deps: Deps): void {
40
+ server.registerTool(
41
+ "run_start",
42
+ {
43
+ description:
44
+ "Start a run that drives itself: catherd plans, dispatches, reviews, verifies and lands each milestone in a background daemon. Returns the run id and the ntfy url its questions go to.",
45
+ inputSchema: {
46
+ repo: z.string().min(1),
47
+ title: z.string().min(1),
48
+ a_lines: z.array(z.string().min(1)).min(1),
49
+ plan: z
50
+ .array(z.string().min(1))
51
+ .max(20)
52
+ .optional()
53
+ .describe(
54
+ "The reviewed plan files the run implements as written (paths, relative to repo or absolute). Without one, the architect writes a plan and the run waits for your approval.",
55
+ ),
56
+ base: BaseBranchSchema.optional().describe(
57
+ "A branch on origin (main, staging): each milestone then gets its own branch and worktree from origin/<base>, and independent milestones run side by side; without it the run works in the repo.",
58
+ ),
59
+ },
60
+ },
61
+ (a) =>
62
+ handle(() =>
63
+ runStart(deps, { repo: a.repo, title: a.title, aLines: a.a_lines, plan: a.plan, base: a.base }),
64
+ ),
65
+ );
66
+
67
+ server.registerTool(
68
+ "status",
69
+ {
70
+ description:
71
+ "Each run's phase, milestones, live roles, parked questions and quota holds, and whether its daemon is alive (a dead one is restarted). Without a run: every unfinished run, else the newest.",
72
+ inputSchema: { run: RUN.optional() },
73
+ },
74
+ (a) =>
75
+ handle(() => {
76
+ registerSavedSecrets();
77
+ const listing = a.run ? null : listRuns();
78
+ const views = a.run ? [runView(deps, a.run)] : listRunViews(deps);
79
+ const open = views.filter(unfinished);
80
+ const shown = a.run ? views : open.length ? open : views.slice(0, 1);
81
+ const queue =
82
+ deps.host.host === "codex" && !deps.host.conflict ? knownQueueCapability(process.env) : null;
83
+ return redact({
84
+ // a pause (the machine's, a workspace's) covers the runs shown, and comes first
85
+ paused: pausesOver(
86
+ (listing?.runs ?? [findRun(a.run as string)]).filter((r) => shown.some((v) => v.id === r.id)),
87
+ ),
88
+ version: deps.version,
89
+ host: inspectionHost(deps.host),
90
+ queue,
91
+ runs: shown,
92
+ warnings: (listing?.corrupt ?? []).map((c) => `skipped run ${c.id}: ${c.reason}`),
93
+ });
94
+ }),
95
+ );
96
+
97
+ server.registerTool(
98
+ "answer",
99
+ {
100
+ description: "Answer a parked unit (the run, a milestone or a lane): it runs again with your answer.",
101
+ inputSchema: { run: RUN, unit: z.string().min(1), answer: z.string().min(1) },
102
+ },
103
+ (a) => handle(() => answerUnit(deps, a)),
104
+ );
105
+
106
+ server.registerTool(
107
+ "cancel",
108
+ {
109
+ description: "Stop the run: every live role is cancelled and the run ends.",
110
+ inputSchema: { run: RUN },
111
+ },
112
+ (a) => handle(() => cancelRun(deps, a)),
113
+ );
114
+
115
+ server.registerTool(
116
+ "result",
117
+ {
118
+ description:
119
+ "The run's report (the MR body: digests, known issues, the judge's decisions), or with name one role's record.",
120
+ inputSchema: { run: RUN, name: z.string().regex(ID_PATTERN).optional() },
121
+ },
122
+ (a) => handle(() => runResult(deps, a)),
123
+ );
124
+
125
+ server.registerTool(
126
+ "profile",
127
+ {
128
+ description:
129
+ "Read, validate or change a profile, or set notify_url, where parked questions are sent. Without a name: the profile this repo runs on (repo, default this server's directory).",
130
+ inputSchema: {
131
+ action: z.enum(["get", "validate", "set"]),
132
+ name: PROFILE,
133
+ repo: z.string().min(1).optional(),
134
+ patch: ProfilePatchSchema.optional(),
135
+ notify_url: z.string().min(1).optional(),
136
+ },
137
+ },
138
+ (a) =>
139
+ handle(async () =>
140
+ profileTool(deps, {
141
+ action: a.action,
142
+ name: a.name,
143
+ repo: (await toplevel(a.repo)) ?? undefined,
144
+ patch: a.patch,
145
+ notifyUrl: a.notify_url,
146
+ }),
147
+ ),
148
+ );
149
+ }
@@ -0,0 +1,189 @@
1
+ import { z } from "zod";
2
+ import {
3
+ CheckSchema,
4
+ IndexedPairSchema,
5
+ IssueSchema,
6
+ ProfileSavedSchema,
7
+ SetupViewSchema,
8
+ } from "../../services/panel-setup.ts";
9
+ import { ParkedViewSchema, RunViewSchema, TimelineEntrySchema } from "../../services/run-read.ts";
10
+
11
+ // The panel's HTTP API, in one table: it writes web/openapi.json (the web client is generated from it) and the
12
+ // server parses every request with the same schemas, so the document and the server cannot drift.
13
+
14
+ export const ErrorBodySchema = z.object({
15
+ error: z.object({ message: z.string(), type: z.string(), code: z.string(), fix: z.string().optional() }),
16
+ });
17
+
18
+ const Page = <T extends z.ZodType>(item: T) => z.object({ items: z.array(item), total: z.number().int() });
19
+
20
+ export const LoginBodySchema = z.object({ token: z.string().min(1).max(200) });
21
+ export const RungsBodySchema = z.object({ rungs: z.array(z.string().min(1)).max(50) });
22
+ export const AnswerBodySchema = z.object({
23
+ unit: z.string().min(1).max(100),
24
+ answer: z.string().trim().min(1).max(10000),
25
+ });
26
+ export const AnswerResultSchema = z.object({ unit: z.string(), parked: z.array(z.string()) });
27
+ export const CancelResultSchema = z.object({ cancelling: z.literal(true), live: z.array(z.string()) });
28
+ export const SessionSchema = z.object({ authenticated: z.literal(true) });
29
+ export const VersionSchema = z.object({ catherd: z.string() });
30
+ export const RunListSchema = Page(RunViewSchema);
31
+ export const TimelinePageSchema = Page(TimelineEntrySchema);
32
+ export const QuestionListSchema = Page(ParkedViewSchema);
33
+
34
+ const paging = {
35
+ limit: z.coerce.number().int().min(1).max(200).default(25),
36
+ offset: z.coerce.number().int().min(0).default(0),
37
+ };
38
+ export const ListRunsQuerySchema = z.object({
39
+ q: z.string().max(200).optional(),
40
+ phase: z.enum(["planning", "approval", "running", "ended"]).optional(),
41
+ ...paging,
42
+ });
43
+ export const TimelineQuerySchema = z.object(paging);
44
+ export const QuestionsQuerySchema = z.object({
45
+ run: z.string().max(200).optional(),
46
+ q: z.string().max(200).optional(),
47
+ ...paging,
48
+ });
49
+
50
+ export type OperationId =
51
+ | "login"
52
+ | "logout"
53
+ | "getSession"
54
+ | "getVersion"
55
+ | "getSetup"
56
+ | "setRoleRungs"
57
+ | "listRuns"
58
+ | "getRun"
59
+ | "getRunTimeline"
60
+ | "cancelRun"
61
+ | "answerUnit"
62
+ | "listQuestions";
63
+
64
+ export interface Operation {
65
+ id: OperationId;
66
+ method: "GET" | "POST" | "PUT";
67
+ path: string;
68
+ public?: true;
69
+ query?: z.ZodObject;
70
+ body?: z.ZodType;
71
+ /** null: 204 */
72
+ response: z.ZodType | null;
73
+ }
74
+
75
+ export const OPERATIONS: readonly Operation[] = [
76
+ {
77
+ id: "login",
78
+ method: "POST",
79
+ path: "/api/auth/login",
80
+ public: true,
81
+ body: LoginBodySchema,
82
+ response: null,
83
+ },
84
+ { id: "logout", method: "POST", path: "/api/auth/logout", response: null },
85
+ { id: "getSession", method: "GET", path: "/api/auth/session", response: SessionSchema },
86
+ { id: "getVersion", method: "GET", path: "/api/system/version", response: VersionSchema },
87
+ { id: "getSetup", method: "GET", path: "/api/setup", response: SetupViewSchema },
88
+ {
89
+ id: "setRoleRungs",
90
+ method: "PUT",
91
+ path: "/api/setup/roles/{role}",
92
+ body: RungsBodySchema,
93
+ response: ProfileSavedSchema,
94
+ },
95
+ { id: "listRuns", method: "GET", path: "/api/runs", query: ListRunsQuerySchema, response: RunListSchema },
96
+ { id: "getRun", method: "GET", path: "/api/runs/{id}", response: RunViewSchema },
97
+ {
98
+ id: "getRunTimeline",
99
+ method: "GET",
100
+ path: "/api/runs/{id}/timeline",
101
+ query: TimelineQuerySchema,
102
+ response: TimelinePageSchema,
103
+ },
104
+ { id: "cancelRun", method: "POST", path: "/api/runs/{id}/cancel", response: CancelResultSchema },
105
+ {
106
+ id: "answerUnit",
107
+ method: "POST",
108
+ path: "/api/runs/{id}/answers",
109
+ body: AnswerBodySchema,
110
+ response: AnswerResultSchema,
111
+ },
112
+ {
113
+ id: "listQuestions",
114
+ method: "GET",
115
+ path: "/api/questions",
116
+ query: QuestionsQuerySchema,
117
+ response: QuestionListSchema,
118
+ },
119
+ ];
120
+
121
+ const SCHEMAS: Record<string, z.ZodType> = {
122
+ RunView: RunViewSchema,
123
+ ParkedView: ParkedViewSchema,
124
+ TimelineEntry: TimelineEntrySchema,
125
+ SetupView: SetupViewSchema,
126
+ Check: CheckSchema,
127
+ Issue: IssueSchema,
128
+ IndexedPair: IndexedPairSchema,
129
+ ProfileSaved: ProfileSavedSchema,
130
+ ErrorBody: ErrorBodySchema,
131
+ RunList: RunListSchema,
132
+ TimelinePage: TimelinePageSchema,
133
+ QuestionList: QuestionListSchema,
134
+ Session: SessionSchema,
135
+ Version: VersionSchema,
136
+ LoginBody: LoginBodySchema,
137
+ RungsBody: RungsBodySchema,
138
+ AnswerBody: AnswerBodySchema,
139
+ AnswerResult: AnswerResultSchema,
140
+ CancelResult: CancelResultSchema,
141
+ };
142
+
143
+ const REF = "#/components/schemas/";
144
+ const nameOf = (s: z.ZodType): string => {
145
+ const hit = Object.entries(SCHEMAS).find(([, v]) => v === s);
146
+ if (!hit) throw new Error("an operation names a schema that panelOpenApi does not register");
147
+ return hit[0];
148
+ };
149
+ const refOf = (s: z.ZodType) => ({ $ref: REF + nameOf(s) });
150
+ const json = (s: z.ZodType) => ({ content: { "application/json": { schema: refOf(s) } } });
151
+
152
+ const without = ({ $schema: _s, $id: _i, ...rest }: Record<string, unknown>) => rest;
153
+
154
+ /** The OpenAPI 3.1 document of OPERATIONS; `info.version` is the API's, never the package's. */
155
+ export function panelOpenApi(): Record<string, unknown> {
156
+ const reg = z.registry<{ id: string }>();
157
+ for (const [id, s] of Object.entries(SCHEMAS)) reg.add(s, { id });
158
+ const out = z.toJSONSchema(reg, { uri: (id) => REF + id, io: "output" }) as {
159
+ schemas: Record<string, Record<string, unknown>>;
160
+ };
161
+ const schemas = Object.fromEntries(Object.entries(out.schemas).map(([k, v]) => [k, without(v)]));
162
+ const paths: Record<string, Record<string, unknown>> = {};
163
+ for (const op of OPERATIONS) {
164
+ const pathParams = [...op.path.matchAll(/\{(\w+)\}/g)].map((m) => ({
165
+ name: m[1],
166
+ in: "path",
167
+ required: true,
168
+ schema: { type: "string" },
169
+ }));
170
+ const queryParams = Object.entries(op.query?.shape ?? {}).map(([name, field]) => ({
171
+ name,
172
+ in: "query",
173
+ required: !(field as z.ZodType).safeParse(undefined).success,
174
+ schema: without(z.toJSONSchema(field as z.ZodType, { io: "input" }) as Record<string, unknown>),
175
+ }));
176
+ (paths[op.path] ??= {})[op.method.toLowerCase()] = {
177
+ operationId: op.id,
178
+ parameters: [...pathParams, ...queryParams],
179
+ ...(op.body ? { requestBody: { required: true, ...json(op.body) } } : {}),
180
+ responses: {
181
+ [op.response ? "200" : "204"]: op.response
182
+ ? { description: "ok", ...json(op.response) }
183
+ : { description: "no content" },
184
+ default: { description: "error", ...json(ErrorBodySchema) },
185
+ },
186
+ };
187
+ }
188
+ return { openapi: "3.1.0", info: { title: "catherd panel", version: "1" }, paths, components: { schemas } };
189
+ }