catherd-cli 1.5.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 +164 -208
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -247
- 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 -421
- package/plugin/skills/catherd-setup/SKILL.md +20 -19
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +1 -2
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +4 -4
- package/src/cli.ts +22 -27
- 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 +21 -6
- package/src/domain/catalog.ts +69 -324
- package/src/domain/cost.ts +15 -30
- package/src/domain/errors.ts +2 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -87
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +1 -0
- 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 +22 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +25 -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 +101 -160
- package/src/domain/profile.ts +116 -63
- package/src/domain/record.ts +53 -2
- package/src/domain/role-prompts.ts +131 -45
- package/src/domain/role-scope.ts +4 -34
- package/src/domain/role-tools.ts +2 -2
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +17 -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 +151 -146
- package/src/domain/slopalytics.ts +72 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +25 -174
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +1 -1
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/init-command.ts +18 -68
- package/src/entry/knowledge-command.ts +45 -4
- package/src/entry/lock-command.ts +6 -7
- package/src/entry/mcp/role-server.ts +18 -0
- package/src/entry/mcp/server.ts +13 -40
- 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/profile-command.ts +10 -14
- package/src/entry/role-cli-command.ts +47 -3
- package/src/entry/runs-command.ts +3 -19
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/infra/env.ts +0 -2
- package/src/infra/git.ts +64 -0
- package/src/infra/heavy-lock.ts +45 -7
- package/src/infra/launch.ts +63 -7
- 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 +2 -2
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/services/admission.ts +24 -9
- package/src/services/backends.ts +4 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -262
- package/src/services/credentials.ts +4 -20
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +5 -1
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor.ts +4 -9
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +12 -0
- package/src/services/gate-env.ts +1 -2
- package/src/services/gate-service.ts +21 -6
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-service.ts +34 -3
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +35 -8
- package/src/services/notifier.ts +69 -2
- package/src/services/notify-service.ts +26 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +30 -4
- package/src/services/preflight.ts +82 -8
- package/src/services/profile-service.ts +30 -3
- package/src/services/profile-store.ts +8 -3
- package/src/services/protocol.ts +4 -0
- package/src/services/provenance.ts +31 -90
- package/src/services/questions.ts +9 -0
- package/src/services/reconcile.ts +11 -0
- package/src/services/routing-service.ts +26 -15
- 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-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +55 -6
- package/src/services/run-store.ts +54 -7
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/summary.ts +27 -3
- package/src/services/tree-path.ts +27 -0
- package/src/services/workspace-admission.ts +5 -13
- package/src/services/workspace-service.ts +5 -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 -44
- 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 -335
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -187
- package/src/adapters/grok/index.ts +0 -358
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -132
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -131
- package/src/entry/mcp/protocol-tools.ts +0 -63
- package/src/entry/mcp/push-tools.ts +0 -16
- package/src/entry/mcp/run-tools.ts +0 -164
- package/src/entry/mcp/setup-tools.ts +0 -109
- package/src/entry/mcp/workspace-tools.ts +0 -142
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -385
- 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 -400
- 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 -619
- package/src/entry/tui/views/save-dialog.tsx +0 -344
- package/src/entry/tui/views/status.tsx +0 -213
- 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 -95
- package/src/infra/sources/artificial-analysis.ts +0 -119
- package/src/infra/sources/cache.ts +0 -126
- 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 -290
- package/src/services/source-derive.ts +0 -397
- package/src/services/source-sync.ts +0 -312
- package/src/services/standins.ts +0 -238
- package/src/services/treat-likes.ts +0 -113
|
@@ -7,7 +7,7 @@ description: Use when the user wants to tune catherd — which models and effort
|
|
|
7
7
|
|
|
8
8
|
You tune the user's catherd profile in conversation. A profile says, per role, which rungs it may run on (`backend:model#effort`, in ladder order) and what it may touch (its access mode); whether routing favours cost or speed; how each backend is billed; whether Jev picks the rung; which rung stands in when a backend hits its usage limit; the run budget and timeouts; whether each vendor harness runs with the user's customizations or isolated; how many heavy commands run at once; and when to push.
|
|
9
9
|
|
|
10
|
-
**You never edit a file by hand.** Use `
|
|
10
|
+
**You never edit a file by hand.** Use `profile({ action: "set" })` for patches and the existing reviewed CLI reset below; both go through ProfileService, as do the `catherd profile` commands and the panel.
|
|
11
11
|
|
|
12
12
|
**Discover the host first with `status()`.** Use the host's available MCP discovery facility; names and deferred tools differ. Claude Code may use `ToolSearch`; Codex never needs invented Claude tools or native Claude agents. Pass the actual project `repo` explicitly to profile, setup and catalog calls that accept it: native Codex starts the MCP server in the installed plugin root, not the project. An unknown terminal can use `catherd init --host codex` or `--host claude-code` for setup; this does not create a conversation owner. Conflicting identity must be resolved before ownership or push.
|
|
13
13
|
|
|
@@ -15,7 +15,7 @@ You tune the user's catherd profile in conversation. A profile says, per role, w
|
|
|
15
15
|
|
|
16
16
|
- **One question at a time.** Each question carries your recommended answer and one line on why, so the user can simply say yes.
|
|
17
17
|
- **The user steers.** When they reverse a proposal, take the reversal and never re-argue it.
|
|
18
|
-
- **Talk in their terms:** minutes, their subscriptions, how often a rung climbed on their own runs, tokens per run.
|
|
18
|
+
- **Talk in their terms:** minutes, their subscriptions, how often a rung climbed on their own runs, tokens per run. Name the Slopalytics intelligence index only when they ask.
|
|
19
19
|
- **Every number you quote comes from a tool result in this conversation.** When there is no data yet, say so instead of estimating.
|
|
20
20
|
|
|
21
21
|
## 1. Learn the goals
|
|
@@ -23,16 +23,17 @@ You tune the user's catherd profile in conversation. A profile says, per role, w
|
|
|
23
23
|
Ask these, one at a time, each with its recommended answer:
|
|
24
24
|
|
|
25
25
|
1. **Their order of speed, cost and quality.** Recommend cost first, the default `objective`: catherd climbs a rung when a cheap one cannot do the work, so the lanes that need speed get it anyway, and the reviewer and the verifier hold quality either way.
|
|
26
|
-
2. **The subscriptions they hold:** a ChatGPT plan (Codex), a Claude plan, OpenCode Go, Zen credit or API keys. They set `billing` per key (`codex`, `claude`, `claude-code`, `opencode-go`, `opencode`, `cursor
|
|
27
|
-
3. **The kind of work they orchestrate:** front-end screens, back-end services, terminal and ops work, docs. Recommend from their own runs when `
|
|
26
|
+
2. **The subscriptions they hold:** a ChatGPT plan (Codex), a Claude plan, OpenCode Go, Zen credit or API keys. They set `billing` per key (`codex`, `claude`, `claude-code`, `opencode-go`, `opencode`, `cursor`): `chatgpt-plan`, `claude-plan`, `subscription` or `metered`. Recommend leaning on subscriptions before metered spend. Explain which workers share this conversation's quota from the actual host and billing facts.
|
|
27
|
+
3. **The kind of work they orchestrate:** front-end screens, back-end services, terminal and ops work, docs. Recommend from their own runs when the catalog's `runs:` evidence has any.
|
|
28
28
|
|
|
29
29
|
## 2. Read the facts before proposing
|
|
30
30
|
|
|
31
31
|
In one message, call:
|
|
32
32
|
|
|
33
|
-
- `
|
|
34
|
-
- `
|
|
35
|
-
|
|
33
|
+
- `catherd catalog list --role <role> --json` (in a shell, from their repo) for each role you will discuss: the models that can fill it, their indexed rungs (each with its `intelligence`, the one number catherd ranks on), each rung's cost under their billing, whether their backend's last listing offers it (`listed: false` means their account does not), and the `runs:` evidence of how each rung has done on their own runs (runs, refusals, climbs, time). The session has no catalog tool;
|
|
34
|
+
- `profile({ action: "get", repo })`: where they stand now, with each role's `access` and `enforcement`. Pass the user's repo: without a name, the profile tools act on the profile this repo runs on (the one bound to it, else the active one), which `here` names; `active` is the global active profile.
|
|
35
|
+
|
|
36
|
+
There is no per-run harness figure in the session: say so when isolation comes up.
|
|
36
37
|
|
|
37
38
|
Verify the exact omitted Codex model ID `gpt-6.1-sol` against native catalog discovery and validation. If its spelling differs, report the mismatch and resolve it explicitly with the owner; never silently normalize or substitute a model. Retain architect high and verifier low effort.
|
|
38
39
|
|
|
@@ -44,7 +45,7 @@ Go through these in order, and skip any the user does not care about:
|
|
|
44
45
|
2. `billing`, from step 1;
|
|
45
46
|
3. the worker's `rungs` and its `defaultRung`;
|
|
46
47
|
4. the reviewer and the UI reviewer;
|
|
47
|
-
5. the architect and the verifier. Recommend omitted host defaults: on Codex, exactly `codex:gpt-6.1-sol#high` and `codex:gpt-6.1-sol#low`; on Claude Code, the existing native Claude defaults. A `claude:` rung requires a Claude Code host
|
|
48
|
+
5. the architect and the verifier. Recommend omitted host defaults: on Codex, exactly `codex:gpt-6.1-sol#high` and `codex:gpt-6.1-sol#low`; on Claude Code, the existing native Claude defaults. A `claude:` rung requires a Claude Code host; the daemon runs it as its headless `claude-code:` twin, and `claude-code:` is that headless rung on either host. Codex architect and verifier run as headless processes the daemon starts. Preserve explicit model IDs, efforts, ladder order and all other fields. Never silently convert `claude:` into `claude-code:` in the profile;
|
|
48
49
|
6. the writer, the researcher and the artist;
|
|
49
50
|
7. each role's `access` (below);
|
|
50
51
|
8. `failover` (below);
|
|
@@ -52,7 +53,7 @@ Go through these in order, and skip any the user does not care about:
|
|
|
52
53
|
10. harness isolation, per harness (below);
|
|
53
54
|
11. `lock.heavy` and `notify`.
|
|
54
55
|
|
|
55
|
-
Existing materialized profiles remain unchanged on read, init, upgrade or host switch. To deliberately opt into host defaults, use the existing CLI from the project directory, naming the profile returned by `
|
|
56
|
+
Existing materialized profiles remain unchanged on read, init, upgrade or host switch. To deliberately opt into host defaults, use the existing CLI from the project directory, naming the profile returned by `profile({ action: "get" })`:
|
|
56
57
|
|
|
57
58
|
```sh
|
|
58
59
|
catherd profile reset-host-defaults <profile> --host codex --preview --json > /tmp/catherd-host-defaults-review.json
|
|
@@ -64,28 +65,28 @@ Use `--host claude-code` for that host. This reviewed reset removes only archite
|
|
|
64
65
|
|
|
65
66
|
Each proposal has three parts: the change, a worked example from their facts, and the tradeoff in their terms. For instance: "Luna high on build lanes: about 5 min slower than Sol medium, no Claude quota, climbs on 1 in 5 of your runs so far."
|
|
66
67
|
|
|
67
|
-
- Offer only the rungs `
|
|
68
|
-
-
|
|
68
|
+
- Offer only the rungs `catherd catalog list` shows as capable for the role, written `backend:model#effort`.
|
|
69
|
+
- Offer only the indexed pairs `catherd catalog list` shows. A model or effort the Slopalytics index does not list is not offered, and a rung that names one is an error: if the user wants it, say it is not indexed yet and propose the nearest indexed pair. Never pick a worker rung below `#medium`. Use `maxEffort` to keep a model from its highest efforts, and `bars` to move a difficulty's bar for one role, rather than removing rungs.
|
|
69
70
|
|
|
70
|
-
**Access.** Each role runs `read-only`, `workspace-write` or `full`. The defaults: architect, reviewer and researcher `read-only`; worker, writer and artist `workspace-write`; verifier and UI reviewer `full`. `
|
|
71
|
+
**Access.** Each role runs `read-only`, `workspace-write` or `full`. The defaults: architect, reviewer and researcher `read-only`; worker, writer and artist `workspace-write`; verifier and UI reviewer `full`. `profile({ action: "get" })` says per role whether its backend holds it to that mode (`enforced`: Codex's sandbox) or only asks (`advisory`: claude-code, opencode and native subagents, where the model can still reach past it). Recommend the defaults; when the user wants a role tighter or looser, say what it can no longer do (a read-only reviewer on claude-code or opencode has no shell, so it cannot run `git diff`) and that `profile({ action: "validate" })` will warn about it. A `workspace-write` role also writes catherd's lock dir and the temp dir, reaches the network, binds loopback ports and talks to a local Docker socket, so it runs its own installs and tests; `roles.<role>.network: false` takes the network, loopback and Docker away from that role (Codex enforces it; claude-code drops its web tools; opencode has no sandbox to enforce it). `catherd doctor` probes each of these per backend.
|
|
71
72
|
|
|
72
|
-
**Failover.** `failover` maps a rung to its stand-in when that rung's backend hits a usage limit. A stand-in must be
|
|
73
|
+
**Failover.** `failover` maps a rung to its stand-in when that rung's backend hits a usage limit. A stand-in must be indexed, on another quota and at least as intelligent as its rung (Go and Zen bill apart; native `claude` and `claude-code` share the Claude plan). The default profile fails a Codex rung over only to an indexed stand-in at least as intelligent: Go's GPT-6 Luna for Luna high, and Kimi K3 for Sol medium. Sol high and xhigh have none in the profile. Beyond the profile's stand-ins, the daemon moves a unit that hit a limit to the nearest indexed rung, on a free quota, of any backend they have configured that is not weaker (a Claude rung last); when none qualifies, the lane waits for the hold rather than dropping a tier. Say so, and offer to change the profile's stand-ins when they have no Go subscription. `null` removes an entry.
|
|
73
74
|
|
|
74
75
|
**Budget and timeouts.** `budget` (`minutes`, `tokens`, `usd`) is a soft cap: from 80 % routing starts at the cheapest rung that clears the bar, and at 100 % no new role starts. `timeouts.idleMin` (15) stops a role that has gone quiet, `timeouts.wallMin` (90) one that runs too long; `roles.<role>.timeouts.idleMin` and `wallMin` override them for one role (a verifier with a long gate), and `null` removes the override. `preflight.confirm: true` makes `preflight` show its commands for the user to approve first.
|
|
75
76
|
|
|
76
|
-
**Notifications
|
|
77
|
+
**Notifications.** `notify` controls the milestone, finish and blocked moments. Where a parked question reaches the owner is `notify.url`, an ntfy topic url: set it with `profile({ action: "set", notify_url })` (ask them for the url; never invent one). The daemon sends to it from the machine, so a question reaches the phone with no session open. `catherd doctor --test-push` sends a labeled test notice.
|
|
77
78
|
|
|
78
|
-
**Harness isolation.** For each harness they use (`codex`, `claude-code`, `opencode`, `cursor
|
|
79
|
+
**Harness isolation.** For each harness they use (`codex`, `claude-code`, `opencode`, `cursor`), offer `harness.<name>.isolated` and this tradeoff: "native keeps your hooks, skills and AGENTS.md; isolated saves some tokens per run, but the role loses them." Recommend native. The session reads no harness line, so quote no number. Codex reports no per-request input, so it has no harness figure (Codex has none: it reports no per-request input).
|
|
79
80
|
|
|
80
81
|
## 4. Write it
|
|
81
82
|
|
|
82
|
-
- Call `
|
|
83
|
+
- Call `profile({ action: "set", repo, patch })` with only what changed; pass `name` only to edit a profile other than the one this repo runs on (a name that does not exist yet starts a new profile from the default one). Lists (`rungs`, `notify`) replace, maps (`billing`, `failover`, `budget`) merge, and `null` removes a key. A key it does not know is refused with `E_INPUT_INVALID`. It validates before it writes:
|
|
83
84
|
- `saved: false` comes with `errors`, each with a `path`, a `message` and often a `fix`. Explain each in their terms, fix the patch, and propose again.
|
|
84
85
|
- `saved: true` comes with the `diff` (`path`, `before`, `after`) and any `warnings`. Read the diff back to the user, one line per change, and each warning with it.
|
|
85
|
-
- Then call `
|
|
86
|
+
- Then call `profile({ action: "validate", repo })`. `errors` block a save: the worker disabled, an enabled role with no usable rung, a rung or failover stand-in the index does not list ("has no Slopalytics intelligence index"), a failover stand-in on the same quota, a backend catherd cannot run. `warnings` do not: an access mode other than the role's default, a model the backend's listing lacks, a stand-in that never runs, a stand-in less intelligent than its rung ("downgrade"), a Claude stand-in while another plan could stand in ("spends Claude quota"), a ladder whose rung is less intelligent than the one before it ("the ladder goes down"). Each has a `fix`; offer it.
|
|
86
87
|
|
|
87
88
|
## 5. Say what applies when
|
|
88
89
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
90
|
+
- Rung and ladder changes apply at the next dispatch, even in a run already under way. Access and isolation are pinned per run when it starts: a run under way keeps them until it is re-pinned (`catherd runs pin <run>`). Isolating Cursor needs `CURSOR_API_KEY` in the environment catherd runs in; `profile` validation refuses it without.
|
|
91
|
+
- Every role runs headless, so no agent file needs a new session: a change applies at the next dispatch. `newSessionNeededFor` stays empty unless a native Claude agent file is linked.
|
|
91
92
|
- ProfileService manages Claude agent files/links only when native Claude roles are used. Codex-only setup and profile operations do not write under `~/.claude`; Claude CLI/login is needed only by an enabled selected role or reachable failover that uses it. Editing an inactive Claude-dependent profile writes its managed files but links none; they apply once active or repo-bound. Keep role-disabled, access, isolation, budget and failover choices intact.
|
package/src/adapters/all.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { antigravityAdapter } from "./antigravity/index.ts";
|
|
2
1
|
import { claudeCodeAdapter } from "./claude-code/index.ts";
|
|
3
2
|
import { codexAdapter } from "./codex/index.ts";
|
|
4
3
|
import { cursorAdapter } from "./cursor/index.ts";
|
|
5
|
-
import { grokAdapter } from "./grok/index.ts";
|
|
6
4
|
import { opencodeAdapter } from "./opencode/index.ts";
|
|
7
5
|
import { registerAdapter } from "./registry.ts";
|
|
8
6
|
|
|
@@ -10,5 +8,3 @@ registerAdapter(codexAdapter);
|
|
|
10
8
|
registerAdapter(claudeCodeAdapter);
|
|
11
9
|
registerAdapter(opencodeAdapter);
|
|
12
10
|
registerAdapter(cursorAdapter);
|
|
13
|
-
registerAdapter(grokAdapter);
|
|
14
|
-
registerAdapter(antigravityAdapter);
|
package/src/adapters/backend.ts
CHANGED
|
@@ -179,8 +179,7 @@ export interface BackendAdapter {
|
|
|
179
179
|
*/
|
|
180
180
|
isolationKey?: string;
|
|
181
181
|
/**
|
|
182
|
-
* Spec 1.3 §9 Q2: the access modes this backend holds a role to only when isolated (
|
|
183
|
-
* its deny rules live in the settings catherd writes in its own home). A native role at one does not validate.
|
|
182
|
+
* Spec 1.3 §9 Q2: the access modes this backend holds a role to only when isolated (one with no read-only flag). A native role at one does not validate.
|
|
184
183
|
*/
|
|
185
184
|
isolatedOnly?: Access[];
|
|
186
185
|
/** Spec 1.3 §6.6: the plan quota left, in the CLI's words, read without spending a model turn (doctor's row). */
|
|
@@ -18,7 +18,7 @@ export const isolatedCursorHome = (access: Access, network = true): string =>
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The env of an isolated run: the moved HOME (the only thing that stops Cursor's hard-coded `~/.claude`,
|
|
21
|
-
* `~/.codex
|
|
21
|
+
* `~/.codex` and `~/.agents` reads, research §2.8), and Cursor's config and data dirs inside it, so a
|
|
22
22
|
* CURSOR_CONFIG_DIR or XDG_CONFIG_HOME of the user's cannot point it back. Its credentials stay in memory: on
|
|
23
23
|
* macOS Cursor otherwise saves the CURSOR_API_KEY login to the moved HOME's keychain, which does not exist, and
|
|
24
24
|
* the failed `security` call raises a "Keychain Not Found" dialog.
|
|
@@ -39,7 +39,7 @@ export { isolatedCursorHome, isolatedCursorRoot } from "./home.ts";
|
|
|
39
39
|
export const CURSOR_MIN_VERSION = "2026.09.28";
|
|
40
40
|
export const CURSOR_INSTALL = "curl https://cursor.com/install -fsS | bash";
|
|
41
41
|
const THREAD = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
42
|
-
/** Cursor's versions are a date and a hash (`2026.09.28-64d2043`)
|
|
42
|
+
/** Cursor's versions are a date and a hash (`2026.09.28-64d2043`). */
|
|
43
43
|
const CURSOR_VERSION = /^\s*\d{4}\.\d{1,2}\.\d{1,2}-[0-9a-f]+\b/;
|
|
44
44
|
const DAY_MS = 24 * 60 * 60_000;
|
|
45
45
|
const LOGIN_FIX = "cursor-agent login, or export CURSOR_API_KEY=<key>";
|
|
@@ -50,7 +50,7 @@ export const cursorShell = { timeoutMs: 15_000 };
|
|
|
50
50
|
const which = (bin: string): string | null => Bun.which(bin, { PATH: process.env.PATH ?? "" });
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Spec 1.3 §4.1: `cursor-agent` first.
|
|
53
|
+
* Spec 1.3 §4.1: `cursor-agent` first. Another CLI's installer may also link `agent` (research §2.1,
|
|
54
54
|
* §3.1), so `agent` is taken only when `cursor-agent` is missing, and the probe checks what it is.
|
|
55
55
|
*/
|
|
56
56
|
export function cursorBin(): "cursor-agent" | "agent" {
|
|
@@ -101,7 +101,7 @@ function plan(r: RunRequest): SpawnPlan {
|
|
|
101
101
|
|
|
102
102
|
async function listModels(): Promise<DiscoveredModel[]> {
|
|
103
103
|
const bin = cursorBin();
|
|
104
|
-
// an `agent` that is not Cursor's (
|
|
104
|
+
// an `agent` that is not Cursor's (another CLI's) is never asked for models: nothing to list, as when absent
|
|
105
105
|
if (bin === "agent") {
|
|
106
106
|
const v = await runCli(bin, ["--version"], cursorShell).catch(() => null);
|
|
107
107
|
if (!v || !CURSOR_VERSION.test(v.out)) return [];
|
|
@@ -219,7 +219,7 @@ async function probe(): Promise<Probe> {
|
|
|
219
219
|
try {
|
|
220
220
|
v = await runCli(bin, ["--version"], cursorShell);
|
|
221
221
|
} catch (e) {
|
|
222
|
-
// an `agent` that is not Cursor's may not even run here (
|
|
222
|
+
// an `agent` that is not Cursor's may not even run here (a Linux build on macOS, research §3.1)
|
|
223
223
|
if (bin === "agent") v = { ok: false, out: "", err: String(e) };
|
|
224
224
|
else throw e;
|
|
225
225
|
}
|
package/src/cli.ts
CHANGED
|
@@ -20,29 +20,23 @@ const { VERSION } = await import("./infra/version.ts");
|
|
|
20
20
|
|
|
21
21
|
type Command = ReturnType<typeof defineCommand>;
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
const
|
|
25
|
-
...HOST_ARG,
|
|
26
|
-
plain: {
|
|
27
|
-
type: "boolean",
|
|
28
|
-
description: "the dashboard in ASCII without colour (NO_COLOR also drops colour)",
|
|
29
|
-
},
|
|
30
|
-
"reduced-motion": { type: "boolean", description: "the dashboard without animation" },
|
|
31
|
-
};
|
|
23
|
+
/** HOST_ARG typed as any command's args, so `main` stays a plain `Command`. */
|
|
24
|
+
const MAIN_ARGS: ArgsDef = HOST_ARG;
|
|
32
25
|
|
|
33
26
|
/**
|
|
34
|
-
* Spec §8. Every subcommand loads lazily, so `mcp`, `lock` and `_supervise` never load
|
|
35
|
-
*
|
|
27
|
+
* Spec §8. Every subcommand loads lazily, so `mcp`, `lock` and `_supervise` never load the panel (spec §3.1);
|
|
28
|
+
* a bare `catherd` runs `catherd panel`.
|
|
36
29
|
*/
|
|
37
30
|
export const main: Command = defineCommand({
|
|
38
31
|
meta: {
|
|
39
32
|
name: "catherd",
|
|
40
33
|
version: VERSION,
|
|
41
|
-
description: "Herds coding agents",
|
|
34
|
+
description: "Herds coding agents. catherd (or catherd panel) opens the panel in your browser.",
|
|
42
35
|
},
|
|
43
|
-
args:
|
|
36
|
+
args: MAIN_ARGS,
|
|
44
37
|
subCommands: {
|
|
45
38
|
init: () => import("./entry/init-command.ts").then((m) => m.initCommand),
|
|
39
|
+
panel: () => import("./entry/panel-command.ts").then((m) => m.panelCommand),
|
|
46
40
|
status: () => import("./entry/runs-command.ts").then((m) => m.statusCommand),
|
|
47
41
|
watch: () => import("./entry/runs-command.ts").then((m) => m.watchCommand),
|
|
48
42
|
runs: () => import("./entry/runs-command.ts").then((m) => m.runsCommand),
|
|
@@ -54,18 +48,15 @@ export const main: Command = defineCommand({
|
|
|
54
48
|
lock: () => import("./entry/lock-command.ts").then((m) => m.lockCommand),
|
|
55
49
|
"run-file": () => import("./entry/role-cli-command.ts").then((m) => m.runFileCommand),
|
|
56
50
|
gate: () => import("./entry/role-cli-command.ts").then((m) => m.gateCommand),
|
|
51
|
+
answer: () => import("./entry/answer-command.ts").then((m) => m.answerCommand),
|
|
57
52
|
pause: () => import("./entry/pause-command.ts").then((m) => m.pauseCommand),
|
|
58
53
|
resume: () => import("./entry/pause-command.ts").then((m) => m.resumeCommand),
|
|
59
54
|
"capture-fixtures": () =>
|
|
60
55
|
import("./entry/capture-fixtures-command.ts").then((m) => m.captureFixturesCommand),
|
|
56
|
+
scorecard: () => import("./entry/scorecard-command.ts").then((m) => m.scorecardCommand),
|
|
61
57
|
mcp: () => import("./entry/mcp/command.ts").then((m) => m.mcpCommand),
|
|
62
58
|
_supervise: () => import("./entry/supervise-command.ts").then((m) => m.superviseCommand),
|
|
63
59
|
},
|
|
64
|
-
// citty runs this after any subcommand too; only a bare `catherd` loads and opens the dashboard
|
|
65
|
-
async run(ctx) {
|
|
66
|
-
if (ctx.args._.length > 0) return;
|
|
67
|
-
await (await import("./entry/tui/run.tsx")).tuiRun(ctx);
|
|
68
|
-
},
|
|
69
60
|
});
|
|
70
61
|
|
|
71
62
|
const resolve = async <T>(v: T | Promise<T> | (() => T | Promise<T>)): Promise<T> =>
|
|
@@ -141,13 +132,10 @@ export async function runCli(argv: string[]): Promise<number> {
|
|
|
141
132
|
return EXIT.ok;
|
|
142
133
|
}
|
|
143
134
|
try {
|
|
144
|
-
const command = parseArgs(own,
|
|
145
|
-
// a bare `catherd`
|
|
135
|
+
const command = parseArgs(own, HOST_ARG)._[0];
|
|
136
|
+
// a bare `catherd` takes only --host: a typo is a usage error
|
|
146
137
|
const flag = (a: string) => (a.startsWith("--") ? (a.slice(2).split("=")[0] ?? "") : "");
|
|
147
|
-
|
|
148
|
-
const known = (name: string) =>
|
|
149
|
-
Object.hasOwn(dashboardArgs, name) ||
|
|
150
|
-
(name.startsWith("no-") && dashboardArgs[name.slice(3)]?.type === "boolean");
|
|
138
|
+
const known = (name: string) => Object.hasOwn(HOST_ARG, name);
|
|
151
139
|
const unknown =
|
|
152
140
|
command === undefined
|
|
153
141
|
? own.find((a, i) => a.startsWith("-") && !known(flag(a)) && own[i - 1] !== "--host")
|
|
@@ -157,9 +145,11 @@ export async function runCli(argv: string[]): Promise<number> {
|
|
|
157
145
|
return EXIT.usage;
|
|
158
146
|
}
|
|
159
147
|
// `lock` forwards signals to its command itself; everything else stops at once on Ctrl-C. The command is
|
|
160
|
-
// the first word that is not a flag: `catherd --
|
|
161
|
-
|
|
162
|
-
|
|
148
|
+
// the first word that is not a flag: `catherd --host codex lock -- …` is `lock` too.
|
|
149
|
+
// the panel stops itself on Ctrl-C
|
|
150
|
+
if (command !== "lock" && command !== undefined)
|
|
151
|
+
process.once("SIGINT", () => process.exit(EXIT.interrupted));
|
|
152
|
+
const parsed = parseArgs(own, HOST_ARG);
|
|
163
153
|
if (parsed.host !== undefined) {
|
|
164
154
|
const forwarded = own.filter(
|
|
165
155
|
(a, i) => a !== "--host" && own[i - 1] !== "--host" && !a.startsWith("--host="),
|
|
@@ -173,6 +163,11 @@ export async function runCli(argv: string[]): Promise<number> {
|
|
|
173
163
|
...(dashdash < 0 ? [] : argv.slice(dashdash)),
|
|
174
164
|
);
|
|
175
165
|
}
|
|
166
|
+
if (command === undefined) {
|
|
167
|
+
const { runPanel } = await import("./entry/panel-command.ts");
|
|
168
|
+
await runPanel({ port: 4317, open: true, host: parsed.host });
|
|
169
|
+
return EXIT.ok;
|
|
170
|
+
}
|
|
176
171
|
await runCommand(main, { rawArgs });
|
|
177
172
|
return Number(process.exitCode ?? EXIT.ok);
|
|
178
173
|
} catch (e) {
|
package/src/domain/bars.ts
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
logic: "the 60th percentile",
|
|
32
|
-
hard: "the 75th percentile",
|
|
33
|
-
};
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Catalog } from "./catalog.ts";
|
|
3
|
+
import { DIFFICULTIES, type Difficulty } from "./lane.ts";
|
|
4
|
+
|
|
5
|
+
/** The percentile of the shipped models' intelligences each difficulty's bar sits at. */
|
|
6
|
+
export const BAR_PERCENTILE: Record<Difficulty, number> = { copy: 20, build: 45, logic: 60, hard: 65 };
|
|
7
|
+
|
|
8
|
+
/** A profile's or a role's own percentiles (0 to 100), one difficulty at a time; a difficulty left out keeps its shipped percentile. */
|
|
9
|
+
export const BarsSchema = z.partialRecord(z.enum(DIFFICULTIES), z.number().min(0).max(100));
|
|
10
|
+
export type Bars = z.infer<typeof BarsSchema>;
|
|
11
|
+
|
|
12
|
+
/** The percentiles in force as stored: the shipped ones, with `p.bars` laid over them; they may decrease. */
|
|
13
|
+
export const effectiveBars = (p: { bars?: Bars }): Record<Difficulty, number> => ({
|
|
14
|
+
...BAR_PERCENTILE,
|
|
15
|
+
...p.bars,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Each pair `[easier, harder]` of neighbouring difficulties whose percentile goes down. */
|
|
19
|
+
export const barInversions = (at: Record<Difficulty, number>): [Difficulty, Difficulty][] =>
|
|
20
|
+
DIFFICULTIES.slice(1).flatMap((d, i): [Difficulty, Difficulty][] =>
|
|
21
|
+
at[d] < at[DIFFICULTIES[i] as Difficulty] ? [[DIFFICULTIES[i] as Difficulty, d]] : [],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/** The percentiles a route reads: the effective ones made non-decreasing, so a harder lane never routes weaker. */
|
|
25
|
+
export function percentilesOf(p: { bars?: Bars }): Record<Difficulty, number> {
|
|
26
|
+
const at = effectiveBars(p);
|
|
27
|
+
let floor = 0;
|
|
28
|
+
for (const d of DIFFICULTIES) at[d] = floor = Math.max(at[d], floor);
|
|
29
|
+
return at;
|
|
30
|
+
}
|
|
34
31
|
|
|
35
32
|
/** The `p`th percentile of `xs`, interpolated linearly between the closest ranks; NaN for none. */
|
|
36
33
|
export function percentile(xs: readonly number[], p: number): number {
|
|
@@ -42,78 +39,7 @@ export function percentile(xs: readonly number[], p: number): number {
|
|
|
42
39
|
return (s[lo] as number) + ((s[hi] as number) - (s[lo] as number)) * (pos - lo);
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* published by someone other than the vendor, which is 1.2's `measured`). Plan 14 Ruling 1.
|
|
49
|
-
*/
|
|
50
|
-
export const measuredOrBetter = (s: Score): boolean =>
|
|
51
|
-
s.confidence === "verified" ||
|
|
52
|
-
s.confidence === "measured" ||
|
|
53
|
-
(s.confidence === "secondary" && s.source === undefined);
|
|
54
|
-
|
|
55
|
-
/** Each dimension's values that bars are derived from: one per canonical rung, measured or better. */
|
|
56
|
-
export function barPool(scores: readonly Score[]): Record<Dim, number[]> {
|
|
57
|
-
const out = Object.fromEntries(DIMS.map((d) => [d, [] as number[]])) as Record<Dim, number[]>;
|
|
58
|
-
const seen = new Set<string>();
|
|
59
|
-
for (const s of scores) {
|
|
60
|
-
const key = `${s.rung} ${s.dim}`;
|
|
61
|
-
if (!measuredOrBetter(s) || seen.has(key)) continue;
|
|
62
|
-
seen.add(key);
|
|
63
|
-
out[s.dim].push(s.value);
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** A bar value, kept to four significant digits (DeepSWE 60.95, an Arena rating 1668, agentic 0.08622). */
|
|
69
|
-
const tidy = (v: number): number => Number(v.toPrecision(4));
|
|
70
|
-
|
|
71
|
-
export type Bars = Record<Kind, Record<Difficulty, Partial<Record<Dim, number>>>>;
|
|
72
|
-
/** `scores.json` `barsWhy`: per dimension and difficulty, where its threshold came from. */
|
|
73
|
-
export type BarsWhy = Partial<Record<Dim, Partial<Record<Difficulty, string>>>>;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Spec 1.2 §5.1–§5.2: the default bars. Each dimension's threshold at a difficulty is that difficulty's
|
|
77
|
-
* percentile of the pool (`barPool`), the same in every kind whose bar spans the dimension. A dimension with
|
|
78
|
-
* no pooled value gets no threshold (and a `barsWhy` line saying so). `benchmark` names each dimension's unit
|
|
79
|
-
* and `date` the day the data was read.
|
|
80
|
-
*/
|
|
81
|
-
export function deriveBars(
|
|
82
|
-
pool: Record<Dim, number[]>,
|
|
83
|
-
benchmark: (d: Dim) => string,
|
|
84
|
-
date: string,
|
|
85
|
-
): { bars: Bars; barsWhy: BarsWhy } {
|
|
86
|
-
const value: Partial<Record<Dim, Partial<Record<Difficulty, number>>>> = {};
|
|
87
|
-
const barsWhy: BarsWhy = {};
|
|
88
|
-
for (const dim of BAR_DIMS)
|
|
89
|
-
for (const d of DIFFICULTIES) {
|
|
90
|
-
if (!KINDS.some((k) => barDimsOf(k, d).includes(dim))) continue;
|
|
91
|
-
const xs = pool[dim];
|
|
92
|
-
const why = (barsWhy[dim] ??= {});
|
|
93
|
-
if (xs.length === 0) {
|
|
94
|
-
why[d] = `no rung is measured or better on ${benchmark(dim)} (${date}): no threshold`;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const v = tidy(percentile(xs, BAR_PERCENTILE[d]));
|
|
98
|
-
(value[dim] ??= {})[d] = v;
|
|
99
|
-
why[d] =
|
|
100
|
-
`${PERCENTILE_WORD[d]} of ${xs.length} rungs measured or better on ${benchmark(dim)} (${date}): ${v}`;
|
|
101
|
-
}
|
|
102
|
-
const bars = Object.fromEntries(
|
|
103
|
-
KINDS.map((k) => [
|
|
104
|
-
k,
|
|
105
|
-
Object.fromEntries(
|
|
106
|
-
DIFFICULTIES.map((d) => [
|
|
107
|
-
d,
|
|
108
|
-
Object.fromEntries(
|
|
109
|
-
barDimsOf(k, d).flatMap((dim) => {
|
|
110
|
-
const v = value[dim]?.[d];
|
|
111
|
-
return v === undefined ? [] : [[dim, v]];
|
|
112
|
-
}),
|
|
113
|
-
),
|
|
114
|
-
]),
|
|
115
|
-
),
|
|
116
|
-
]),
|
|
117
|
-
) as Bars;
|
|
118
|
-
return { bars, barsWhy };
|
|
42
|
+
/** A difficulty's bar: an intelligence percentile of the catalog's pool, 0 when there is no index. */
|
|
43
|
+
export function barOf(c: Catalog, d: Difficulty, at: Record<Difficulty, number> = BAR_PERCENTILE): number {
|
|
44
|
+
return c.pool.length === 0 ? 0 : percentile(c.pool, at[d]);
|
|
119
45
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Why a role stopped without finishing (spec 2.0 decision 3); the machine maps each to a transition. */
|
|
2
|
+
export const BLOCKED_REASONS = [
|
|
3
|
+
"needs_user",
|
|
4
|
+
"too_hard",
|
|
5
|
+
"provider_error",
|
|
6
|
+
"no_access",
|
|
7
|
+
"check_broken",
|
|
8
|
+
"out_of_scope",
|
|
9
|
+
] as const;
|
|
10
|
+
export type BlockedReason = (typeof BLOCKED_REASONS)[number];
|
|
11
|
+
|
|
12
|
+
export type StopAction = "climb" | "failover" | "park" | "wait";
|
|
13
|
+
|
|
14
|
+
/** What the machine does with each reason (spec 2.0 decision 8): climb, retry then fail over, park, or wait. */
|
|
15
|
+
export const REASON_ACTION: Readonly<Record<BlockedReason, StopAction>> = {
|
|
16
|
+
needs_user: "park",
|
|
17
|
+
too_hard: "climb",
|
|
18
|
+
provider_error: "failover",
|
|
19
|
+
no_access: "park",
|
|
20
|
+
check_broken: "wait",
|
|
21
|
+
out_of_scope: "park",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const REASON_AT_START = /^[`*\s]*([a-z][a-z_-]*)[`*]*\s*(?:[:—–-]\s*)?([\s\S]*)$/i;
|
|
25
|
+
|
|
26
|
+
/** The reason a `STATUS: blocked — <reason>: <why>` line names, and its why; null when it names none. */
|
|
27
|
+
export function parseBlockedReason(why: string | null): { reason: BlockedReason; why: string } | null {
|
|
28
|
+
const m = why === null ? null : REASON_AT_START.exec(why.trim());
|
|
29
|
+
if (!m) return null;
|
|
30
|
+
const word = (m[1] as string).toLowerCase().replace(/-/g, "_");
|
|
31
|
+
const reason = BLOCKED_REASONS.find((r) => r === word);
|
|
32
|
+
return reason ? { reason, why: (m[2] ?? "").trim() } : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The reply contract's blocked line, ported from t3herd `prompts/_no_questions.md`. */
|
|
36
|
+
export const BLOCKED_LINE = `Nobody reads your thread until you finish: never ask a question and never wait for an answer. When you cannot finish, leave the finished work in place, say what is left, and end with STATUS: blocked — <reason>: <why>, where <reason> is one of: ${[
|
|
37
|
+
"needs_user (only the user can decide; <why> is the question)",
|
|
38
|
+
"too_hard (you tried and cannot solve it; a stronger model should)",
|
|
39
|
+
"provider_error (your model, tools or sandbox failed)",
|
|
40
|
+
"no_access (you cannot reach what you need: the running app, a service, credentials)",
|
|
41
|
+
"check_broken (the check cannot pass for a reason outside your task; name each failing file:line)",
|
|
42
|
+
"out_of_scope (the task needs changes outside the files you own)",
|
|
43
|
+
].join(", ")}.`;
|
|
44
|
+
|
|
45
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
46
|
+
|
|
47
|
+
/** An `Owns:` entry as a path: cut at the first glob, trailing slashes dropped. */
|
|
48
|
+
const pathOf = (entry: string): string => entry.replace(/[*?[{].*$/, "").replace(/\/+$/, "");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The sibling `Owns:` entries a failed check blames, or [] when it blames none, or also blames one of the lane's
|
|
52
|
+
* own. Output names a file by its tail (`pkg/x.go:3`, `src/a.tsx(4,2)`), so a sibling's entry counts from two
|
|
53
|
+
* path segments, and an own entry already from its base name (t3herd `foreignFailure`).
|
|
54
|
+
*/
|
|
55
|
+
export function foreignFailure(log: string, own: string[], siblings: string[]): string[] {
|
|
56
|
+
const lines = log
|
|
57
|
+
.replace(ANSI, "")
|
|
58
|
+
.split("\n")
|
|
59
|
+
.filter((l) => !/^\s*(✓|ok\s|PASS\b)/.test(l));
|
|
60
|
+
const named = (p: string, min: number): boolean => {
|
|
61
|
+
const parts = p.split("/");
|
|
62
|
+
for (let i = 0; i <= parts.length - min; i++) {
|
|
63
|
+
const tail = parts.slice(i).join("/");
|
|
64
|
+
if (lines.some((l) => l.includes(tail))) return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
const mine = own.map(pathOf).filter(Boolean);
|
|
69
|
+
if (mine.some((f) => named(f, 1))) return [];
|
|
70
|
+
const theirs = siblings.map(pathOf).filter((f) => f && !mine.includes(f));
|
|
71
|
+
return [...new Set(theirs.filter((f) => named(f, Math.min(2, f.split("/").length))))];
|
|
72
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { headerLine } from "./lane.ts";
|
|
2
|
+
import { slug } from "./util.ts";
|
|
3
|
+
import { BaseRefSchema } from "./workspace.ts";
|
|
4
|
+
|
|
5
|
+
/** The run title as a folder and branch stem: at most 40 characters, `run` when nothing is left. */
|
|
6
|
+
export const runSlug = (title: string): string => slug(title).slice(0, 40) || "run";
|
|
7
|
+
|
|
8
|
+
/** A milestone's branch: the run's slug and the milestone, lower case (`wallet-split-m1`). */
|
|
9
|
+
export const milestoneBranch = (title: string, m: string): string => `${runSlug(title)}-${m.toLowerCase()}`;
|
|
10
|
+
|
|
11
|
+
/** A branch on origin as `run_start`'s base: a plain name, never a remote ref (`origin/…`) or a full ref. */
|
|
12
|
+
export const BaseBranchSchema = BaseRefSchema.refine(
|
|
13
|
+
(s) => !s.startsWith("origin/") && !s.startsWith("refs/"),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The `Setup:` command in the lines right under plan.md's `# ` title, up to the first blank line or heading
|
|
18
|
+
* (backticks stripped); null without one. A blank line after the title ends the header, so body prose never counts.
|
|
19
|
+
*/
|
|
20
|
+
export function planSetup(planText: string): string | null {
|
|
21
|
+
const lines = planText.split("\n");
|
|
22
|
+
const start = lines.findIndex((l) => /^#\s+/.test(l)) + 1;
|
|
23
|
+
let end = start;
|
|
24
|
+
while (end < lines.length && (lines[end] ?? "").trim() !== "" && !(lines[end] ?? "").startsWith("#")) end++;
|
|
25
|
+
const re = headerLine("setup");
|
|
26
|
+
for (const line of lines.slice(start, end)) {
|
|
27
|
+
const m = re.exec(line);
|
|
28
|
+
if (m)
|
|
29
|
+
return (
|
|
30
|
+
(m[1] ?? "")
|
|
31
|
+
.trim()
|
|
32
|
+
.replace(/^`+|`+$/g, "")
|
|
33
|
+
.trim() || null
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether the owner's answer to a landed milestone's question is the word that has catherd push its branch. */
|
|
40
|
+
export const isPushAnswer = (answer: string): boolean => answer.trim().toLowerCase() === "push";
|
|
41
|
+
|
|
42
|
+
/** The command that opens a milestone's MR, for the origin's host: gh on github.com, else glab. */
|
|
43
|
+
export function mrCommand(o: { originUrl: string; base: string; branch: string; body: string }): string {
|
|
44
|
+
return /(^|[@/.])github\.com[:/]/.test(o.originUrl)
|
|
45
|
+
? `gh pr create --base ${o.base} --head ${o.branch} --body-file ${o.body}`
|
|
46
|
+
: `glab mr create --source-branch ${o.branch} --target-branch ${o.base} --description-file ${o.body}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The milestone a unit (`run`, `M1`, `M1.L2`) belongs to; null for the run itself. */
|
|
50
|
+
export const unitMilestone = (unit: string): string | null =>
|
|
51
|
+
unit === "run" ? null : (unit.split(".")[0] ?? null);
|