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
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
import type { OrchestrationHost } from "./host.ts";
|
|
2
|
+
import { type Bars, barInversions, barOf, effectiveBars, percentilesOf } from "./bars.ts";
|
|
3
|
+
import { type Catalog, capableFor, effortOffered, ROLE_NEEDS, rungInfo } from "./catalog.ts";
|
|
2
4
|
import {
|
|
3
|
-
|
|
4
|
-
type Dim,
|
|
5
|
-
capableFor,
|
|
6
|
-
effortOffered,
|
|
7
|
-
ROLE_NEEDS,
|
|
8
|
-
type RungInfo,
|
|
9
|
-
rungInfo,
|
|
10
|
-
scoresOf,
|
|
11
|
-
} from "./catalog.ts";
|
|
12
|
-
import {
|
|
13
|
-
catalogRungs,
|
|
5
|
+
catalogPairs,
|
|
14
6
|
claudeBilled,
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
downgrade,
|
|
8
|
+
intelligenceOf,
|
|
9
|
+
ladderDrop,
|
|
17
10
|
quotaOf,
|
|
18
11
|
rankStandIns,
|
|
19
12
|
} from "./failover.ts";
|
|
20
|
-
import { tryParseRung } from "./ids.ts";
|
|
21
|
-
import { DIFFICULTIES,
|
|
13
|
+
import { effortRank, tryParseRung } from "./ids.ts";
|
|
14
|
+
import { DIFFICULTIES, type Difficulty } from "./lane.ts";
|
|
22
15
|
import { type Profile, type ProfileDoc, unknownValues } from "./profile.ts";
|
|
23
16
|
import { DEFAULT_ACCESS, ROLES, type Role } from "./roles.ts";
|
|
24
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
type Candidate,
|
|
19
|
+
candidates,
|
|
20
|
+
CODING_FLOOR,
|
|
21
|
+
CODING_ROLES,
|
|
22
|
+
clearsBar,
|
|
23
|
+
defaultLadder,
|
|
24
|
+
type RoutingProfile,
|
|
25
|
+
select,
|
|
26
|
+
} from "./select.ts";
|
|
25
27
|
|
|
26
28
|
export { quotaOf };
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* `reach`: also warn about the difficulties no worker rung clears (spec 1.5 plan 24), which only an explicit
|
|
32
|
+
* `profile validate` asks for. `unindexed: "warning"` (doctor's) reads a rung with no Slopalytics index as a
|
|
33
|
+
* warning, since a profile can lose a pair overnight; everywhere else it is an error.
|
|
34
|
+
*/
|
|
35
|
+
export interface ValidateOptions {
|
|
36
|
+
reach?: boolean;
|
|
37
|
+
unindexed?: "error" | "warning";
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
/** One finding of `validate`: where in the profile, what is wrong, and the action that fixes it. */
|
|
29
41
|
export interface Issue {
|
|
30
42
|
path: string;
|
|
@@ -37,10 +49,8 @@ export interface Validation {
|
|
|
37
49
|
warnings: Issue[];
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const SUGGEST_FIX = (rung: string) =>
|
|
43
|
-
`confirm or replace it: catherd catalog treat-like --suggest ${rung}, then catherd catalog treat-like ${rung} <a rung>`;
|
|
52
|
+
const SYNC_FIX = "catherd catalog sync";
|
|
53
|
+
const unindexedMessage = (rung: string) => `${rung} has no Slopalytics intelligence index: routing skips it`;
|
|
44
54
|
|
|
45
55
|
/** Spec 1.2 §6.2: two findings are the same one when they name the same path with the same message. */
|
|
46
56
|
const sameIssue = (a: Issue, b: Issue): boolean => a.path === b.path && a.message === b.message;
|
|
@@ -57,89 +67,23 @@ export function repairs(before: Validation | null, after: Validation): boolean {
|
|
|
57
67
|
return removed && !added;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
/** Spec 1.2 §6.1 "stand-in to confirm": a rung whose values on some bar dimensions are an inferred stand-in's. */
|
|
61
|
-
export interface StandInToConfirm {
|
|
62
|
-
canonical: string;
|
|
63
|
-
/** the profile's rungs that run it, and where the first one is */
|
|
64
|
-
rungs: string[];
|
|
65
|
-
path: string;
|
|
66
|
-
dims: { dim: Dim; like: string }[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Spec 1.2 §6.1: the profile's rungs that lean on an inferred stand-in, one entry per canonical rung. Only
|
|
71
|
-
* where bars choose: rungs of a role with more than one usable rung, and failover stand-ins; a role with one
|
|
72
|
-
* rung runs it whatever the bars say (plan 14 Ruling 13).
|
|
73
|
-
*/
|
|
74
|
-
export function standInsToConfirm(p: Profile, c: Catalog, backends: readonly string[]): StandInToConfirm[] {
|
|
75
|
-
const seen = new Map<string, StandInToConfirm>();
|
|
76
|
-
const add = (rung: string, path: string) => {
|
|
77
|
-
const r = tryParseRung(rung);
|
|
78
|
-
if (!r || !backends.includes(r.backend)) return;
|
|
79
|
-
const info = rungInfo(c, rung);
|
|
80
|
-
const s = scoresOf(c, info.canonical);
|
|
81
|
-
if (!s || s.inferred.length === 0) return;
|
|
82
|
-
const had = seen.get(info.canonical);
|
|
83
|
-
if (had) {
|
|
84
|
-
if (!had.rungs.includes(rung)) had.rungs.push(rung);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const dims = s.inferred.map((dim) => ({ dim, like: s.standIns[dim] as string }));
|
|
88
|
-
seen.set(info.canonical, { canonical: info.canonical, rungs: [rung], path, dims });
|
|
89
|
-
};
|
|
90
|
-
for (const role of ROLES) {
|
|
91
|
-
const rc = p.roles[role];
|
|
92
|
-
if (!rc.enabled || candidates(c, routingProfileOf(p, role), role).length < 2) continue;
|
|
93
|
-
for (const rung of rc.rungs) add(rung, `roles.${role}.rungs`);
|
|
94
|
-
}
|
|
95
|
-
for (const [from, to] of Object.entries(p.failover)) add(to, `failover.${from}`);
|
|
96
|
-
return [...seen.values()];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** One "stand-in to confirm" warning's words. */
|
|
100
|
-
export function standInMessage(x: StandInToConfirm): string {
|
|
101
|
-
const words = (xs: string[]) =>
|
|
102
|
-
xs.length < 3 ? xs.join(" or ") : `${xs.slice(0, -1).join(", ")} or ${xs.at(-1)}`;
|
|
103
|
-
const lent = x.dims.map((d) => `${d.like}'s ${d.dim}`).join(", ");
|
|
104
|
-
const also = x.rungs.length > 1 ? ` (${x.rungs.join(", ")})` : "";
|
|
105
|
-
return `stand-in to confirm: ${x.canonical}${also} has no ${words(x.dims.map((d) => d.dim))} value of its own; routing uses ${lent} (inferred)`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
70
|
export const routingProfileOf = (p: Profile, role: Role): RoutingProfile => ({
|
|
109
71
|
objective: p.objective,
|
|
110
72
|
billing: p.billing,
|
|
111
73
|
role: p.roles[role],
|
|
74
|
+
maxEffort: p.maxEffort,
|
|
75
|
+
bars: { ...p.bars, ...p.roles[role].bars },
|
|
112
76
|
});
|
|
113
77
|
|
|
114
|
-
/**
|
|
115
|
-
* Whether a rung's scores are catherd's guess: borrowed through a treat-like, filled by an inferred stand-in
|
|
116
|
-
* (spec 1.2 §6.1), or only `inferred` ones. `note` says what a treat-like lends: "scores borrowed from X"
|
|
117
|
-
* when the rung has no value of its own, else the dimensions it borrows (a rung a sync scored on some).
|
|
118
|
-
*/
|
|
119
|
-
export function inferredScores(
|
|
120
|
-
c: Catalog,
|
|
121
|
-
info: RungInfo,
|
|
122
|
-
): { inferred: boolean; via: string | null; note: string | null } {
|
|
123
|
-
const s = scoresOf(c, info.canonical);
|
|
124
|
-
if (!s) return { inferred: false, via: null, note: null };
|
|
125
|
-
const records = Object.values(s.records);
|
|
126
|
-
const guessed =
|
|
127
|
-
s.via !== null || s.inferred.length > 0 || records.every((r) => r.confidence === "inferred");
|
|
128
|
-
const all = s.borrowed.length + s.inferred.length === records.length;
|
|
129
|
-
const note = s.via ? `${all ? "scores" : s.borrowed.join(", ")} borrowed from ${s.via}` : null;
|
|
130
|
-
return { inferred: guessed, via: s.via, note };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
78
|
/**
|
|
134
79
|
* Spec §7.1. Errors: the worker disabled; an enabled role with no usable rung; a failover stand-in on the
|
|
135
80
|
* same quota; a rung whose backend catherd cannot run (`backends` lists those it can, `claude` included).
|
|
136
81
|
* Everything else worth knowing is a warning: an access mode other than the role's default, an effort or
|
|
137
82
|
* model the last listing does not offer, a stand-in that never runs, (spec 1.1 §11) a stand-in that
|
|
138
83
|
* downgrades its rung, one that spends Claude quota while another plan could stand in, and a ladder that goes
|
|
139
|
-
* down
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* value is read.
|
|
84
|
+
* down. A rung or stand-in with no Slopalytics index is an error (spec 2.0 decision 12; a warning with
|
|
85
|
+
* `o.unindexed: "warning"`): routing skips it. With the stored `doc` `p` came from, a value this catherd does
|
|
86
|
+
* not know is also a warning, one that says how the value is read.
|
|
143
87
|
*/
|
|
144
88
|
export function validateProfile(
|
|
145
89
|
p: Profile,
|
|
@@ -147,6 +91,7 @@ export function validateProfile(
|
|
|
147
91
|
backends: readonly string[],
|
|
148
92
|
doc?: ProfileDoc,
|
|
149
93
|
host: OrchestrationHost = "unknown",
|
|
94
|
+
o: ValidateOptions = {},
|
|
150
95
|
): Validation {
|
|
151
96
|
const errors: Issue[] = [];
|
|
152
97
|
const warnings: Issue[] = [];
|
|
@@ -163,6 +108,21 @@ export function validateProfile(
|
|
|
163
108
|
fix: "catherd profile set roles.worker.enabled true",
|
|
164
109
|
});
|
|
165
110
|
|
|
111
|
+
// a harder difficulty's percentile below an easier one's would route a harder lane to a weaker rung
|
|
112
|
+
const barOrder = (at: string, bars: Bars): void => {
|
|
113
|
+
const eff = effectiveBars({ bars });
|
|
114
|
+
for (const [easier, harder] of barInversions(eff))
|
|
115
|
+
errors.push({
|
|
116
|
+
path: at,
|
|
117
|
+
message: `the ${harder} bar (${eff[harder]}) is below the ${easier} bar (${eff[easier]})`,
|
|
118
|
+
fix: `catherd profile set ${at}.${harder} ${eff[easier]}, or lower ${at}.${easier} to ${eff[harder]} or less`,
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
barOrder("bars", p.bars);
|
|
122
|
+
for (const role of ROLES)
|
|
123
|
+
if (Object.keys(p.roles[role].bars ?? {}).length > 0)
|
|
124
|
+
barOrder(`roles.${role}.bars`, { ...p.bars, ...p.roles[role].bars });
|
|
125
|
+
|
|
166
126
|
for (const role of ROLES) {
|
|
167
127
|
const rc = p.roles[role];
|
|
168
128
|
const at = `roles.${role}`;
|
|
@@ -172,6 +132,14 @@ export function validateProfile(
|
|
|
172
132
|
message: `${role} runs ${rc.access}; catherd's default for it is ${DEFAULT_ACCESS[role]}`,
|
|
173
133
|
});
|
|
174
134
|
if (!rc.enabled) continue;
|
|
135
|
+
// spec §5: network: false takes a workspace-write role's network grants; read-only and full have none to take
|
|
136
|
+
if (rc.network === false && rc.access !== "workspace-write")
|
|
137
|
+
warnings.push({
|
|
138
|
+
path: `${at}.network`,
|
|
139
|
+
message: `network: false does nothing here: it applies to workspace-write roles, and ${role} runs ${rc.access}`,
|
|
140
|
+
fix: `catherd profile set ${at}.network null`,
|
|
141
|
+
});
|
|
142
|
+
let unindexed = 0;
|
|
175
143
|
for (const rung of rc.rungs) {
|
|
176
144
|
const native = nativeClaudeIssue(rung, host, `${at}.rungs`);
|
|
177
145
|
if (native) errors.push(native);
|
|
@@ -184,6 +152,18 @@ export function validateProfile(
|
|
|
184
152
|
});
|
|
185
153
|
continue;
|
|
186
154
|
}
|
|
155
|
+
const above = aboveCeiling(p.maxEffort, rung, `${at}.rungs`);
|
|
156
|
+
if (above) warnings.push(above);
|
|
157
|
+
if (
|
|
158
|
+
CODING_ROLES.includes(role) &&
|
|
159
|
+
effortRank(r.effort) >= 0 &&
|
|
160
|
+
effortRank(r.effort) < effortRank(CODING_FLOOR)
|
|
161
|
+
)
|
|
162
|
+
warnings.push({
|
|
163
|
+
path: `${at}.rungs`,
|
|
164
|
+
message: `${rung} is below ${CODING_FLOOR} effort: a coding worker never runs it`,
|
|
165
|
+
fix: `pick #${CODING_FLOOR} or above: catherd catalog list --role ${role}`,
|
|
166
|
+
});
|
|
187
167
|
if (!backends.includes(r.backend)) {
|
|
188
168
|
errors.push({
|
|
189
169
|
path: `${at}.rungs`,
|
|
@@ -199,36 +179,31 @@ export function validateProfile(
|
|
|
199
179
|
message: `${rung} cannot fill the ${role} role (it needs ${Object.keys(ROLE_NEEDS[role]).join(" + ")})`,
|
|
200
180
|
fix: `catalog_query({ role: "${role}" }) lists the models that can`,
|
|
201
181
|
});
|
|
202
|
-
if (info.
|
|
182
|
+
if (info.listed === true && !effortOffered(info))
|
|
203
183
|
errors.push({
|
|
204
184
|
path: `${at}.rungs`,
|
|
205
185
|
message: `${r.model} has no effort "${r.effort}" on ${r.backend} (it has ${info.efforts.join(", ")})`,
|
|
206
186
|
});
|
|
207
|
-
else if (info.
|
|
208
|
-
|
|
187
|
+
else if (!info.pair) {
|
|
188
|
+
unindexed++;
|
|
189
|
+
(o.unindexed === "warning" ? warnings : errors).push({
|
|
209
190
|
path: `${at}.rungs`,
|
|
210
|
-
message:
|
|
211
|
-
fix:
|
|
191
|
+
message: unindexedMessage(rung),
|
|
192
|
+
fix: c.indexedAt === null ? SYNC_FIX : `pick an indexed pair: catherd catalog list --role ${role}`,
|
|
212
193
|
});
|
|
194
|
+
}
|
|
213
195
|
if (info.listed === false)
|
|
214
196
|
warnings.push({
|
|
215
197
|
path: `${at}.rungs`,
|
|
216
198
|
message: `${r.backend}'s last listing does not offer ${r.model}; routing skips it`,
|
|
217
199
|
});
|
|
218
|
-
if (!scoresOf(c, info.canonical))
|
|
219
|
-
warnings.push({
|
|
220
|
-
path: `${at}.rungs`,
|
|
221
|
-
message: `${rung} is unscored and no rung is near enough to stand in for it: routing skips it`,
|
|
222
|
-
fix: TREAT_LIKE_FIX(rung),
|
|
223
|
-
});
|
|
224
200
|
}
|
|
225
201
|
rc.rungs.forEach((rung, i) => {
|
|
226
202
|
const prev = rc.rungs[i - 1];
|
|
227
|
-
|
|
228
|
-
if (down.length)
|
|
203
|
+
if (prev !== undefined && ladderDrop(c, prev, rung))
|
|
229
204
|
warnings.push({
|
|
230
205
|
path: `${at}.rungs`,
|
|
231
|
-
message: `the ladder goes down at ${rung}:
|
|
206
|
+
message: `the ladder goes down at ${rung}: intelligence ${(intelligenceOf(c, rung) as number).toFixed(1)} below ${prev}'s ${(intelligenceOf(c, prev) as number).toFixed(1)}`,
|
|
232
207
|
fix: `order ${at}.rungs weakest first`,
|
|
233
208
|
});
|
|
234
209
|
});
|
|
@@ -240,41 +215,62 @@ export function validateProfile(
|
|
|
240
215
|
});
|
|
241
216
|
const usable = candidates(c, routingProfileOf(p, role), role);
|
|
242
217
|
if (usable.length === 0)
|
|
243
|
-
|
|
218
|
+
// doctor's mode: rungs that are only unindexed are the same warning, so the role's total is one too, but
|
|
219
|
+
// only when an index exists: with none (not fetched, corrupt, empty) nothing can route, which is an error
|
|
220
|
+
(o.unindexed === "warning" &&
|
|
221
|
+
c.indexedAt !== null &&
|
|
222
|
+
Object.keys(c.index).length > 0 &&
|
|
223
|
+
unindexed > 0 &&
|
|
224
|
+
unindexed === rc.rungs.length
|
|
225
|
+
? warnings
|
|
226
|
+
: errors
|
|
227
|
+
).push({
|
|
244
228
|
path: `${at}.rungs`,
|
|
245
229
|
message: `the ${role} role has no usable rung`,
|
|
246
|
-
fix: `enable
|
|
230
|
+
fix: `enable an indexed rung its backend offers, or turn the role off with: catherd profile set ${at}.enabled false`,
|
|
247
231
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// spec 1.
|
|
261
|
-
//
|
|
262
|
-
//
|
|
232
|
+
// spec 1.5 plan 24: every difficulty no worker rung clears, in one warning; those lanes start at the
|
|
233
|
+
// default rung (or an easier difficulty's start) and climb only onto rungs at least as intelligent. Only an
|
|
234
|
+
// explicit validate says it (Ruling 3): the shipped bars put logic and hard above every Sol rung, the
|
|
235
|
+
// owner's call, so every save and doctor would repeat it
|
|
236
|
+
if (o.reach && role === "worker" && usable.length > 1) {
|
|
237
|
+
const unreached = unreachable(c, usable, percentilesOf(routingProfileOf(p, role)));
|
|
238
|
+
if (unreached.length)
|
|
239
|
+
warnings.push({
|
|
240
|
+
path: `${at}.rungs`,
|
|
241
|
+
message: `no worker rung clears ${unreached.join(", ")}: those lanes start at the default rung and climb only onto rungs at least as intelligent (route names the closest)`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// spec 1.5 plan 24: a quota whose rungs never start a lane sits idle but for climbs (the identity run's Go).
|
|
245
|
+
// Only the worker routes lanes and climbs (Ruling 9); another role routes lane-less to one rung, so a rung
|
|
246
|
+
// on a second quota is a deliberate spare there, not an idle quota (final review Important 4, Ruling 8)
|
|
263
247
|
if (role === "worker" && usable.length > 1) {
|
|
264
|
-
const
|
|
265
|
-
|
|
248
|
+
const idle = idleQuotas(c, p, role, usable);
|
|
249
|
+
for (const [q, rungs] of idle) {
|
|
250
|
+
// the billing key, not the quota, is what billing is set on (`claude:` and `claude-code:` share one)
|
|
251
|
+
const keys = [...new Set(usable.filter((x) => rungs.includes(x.rung)).map((x) => x.info.key))];
|
|
266
252
|
warnings.push({
|
|
267
253
|
path: `${at}.rungs`,
|
|
268
|
-
message:
|
|
254
|
+
message: `${q} never starts a ${role} lane: ${rungs.join(", ")} ${rungs.length > 1 ? "run" : "runs"} only on a climb, since another rung starts every lane`,
|
|
255
|
+
fix: `check ${keys.map((k) => `billing.${k}`).join(" and ")} (a metered rung starts only where nothing paid from a plan clears the bar), or order ${at}.rungs so its rungs come first among equals`,
|
|
269
256
|
});
|
|
257
|
+
}
|
|
270
258
|
}
|
|
271
259
|
}
|
|
272
260
|
|
|
273
261
|
const ladders = new Set(ROLES.filter((r) => p.roles[r].enabled).flatMap((r) => p.roles[r].rungs));
|
|
274
|
-
// the stand-ins
|
|
275
|
-
|
|
262
|
+
// the stand-ins the fixes below may suggest: on a backend catherd runs and the profile already names (a
|
|
263
|
+
// ladder or a failover entry), never one this machine may not have (Cursor on a machine without it); a
|
|
264
|
+
// native claude: rung names claude-code too, the same Claude Code CLI
|
|
265
|
+
const named = new Set(
|
|
266
|
+
[...ladders, ...Object.keys(p.failover), ...Object.values(p.failover)].flatMap((x) => {
|
|
267
|
+
const b = tryParseRung(x)?.backend;
|
|
268
|
+
return b === undefined ? [] : b === "claude" ? [b, "claude-code"] : [b];
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
const pool = catalogPairs(c).filter((x) => {
|
|
276
272
|
const r = tryParseRung(x);
|
|
277
|
-
return r !== null && backends.includes(r.backend);
|
|
273
|
+
return r !== null && backends.includes(r.backend) && named.has(r.backend);
|
|
278
274
|
});
|
|
279
275
|
for (const [from, to] of Object.entries(p.failover)) {
|
|
280
276
|
const at = `failover.${from}`;
|
|
@@ -290,15 +286,19 @@ export function validateProfile(
|
|
|
290
286
|
});
|
|
291
287
|
continue;
|
|
292
288
|
}
|
|
289
|
+
const above = aboveCeiling(p.maxEffort, to, at);
|
|
290
|
+
if (above) warnings.push(above);
|
|
293
291
|
if (!backends.includes(b.backend)) {
|
|
294
292
|
errors.push({ path: at, message: `stand-in ${to}: catherd cannot run ${b.backend} yet` });
|
|
295
293
|
continue;
|
|
296
294
|
}
|
|
297
|
-
|
|
298
|
-
|
|
295
|
+
const ranked = rankStandIns(c, p.billing, from, pool);
|
|
296
|
+
const unindexed = intelligenceOf(c, to) === null;
|
|
297
|
+
if (unindexed)
|
|
298
|
+
(o.unindexed === "warning" ? warnings : errors).push({
|
|
299
299
|
path: at,
|
|
300
|
-
message:
|
|
301
|
-
fix:
|
|
300
|
+
message: unindexedMessage(to),
|
|
301
|
+
fix: c.indexedAt === null ? SYNC_FIX : `catherd profile set failover.${from} ${ranked[0] ?? "null"}`,
|
|
302
302
|
});
|
|
303
303
|
if (quotaOf(a) === quotaOf(b))
|
|
304
304
|
errors.push({
|
|
@@ -306,12 +306,11 @@ export function validateProfile(
|
|
|
306
306
|
message: `stand-in ${to} draws on the same quota as ${from}, which is out when ${from} hits its limit`,
|
|
307
307
|
fix: "name a stand-in on another backend or plan",
|
|
308
308
|
});
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (down.length)
|
|
309
|
+
// an unindexed stand-in has its own finding above: no "downgrade" besides
|
|
310
|
+
if (!unindexed && downgrade(c, from, to))
|
|
312
311
|
warnings.push({
|
|
313
312
|
path: at,
|
|
314
|
-
message: `downgrade: ${to} stands in for ${from},
|
|
313
|
+
message: `downgrade: ${to} stands in for ${from} at intelligence ${(intelligenceOf(c, to) as number).toFixed(1)}, below its ${(intelligenceOf(c, from) as number).toFixed(1)}`,
|
|
315
314
|
fix: `catherd profile set failover.${from} ${ranked[0] ?? "null"}`,
|
|
316
315
|
});
|
|
317
316
|
const other = ranked.find((x) => !claudeBilled(x));
|
|
@@ -329,11 +328,49 @@ export function validateProfile(
|
|
|
329
328
|
if (!ladders.has(from))
|
|
330
329
|
warnings.push({ path: at, message: `${from} is on no enabled role's ladder, so this never runs` });
|
|
331
330
|
}
|
|
332
|
-
for (const x of standInsToConfirm(p, c, backends))
|
|
333
|
-
warnings.push({ path: x.path, message: standInMessage(x), fix: SUGGEST_FIX(x.rungs[0] as string) });
|
|
334
331
|
return { errors, warnings };
|
|
335
332
|
}
|
|
336
333
|
|
|
334
|
+
/** Plan 33: a warning for a rung above its model's `maxEffort`, null for any other. */
|
|
335
|
+
function aboveCeiling(maxEffort: Readonly<Record<string, string>>, rung: string, path: string): Issue | null {
|
|
336
|
+
const r = tryParseRung(rung);
|
|
337
|
+
const key = r ? `${r.backend}:${r.model}` : "";
|
|
338
|
+
const cap = maxEffort[key];
|
|
339
|
+
if (!r || cap === undefined || effortRank(r.effort) <= effortRank(cap) || effortRank(r.effort) < 0)
|
|
340
|
+
return null;
|
|
341
|
+
return {
|
|
342
|
+
path,
|
|
343
|
+
message: `${rung} is above maxEffort ${cap} for ${key}: routing skips it`,
|
|
344
|
+
fix: `catherd profile set maxEffort.${key} ${r.effort}, or remove the rung`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The difficulties no rung of `usable` clears, as `logic, hard`. */
|
|
349
|
+
function unreachable(c: Catalog, usable: Candidate[], at: Record<Difficulty, number>): string[] {
|
|
350
|
+
return DIFFICULTIES.filter((d) => !usable.some((x) => clearsBar(x, barOf(c, d, at))));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Spec 1.5 plan 24: each quota of the role's usable rungs that no kind and difficulty (nor a lane-less route)
|
|
355
|
+
* ever starts on, whichever quota the run has used least, with its rungs.
|
|
356
|
+
*/
|
|
357
|
+
function idleQuotas(c: Catalog, p: Profile, role: Role, usable: Candidate[]): [string, string[]][] {
|
|
358
|
+
const quotas = [...new Set(usable.map((x) => quotaOf(x.info.parsed)))];
|
|
359
|
+
if (quotas.length < 2) return [];
|
|
360
|
+
const starts = new Set<string>();
|
|
361
|
+
// every quota gets its turn as the least used, so a tie it could win counts as a start
|
|
362
|
+
for (const fresh of [null, ...quotas]) {
|
|
363
|
+
const usage = Object.fromEntries(quotas.map((q) => [q, q === fresh ? 0 : 1]));
|
|
364
|
+
const rp = { ...routingProfileOf(p, role), usage };
|
|
365
|
+
starts.add(defaultLadder(c, rp, role).rung);
|
|
366
|
+
for (const d of DIFFICULTIES) starts.add(select(c, rp, role, d).rung);
|
|
367
|
+
}
|
|
368
|
+
const started = new Set(usable.filter((x) => starts.has(x.rung)).map((x) => quotaOf(x.info.parsed)));
|
|
369
|
+
return quotas
|
|
370
|
+
.filter((q) => !started.has(q))
|
|
371
|
+
.map((q) => [q, usable.filter((x) => quotaOf(x.info.parsed) === q).map((x) => x.rung)]);
|
|
372
|
+
}
|
|
373
|
+
|
|
337
374
|
export function nativeClaudeIssue(rung: string, host: OrchestrationHost, path: string): Issue | null {
|
|
338
375
|
const r = tryParseRung(rung);
|
|
339
376
|
return host !== "claude-code" && r?.backend === "claude"
|