catherd-cli 1.4.0 → 2.0.0-beta.20261022113309

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +188 -169
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -239
  5. package/package.json +6 -8
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/bin/catherd-mcp +1 -1
  9. package/plugin/commands/catherd.md +2 -2
  10. package/plugin/skills/catherd/SKILL.md +97 -356
  11. package/plugin/skills/catherd-setup/SKILL.md +21 -20
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +19 -4
  14. package/src/adapters/claude-code/index.ts +126 -14
  15. package/src/adapters/codex/events.ts +10 -0
  16. package/src/adapters/codex/index.ts +17 -5
  17. package/src/adapters/cursor/home.ts +1 -1
  18. package/src/adapters/cursor/index.ts +10 -4
  19. package/src/adapters/opencode/index.ts +38 -1
  20. package/src/cli.ts +27 -27
  21. package/src/domain/agents.ts +3 -1
  22. package/src/domain/bars.ts +33 -107
  23. package/src/domain/blocked.ts +72 -0
  24. package/src/domain/branch.ts +51 -0
  25. package/src/domain/brief.ts +106 -0
  26. package/src/domain/catalog.ts +69 -313
  27. package/src/domain/changes.ts +23 -1
  28. package/src/domain/cost.ts +19 -29
  29. package/src/domain/errors.ts +6 -2
  30. package/src/domain/fail-lines.ts +26 -0
  31. package/src/domain/failover.ts +37 -75
  32. package/src/domain/findings.ts +47 -0
  33. package/src/domain/gate-brief.ts +53 -0
  34. package/src/domain/hints.ts +13 -2
  35. package/src/domain/ids.ts +13 -2
  36. package/src/domain/jev.ts +47 -2
  37. package/src/domain/judge.ts +124 -0
  38. package/src/domain/lane.ts +118 -3
  39. package/src/domain/milestone-memory.ts +53 -0
  40. package/src/domain/notice.ts +32 -8
  41. package/src/domain/notify.ts +32 -0
  42. package/src/domain/plan-doc.ts +198 -0
  43. package/src/domain/plan-lint.ts +141 -0
  44. package/src/domain/profile-rules.ts +175 -138
  45. package/src/domain/profile.ts +140 -63
  46. package/src/domain/record.ts +68 -3
  47. package/src/domain/reply.ts +29 -0
  48. package/src/domain/role-prompts.ts +182 -56
  49. package/src/domain/role-scope.ts +76 -0
  50. package/src/domain/role-tools.ts +34 -0
  51. package/src/domain/roles.ts +2 -0
  52. package/src/domain/route.ts +90 -0
  53. package/src/domain/rules.ts +33 -0
  54. package/src/domain/run-events.ts +298 -0
  55. package/src/domain/run-machine.ts +668 -0
  56. package/src/domain/run-state.ts +1310 -0
  57. package/src/domain/scorecard.ts +197 -0
  58. package/src/domain/select.ts +229 -72
  59. package/src/domain/slopalytics.ts +72 -0
  60. package/src/domain/state.ts +5 -0
  61. package/src/domain/util.ts +22 -0
  62. package/src/domain/workspace.ts +130 -0
  63. package/src/entry/answer-command.ts +39 -0
  64. package/src/entry/catalog-command.ts +24 -165
  65. package/src/entry/check-bin.ts +17 -0
  66. package/src/entry/cli-kit.ts +18 -2
  67. package/src/entry/daemon-bin.ts +22 -0
  68. package/src/entry/deps.ts +2 -0
  69. package/src/entry/doctor-command.ts +20 -2
  70. package/src/entry/glyphs.ts +15 -2
  71. package/src/entry/host-arg.ts +28 -0
  72. package/src/entry/init-command.ts +20 -69
  73. package/src/entry/knowledge-command.ts +121 -5
  74. package/src/entry/lock-command.ts +103 -8
  75. package/src/entry/mcp/handshake.ts +6 -2
  76. package/src/entry/mcp/role-bin.ts +20 -0
  77. package/src/entry/mcp/role-server.ts +134 -0
  78. package/src/entry/mcp/server.ts +85 -21
  79. package/src/entry/mcp/session-tools.ts +149 -0
  80. package/src/entry/panel/openapi.ts +189 -0
  81. package/src/entry/panel/server.ts +410 -0
  82. package/src/entry/panel-command.ts +68 -0
  83. package/src/entry/pause-command.ts +67 -0
  84. package/src/entry/profile-command.ts +33 -18
  85. package/src/entry/role-cli-command.ts +159 -0
  86. package/src/entry/runs-command.ts +81 -27
  87. package/src/entry/scorecard-command.ts +187 -0
  88. package/src/entry/supervise-bin.ts +1 -1
  89. package/src/entry/supervise-command.ts +30 -3
  90. package/src/entry/workspace-command.ts +44 -0
  91. package/src/infra/boot-lock.ts +25 -0
  92. package/src/infra/delivery.ts +3 -0
  93. package/src/infra/dispatch-dir.ts +2 -0
  94. package/src/infra/env.ts +11 -3
  95. package/src/infra/git.ts +70 -0
  96. package/src/infra/heavy-lock.ts +133 -6
  97. package/src/infra/host-probe.ts +16 -0
  98. package/src/infra/launch.ts +63 -7
  99. package/src/infra/lock-activity.ts +88 -0
  100. package/src/infra/login-env.ts +48 -0
  101. package/src/infra/memory.ts +45 -0
  102. package/src/infra/ntfy.ts +40 -0
  103. package/src/infra/open-url.ts +42 -0
  104. package/src/infra/paths.ts +37 -2
  105. package/src/infra/role-mcp.ts +69 -0
  106. package/src/infra/slopalytics.ts +30 -0
  107. package/src/infra/store.ts +6 -1
  108. package/src/infra/supervisor.ts +107 -16
  109. package/src/services/admission.ts +133 -31
  110. package/src/services/backends.ts +4 -6
  111. package/src/services/budget.ts +26 -6
  112. package/src/services/capture.ts +1 -22
  113. package/src/services/catalog-service.ts +155 -254
  114. package/src/services/credentials.ts +15 -26
  115. package/src/services/daemon.ts +181 -0
  116. package/src/services/dispatch-service.ts +209 -17
  117. package/src/services/dispatches.ts +30 -2
  118. package/src/services/doctor-access.ts +8 -3
  119. package/src/services/doctor-backends.ts +1 -1
  120. package/src/services/doctor-checks.ts +47 -0
  121. package/src/services/doctor-docker.ts +221 -0
  122. package/src/services/doctor-role-mcp.ts +65 -0
  123. package/src/services/doctor.ts +72 -15
  124. package/src/services/end-push.ts +157 -0
  125. package/src/services/failover-rungs.ts +109 -0
  126. package/src/services/finalize.ts +24 -2
  127. package/src/services/gate-env.ts +217 -0
  128. package/src/services/gate-service.ts +231 -45
  129. package/src/services/global-install.ts +46 -13
  130. package/src/services/jev-service.ts +2 -4
  131. package/src/services/lane-edit.ts +154 -0
  132. package/src/services/lane-service.ts +291 -55
  133. package/src/services/milestone-check.ts +433 -0
  134. package/src/services/milestone-tree.ts +170 -0
  135. package/src/services/milestones.ts +167 -29
  136. package/src/services/notifier.ts +124 -4
  137. package/src/services/notify-service.ts +26 -0
  138. package/src/services/orchestrator-wait.ts +82 -0
  139. package/src/services/panel-setup.ts +116 -0
  140. package/src/services/pause.ts +161 -0
  141. package/src/services/peek.ts +79 -17
  142. package/src/services/plan-input.ts +151 -0
  143. package/src/services/ports.ts +60 -5
  144. package/src/services/preflight.ts +232 -24
  145. package/src/services/profile-service.ts +31 -3
  146. package/src/services/profile-store.ts +18 -6
  147. package/src/services/protocol.ts +123 -25
  148. package/src/services/provenance.ts +34 -89
  149. package/src/services/questions.ts +24 -0
  150. package/src/services/reconcile.ts +33 -1
  151. package/src/services/reentry.ts +7 -6
  152. package/src/services/role-access.ts +27 -0
  153. package/src/services/routing-service.ts +161 -42
  154. package/src/services/run-briefs.ts +477 -0
  155. package/src/services/run-client.ts +217 -0
  156. package/src/services/run-effects.ts +837 -0
  157. package/src/services/run-events-store.ts +174 -0
  158. package/src/services/run-evidence.ts +1 -1
  159. package/src/services/run-pin.ts +125 -0
  160. package/src/services/run-read.ts +221 -0
  161. package/src/services/run-report.ts +176 -0
  162. package/src/services/run-service.ts +204 -23
  163. package/src/services/run-store.ts +159 -18
  164. package/src/services/scorecard-metrics.ts +86 -0
  165. package/src/services/scorecard.ts +382 -0
  166. package/src/services/scratch.ts +60 -0
  167. package/src/services/sessions.ts +3 -1
  168. package/src/services/state.ts +21 -0
  169. package/src/services/summary.ts +76 -12
  170. package/src/services/tree-path.ts +27 -0
  171. package/src/services/verifier-brief.ts +23 -0
  172. package/src/services/verifier-step.ts +48 -0
  173. package/src/services/workspace-admission.ts +259 -0
  174. package/src/services/workspace-service.ts +259 -0
  175. package/src/services/workspace-store.ts +95 -0
  176. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  177. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  178. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  179. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  180. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  181. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  182. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  183. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  184. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  185. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  186. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  187. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  188. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  189. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  190. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  191. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  192. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  193. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  194. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  195. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  196. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  197. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  198. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  199. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  200. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  201. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  202. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  203. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  204. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  205. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  206. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  207. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  208. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  209. package/web/dist/assets/index-C5IbylvM.css +2 -0
  210. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  211. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  212. package/web/dist/assets/label-DFSYBc62.js +1 -0
  213. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  214. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  215. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  216. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  217. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  218. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  219. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  220. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  221. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  222. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  223. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  224. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  225. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  226. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  227. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  228. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  229. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  230. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  231. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  232. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  233. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  234. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  235. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  236. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  237. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  238. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  239. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  240. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  241. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  242. package/web/dist/assets/time--9kSQbbp.js +1 -0
  243. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  244. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  245. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  246. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  247. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  248. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  249. package/web/dist/index.html +15 -0
  250. package/catalog/ATTRIBUTION.md +0 -36
  251. package/catalog/scores.json +0 -3202
  252. package/catalog/sources.json +0 -97
  253. package/src/adapters/antigravity/events.ts +0 -131
  254. package/src/adapters/antigravity/home.ts +0 -46
  255. package/src/adapters/antigravity/index.ts +0 -320
  256. package/src/adapters/antigravity/models.ts +0 -37
  257. package/src/adapters/grok/events.ts +0 -112
  258. package/src/adapters/grok/home.ts +0 -132
  259. package/src/adapters/grok/index.ts +0 -339
  260. package/src/adapters/grok/models.ts +0 -48
  261. package/src/domain/calibration.ts +0 -125
  262. package/src/domain/sources.ts +0 -144
  263. package/src/entry/mcp/dispatch-tools.ts +0 -52
  264. package/src/entry/mcp/lane-tools.ts +0 -84
  265. package/src/entry/mcp/protocol-tools.ts +0 -57
  266. package/src/entry/mcp/run-tools.ts +0 -148
  267. package/src/entry/mcp/setup-tools.ts +0 -108
  268. package/src/entry/tui/commands.ts +0 -563
  269. package/src/entry/tui/effects.ts +0 -381
  270. package/src/entry/tui/fixtures.ts +0 -637
  271. package/src/entry/tui/profile-edits.ts +0 -146
  272. package/src/entry/tui/profile-tree.ts +0 -429
  273. package/src/entry/tui/providers/app.tsx +0 -162
  274. package/src/entry/tui/providers/boundary.tsx +0 -57
  275. package/src/entry/tui/providers/data.tsx +0 -160
  276. package/src/entry/tui/providers/keymap.tsx +0 -166
  277. package/src/entry/tui/providers/root.tsx +0 -45
  278. package/src/entry/tui/providers/theme.tsx +0 -17
  279. package/src/entry/tui/providers/toast.tsx +0 -77
  280. package/src/entry/tui/run.tsx +0 -114
  281. package/src/entry/tui/state.ts +0 -373
  282. package/src/entry/tui/stories.ts +0 -114
  283. package/src/entry/tui/text.ts +0 -130
  284. package/src/entry/tui/theme.ts +0 -140
  285. package/src/entry/tui/views/app.tsx +0 -208
  286. package/src/entry/tui/views/dialogs.tsx +0 -41
  287. package/src/entry/tui/views/profile-actions.ts +0 -331
  288. package/src/entry/tui/views/profiles.tsx +0 -520
  289. package/src/entry/tui/views/runs.tsx +0 -573
  290. package/src/entry/tui/views/save-dialog.tsx +0 -340
  291. package/src/entry/tui/views/status.tsx +0 -209
  292. package/src/entry/tui/widgets/chrome.tsx +0 -114
  293. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  294. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  295. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  296. package/src/entry/tui/widgets/dialog.tsx +0 -68
  297. package/src/entry/tui/widgets/line.tsx +0 -92
  298. package/src/entry/tui/widgets/list.tsx +0 -197
  299. package/src/entry/tui/widgets/spinner.tsx +0 -49
  300. package/src/entry/tui/widgets/toast.tsx +0 -62
  301. package/src/infra/sources/arena.ts +0 -87
  302. package/src/infra/sources/artificial-analysis.ts +0 -112
  303. package/src/infra/sources/cache.ts +0 -124
  304. package/src/infra/sources/csv.ts +0 -38
  305. package/src/infra/sources/epoch.ts +0 -82
  306. package/src/infra/sources/http.ts +0 -75
  307. package/src/infra/sources/litellm.ts +0 -58
  308. package/src/infra/sources/models-dev.ts +0 -59
  309. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  310. package/src/infra/sources/openrouter-models.ts +0 -60
  311. package/src/infra/sources/rows.ts +0 -38
  312. package/src/infra/sources/vectara.ts +0 -69
  313. package/src/infra/sources/zip.ts +0 -48
  314. package/src/services/catalog-refresh.ts +0 -201
  315. package/src/services/doctor-sources.ts +0 -76
  316. package/src/services/runs-page.ts +0 -255
  317. package/src/services/source-derive.ts +0 -374
  318. package/src/services/source-sync.ts +0 -305
  319. package/src/services/standins.ts +0 -204
  320. package/src/services/treat-likes.ts +0 -97
