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
package/src/domain/util.ts
CHANGED
|
@@ -16,6 +16,28 @@ export function median(xs: readonly number[]): number | null {
|
|
|
16
16
|
return s.length % 2 ? (s[m] as number) : ((s[m - 1] as number) + (s[m] as number)) / 2;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** A stretch of time in epoch ms; `to` null while it is still open. */
|
|
20
|
+
export interface Span {
|
|
21
|
+
from: number;
|
|
22
|
+
to: number | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** How many ms of [from, to] any of `spans` covers, overlaps counted once; an open span runs to `to`. */
|
|
26
|
+
export function coveredMs(from: number, to: number, spans: readonly Span[]): number {
|
|
27
|
+
const clipped = spans
|
|
28
|
+
.map((s) => [Math.max(from, s.from), Math.min(to, s.to ?? to)] as const)
|
|
29
|
+
.filter(([a, b]) => b > a)
|
|
30
|
+
.sort((x, y) => x[0] - y[0]);
|
|
31
|
+
let total = 0;
|
|
32
|
+
let end = -Infinity;
|
|
33
|
+
for (const [a, b] of clipped) {
|
|
34
|
+
if (b <= end) continue;
|
|
35
|
+
total += b - Math.max(a, end);
|
|
36
|
+
end = b;
|
|
37
|
+
}
|
|
38
|
+
return total;
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
/** A plain object: not null and not an array. */
|
|
20
42
|
export const isPlain = (v: unknown): v is Record<string, unknown> =>
|
|
21
43
|
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ID_PATTERN } from "./ids.ts";
|
|
3
|
+
|
|
4
|
+
export const WORKSPACE_LIMIT = 100;
|
|
5
|
+
|
|
6
|
+
export const WorkspaceIdSchema = z
|
|
7
|
+
.string()
|
|
8
|
+
.regex(ID_PATTERN)
|
|
9
|
+
.refine((s) => !s.includes(".."));
|
|
10
|
+
export const WorkspaceBudgetSchema = z.strictObject({
|
|
11
|
+
minutes: z.number().finite().nonnegative().optional(),
|
|
12
|
+
tokens: z.number().finite().nonnegative().optional(),
|
|
13
|
+
usd: z.number().finite().nonnegative().optional(),
|
|
14
|
+
});
|
|
15
|
+
/** A git ref a release waits on (`origin/main`): no option, no range, no whitespace. */
|
|
16
|
+
export const BaseRefSchema = z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(/^[A-Za-z0-9_][A-Za-z0-9._/-]*$/)
|
|
19
|
+
.refine((s) => !s.includes("..") && !s.endsWith("/") && !s.endsWith(".lock"));
|
|
20
|
+
export const RELEASES = ["land", "merge"] as const;
|
|
21
|
+
export const WorkspaceStepSchema = z
|
|
22
|
+
.strictObject({
|
|
23
|
+
id: WorkspaceIdSchema,
|
|
24
|
+
repo: WorkspaceIdSchema,
|
|
25
|
+
title: z.string().min(1),
|
|
26
|
+
aLines: z.array(z.string()),
|
|
27
|
+
dependsOn: z.array(WorkspaceIdSchema).max(WORKSPACE_LIMIT).default([]),
|
|
28
|
+
milestone: WorkspaceIdSchema.default("M1"),
|
|
29
|
+
/**
|
|
30
|
+
* When the step releases its dependents: once its milestone lands (the default), or once the landed
|
|
31
|
+
* commit is an ancestor of `base` in its repository (`git merge-base --is-ancestor`), checked when a
|
|
32
|
+
* dependent asks, never polled.
|
|
33
|
+
*/
|
|
34
|
+
release: z.enum(RELEASES).default("land"),
|
|
35
|
+
base: BaseRefSchema.optional(),
|
|
36
|
+
})
|
|
37
|
+
.refine((s) => (s.release === "merge") === (s.base !== undefined), {
|
|
38
|
+
message: 'release "merge" needs a base ref (e.g. origin/main), and only it takes one',
|
|
39
|
+
});
|
|
40
|
+
export type WorkspaceStep = z.infer<typeof WorkspaceStepSchema>;
|
|
41
|
+
export const WorkspaceSchema = z
|
|
42
|
+
.strictObject({
|
|
43
|
+
schema: z.literal(1),
|
|
44
|
+
id: WorkspaceIdSchema,
|
|
45
|
+
root: z.string().min(1),
|
|
46
|
+
title: z.string().min(1),
|
|
47
|
+
aLines: z.array(z.string()),
|
|
48
|
+
createdAt: z.iso.datetime(),
|
|
49
|
+
budget: WorkspaceBudgetSchema,
|
|
50
|
+
repos: z
|
|
51
|
+
.record(WorkspaceIdSchema, z.string().min(1))
|
|
52
|
+
.refine((repos) => Object.keys(repos).length <= WORKSPACE_LIMIT),
|
|
53
|
+
steps: z.array(WorkspaceStepSchema).min(1).max(WORKSPACE_LIMIT),
|
|
54
|
+
})
|
|
55
|
+
.superRefine((workspace, ctx) => {
|
|
56
|
+
// Zod may still refine an oversized array; do not traverse invalid graphs.
|
|
57
|
+
if (
|
|
58
|
+
workspace.steps.length > WORKSPACE_LIMIT ||
|
|
59
|
+
Object.keys(workspace.repos).length > WORKSPACE_LIMIT ||
|
|
60
|
+
workspace.steps.some((step) => step.dependsOn.length > WORKSPACE_LIMIT)
|
|
61
|
+
)
|
|
62
|
+
return;
|
|
63
|
+
const ids = new Set<string>();
|
|
64
|
+
let invalidIdentity = false;
|
|
65
|
+
for (const step of workspace.steps) {
|
|
66
|
+
if (ids.has(step.id) || !Object.hasOwn(workspace.repos, step.repo)) {
|
|
67
|
+
invalidIdentity = true;
|
|
68
|
+
ctx.addIssue({
|
|
69
|
+
code: "custom",
|
|
70
|
+
message: "steps require unique ids and repositories present in the snapshot",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
ids.add(step.id);
|
|
74
|
+
}
|
|
75
|
+
if (invalidIdentity) return;
|
|
76
|
+
const byId = new Map(workspace.steps.map((step) => [step.id, step]));
|
|
77
|
+
const remaining = new Map<string, number>();
|
|
78
|
+
const dependents = new Map<string, string[]>();
|
|
79
|
+
const ready: string[] = [];
|
|
80
|
+
for (const step of workspace.steps) {
|
|
81
|
+
if (
|
|
82
|
+
new Set(step.dependsOn).size !== step.dependsOn.length ||
|
|
83
|
+
step.dependsOn.some((id) => !byId.has(id))
|
|
84
|
+
) {
|
|
85
|
+
ctx.addIssue({
|
|
86
|
+
code: "custom",
|
|
87
|
+
message: "dependencies must form a graph without cycles or unknown steps",
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
remaining.set(step.id, step.dependsOn.length);
|
|
92
|
+
if (!step.dependsOn.length) ready.push(step.id);
|
|
93
|
+
for (const id of step.dependsOn) {
|
|
94
|
+
const next = dependents.get(id) ?? [];
|
|
95
|
+
next.push(step.id);
|
|
96
|
+
dependents.set(id, next);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const ancestors = new Map<string, Set<string>>();
|
|
100
|
+
const latestByRepo = new Map<string, string>();
|
|
101
|
+
// Topological order makes adjacent owners sufficient: if each owner follows
|
|
102
|
+
// the previous owner transitively, every pair of owners is ordered.
|
|
103
|
+
for (let i = 0; i < ready.length; i++) {
|
|
104
|
+
const step = byId.get(ready[i]!)!;
|
|
105
|
+
const before = new Set<string>();
|
|
106
|
+
for (const id of step.dependsOn) {
|
|
107
|
+
before.add(id);
|
|
108
|
+
for (const ancestor of ancestors.get(id)!) before.add(ancestor);
|
|
109
|
+
}
|
|
110
|
+
ancestors.set(step.id, before);
|
|
111
|
+
const previous = latestByRepo.get(step.repo);
|
|
112
|
+
if (previous && !before.has(previous))
|
|
113
|
+
ctx.addIssue({
|
|
114
|
+
code: "custom",
|
|
115
|
+
message: "steps sharing a repository must be ordered by dependencies",
|
|
116
|
+
});
|
|
117
|
+
latestByRepo.set(step.repo, step.id);
|
|
118
|
+
for (const id of dependents.get(step.id) ?? []) {
|
|
119
|
+
const count = remaining.get(id)! - 1;
|
|
120
|
+
remaining.set(id, count);
|
|
121
|
+
if (!count) ready.push(id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (ready.length !== workspace.steps.length)
|
|
125
|
+
ctx.addIssue({
|
|
126
|
+
code: "custom",
|
|
127
|
+
message: "dependencies must form a graph without cycles or unknown steps",
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
export type Workspace = z.infer<typeof WorkspaceSchema>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { CatherdError } from "../domain/errors.ts";
|
|
3
|
+
import { answerUnit } from "../services/run-client.ts";
|
|
4
|
+
import { defaultDeps } from "./deps.ts";
|
|
5
|
+
import { JSON_ARG, printJson, refuseInRole } from "./cli-kit.ts";
|
|
6
|
+
|
|
7
|
+
/** What the phone message names: the run, the unit and the answer, as one command. */
|
|
8
|
+
export const answerCommand = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "answer",
|
|
11
|
+
description: "Answer a parked unit of a run: it runs again with your answer",
|
|
12
|
+
},
|
|
13
|
+
args: {
|
|
14
|
+
run: { type: "positional", required: true, description: "the run id" },
|
|
15
|
+
unit: {
|
|
16
|
+
type: "positional",
|
|
17
|
+
required: true,
|
|
18
|
+
description: "the parked unit: the run, a milestone or a lane",
|
|
19
|
+
},
|
|
20
|
+
answer: {
|
|
21
|
+
type: "positional",
|
|
22
|
+
required: false,
|
|
23
|
+
description: "your answer (several words need no quotes)",
|
|
24
|
+
},
|
|
25
|
+
...JSON_ARG,
|
|
26
|
+
},
|
|
27
|
+
async run({ args }) {
|
|
28
|
+
refuseInRole(process.env, "catherd answer");
|
|
29
|
+
// citty names the first words; the rest of the line is the answer's too
|
|
30
|
+
const answer = (args._ as string[]).slice(2).join(" ").trim();
|
|
31
|
+
if (!answer)
|
|
32
|
+
throw new CatherdError("E_INPUT_INVALID", "no answer given", {
|
|
33
|
+
fix: 'catherd answer <run> <unit> "<your answer>"',
|
|
34
|
+
});
|
|
35
|
+
const r = await answerUnit(defaultDeps(), { run: args.run, unit: args.unit, answer });
|
|
36
|
+
if (args.json) return printJson(r);
|
|
37
|
+
console.log(`answered ${r.unit}${r.parked.length ? `; still waiting: ${r.parked.join(", ")}` : ""}`);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { HOST_ARG, terminalHost } from "./host-arg.ts";
|
|
2
1
|
import { defineCommand } from "citty";
|
|
3
2
|
import { CatherdError, isCatherdError } from "../domain/errors.ts";
|
|
4
3
|
import { ROLES, type Role } from "../domain/roles.ts";
|
|
@@ -6,68 +5,35 @@ import { gitToplevel } from "../infra/git.ts";
|
|
|
6
5
|
import {
|
|
7
6
|
type CatalogModel,
|
|
8
7
|
catalogQuery,
|
|
8
|
+
type IndexStatus,
|
|
9
9
|
type Refreshed,
|
|
10
10
|
refreshDiscovery,
|
|
11
|
-
|
|
11
|
+
refreshIndex,
|
|
12
12
|
} from "../services/catalog-service.ts";
|
|
13
|
-
import {
|
|
14
|
-
import { type SyncReport, syncSources } from "../services/source-sync.ts";
|
|
15
|
-
import { clearTreatLike, type LeftOnStandIn, resetTreatLikes, suggestFor } from "../services/treat-likes.ts";
|
|
13
|
+
import { indexCheck } from "../services/doctor-checks.ts";
|
|
16
14
|
import { exitCodeOf, JSON_ARG, mark, printError } from "./cli-kit.ts";
|
|
17
15
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export function syncLines(r: SyncReport, plain = false): string[] {
|
|
23
|
-
if (r.busy) return ["- sources: another sync is running; its results apply when it ends"];
|
|
24
|
-
const out: string[] = [];
|
|
25
|
-
for (const s of r.sources) {
|
|
26
|
-
if (s.state === "fetched") out.push(`${mark("ok", plain)} ${s.source}: fetched`);
|
|
27
|
-
else if (s.state === "fresh") out.push(`- ${s.source}: fresh (fetched ${s.fetchedAt})`);
|
|
28
|
-
else if (s.state === "skipped") out.push(`- ${s.source}: ${s.detail}`);
|
|
29
|
-
else
|
|
30
|
-
out.push(
|
|
31
|
-
`${mark("warn", plain)} ${s.source}: ${s.error}; ${s.fetchedAt ? `keeps the answer fetched ${s.fetchedAt}` : "no earlier answer"}`,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
if (r.newlyScored.length) out.push(`newly scored: ${r.newlyScored.join(", ")}`);
|
|
35
|
-
for (const n of r.noLongerNeeded)
|
|
36
|
-
out.push(`stand-in no longer needed: ${n.rung} has values of its own for what ${n.like} lent it`);
|
|
37
|
-
for (const w of r.warnings) out.push(`${mark("warn", plain)} ${w}`);
|
|
38
|
-
return out;
|
|
16
|
+
/** `sync`'s one line: the index's pair count and age, or why the last fetch failed and what was kept. */
|
|
17
|
+
export function syncLine(s: IndexStatus, plain = false, now = Date.now()): string {
|
|
18
|
+
const c = indexCheck(now, s);
|
|
19
|
+
return `${mark(c.state === "ok" ? "ok" : "warn", plain)} ${c.label}: ${c.detail}`;
|
|
39
20
|
}
|
|
40
21
|
|
|
41
22
|
const sync = defineCommand({
|
|
42
23
|
meta: {
|
|
43
24
|
name: "sync",
|
|
44
25
|
description:
|
|
45
|
-
"Fetch the
|
|
26
|
+
"Fetch the Slopalytics index now (it is refreshed at most hourly unless --force) and show how many pairs it holds",
|
|
46
27
|
},
|
|
47
28
|
args: {
|
|
48
|
-
force: { type: "boolean", description: "fetch
|
|
49
|
-
unmatched: {
|
|
50
|
-
type: "boolean",
|
|
51
|
-
description: "also list each source's ids that match no model in the catalog",
|
|
52
|
-
},
|
|
29
|
+
force: { type: "boolean", description: "fetch even when the index was checked within the hour" },
|
|
53
30
|
...JSON_ARG,
|
|
54
31
|
},
|
|
55
32
|
async run({ args }) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (args.unmatched && !args.json) {
|
|
61
|
-
const unmatched = readDerived()?.unmatched ?? {};
|
|
62
|
-
if (Object.keys(unmatched).length === 0) console.log("unmatched: none");
|
|
63
|
-
for (const [source, ids] of Object.entries(unmatched))
|
|
64
|
-
console.log(`unmatched in ${source}: ${ids.join(", ")}`);
|
|
65
|
-
}
|
|
66
|
-
const got = r.sources.some((s) => s.state === "fetched" || s.state === "fresh");
|
|
67
|
-
process.exitCode = r.failed.length && !got ? 1 : 0;
|
|
68
|
-
} catch (e) {
|
|
69
|
-
fail(e);
|
|
70
|
-
}
|
|
33
|
+
const s = await refreshIndex({ force: args.force === true });
|
|
34
|
+
if (args.json) console.log(JSON.stringify(s, null, 2));
|
|
35
|
+
else console.log(syncLine(s));
|
|
36
|
+
process.exitCode = s.pairs === 0 ? 1 : 0;
|
|
71
37
|
},
|
|
72
38
|
});
|
|
73
39
|
|
|
@@ -80,29 +46,19 @@ export function formatRefreshed(r: Refreshed, plain = false): string {
|
|
|
80
46
|
].join("\n");
|
|
81
47
|
}
|
|
82
48
|
|
|
83
|
-
/**
|
|
84
|
-
* One model: its rungs scored and roles, then (spec 1.2 §4.1, §5.3, §8) its price and speed facts, and each
|
|
85
|
-
* rung's values with their confidence and source, and the runs catherd has made on it.
|
|
86
|
-
*/
|
|
49
|
+
/** One model: its indexed pairs and roles, then each pair's intelligence, price, speed and the runs catherd has made on it. */
|
|
87
50
|
export function formatModel(m: CatalogModel): string {
|
|
88
|
-
const scored = m.rungs.filter((r) => r.enabled).length;
|
|
89
51
|
const listed = m.listed === false ? " not offered by this account" : "";
|
|
90
52
|
const lines = [
|
|
91
|
-
`${m.backend}:${m.model} ${
|
|
92
|
-
];
|
|
93
|
-
const facts = [
|
|
94
|
-
...(m.price ? [`$${m.price.input}/$${m.price.output} per M tokens in/out`] : []),
|
|
95
|
-
...Object.entries(m.speed).map(([k, v]) => `${k} ${Number(v.toPrecision(4))}`),
|
|
53
|
+
`${m.backend}:${m.model} ${m.rungs.length} ${m.rungs.length === 1 ? "pair" : "pairs"} roles ${m.roles.join(",") || "none"}${listed}`,
|
|
96
54
|
];
|
|
97
|
-
if (facts.length) lines.push(` ${facts.join(" · ")}`);
|
|
98
55
|
for (const r of m.rungs) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
([
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
lines.push(` #${r.rung.slice(r.rung.lastIndexOf("#") + 1)} ${values.join(" · ") || "unscored"}`);
|
|
56
|
+
const parts = [
|
|
57
|
+
`intelligence ${r.intelligence.toFixed(1)}`,
|
|
58
|
+
...(r.price ? [`$${r.price.input}/$${r.price.output} per M`] : []),
|
|
59
|
+
...(r.speed !== null ? [`${Number(r.speed.toPrecision(3))} tok/s`] : []),
|
|
60
|
+
];
|
|
61
|
+
lines.push(` #${r.rung.slice(r.rung.lastIndexOf("#") + 1)} ${parts.join(" · ")}`);
|
|
106
62
|
if (r.evidence) lines.push(` runs: ${r.evidence}`);
|
|
107
63
|
}
|
|
108
64
|
return lines.join("\n");
|
|
@@ -129,12 +85,11 @@ const refresh = defineCommand({
|
|
|
129
85
|
});
|
|
130
86
|
|
|
131
87
|
const list = defineCommand({
|
|
132
|
-
meta: { name: "list", description: "The models catherd can place, with their
|
|
88
|
+
meta: { name: "list", description: "The models catherd can place, with their indexed pairs" },
|
|
133
89
|
args: {
|
|
134
90
|
backend: { type: "string", description: "only this backend or billing key" },
|
|
135
91
|
role: { type: "string", description: `only models for this role (${ROLES.join(", ")})` },
|
|
136
92
|
text: { type: "string", description: "only ids containing this" },
|
|
137
|
-
scored: { type: "boolean", description: "only models with a scored rung" },
|
|
138
93
|
...JSON_ARG,
|
|
139
94
|
},
|
|
140
95
|
async run({ args }) {
|
|
@@ -149,7 +104,6 @@ const list = defineCommand({
|
|
|
149
104
|
backend: args.backend,
|
|
150
105
|
role: args.role as Role | undefined,
|
|
151
106
|
text: args.text,
|
|
152
|
-
scoredOnly: args.scored === true,
|
|
153
107
|
limit: 10_000,
|
|
154
108
|
repo: await hereRepo(),
|
|
155
109
|
});
|
|
@@ -161,103 +115,8 @@ const list = defineCommand({
|
|
|
161
115
|
},
|
|
162
116
|
});
|
|
163
117
|
|
|
164
|
-
/** Spec
|
|
165
|
-
export function leftLines(left: LeftOnStandIn[], plain = false): string[] {
|
|
166
|
-
return left.map(
|
|
167
|
-
(l) =>
|
|
168
|
-
`${mark("warn", plain)} ${l.profile}: ${l.rung} is left ${l.unscored ? "unscored: routing skips it" : `on an inferred stand-in for ${l.dims.join(", ")}`}`,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Spec 1.2 §6.4 `--suggest`: the three nearest stand-ins, each with its distance and the features it rests on. */
|
|
173
|
-
export function suggestLines(r: ReturnType<typeof suggestFor>, rung: string): string[] {
|
|
174
|
-
const head = r.lacking.length
|
|
175
|
-
? `${r.canonical} has no ${r.lacking.join(", ")} value of its own; the nearest stand-ins:`
|
|
176
|
-
: `${r.canonical} has every value the bars use; the nearest scored rungs:`;
|
|
177
|
-
if (r.suggestions.length === 0)
|
|
178
|
-
return [
|
|
179
|
-
`${r.canonical}: no scored rung shares 3 features with it; map it by hand: catherd catalog treat-like ${rung} <rung>`,
|
|
180
|
-
];
|
|
181
|
-
return [
|
|
182
|
-
head,
|
|
183
|
-
...r.suggestions.map(
|
|
184
|
-
(s, i) =>
|
|
185
|
-
` ${i + 1}. ${s.like} distance ${s.distance.toFixed(2)} lends ${s.lends.join(", ")} on ${s.features.join(", ")}`,
|
|
186
|
-
),
|
|
187
|
-
`map one: catherd catalog treat-like ${rung} <rung>`,
|
|
188
|
-
];
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const treatLike = defineCommand({
|
|
192
|
-
meta: {
|
|
193
|
-
name: "treat-like",
|
|
194
|
-
description:
|
|
195
|
-
"Score an unscored rung as a scored one; --suggest ranks stand-ins, --clear and --reset remove your mappings",
|
|
196
|
-
},
|
|
197
|
-
args: {
|
|
198
|
-
...HOST_ARG,
|
|
199
|
-
rung: {
|
|
200
|
-
type: "positional",
|
|
201
|
-
required: false,
|
|
202
|
-
description: "the unscored rung, backend:model#effort or model#effort",
|
|
203
|
-
},
|
|
204
|
-
like: { type: "positional", required: false, description: "the scored rung whose scores it borrows" },
|
|
205
|
-
suggest: { type: "string", description: "list the 3 nearest stand-ins for this rung" },
|
|
206
|
-
clear: { type: "string", description: "remove your treat-like for this rung" },
|
|
207
|
-
reset: { type: "boolean", description: "remove every treat-like of yours" },
|
|
208
|
-
...JSON_ARG,
|
|
209
|
-
},
|
|
210
|
-
async run({ args }) {
|
|
211
|
-
try {
|
|
212
|
-
const modes = [
|
|
213
|
-
args.suggest !== undefined,
|
|
214
|
-
args.clear !== undefined,
|
|
215
|
-
args.reset === true,
|
|
216
|
-
args.rung !== undefined,
|
|
217
|
-
];
|
|
218
|
-
if (modes.filter(Boolean).length !== 1 || (args.rung !== undefined && args.like === undefined))
|
|
219
|
-
throw new CatherdError(
|
|
220
|
-
"E_INPUT_INVALID",
|
|
221
|
-
"treat-like takes <rung> <like>, or one of --suggest, --clear, --reset",
|
|
222
|
-
{
|
|
223
|
-
fix: "catherd catalog treat-like <rung> <like> | --suggest <rung> | --clear <rung> | --reset",
|
|
224
|
-
},
|
|
225
|
-
);
|
|
226
|
-
if (args.suggest !== undefined) {
|
|
227
|
-
const r = suggestFor(args.suggest);
|
|
228
|
-
if (args.json) console.log(JSON.stringify(r, null, 2));
|
|
229
|
-
else for (const l of suggestLines(r, args.suggest)) console.log(l);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (args.clear !== undefined) {
|
|
233
|
-
const r = await clearTreatLike(args.clear, terminalHost(args.host).host);
|
|
234
|
-
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
235
|
-
for (const l of leftLines(r.left)) console.log(l);
|
|
236
|
-
console.log(`${mark("ok")} ${r.rung} is no longer treated like ${r.like}`);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (args.reset) {
|
|
240
|
-
const r = await resetTreatLikes(terminalHost(args.host).host);
|
|
241
|
-
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
242
|
-
for (const l of leftLines(r.left)) console.log(l);
|
|
243
|
-
console.log(
|
|
244
|
-
r.removed.length
|
|
245
|
-
? `${mark("ok")} removed ${r.removed.length} treat-like${r.removed.length === 1 ? "" : "s"}: ${r.removed.map(([a, b]) => `${a} → ${b}`).join(", ")}`
|
|
246
|
-
: "no treat-like of yours to remove",
|
|
247
|
-
);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
const r = await saveTreatLike(args.rung as string, args.like as string);
|
|
251
|
-
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
252
|
-
console.log(`${mark("ok")} ${r.rung} is treated like ${r.like}`);
|
|
253
|
-
} catch (e) {
|
|
254
|
-
fail(e);
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
/** Spec §8 and 1.2 §9: `catherd catalog refresh|list|sync|treat-like <rung> <like> | --suggest|--clear|--reset`. */
|
|
118
|
+
/** Spec §8: `catherd catalog refresh|list|sync`. */
|
|
260
119
|
export const catalogCommand = defineCommand({
|
|
261
120
|
meta: { name: "catalog", description: "The model catalog" },
|
|
262
|
-
subCommands: { refresh, list, sync
|
|
121
|
+
subCommands: { refresh, list, sync },
|
|
263
122
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// The detached check worker's entry: it imports only what running one milestone check needs, never src/cli.ts.
|
|
3
|
+
import { errorMessage } from "../domain/errors.ts";
|
|
4
|
+
import { checkWorker } from "../services/milestone-check.ts";
|
|
5
|
+
|
|
6
|
+
const flight = process.argv[2];
|
|
7
|
+
if (!flight) {
|
|
8
|
+
console.error("usage: check-bin.ts <flight file>");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
await checkWorker(flight);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
console.error(`catherd check: ${errorMessage(e)}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
process.exit(0);
|
package/src/entry/cli-kit.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CatherdError, isCatherdError } from "../domain/errors.ts";
|
|
2
|
+
import { roleScopeFromEnv } from "../domain/role-scope.ts";
|
|
2
3
|
import { RUN_NOT_FOUND_FIX } from "../services/run-store.ts";
|
|
3
4
|
import { glyph, type State } from "./glyphs.ts";
|
|
4
5
|
|
|
@@ -24,13 +25,28 @@ export function printError(e: Pick<CatherdError, "code" | "message" | "fix">): v
|
|
|
24
25
|
export const exitCodeOf = (e: unknown): number =>
|
|
25
26
|
isCatherdError(e) && e.code === "E_INPUT_INVALID" ? EXIT.usage : EXIT.error;
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Spec 1.5 plan 21: a role's shell (CATHERD_ROLE, or its scratch TMPDIR) never runs a command that writes the
|
|
30
|
+
* orchestrator's state (pause, runs cancel, profile set, …): E_ROLE_SCOPE, as `catherd run-file` and `catherd gate`
|
|
31
|
+
* refuse an operation the role has not. `command` is the command as typed, like `catherd pause`.
|
|
32
|
+
*/
|
|
33
|
+
export function refuseInRole(env: Record<string, string | undefined>, command: string): void {
|
|
34
|
+
const scope = roleScopeFromEnv(env);
|
|
35
|
+
if (!scope) return;
|
|
36
|
+
throw new CatherdError(
|
|
37
|
+
"E_ROLE_SCOPE",
|
|
38
|
+
`${command} is the orchestrator's: this process runs ${scope.name} of run ${scope.run}`,
|
|
39
|
+
{ fix: "report it in your reply; the orchestrator does it" },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
export const printJson = (v: unknown): void => console.log(JSON.stringify(v, null, 2));
|
|
28
44
|
|
|
29
45
|
/** The `--json` flag every read command takes. */
|
|
30
46
|
export const JSON_ARG = { json: { type: "boolean", description: "print JSON" } } as const;
|
|
31
47
|
|
|
32
48
|
/**
|
|
33
|
-
* The glyph for a state, as in `✓ ready`:
|
|
49
|
+
* The glyph for a state, as in `✓ ready`: catherd's own (spec §9.3), ASCII with `--plain` only;
|
|
34
50
|
* NO_COLOR drops colour, never glyphs. The caller adds the word.
|
|
35
51
|
*/
|
|
36
52
|
export const mark = (state: State, plain = false): string => glyph(state, plain);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// The detached run daemon's entry: like the supervisor's it imports only what driving a run needs, never
|
|
3
|
+
// src/cli.ts.
|
|
4
|
+
import { errorMessage } from "../domain/errors.ts";
|
|
5
|
+
import { runDaemon } from "../services/daemon.ts";
|
|
6
|
+
import { defaultDeps } from "./deps.ts";
|
|
7
|
+
|
|
8
|
+
const runId = process.argv[2];
|
|
9
|
+
if (!runId) {
|
|
10
|
+
console.error("usage: daemon-bin.ts <run id>");
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
// nobody's role and no Claude Code session: the daemon answers to the run's events
|
|
15
|
+
const result = await runDaemon({ ...defaultDeps(), session: null, role: null }, runId);
|
|
16
|
+
if (result === "lost-lock") console.error(`catherd daemon: another daemon holds run ${runId}`);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error(`catherd daemon: ${errorMessage(e)}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
// the lock is released and the run's events are written: a hook's call left hanging must not keep it alive
|
|
22
|
+
process.exit(0);
|
package/src/entry/deps.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HostContext } from "../domain/host.ts";
|
|
2
|
+
import { roleScopeFromEnv } from "../domain/role-scope.ts";
|
|
2
3
|
import { readSessionEnv } from "../infra/claude-session.ts";
|
|
3
4
|
import { VERSION } from "../infra/version.ts";
|
|
4
5
|
import type { Deps } from "../services/ports.ts";
|
|
@@ -15,6 +16,7 @@ export function defaultDeps(host: HostContext = { host: "unknown", session: null
|
|
|
15
16
|
pollMs: 250,
|
|
16
17
|
session: readSessionEnv(process.env),
|
|
17
18
|
now: Date.now,
|
|
19
|
+
role: roleScopeFromEnv(process.env),
|
|
18
20
|
};
|
|
19
21
|
return deps;
|
|
20
22
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { gitToplevel } from "../infra/git.ts";
|
|
2
|
-
import {
|
|
2
|
+
import { CatherdError } from "../domain/errors.ts";
|
|
3
|
+
import { HOST_ARG, terminalHost, withThread } from "./host-arg.ts";
|
|
3
4
|
import { defineCommand } from "citty";
|
|
4
5
|
import { VERSION } from "../infra/version.ts";
|
|
5
6
|
import { type DoctorReport, doctor } from "../services/doctor.ts";
|
|
6
7
|
import type { Check } from "../services/doctor-checks.ts";
|
|
7
8
|
import { EXIT, JSON_ARG, mark, printJson } from "./cli-kit.ts";
|
|
8
9
|
import { mcpHandshake } from "./mcp/handshake.ts";
|
|
10
|
+
import { probeRoleServer } from "../infra/role-mcp.ts";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* One row per check: `✓ ready Bun — 1.4.2`, then the full fix on its own line; a fix of several lines
|
|
@@ -42,16 +44,32 @@ export const doctorCommand = defineCommand({
|
|
|
42
44
|
description:
|
|
43
45
|
"send one labeled smoke to the validated original session (receipt does not prove processing)",
|
|
44
46
|
},
|
|
47
|
+
thread: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description:
|
|
50
|
+
"with --test-push: the Codex thread to send the smoke to (Codex does not export it to the commands it runs)",
|
|
51
|
+
},
|
|
52
|
+
docker: {
|
|
53
|
+
type: "boolean",
|
|
54
|
+
description:
|
|
55
|
+
"also probe Docker as a gate sees it: two compose services reaching each other by name, and the free disk (pulls busybox)",
|
|
56
|
+
},
|
|
45
57
|
plain: { type: "boolean", description: "ASCII glyphs (NO_COLOR drops only colour)" },
|
|
46
58
|
},
|
|
47
59
|
async run({ args }) {
|
|
60
|
+
if (args.thread !== undefined && args["test-push"] !== true)
|
|
61
|
+
throw new CatherdError("E_INPUT_INVALID", "--thread only names where --test-push sends its smoke", {
|
|
62
|
+
fix: `catherd doctor --test-push --thread ${args.thread}`,
|
|
63
|
+
});
|
|
48
64
|
const r = await doctor({
|
|
49
|
-
host: terminalHost(args.host),
|
|
65
|
+
host: withThread(terminalHost(args.host), args.thread),
|
|
50
66
|
repo: await gitToplevel(process.cwd()),
|
|
51
67
|
bunVersion: Bun.version,
|
|
52
68
|
version: VERSION,
|
|
53
69
|
handshake: () => mcpHandshake(),
|
|
70
|
+
roleServerStart: () => probeRoleServer(),
|
|
54
71
|
testPush: args["test-push"] === true,
|
|
72
|
+
docker: args.docker === true,
|
|
55
73
|
});
|
|
56
74
|
if (args.json) printJson(r);
|
|
57
75
|
else for (const l of formatReport(r, args.plain === true)) console.log(l);
|
package/src/entry/glyphs.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Spec §9.3's glyphs, each with its ASCII twin for `--plain`:
|
|
3
|
-
*
|
|
2
|
+
* Spec §9.3's glyphs, each with its ASCII twin for `--plain`: every CLI command draws state with these, so
|
|
3
|
+
* `mcp`, `lock` and `_supervise` can load them (spec §3.1).
|
|
4
4
|
*/
|
|
5
5
|
const GLYPHS = {
|
|
6
6
|
ok: ["✓", "+"],
|
|
@@ -32,3 +32,16 @@ export const glyph = (g: Glyph, plain: boolean): string => GLYPHS[g][plain ? 1 :
|
|
|
32
32
|
|
|
33
33
|
/** The words state always travels with (spec §9.3: green/amber/red only for state, always with a word). */
|
|
34
34
|
export type State = "ok" | "warn" | "fail" | "skip" | "info";
|
|
35
|
+
|
|
36
|
+
export type Mood = "good" | "working" | "waiting" | "failed";
|
|
37
|
+
export const FACES: Record<Mood, string> = {
|
|
38
|
+
good: "=^.^=",
|
|
39
|
+
working: "=o.o=",
|
|
40
|
+
waiting: "=-.-=",
|
|
41
|
+
failed: "=x.x=",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** The conductor, for the init welcome only (spec §9.3). ASCII already. */
|
|
45
|
+
export function mascot(mood: Mood): [string, string, string] {
|
|
46
|
+
return [" /\\_/\\ .", `(${FACES[mood]})/`, ' (")(")'].map((l) => l.padEnd(9)) as [string, string, string];
|
|
47
|
+
}
|
package/src/entry/host-arg.ts
CHANGED
|
@@ -10,6 +10,34 @@ export const HOST_ARG = {
|
|
|
10
10
|
},
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Plan 22, `doctor --test-push --thread <uuid>`: Codex does not export its thread id to the commands it runs, so a
|
|
17
|
+
* shell names the thread the smoke goes to. It makes the host that Codex thread; a Claude Code session refuses it.
|
|
18
|
+
*/
|
|
19
|
+
export function withThread(host: HostContext, thread: string | undefined): HostContext {
|
|
20
|
+
if (thread === undefined) return host;
|
|
21
|
+
if (!UUID.test(thread))
|
|
22
|
+
throw new CatherdError("E_INPUT_INVALID", `--thread ${thread} is not a Codex thread id`, {
|
|
23
|
+
fix: "pass the thread's UUID, as the first line of a catherd message names it (thread: <uuid>)",
|
|
24
|
+
});
|
|
25
|
+
if (host.host === "claude-code")
|
|
26
|
+
throw new CatherdError(
|
|
27
|
+
"E_INPUT_INVALID",
|
|
28
|
+
"--thread names a Codex thread, and this is a Claude Code session",
|
|
29
|
+
{
|
|
30
|
+
fix: "run catherd doctor --test-push without --thread from Claude Code",
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
const sessionId = thread.toLowerCase();
|
|
34
|
+
return {
|
|
35
|
+
host: "codex",
|
|
36
|
+
session: { host: "codex", sessionId, hostSessionId: null, name: null },
|
|
37
|
+
conflict: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
13
41
|
export function terminalHost(value: string | undefined, env = process.env): HostContext {
|
|
14
42
|
if (value === undefined || value === "auto") return resolveHost({ env });
|
|
15
43
|
if (value !== "codex" && value !== "claude-code")
|