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
@@ -6,20 +6,17 @@ import { errorMessage, isCatherdError } from "../domain/errors.ts";
6
6
  import { configDir } from "../infra/paths.ts";
7
7
  import { VERSION } from "../infra/version.ts";
8
8
  import { doctor } from "../services/doctor.ts";
9
- import { testAaKey } from "../infra/sources/artificial-analysis.ts";
10
- import { aaKey, saveAaKey } from "../services/credentials.ts";
9
+ import { type IndexStatus, refreshIndex } from "../services/catalog-service.ts";
11
10
  import { jevKey, saveJevKey, testJevKey } from "../services/jev-service.ts";
12
- import { type SyncReport, syncSources } from "../services/source-sync.ts";
13
11
  import { reinstallCommand } from "../services/doctor-checks.ts";
14
12
  import { ensureGlobal, type GlobalInstallDeps, realGlobalInstall } from "../services/global-install.ts";
15
13
  import { hasProfileFile, type InitResult, initSetup, moveLegacy } from "../services/setup.ts";
16
14
  import { activeName } from "../services/profile-store.ts";
17
- import { formatRefreshed, syncLines } from "./catalog-command.ts";
18
- import { mark as markOf } from "./cli-kit.ts";
15
+ import { formatRefreshed } from "./catalog-command.ts";
16
+ import { mark as markOf, refuseInRole } from "./cli-kit.ts";
19
17
  import { formatReport } from "./doctor-command.ts";
20
18
  import { mcpHandshake } from "./mcp/handshake.ts";
21
- import type { State } from "./glyphs.ts";
22
- import { mascot } from "./tui/theme.ts";
19
+ import { mascot, type State } from "./glyphs.ts";
23
20
  import { type Prompter, prompter } from "./prompt.ts";
24
21
 
25
22
  /** What `init` prints last (spec §9.1): the plugin install commands, to paste into a terminal. */
@@ -30,7 +27,7 @@ export const PLUGIN_STEPS = [
30
27
  "Then start a new Claude Code session, so it loads the plugin and the catherd agents.",
31
28
  ];
32
29
 