@@ -4,24 +4,70 @@ 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 { errorMessage } from "../../domain/errors.ts";
7
+ import { type CatherdError, errorMessage } from "../../domain/errors.ts";
8
+ import {
9
+ isCoordinatorTool,
10
+ refusedForRole,
11
+ type RoleScope,
12
+ roleScopeError,
13
+ } from "../../domain/role-scope.ts";
14
+ import { takeBootLock } from "../../infra/boot-lock.ts";
8
15
  import { resolveHost } from "../../infra/host-context.ts";
9
16
  import { log } from "../../infra/log.ts";
10
17
  import { currentSession } from "../../services/sessions.ts";
11
18
  import { startNotifier } from "../../services/notifier.ts";
12
19
  import type { Deps } from "../../services/ports.ts";
13
- import { reconcileAll } from "../../services/reconcile.ts";
14
- import { backgroundSync } from "../../services/source-sync.ts";
20
+ import { adoptOwned, reconcileAll } from "../../services/reconcile.ts";
21
+ import { refreshIndex } from "../../services/catalog-service.ts";
15
22
  import { defaultDeps } from "../deps.ts";
16
- import { registerDispatchTools } from "./dispatch-tools.ts";
17
- import { registerLaneTools } from "./lane-tools.ts";
18
- import { registerProtocolTools } from "./protocol-tools.ts";
19
- import { sdkToolError, toolOf } from "./result.ts";
20
- import { registerRunTools } from "./run-tools.ts";
21
- import { registerSetupTools } from "./setup-tools.ts";
23
+ import { handle, sdkToolError, toolOf } from "./result.ts";
24
+ import { registerSessionTools } from "./session-tools.ts";
22
25
 
