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.
- package/README.md +188 -169
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -239
- package/package.json +6 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +97 -356
- package/plugin/skills/catherd-setup/SKILL.md +21 -20
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +19 -4
- package/src/adapters/claude-code/index.ts +126 -14
- package/src/adapters/codex/events.ts +10 -0
- package/src/adapters/codex/index.ts +17 -5
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +10 -4
- package/src/adapters/opencode/index.ts +38 -1
- package/src/cli.ts +27 -27
- package/src/domain/agents.ts +3 -1
- package/src/domain/bars.ts +33 -107
- package/src/domain/blocked.ts +72 -0
- package/src/domain/branch.ts +51 -0
- package/src/domain/brief.ts +106 -0
- package/src/domain/catalog.ts +69 -313
- package/src/domain/changes.ts +23 -1
- package/src/domain/cost.ts +19 -29
- package/src/domain/errors.ts +6 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -75
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +53 -0
- package/src/domain/hints.ts +13 -2
- package/src/domain/ids.ts +13 -2
- package/src/domain/jev.ts +47 -2
- package/src/domain/judge.ts +124 -0
- package/src/domain/lane.ts +118 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +32 -8
- package/src/domain/notify.ts +32 -0
- package/src/domain/plan-doc.ts +198 -0
- package/src/domain/plan-lint.ts +141 -0
- package/src/domain/profile-rules.ts +175 -138
- package/src/domain/profile.ts +140 -63
- package/src/domain/record.ts +68 -3
- package/src/domain/reply.ts +29 -0
- package/src/domain/role-prompts.ts +182 -56
- package/src/domain/role-scope.ts +76 -0
- package/src/domain/role-tools.ts +34 -0
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +90 -0
- package/src/domain/rules.ts +33 -0
- package/src/domain/run-events.ts +298 -0
- package/src/domain/run-machine.ts +668 -0
- package/src/domain/run-state.ts +1310 -0
- package/src/domain/scorecard.ts +197 -0
- package/src/domain/select.ts +229 -72
- package/src/domain/slopalytics.ts +72 -0
- package/src/domain/state.ts +5 -0
- package/src/domain/util.ts +22 -0
- package/src/domain/workspace.ts +130 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +24 -165
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +18 -2
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/deps.ts +2 -0
- package/src/entry/doctor-command.ts +20 -2
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/host-arg.ts +28 -0
- package/src/entry/init-command.ts +20 -69
- package/src/entry/knowledge-command.ts +121 -5
- package/src/entry/lock-command.ts +103 -8
- package/src/entry/mcp/handshake.ts +6 -2
- package/src/entry/mcp/role-bin.ts +20 -0
- package/src/entry/mcp/role-server.ts +134 -0
- package/src/entry/mcp/server.ts +85 -21
- package/src/entry/mcp/session-tools.ts +149 -0
- package/src/entry/panel/openapi.ts +189 -0
- package/src/entry/panel/server.ts +410 -0
- package/src/entry/panel-command.ts +68 -0
- package/src/entry/pause-command.ts +67 -0
- package/src/entry/profile-command.ts +33 -18
- package/src/entry/role-cli-command.ts +159 -0
- package/src/entry/runs-command.ts +81 -27
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/entry/supervise-command.ts +30 -3
- package/src/entry/workspace-command.ts +44 -0
- package/src/infra/boot-lock.ts +25 -0
- package/src/infra/delivery.ts +3 -0
- package/src/infra/dispatch-dir.ts +2 -0
- package/src/infra/env.ts +11 -3
- package/src/infra/git.ts +70 -0
- package/src/infra/heavy-lock.ts +133 -6
- package/src/infra/host-probe.ts +16 -0
- package/src/infra/launch.ts +63 -7
- package/src/infra/lock-activity.ts +88 -0
- package/src/infra/login-env.ts +48 -0
- package/src/infra/memory.ts +45 -0
- package/src/infra/ntfy.ts +40 -0
- package/src/infra/open-url.ts +42 -0
- package/src/infra/paths.ts +37 -2
- package/src/infra/role-mcp.ts +69 -0
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/infra/supervisor.ts +107 -16
- package/src/services/admission.ts +133 -31
- package/src/services/backends.ts +4 -6
- package/src/services/budget.ts +26 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -254
- package/src/services/credentials.ts +15 -26
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +209 -17
- package/src/services/dispatches.ts +30 -2
- package/src/services/doctor-access.ts +8 -3
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor-docker.ts +221 -0
- package/src/services/doctor-role-mcp.ts +65 -0
- package/src/services/doctor.ts +72 -15
- package/src/services/end-push.ts +157 -0
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +24 -2
- package/src/services/gate-env.ts +217 -0
- package/src/services/gate-service.ts +231 -45
- package/src/services/global-install.ts +46 -13
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-edit.ts +154 -0
- package/src/services/lane-service.ts +291 -55
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +167 -29
- package/src/services/notifier.ts +124 -4
- package/src/services/notify-service.ts +26 -0
- package/src/services/orchestrator-wait.ts +82 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/pause.ts +161 -0
- package/src/services/peek.ts +79 -17
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +60 -5
- package/src/services/preflight.ts +232 -24
- package/src/services/profile-service.ts +31 -3
- package/src/services/profile-store.ts +18 -6
- package/src/services/protocol.ts +123 -25
- package/src/services/provenance.ts +34 -89
- package/src/services/questions.ts +24 -0
- package/src/services/reconcile.ts +33 -1
- package/src/services/reentry.ts +7 -6
- package/src/services/role-access.ts +27 -0
- package/src/services/routing-service.ts +161 -42
- package/src/services/run-briefs.ts +477 -0
- package/src/services/run-client.ts +217 -0
- package/src/services/run-effects.ts +837 -0
- package/src/services/run-events-store.ts +174 -0
- package/src/services/run-evidence.ts +1 -1
- package/src/services/run-pin.ts +125 -0
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +204 -23
- package/src/services/run-store.ts +159 -18
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/scratch.ts +60 -0
- package/src/services/sessions.ts +3 -1
- package/src/services/state.ts +21 -0
- package/src/services/summary.ts +76 -12
- package/src/services/tree-path.ts +27 -0
- package/src/services/verifier-brief.ts +23 -0
- package/src/services/verifier-step.ts +48 -0
- package/src/services/workspace-admission.ts +259 -0
- package/src/services/workspace-service.ts +259 -0
- package/src/services/workspace-store.ts +95 -0
- package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js +1 -0
- package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
- package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js +1 -0
- package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-B6GiwDkh.js +11 -0
- package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
- package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
- package/web/dist/assets/index-C5IbylvM.css +2 -0
- package/web/dist/assets/index-C5IbylvM.css.br +0 -0
- package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
- package/web/dist/assets/label-DFSYBc62.js +1 -0
- package/web/dist/assets/label-DFSYBc62.js.br +0 -0
- package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
- package/web/dist/assets/link-D8EcKZUt.js +1 -0
- package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
- package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
- package/web/dist/assets/login-DaN5HPPW.js +1 -0
- package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
- package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
- package/web/dist/assets/query-Cu--ccT3.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
- package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
- package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
- package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
- package/web/dist/assets/table-BOxXhqS4.js +1 -0
- package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
- package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
- package/web/dist/assets/time--9kSQbbp.js +1 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
- package/web/dist/index.html +15 -0
- package/catalog/ATTRIBUTION.md +0 -36
- package/catalog/scores.json +0 -3202
- package/catalog/sources.json +0 -97
- package/src/adapters/antigravity/events.ts +0 -131
- package/src/adapters/antigravity/home.ts +0 -46
- package/src/adapters/antigravity/index.ts +0 -320
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -132
- package/src/adapters/grok/index.ts +0 -339
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -125
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -84
- package/src/entry/mcp/protocol-tools.ts +0 -57
- package/src/entry/mcp/run-tools.ts +0 -148
- package/src/entry/mcp/setup-tools.ts +0 -108
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -381
- package/src/entry/tui/fixtures.ts +0 -637
- package/src/entry/tui/profile-edits.ts +0 -146
- package/src/entry/tui/profile-tree.ts +0 -429
- package/src/entry/tui/providers/app.tsx +0 -162
- package/src/entry/tui/providers/boundary.tsx +0 -57
- package/src/entry/tui/providers/data.tsx +0 -160
- package/src/entry/tui/providers/keymap.tsx +0 -166
- package/src/entry/tui/providers/root.tsx +0 -45
- package/src/entry/tui/providers/theme.tsx +0 -17
- package/src/entry/tui/providers/toast.tsx +0 -77
- package/src/entry/tui/run.tsx +0 -114
- package/src/entry/tui/state.ts +0 -373
- package/src/entry/tui/stories.ts +0 -114
- package/src/entry/tui/text.ts +0 -130
- package/src/entry/tui/theme.ts +0 -140
- package/src/entry/tui/views/app.tsx +0 -208
- package/src/entry/tui/views/dialogs.tsx +0 -41
- package/src/entry/tui/views/profile-actions.ts +0 -331
- package/src/entry/tui/views/profiles.tsx +0 -520
- package/src/entry/tui/views/runs.tsx +0 -573
- package/src/entry/tui/views/save-dialog.tsx +0 -340
- package/src/entry/tui/views/status.tsx +0 -209
- package/src/entry/tui/widgets/chrome.tsx +0 -114
- package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
- package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
- package/src/entry/tui/widgets/dialog-select.tsx +0 -212
- package/src/entry/tui/widgets/dialog.tsx +0 -68
- package/src/entry/tui/widgets/line.tsx +0 -92
- package/src/entry/tui/widgets/list.tsx +0 -197
- package/src/entry/tui/widgets/spinner.tsx +0 -49
- package/src/entry/tui/widgets/toast.tsx +0 -62
- package/src/infra/sources/arena.ts +0 -87
- package/src/infra/sources/artificial-analysis.ts +0 -112
- package/src/infra/sources/cache.ts +0 -124
- package/src/infra/sources/csv.ts +0 -38
- package/src/infra/sources/epoch.ts +0 -82
- package/src/infra/sources/http.ts +0 -75
- package/src/infra/sources/litellm.ts +0 -58
- package/src/infra/sources/models-dev.ts +0 -59
- package/src/infra/sources/openrouter-endpoints.ts +0 -52
- package/src/infra/sources/openrouter-models.ts +0 -60
- package/src/infra/sources/rows.ts +0 -38
- package/src/infra/sources/vectara.ts +0 -69
- package/src/infra/sources/zip.ts +0 -48
- package/src/services/catalog-refresh.ts +0 -201
- package/src/services/doctor-sources.ts +0 -76
- package/src/services/runs-page.ts +0 -255
- package/src/services/source-derive.ts +0 -374
- package/src/services/source-sync.ts +0 -305
- package/src/services/standins.ts +0 -204
- 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 {
|
|
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
|
|
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
|
|
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`
|
|
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
|
-
*
|
|
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(
|
|
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("-
|
|
130
|
-
console.log("
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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,
|
|
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 {
|
|
4
|
-
import {
|
|
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: {
|
|
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: {
|
|
49
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
+
}
|