33
- /** Spec §9.3: the full mascot greets `init` (and the TUI's empty states), nowhere else. */
30
+ /** Spec §9.3: the full mascot greets `init` nowhere else. */
34
31
  export function welcomeLines(version: string): string[] {
35
32
  const [ears, face, paws] = mascot("good");
36
33
  return [ears, `${face} catherd ${version}`, `${paws} herds your coding agents`, ""];
@@ -72,68 +69,22 @@ export async function jevStep(
72
69
  }
73
70
 
74
71
  /**
75
- * Spec 1.2 §9: the Artificial Analysis key step, after Jev's. ARTIFICIAL_ANALYSIS_API_KEY wins, else the saved
76
- * key, else a prompt (Enter skips). A typed key is tested with one request: a 200 saves it, a 401 does not,
77
- * and a key that cannot be checked (no network) is not saved either. It never stops `init`.
78
- */
79
- export async function aaStep(
80
- ask: Prompter | null,
81
- d: {
82
- testAaKey?: (key: string) => Promise<{ result: "ok" | "refused" | "unchecked"; error?: string }>;
83
- saveAaKey?: (key: string) => void;
84
- } = {},
85
- plain = false,
86
- ): Promise<void> {
87
- const mark = (state: State) => markOf(state, plain);
88
- if (process.env.ARTIFICIAL_ANALYSIS_API_KEY?.trim()) {
89
- ask?.skip?.();
90
- return console.log(`${mark("ok")} Artificial Analysis: using ARTIFICIAL_ANALYSIS_API_KEY`);
91
- }
92
- if (aaKey()) {
93
- ask?.skip?.();
94
- return console.log(`${mark("ok")} Artificial Analysis: using the saved key`);
95
- }
96
- const key = ask ? await ask.secret("Artificial Analysis API key (optional; Enter skips): ") : "";
97
- if (!key)
98
- return console.log(
99
- "- Artificial Analysis: no key; scores come from the keyless sources (add one later with catherd init)",
100
- );
101
- const t = await (d.testAaKey ?? testAaKey)(key).catch((e: unknown) => ({
102
- result: "unchecked" as const,
103
- error: errorMessage(e),
104
- }));
105
- if (t.result === "refused")
106
- return console.log(`${mark("warn")} Artificial Analysis: the key was refused (401), so it was not saved`);
107
- if (t.result === "unchecked")
108
- return console.log(
109
- `${mark("warn")} Artificial Analysis: the key could not be checked (${t.error ?? "no answer"}), so it was not saved; run catherd init again to retry`,
110
- );
111
- try {
112
- (d.saveAaKey ?? saveAaKey)(key);
113
- console.log(`${mark("ok")} Artificial Analysis: the key answers; saved with mode 600`);
114
- } catch (e) {
115
- console.log(
116
- `${mark("warn")} Artificial Analysis: could not save the key: ${errorMessage(e).split("\n").join(" ")}`,
117
- );
118
- if (isCatherdError(e) && e.fix) console.log(` fix: ${e.fix}`);
119
- }
120
- }
121
-
122
- /**
123
- * Spec 1.2 §9: `init` syncs the public sources in the foreground, after the keys. It never stops `init`;
72
+ * `init` refreshes the Slopalytics index in the foreground, after the Jev key. It never stops `init`;
124
73
  * CATHERD_NO_SYNC=1 skips it.
125
74
  */
126
- export async function syncStep(o: { plain?: boolean; sync?: () => Promise<SyncReport> } = {}): Promise<void> {
75
+ export async function syncStep(
76
+ o: { plain?: boolean; sync?: () => Promise<IndexStatus> } = {},
77
+ ): Promise<void> {
127
78
  const plain = o.plain === true;
128
79
  if (process.env.CATHERD_NO_SYNC === "1")
129
- return console.log("- sources: not synced (CATHERD_NO_SYNC=1); catherd catalog sync fetches them");
130
- console.log("syncing the public model sources…");
131
- try {
132
- for (const l of syncLines(await (o.sync ?? (() => syncSources()))(), plain)) console.log(l);
133
- } catch (e) {
134
- console.log(`${markOf("warn", plain)} sources: ${errorMessage(e).split("\n").join(" ")}`);
135
- console.log(" fix: catherd catalog sync");
136
- }
80
+ return console.log("- catalog: not fetched (CATHERD_NO_SYNC=1); catherd catalog sync fetches it");
81
+ console.log("fetching the Slopalytics index…");
82
+ const s = await (o.sync ?? (() => refreshIndex({ auto: true })))().catch(
83
+ (e: unknown) => ({ error: errorMessage(e), pairs: 0 }) as Pick<IndexStatus, "error" | "pairs">,
84
+ );
85
+ if (s.error === null) return console.log(`${markOf("ok", plain)} Slopalytics: ${s.pairs} pairs indexed`);
86
+ console.log(`${markOf("warn", plain)} Slopalytics: ${s.error.split("\n").join(" ")}`);
87
+ console.log(" fix: catherd catalog sync");
137
88
  }
138
89
 
139
90
  /**
@@ -190,12 +141,12 @@ export function profileLines(r: InitResult, plain = false): string[] {
190
141
  return out;
191
142
  }
192
143
 
193
- /** Spec §8 `catherd init [--no-input]`: first-run setup without the TUI (plan 6 adds the TUI wizard). */
144
+ /** Spec §8 `catherd init [--no-input]`: first-run setup without the panel. */
194
145
  export const initCommand = defineCommand({
195
146
  meta: {
196
147
  name: "init",
197
148
  description:
198
- "First run: the global catherd command, the Jev key, the Artificial Analysis key, a sync of the public model sources, the default profile, its agents, and a readiness report. Piped, it reads the answers from stdin one per line, a line per question even when this machine skips it (the Jev key, the Artificial Analysis key, the profile, whether to replace it), and waits for stdin to close; --no-input asks nothing",
149
+ "First run: the global catherd command, the Jev key, a fetch of the Slopalytics model index, the default profile, its agents, and a readiness report. Piped, it reads the answers from stdin one per line, a line per question even when this machine skips it (the Jev key, the profile, whether to replace it), and waits for stdin to close; --no-input asks nothing",
199
150
  },
200
151
  args: {
201
152
  ...HOST_ARG,
@@ -213,6 +164,7 @@ export const initCommand = defineCommand({
213
164
  plain: { type: "boolean", description: "ASCII glyphs (NO_COLOR drops only colour)" },
214
165
  },
215
166
  async run({ args }) {
167
+ refuseInRole(process.env, "catherd init");
216
168
  // a bad --profile is refused before any question is asked
217
169
  if (args.profile !== undefined) assertProfileName(args.profile);
218
170
  const ask = (args as { input?: boolean }).input === false ? null : await prompter();
@@ -223,7 +175,6 @@ export const initCommand = defineCommand({
223
175
  console.log(`catherd ${VERSION}: setting up in ${configDir()}`);
224
176
  await globalStep(VERSION, { skip: (args as { global?: boolean }).global === false, plain });
225
177
  await jevStep(ask, {}, plain);
226
- await aaStep(ask, {}, plain);
227
178
  await syncStep({ plain });
228
179
  const repo = await gitToplevel(process.cwd());
229
180
  // 0.x files go first, so a legacy profile about to be moved aside is never asked about
@@ -1,7 +1,17 @@
1
1
  import { resolve } from "node:path";
2
2
  import { defineCommand } from "citty";
3
- import { addKnowledge, knowledgeLines, knowledgePath, readKnowledge } from "../services/run-service.ts";
4
- import { JSON_ARG, printJson } from "./cli-kit.ts";
3
+ import { CatherdError } from "../domain/errors.ts";
4
+ import { gateEnvLines, readGateEnv, removeGateEnv, setGateEnv } from "../services/gate-env.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";
14
+ import { JSON_ARG, printJson, refuseInRole } from "./cli-kit.ts";
5
15
 
6
16
  /** `--repo`, else the repository the command runs in, as run_start and read_knowledge resolve it. */
7
17
  const REPO_ARG = {
@@ -11,8 +21,26 @@ const repoOf = (repo: string | undefined): string => resolve(repo ?? process.cwd
11
21
 
12
22
  const show = defineCommand({
13
23
  meta: { name: "show", description: "Print what catherd knows about the repo (its knowledge.md)" },
14
- 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
+ },
15
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
+ }
16
44
  if (args.json) return printJson(await knowledgeLines(repoOf(args.repo)));
17
45
  const text = await readKnowledge(repoOf(args.repo));
18
46
  process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
@@ -29,10 +57,26 @@ const add = defineCommand({
29
57
  ...REPO_ARG,
30
58
  },
31
59
  async run({ args }) {
60
+ refuseInRole(process.env, "catherd knowledge add");
32
61
  console.log(await addKnowledge(repoOf(args.repo), args.line));
33
62
  },
34
63
  });
35
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
+
36
80
  const path = defineCommand({
37
81
  meta: { name: "path", description: "Print the path of the repo's knowledge.md (it may not exist yet)" },
38
82
  args: { ...REPO_ARG, ...JSON_ARG },
@@ -43,8 +87,80 @@ const path = defineCommand({
43
87
  },
44
88
  });
45
89
 
90
+ /** `NAME=value` as name and value; null when it has no `=`. */
91
+ function assignment(raw: string): { name: string; value: string } | null {
92
+ const eq = raw.indexOf("=");
93
+ return eq > 0 ? { name: raw.slice(0, eq), value: raw.slice(eq + 1) } : null;
94
+ }
95
+
96
+ const envSet = defineCommand({
97
+ meta: {
98
+ name: "set",
99
+ description:
100
+ "Set one variable of the repo's gate environment, which the verifier and preflight run with: NAME=value, or NAME --from ENV_VAR for a secret (stored by reference, read when a role starts)",
101
+ },
102
+ args: {
103
+ variable: { type: "positional", required: true, description: "NAME=value, or NAME with --from" },
104
+ from: { type: "string", description: "the env var that holds the secret value" },
105
+ ...REPO_ARG,
106
+ ...JSON_ARG,
107
+ },
108
+ async run({ args }) {
109
+ refuseInRole(process.env, "catherd knowledge env set");
110
+ const set = assignment(args.variable);
111
+ if (args.from !== undefined && set)
112
+ throw new CatherdError("E_INPUT_INVALID", "pass NAME=value or NAME --from ENV_VAR, not both", {
113
+ fix: "catherd knowledge env set DOCKER_HOST=unix:///var/run/docker.sock",
114
+ });
115
+ if (args.from === undefined && !set)
116
+ throw new CatherdError("E_INPUT_INVALID", `"${args.variable}" has no value`, {
117
+ fix: "catherd knowledge env set NAME=value, or catherd knowledge env set NAME --from ENV_VAR",
118
+ });
119
+ const r = set
120
+ ? await setGateEnv(repoOf(args.repo), set.name, { value: set.value })
121
+ : await setGateEnv(repoOf(args.repo), args.variable, { from: args.from as string });
122
+ if (args.json) return printJson(r);
123
+ for (const line of gateEnvLines(r.vars)) console.log(line);
124
+ },
125
+ });
126
+
127
+ const envRm = defineCommand({
128
+ meta: { name: "rm", description: "Remove one variable from the repo's gate environment" },
129
+ args: { name: { type: "positional", required: true }, ...REPO_ARG, ...JSON_ARG },
130
+ async run({ args }) {
131
+ refuseInRole(process.env, "catherd knowledge env rm");
132
+ const r = await removeGateEnv(repoOf(args.repo), args.name);
133
+ if (args.json) return printJson(r);
134
+ for (const line of gateEnvLines(r.vars)) console.log(line);
135
+ },
136
+ });
137
+
138
+ const envList = defineCommand({
139
+ meta: { name: "list", description: "Print the repo's gate environment (a secret shows as $ENV_VAR)" },
140
+ args: { ...REPO_ARG, ...JSON_ARG },
141
+ async run({ args }) {
142
+ const at = await knowledgePath(repoOf(args.repo));
143
+ const vars = await readGateEnv(at.repo);
144
+ if (args.json) return printJson({ repo: at.repo, vars });
145
+ const lines = gateEnvLines(vars);
146
+ console.log(lines.length ? lines.join("\n") : "catherd: no gate environment set for this repo");
147
+ },
148
+ });
149
+
150
+ /** Plan 23: the repo's gate environment, beside its knowledge.md. */
151
+ const env = defineCommand({
152
+ meta: {
153
+ name: "env",
154
+ description: "The repo's gate environment, which the verifier and preflight run with: set, rm or list",
155
+ },
156
+ subCommands: { set: envSet, rm: envRm, list: envList },
157
+ });
158
+
46
159
  /** What past runs of a repo learned, which the read_knowledge tool gives every new run (spec §4.7). */
47
160
  export const knowledgeCommand = defineCommand({
48
- meta: { name: "knowledge", description: "What catherd knows about a repo: show, add or path" },
49
- subCommands: { show, add, path },
161
+ meta: {
162
+ name: "knowledge",
163
+ description: "What catherd knows about a repo: show, add, rm or path, and its gate environment (env)",
164
+ },
165
+ subCommands: { show, add, rm, path, env },
50
166
  });
@@ -3,7 +3,8 @@ 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 } from "../infra/heavy-lock.ts";
6
+ import { heavySlots, LOCK_HELD_ENV, withHeavySlot, withRoleLock } from "../infra/heavy-lock.ts";
7
+ import { activityReporter, DISPATCH_ID_ENV } from "../infra/lock-activity.ts";
7
8
  import { killGroup } from "../infra/proc.ts";
8
9
  import { activeName, readProfileDoc } from "../services/profile-store.ts";
9
10
  import { printError } from "./cli-kit.ts";
@@ -44,14 +45,26 @@ export const DOUBLE_INTERRUPT_MS = 2_000;
44
45
  * a terminal's Ctrl-C reaches catherd only, so the command sees it exactly once, and so does every
45
46
  * process it started. A second Ctrl-C within 2 s kills the group. Resolves to the command's exit code.
46
47
  */
47
- export async function runForwarding(argv: string[]): Promise<number> {
48
+ export async function runForwarding(
49
+ argv: string[],
50
+ o: { onOutput?: () => void; extraEnv?: Record<string, string> } = {},
51
+ ): Promise<number> {
52
+ // plan 23: inside a dispatch the output passes through catherd, which notes when the command last wrote
53
+ const piped = o.onOutput !== undefined;
48
54
  const child = Bun.spawn(argv, {
49
55
  stdin: "inherit",
50
- stdout: "inherit",
51
- stderr: "inherit",
52
- env: scrubSecrets(process.env),
56
+ stdout: piped ? "pipe" : "inherit",
57
+ stderr: piped ? "pipe" : "inherit",
58
+ env: { ...scrubSecrets(process.env), ...o.extraEnv },
53
59
  detached: true,
54
60
  });
61
+ const pumps =
62
+ piped && o.onOutput
63
+ ? [
64
+ pump(child.stdout as ReadableStream<Uint8Array>, process.stdout, o.onOutput),
65
+ pump(child.stderr as ReadableStream<Uint8Array>, process.stderr, o.onOutput),
66
+ ]
67
+ : [];
55
68
  let lastInt = 0;
56
69
  const handlers: [NodeJS.Signals, () => void][] = [
57
70
  [
@@ -80,15 +93,52 @@ export async function runForwarding(argv: string[]): Promise<number> {
80
93
  },
81
94
  );
82
95
  try {
83
- return await child.exited;
96
+ const code = await child.exited;
97
+ // a process the command left behind may hold the pipes open: what it wrote by then is passed on
98
+ await Promise.race([Promise.all(pumps), Bun.sleep(PIPE_DRAIN_MS)]);
99
+ return code;
84
100
  } finally {
85
101
  watchdog.kill("SIGKILL");
86
102
  for (const [sig, h] of handlers) process.off(sig, h);
87
103
  }
88
104
  }
89
105
 
106
+ /** How long the pipes may stay open after the command exits. */
107
+ const PIPE_DRAIN_MS = 500;
108
+
109
+ /** Copies a piped stream to `out`, calling `tick` for each chunk; a broken pipe ends the copy. */
110
+ async function pump(
111
+ stream: ReadableStream<Uint8Array>,
112
+ out: NodeJS.WriteStream,
113
+ tick: () => void,
114
+ ): Promise<void> {
115
+ try {
116
+ for await (const chunk of stream) {
117
+ out.write(chunk);
118
+ tick();
119
+ }
120
+ } catch {
121
+ // the command's end closes the pipe
122
+ }
123
+ }
124
+
90
125
  /** Spec §8: what `--help` and the usage error show; the command runs after `--`. */
91
- export const LOCK_USAGE = "catherd lock [--slots N] -- <command> [args...]";
126
+ export const LOCK_USAGE = "catherd lock [--slots N] [--role verifier [--run <run>]] -- <command> [args...]";
127
+
128
+ /** The roles that take a machine-wide role lock besides their slot (spec 1.5 "Cross-run verifier contention"). */
129
+ export const ROLE_LOCKS = ["verifier"] as const;
130
+
131
+ /**
132
+ * Whose a role lock is: `--run`, else the run in `CATHERD_ROLE` (`<run>/<role-name>`, which the supervisor
133
+ * sets in every role's env), else this process alone. Commands of one run share the lock.
134
+ */
135
+ export function roleLockOwnerOf(run: string | undefined, env: NodeJS.ProcessEnv): string {
136
+ if (run) return `run:${run}`;
137
+ const fromRole = env.CATHERD_ROLE?.split("/")[0];
138
+ return fromRole ? `run:${fromRole}` : `process:${process.pid}`;
139
+ }
140
+
141
+ export { LOCK_HELD_ENV };
92
142
 
93
143
  export const lockCommand = defineCommand({
94
144
  meta: {
@@ -102,6 +152,15 @@ export const lockCommand = defineCommand({
102
152
  type: "string",
103
153
  description: "Slots (default: CATHERD_LOCK_SLOTS, else the profile's lock.heavy, else half the cores)",
104
154
  },
155
+ role: {
156
+ type: "string",
157
+ description:
158
+ "verifier: also hold the machine-wide verifier lock, shared by one run's commands, so two runs' verifiers never overlap",
159
+ },
160
+ run: {
161
+ type: "string",
162
+ description: "with --role: the run whose commands share it (default: CATHERD_ROLE's)",
163
+ },
105
164
  },
106
165
  async run({ args, rawArgs }) {
107
166
  const sep = rawArgs.indexOf("--");
@@ -115,6 +174,31 @@ export const lockCommand = defineCommand({
115
174
  process.exitCode = 2;
116
175
  return;
117
176
  }
177
+ if (args.role !== undefined && !(ROLE_LOCKS as readonly string[]).includes(args.role)) {
178
+ printError(
179
+ new CatherdError("E_INPUT_INVALID", `no role lock "${args.role}"`, {
180
+ fix: "catherd lock --role verifier -- <command>",
181
+ }),
182
+ );
183
+ process.exitCode = 2;
184
+ return;
185
+ }
186
+ // already inside a locked command: run at once, so a nested lock never waits on its own parent's slot
187
+ if (process.env[LOCK_HELD_ENV] === "1") {
188
+ process.exitCode = await runForwarding(argv);
189
+ return;
190
+ }
191
+ // plan 23: a role's lock reports its output, so the role's wall timeout counts from its last line; it
192
+ // reports while it waits for the role lock or a slot too, so a gate queued behind another run's is not
193
+ // killed before its first command
194
+ const activity = activityReporter(process.env[DISPATCH_ID_ENV]);
195
+ const waiting = activity ? { onWait: activity.tick } : {};
196
+ const run = (_slot: number, tmp: string) =>
197
+ runForwarding(argv, {
198
+ extraEnv: { [LOCK_HELD_ENV]: "1", TMPDIR: tmp, GOTMPDIR: tmp },
199
+ ...(activity ? { onOutput: activity.tick } : {}),
200
+ });
201
+ const role = args.role;
118
202
  const repo = await gitToplevel(process.cwd());
119
203
  const slots = resolveSlots(
120
204
  args.slots,
@@ -123,6 +207,17 @@ export const lockCommand = defineCommand({
123
207
  () => readProfileDoc(activeName(repo)).lock?.heavy ?? "cpus/2",
124
208
  (m) => console.error(m),
125
209
  );
126
- process.exitCode = await withHeavySlot(slots, () => runForwarding(argv));
210
+ try {
211
+ process.exitCode = role
212
+ ? await withRoleLock(
213
+ role,
214
+ roleLockOwnerOf(args.run, process.env),
215
+ () => withHeavySlot(slots, run, { ...waiting, tmpBase: process.env.TMPDIR }),
216
+ waiting,
217
+ )
218
+ : await withHeavySlot(slots, run, { ...waiting, tmpBase: process.env.TMPDIR });
219
+ } finally {
220
+ activity?.done();
221
+ }
127
222
  },
128
223
  });
@@ -12,11 +12,15 @@ export const LAUNCHER = fileURLToPath(new URL("../../../plugin/bin/catherd-mcp",
12
12
  const TIMEOUT_MS = 60_000;
13
13
  const STDERR_TAIL = 4_000;
14
14
 
15
- /** The env the doctor's MCP server starts with: catherd's own secrets scrubbed, as for every process it starts. */
15
+ /**
16
+ * The env the doctor's MCP server starts with: catherd's own secrets scrubbed, as for every process it starts,
17
+ * and no boot sync (1.2 minor): the server lives for one tools/list, so a sync there spends fetches (AA's
18
+ * included) and records nothing.
19
+ */
16
20
  export const handshakeEnv = (
17
21
  base: Record<string, string | undefined> = process.env,
18
22
  ): Record<string, string> => {
19
- return scrubSecrets(base);
23
+ return { ...scrubSecrets(base), CATHERD_NO_SYNC: "1" };
20
24
  };
21
25
 
22
26
  /**
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { errorMessage } from "../../domain/errors.ts";
3
+ import { ROLES } from "../../domain/roles.ts";
4
+ import { restoreTmpdir } from "../../infra/env.ts";
5
+ import { startRoleMcpServer } from "./role-server.ts";
6
+
7
+ if (process.argv[2] === "--probe") {
8
+ // doctor's start probe: the server's module graph is loaded by now (spec 1.5 plan 21)
9
+ console.log("ready");
10
+ process.exit(0);
11
+ }
12
+
13
+ try {
14
+ const [role, run] = z.tuple([z.enum(ROLES), z.string().min(1)]).parse(process.argv.slice(2));
15
+ restoreTmpdir(process.env);
16
+ await startRoleMcpServer({ role, run });
17
+ } catch (e) {
18
+ console.error(`catherd role MCP: ${errorMessage(e)}`);
19
+ process.exitCode = 1;
20
+ }
@@ -0,0 +1,134 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
+ import { z } from "zod";
5
+ import { ID_PATTERN } from "../../domain/ids.ts";
6
+ import { ROLE_MCP_SERVER, type RoleMcpContext, roleMcpTools } from "../../domain/role-tools.ts";
7
+ import { log } from "../../infra/log.ts";
8
+ import { gateCheckOrList, gatePass } from "../../services/gate-service.ts";
9
+ import { runMilestoneCheck } from "../../services/milestone-check.ts";
10
+ import type { Deps } from "../../services/ports.ts";
11
+ import { readKnowledge, readRunFile, writeRunFile } from "../../services/run-service.ts";
12
+ import { findRun } from "../../services/run-store.ts";
13
+ import { defaultDeps } from "../deps.ts";
14
+ import { handle, sdkToolError, toolOf } from "./result.ts";
15
+
16
+ /** A role can reach only its run artifacts and, for a verifier, that run's gate evidence. */
17
+ export function buildRoleServer(context: RoleMcpContext, deps: Deps = defaultDeps()): McpServer {
18
+ const run = findRun(context.run);
19
+ const server = new McpServer({ name: ROLE_MCP_SERVER, version: deps.version });
20
+ const tools = new Set(roleMcpTools(context.role));
21
+ const boundRun = z.literal(run.id);
22
+ const runFile = { run: boundRun, path: z.string().min(1) };
23
+ const call = async (tool: string, f: () => unknown) => {
24
+ const started = Date.now();
25
+ const result = await handle(f);
26
+ log(result.isError ? "warn" : "info", "tool", {
27
+ tool,
28
+ ms: Date.now() - started,
29
+ ok: !result.isError,
30
+ code: result.isError ? (result.structuredContent as { code?: string } | undefined)?.code : undefined,
31
+ });
32
+ return result;
33
+ };
34
+ (server as unknown as { createToolError: typeof sdkToolError }).createToolError = (message) => {
35
+ const result = sdkToolError(message);
36
+ log("warn", "tool", {
37
+ tool: toolOf(message),
38
+ ok: false,
39
+ code: (result.structuredContent as { code?: string } | undefined)?.code,
40
+ });
41
+ return result;
42
+ };
43
+
44
+ server.registerTool(
45
+ "read_run_file",
46
+ {
47
+ description: "Read an artifact from this role's run folder, using a run-relative path.",
48
+ inputSchema: runFile,
49
+ annotations: { readOnlyHint: true, openWorldHint: false },
50
+ },
51
+ (a) => call("read_run_file", () => readRunFile(a)),
52
+ );
53
+ server.registerTool(
54
+ "read_knowledge",
55
+ {
56
+ description:
57
+ "Read what past runs learned about this role's repository. Takes no argument: it reads the run's own repository.",
58
+ // #42 finding 4: `.`, the pwd or /tmp vs /private/tmp never matched a literal; any repo given is ignored
59
+ inputSchema: { repo: z.string().optional() },
60
+ annotations: { readOnlyHint: true, openWorldHint: false },
61
+ },
62
+ () => call("read_knowledge", () => readKnowledge(run.meta.repo)),
63
+ );
64
+ if (tools.has("write_run_file"))
65
+ server.registerTool(
66
+ "write_run_file",
67
+ {
68
+ description: "Write a plan, lane, dossier or note in this run; catherd's own files are protected.",
69
+ inputSchema: { ...runFile, content: z.string() },
70
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
71
+ },
72
+ (a) => call("write_run_file", () => writeRunFile(a)),
73
+ );
74
+ if (tools.has("gate_check")) {
75
+ const gate = {
76
+ run: boundRun,
77
+ item: z.string().min(1),
78
+ command: z.string().min(1),
79
+ paths: z.array(z.string().min(1)).min(1),
80
+ };
81
+ server.registerTool(
82
+ "gate_check",
83
+ {
84
+ description:
85
+ "Check whether this run's gate item passed on unchanged content; records the current step. With only run and milestone, lists the items already checked for that milestone in this run, each with its command and whether it passed: reuse those names and re-check the failed ones first.",
86
+ inputSchema: {
87
+ run: boundRun,
88
+ item: gate.item.optional(),
89
+ command: gate.command.optional(),
90
+ paths: gate.paths.optional(),
91
+ milestone: z.string().regex(ID_PATTERN).optional(),
92
+ },
93
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
94
+ },
95
+ (a) => call("gate_check", () => gateCheckOrList(deps, a)),
96
+ );
97
+ server.registerTool(
98
+ "gate_pass",
99
+ {
100
+ description: "Record this run's passed gate item with the evidence, command and content hash.",
101
+ inputSchema: { ...gate, evidence: z.string().min(1) },
102
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
103
+ },
104
+ (a) => call("gate_pass", () => gatePass(deps, a)),
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
+ );
124
+ return server;
125
+ }
126
+
127
+ /** Role servers have no orchestrator ownership, notifier, reconciliation or background catalog sync. */
128
+ export async function startRoleMcpServer(
129
+ context: RoleMcpContext,
130
+ o: { deps?: Deps; transport?: Transport } = {},
131
+ ): Promise<void> {
132
+ const server = buildRoleServer(context, o.deps);
133
+ await server.connect(o.transport ?? new StdioServerTransport());
134
+ }