23
26
  type ObserveSession = (context: Deps) => (() => void) | undefined;
24
27
 
28
+ /** Spec 1.5 plan 21: a coordinator tool's description says, first, that only the orchestrator calls it. */
29
+ function orchestratorOnly(name: string, config: unknown): unknown {
30
+ const c = config as { description?: string };
31
+ return isCoordinatorTool(name) && c.description
32
+ ? { ...c, description: `Orchestrator only (a role process gets E_ROLE_SCOPE). ${c.description}` }
33
+ : config;
34
+ }
35
+
36
+ type RequestHandler = (
37
+ request: { params?: { name?: unknown; arguments?: unknown } },
38
+ extra: unknown,
39
+ ) => unknown;
40
+
41
+ /** How often a session looks for the new reports of the 2.0 runs it owns. */
42
+ const REPORT_POLL_MS = 3_000;
43
+
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;
47
+
48
+ /**
49
+ * Spec 1.5 plan 21: in a role's process (`deps.role`) a coordinator tool call is refused with E_ROLE_SCOPE
50
+ * before anything else, its input check included, so a role learns at once that the tool is not its own. The
51
+ * SDK keeps its request handlers in a private map; test/entry/role-scope-mcp.test.ts fails if it moves.
52
+ */
53
+ function refuseCoordinatorTools(server: McpServer, deps: Deps): void {
54
+ const handlers = (server.server as unknown as { _requestHandlers: Map<string, RequestHandler> })
55
+ ._requestHandlers;
56
+ const callTool = handlers.get("tools/call");
57
+ if (!callTool) return;
58
+ handlers.set("tools/call", async (request, extra) => {
59
+ const role = deps.role;
60
+ const name = String(request.params?.name ?? "");
61
+ const refusal = role ? roleRefusal(name, request.params?.arguments, role) : null;
62
+ if (!refusal) return callTool(request, extra);
63
+ const r = await handle(() => {
64
+ throw refusal;
65
+ });
66
+ log("warn", "tool", { tool: name, ok: false, code: "E_ROLE_SCOPE" });
67
+ return r;
68
+ });
69
+ }
70
+
25
71
  type Handler = (...args: unknown[]) => CallToolResult | Promise<CallToolResult>;
26
72
 
27
73
  /** Spec §10.2: every tool call is logged with its duration and outcome (the error code), never its input. */
@@ -38,7 +84,7 @@ function logToolCalls(
38
84
  h: Handler,
39
85
  ) => unknown;
40
86
  (server as unknown as { registerTool: typeof register }).registerTool = (name, config, handler) =>
41
- register(name, config, async (...args: unknown[]) => {
87
+ register(name, orchestratorOnly(name, config), async (...args: unknown[]) => {
42
88
  const started = Date.now();
43
89
  const epoch = generation();
44
90
  const extra = args.at(-1) as { _meta?: Record<string, unknown> } | undefined;
@@ -101,17 +147,20 @@ export function buildServer(deps: Deps = defaultDeps(), observe?: ObserveSession
101
147
  log("warn", "tool", { tool: toolOf(message), ok: false, code });
102
148
  return r;
103
149
  };
104
- registerRunTools(server, scoped);
105
- registerLaneTools(server, scoped);
106
- registerDispatchTools(server, scoped);
107
- registerSetupTools(server, scoped);
108
- registerProtocolTools(server, scoped);
150
+ registerSessionTools(server, scoped);
151
+ refuseCoordinatorTools(server, deps);
109
152
  return server;
110
153
  }
111
154
 
112
155
  /** Connect immediately; initialized triggers recovery without delaying the handshake. */
113
156
  export async function startMcpServer(
114
- o: { transport?: Transport; sync?: () => Promise<unknown>; deps?: Deps } = {},
157
+ o: {
158
+ transport?: Transport;
159
+ sync?: () => Promise<unknown>;
160
+ deps?: Deps;
161
+ /** plan 22: the boot lock (tests replace it); null while another live server holds it */
162
+ bootLock?: () => (() => void) | null;
163
+ } = {},
115
164
  ): Promise<void> {
116
165
  const deps = o.deps ?? defaultDeps();
117
166
  const observed = new Map<string, ReturnType<typeof startNotifier>>();
@@ -121,7 +170,7 @@ export async function startMcpServer(
121
170
  const key = sessionKey(target);
122
171
  let active = observed.get(key);
123
172
  if (!active) {
124
- active = startNotifier(context);
173
+ active = startNotifier(context, { reportPollMs: REPORT_POLL_MS });
125
174
  observed.set(key, active);
126
175
  }
127
176
  const n = active;
@@ -134,6 +183,8 @@ export async function startMcpServer(
134
183
  let generation = 0;
135
184
  let notifier: ReturnType<typeof startNotifier> | undefined;
136
185
  let recovery: Deps | undefined;
186
+ // plan 22: taken at the first initialize, held until this server closes; null: another server leads the boot
187
+ let boot: (() => void) | null | undefined;
137
188
  const invalidate = () => {
138
189
  generation++;
139
190
  if (recovery) recovery.host = { host: "unknown", session: null, conflict: null };
@@ -143,10 +194,17 @@ export async function startMcpServer(
143
194
  observed.clear();
144
195
  closed();
145
196
  };
146
- server.server.onclose = invalidate;
197
+ server.server.onclose = () => {
198
+ invalidate();
199
+ boot?.();
200
+ boot = undefined;
201
+ };
147
202
  server.server.oninitialized = () => {
148
203
  invalidate();
149
204
  initialized();
205
+ boot ??= (o.bootLock ?? takeBootLock)();
206
+ const lead = boot !== null;
207
+ if (!lead) log("info", "boot", { skipped: "another catherd server runs the boot sync and reconcile" });
150
208
  log("info", "session", {
151
209
  host: deps.host.host,
152
210
  conflict: deps.host.conflict,
@@ -158,18 +216,24 @@ export async function startMcpServer(
158
216
  const epoch = generation;
159
217
  recovery = { ...deps };
160
218
  const context = recovery;
161
- notifier = startNotifier(context);
219
+ notifier = startNotifier(context, { reportPollMs: REPORT_POLL_MS });
162
220
  const active = notifier;
163
221
  const target = currentSession(context);
164
222
  if (target) observed.set(sessionKey(target), active);
165
223
  void Promise.resolve()
166
224
  .then(() => {
167
- if (epoch === generation) return (o.sync ?? (() => backgroundSync()))();
225
+ if (lead && epoch === generation) return (o.sync ?? (() => refreshIndex({ auto: true })))();
168
226
  })
169
- .catch((e: unknown) => log("debug", "sources", { error: errorMessage(e) }));
227
+ .catch((e: unknown) => log("debug", "catalog", { error: errorMessage(e) }));
170
228
  void (async () => {
171
229
  try {
172
230
  if (epoch !== generation) return;
231
+ // the leading server reconciles every run; this one still watches the live roles of the runs its own
232
+ // session owns (a coordinator's server restarted mid-run) and tells its session what it owns
233
+ if (!lead) {
234
+ adoptOwned(context);
235
+ return void active.scan();
236
+ }
173
237
  const r = await reconcileAll(context);
174
238
  if (epoch !== generation) return;
175
239
  const shown = r.warnings.length;
@@ -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
+ }