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/profile.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { OrchestrationHost } from "./host.ts";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { Budget } from "./budget.ts";
|
|
4
|
+
import type { Bars } from "./bars.ts";
|
|
4
5
|
import { BILLING_KEYS, type BillingKey } from "./catalog.ts";
|
|
5
6
|
import { BILLING_MODES, type BillingMode, DEFAULT_BILLING } from "./cost.ts";
|
|
6
7
|
import { CatherdError } from "./errors.ts";
|
|
7
|
-
import { ADAPTER_IDS, parseRung, tryParseRung } from "./ids.ts";
|
|
8
|
+
import { ADAPTER_IDS, EFFORT_ORDER, type EffortWord, effortRank, parseRung, tryParseRung } from "./ids.ts";
|
|
9
|
+
import { DIFFICULTIES } from "./lane.ts";
|
|
8
10
|
import { ACCESS, type Access } from "./record.ts";
|
|
9
11
|
import { DEFAULT_ACCESS, ROLES, type Role } from "./roles.ts";
|
|
10
12
|
import { isPlain, slug } from "./util.ts";
|
|
@@ -45,6 +47,9 @@ const RoleDocSchema = z.looseObject({
|
|
|
45
47
|
rungs: z.array(z.string()).optional(),
|
|
46
48
|
defaultRung: z.string().optional(),
|
|
47
49
|
network: z.boolean().optional(),
|
|
50
|
+
timeouts: z.looseObject({ idleMin: positive.optional(), wallMin: positive.optional() }).optional(),
|
|
51
|
+
// plan 33: unknown, so a hand edit never makes the file unreadable; resolveProfile reads what it can
|
|
52
|
+
bars: z.unknown().optional(),
|
|
48
53
|
});
|
|
49
54
|
|
|
50
55
|
export const ProfileDocSchema = z.looseObject({
|
|
@@ -56,6 +61,9 @@ export const ProfileDocSchema = z.looseObject({
|
|
|
56
61
|
roles: z.record(z.string(), RoleDocSchema).optional(),
|
|
57
62
|
harness: z.record(z.string(), z.looseObject({ isolated: z.boolean().optional() })).optional(),
|
|
58
63
|
failover: z.record(z.string(), z.string()).optional(),
|
|
64
|
+
// unknown values, like `bars`: an entry that is no effort word never makes the file unreadable
|
|
65
|
+
maxEffort: z.record(z.string(), z.unknown()).optional(),
|
|
66
|
+
bars: z.unknown().optional(),
|
|
59
67
|
budget: z
|
|
60
68
|
.looseObject({ minutes: positive.optional(), tokens: positive.optional(), usd: positive.optional() })
|
|
61
69
|
.optional(),
|
|
@@ -65,9 +73,20 @@ export const ProfileDocSchema = z.looseObject({
|
|
|
65
73
|
.looseObject({ heavy: z.union([z.number().int().positive(), z.literal("cpus/2")]).optional() })
|
|
66
74
|
.optional(),
|
|
67
75
|
notify: z.array(z.string()).optional(),
|
|
76
|
+
// plan 35: unknown, so a hand edit never makes the file unreadable; resolveProfile reads what it can
|
|
77
|
+
limits: z.unknown().optional(),
|
|
68
78
|
});
|
|
69
79
|
export type ProfileDoc = z.infer<typeof ProfileDocSchema>;
|
|
70
80
|
|
|
81
|
+
/** plan 35: the profile's memory numbers for starting milestones side by side (megabytes). */
|
|
82
|
+
const MEMORY_KEYS = ["reserveMb", "perMilestoneMb", "floorMb"] as const;
|
|
83
|
+
type MemoryKey = (typeof MEMORY_KEYS)[number];
|
|
84
|
+
const positiveInt = (v: unknown): v is number => typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
85
|
+
const storedMemory = (doc: ProfileDoc): Record<string, unknown> => {
|
|
86
|
+
const memory = isPlain(doc.limits) ? doc.limits.memory : undefined;
|
|
87
|
+
return isPlain(memory) ? memory : {};
|
|
88
|
+
};
|
|
89
|
+
|
|
71
90
|
const OBJECTIVES = ["cost", "speed"] as const;
|
|
72
91
|
const JEV_USES = ["auto", "off"] as const;
|
|
73
92
|
const known = <T extends string>(values: readonly T[], v: string | undefined): v is T =>
|
|
@@ -85,6 +104,16 @@ const STORED_FALLBACK = {
|
|
|
85
104
|
billing: (key: string): BillingMode => DEFAULT_BILLING[key as BillingKey] ?? "metered",
|
|
86
105
|
} as const;
|
|
87
106
|
|
|
107
|
+
const barOk = (k: string, v: unknown): boolean =>
|
|
108
|
+
(DIFFICULTIES as readonly string[]).includes(k) && typeof v === "number" && v >= 0 && v <= 100;
|
|
109
|
+
|
|
110
|
+
const isEffort = (v: unknown): v is EffortWord => typeof v === "string" && effortRank(v) >= 0;
|
|
111
|
+
|
|
112
|
+
/** The entries of a stored `bars` this catherd can read; anything else is not a bar. */
|
|
113
|
+
function readBars(v: unknown): Bars {
|
|
114
|
+
return isPlain(v) ? (Object.fromEntries(Object.entries(v).filter((e) => barOk(e[0], e[1]))) as Bars) : {};
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
/** A stored value this catherd does not know, with what it is read as. */
|
|
89
118
|
export interface UnknownValue {
|
|
90
119
|
path: string;
|
|
@@ -102,9 +131,25 @@ export function unknownValues(doc: ProfileDoc): UnknownValue[] {
|
|
|
102
131
|
check("jev.use", JEV_USES, doc.jev?.use, STORED_FALLBACK.jevUse);
|
|
103
132
|
for (const [k, v] of Object.entries(doc.billing ?? {}))
|
|
104
133
|
check(`billing.${k}`, BILLING_MODES, v, STORED_FALLBACK.billing(k));
|
|
134
|
+
for (const [k, v] of Object.entries(doc.maxEffort ?? {}))
|
|
135
|
+
if (!isEffort(v)) {
|
|
136
|
+
const value = typeof v === "string" ? v : JSON.stringify(v);
|
|
137
|
+
out.push({ path: `maxEffort.${k}`, value, readAs: "no ceiling" });
|
|
138
|
+
}
|
|
139
|
+
const checkBars = (path: string, v: unknown, readAs: string) => {
|
|
140
|
+
if (v === undefined) return;
|
|
141
|
+
if (!isPlain(v)) return void out.push({ path, value: JSON.stringify(v), readAs });
|
|
142
|
+
for (const [k, x] of Object.entries(v))
|
|
143
|
+
if (!barOk(k, x)) out.push({ path: `${path}.${k}`, value: String(x), readAs });
|
|
144
|
+
};
|
|
145
|
+
checkBars("bars", doc.bars, "the shipped percentile");
|
|
146
|
+
for (const role of ROLES) checkBars(`roles.${role}.bars`, doc.roles?.[role]?.bars, "the profile's bar");
|
|
105
147
|
for (const role of ROLES)
|
|
106
148
|
check(`roles.${role}.access`, ACCESS, doc.roles?.[role]?.access, STORED_FALLBACK.access);
|
|
107
149
|
for (const n of doc.notify ?? []) check("notify", NOTIFY, n, "skipped");
|
|
150
|
+
for (const [k, v] of Object.entries(storedMemory(doc)))
|
|
151
|
+
if (!(MEMORY_KEYS as readonly string[]).includes(k) || !positiveInt(v))
|
|
152
|
+
out.push({ path: `limits.memory.${k}`, value: JSON.stringify(v), readAs: "the default" });
|
|
108
153
|
return out;
|
|
109
154
|
}
|
|
110
155
|
|
|
@@ -117,8 +162,21 @@ export type RoleConfig = {
|
|
|
117
162
|
defaultRung?: string;
|
|
118
163
|
/** spec §5: false drops a workspace-write role's network, loopback and Docker grants; absent means granted */
|
|
119
164
|
network?: false;
|
|
165
|
+
/** plan 23: this role's own idle and wall timeouts, over the profile's `timeouts` (a long gate, a slow model) */
|
|
166
|
+
timeouts?: { idleMin?: number; wallMin?: number };
|
|
167
|
+
/** plan 33: this role's own bar percentiles, over the profile's `bars` (decision 11) */
|
|
168
|
+
bars?: Bars;
|
|
120
169
|
};
|
|
121
170
|
|
|
171
|
+
/** The idle and wall timeouts a role runs under: its own `timeouts`, else the profile's. */
|
|
172
|
+
export const roleTimeouts = (
|
|
173
|
+
profile: { timeouts: { idleMin: number; wallMin: number } },
|
|
174
|
+
role: { timeouts?: { idleMin?: number; wallMin?: number } } | undefined,
|
|
175
|
+
): { idleMin: number; wallMin: number } => ({
|
|
176
|
+
idleMin: role?.timeouts?.idleMin ?? profile.timeouts.idleMin,
|
|
177
|
+
wallMin: role?.timeouts?.wallMin ?? profile.timeouts.wallMin,
|
|
178
|
+
});
|
|
179
|
+
|
|
122
180
|
/** A profile with every default filled in: what the run engine, the CLI and the agent files read. */
|
|
123
181
|
export interface Profile {
|
|
124
182
|
name: string;
|
|
@@ -128,11 +186,17 @@ export interface Profile {
|
|
|
128
186
|
roles: Record<Role, RoleConfig>;
|
|
129
187
|
harness: Record<string, { isolated: boolean }>;
|
|
130
188
|
failover: Record<string, string>;
|
|
189
|
+
/** plan 33: `<backend>:<model>` → the highest effort routing starts it at; a model not named has no ceiling */
|
|
190
|
+
maxEffort: Record<string, EffortWord>;
|
|
191
|
+
/** plan 33: bar percentiles per difficulty, over the shipped ones (`percentilesOf`) */
|
|
192
|
+
bars: Bars;
|
|
131
193
|
budget: Budget;
|
|
132
194
|
timeouts: { idleMin: number; wallMin: number };
|
|
133
195
|
preflight: { confirm: boolean };
|
|
134
196
|
lock: { heavy: number | "cpus/2" };
|
|
135
197
|
notify: NotifyMoment[];
|
|
198
|
+
/** plan 35: only the memory numbers the profile sets; the defaults are `MEMORY_LIMITS` */
|
|
199
|
+
limits: { memory: Partial<Record<MemoryKey, number>> };
|
|
136
200
|
}
|
|
137
201
|
|
|
138
202
|
const LUNA_HIGH = "codex:gpt-6-luna#high";
|
|
@@ -156,14 +220,15 @@ export const BUILTIN_ROLES: Record<Role, RoleConfig> = {
|
|
|
156
220
|
artist: { enabled: true, access: DEFAULT_ACCESS.artist, rungs: [SOL("medium")] },
|
|
157
221
|
writer: { enabled: true, access: DEFAULT_ACCESS.writer, rungs: [LUNA_HIGH] },
|
|
158
222
|
researcher: { enabled: true, access: DEFAULT_ACCESS.researcher, rungs: [LUNA_HIGH] },
|
|
223
|
+
judge: { enabled: true, access: DEFAULT_ACCESS.judge, rungs: ["claude-code:claude-opus-5-5#medium"] },
|
|
159
224
|
};
|
|
160
225
|
|
|
161
226
|
/**
|
|
162
|
-
* Spec 1.1 §11: each shipped worker rung fails over to the best stand-in the
|
|
163
|
-
* (`rankStandIns` over `
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
227
|
+
* Spec 1.1 §11: each shipped worker rung fails over to the best stand-in the Slopalytics index offers
|
|
228
|
+
* (`rankStandIns` over `catalogPairs`, pinned by test/domain/failover.test.ts): on another quota, paid from a
|
|
229
|
+
* plan, and at least as intelligent. Go serves GPT-6 Luna itself; Kimi K3 stands on its own index, above Sol
|
|
230
|
+
* medium. Sol high and xhigh have no such stand-in on the owner's plans, so they have none: a limit on them
|
|
231
|
+
* pauses the lane instead of dropping it a tier.
|
|
167
232
|
*/
|
|
168
233
|
export const DEFAULT_FAILOVER: Record<string, string> = {
|
|
169
234
|
[LUNA_HIGH]: "opencode:opencode-go/gpt-6-luna#high",
|
|
@@ -171,63 +236,22 @@ export const DEFAULT_FAILOVER: Record<string, string> = {
|
|
|
171
236
|
};
|
|
172
237
|
|
|
173
238
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
239
|
+
* Plan 33 Ruling 8: t3herd's effort caps under the shipped catalog's ids, on every backend that lists the model.
|
|
240
|
+
* The default profile writes them, so a failover to any backend can never start a `#max` Opus; a profile without
|
|
241
|
+
* the key has no ceiling.
|
|
176
242
|
*/
|
|
177
|
-
const
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"
|
|
243
|
+
export const DEFAULT_MAX_EFFORT: Readonly<Record<string, EffortWord>> = {
|
|
244
|
+
"claude:claude-opus-5-5": "high",
|
|
245
|
+
"claude-code:claude-opus-5-5": "high",
|
|
246
|
+
"opencode:opencode/claude-opus-5-5": "high",
|
|
247
|
+
"cursor:claude-opus-5-5": "high",
|
|
248
|
+
"claude:claude-sonnet-5-5": "high",
|
|
249
|
+
"claude-code:claude-sonnet-5-5": "high",
|
|
250
|
+
"opencode:opencode/claude-sonnet-5-5": "high",
|
|
251
|
+
"codex:gpt-6.1-sol": "xhigh",
|
|
185
252
|
};
|
|
186
253
|
|
|
187
|
-
/** The
|
|
188
|
-
function cursorPairEffort(model: string, effort: string): string {
|
|
189
|
-
const list = CURSOR_PAIR[model]?.efforts ?? [];
|
|
190
|
-
if (list.length === 0) return "default";
|
|
191
|
-
if (effort !== "default" && list.includes(effort)) return effort;
|
|
192
|
-
return list.includes("high") ? "high" : (list[0] as string);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Spec 1.3 §7.3: the same model through two backends bills two pools, so each stands in for the other on a
|
|
197
|
-
* limit, at the same effort where both list it. Otherwise the stand-in runs at `high`: Grok 4.5 `xhigh` at Cursor
|
|
198
|
-
* `high`, Cursor's Gemini 3.6 `minimal` at agy `high`; Gemini 3.1 Pro's bare Cursor slug pairs with agy `high`.
|
|
199
|
-
* Grok and Gemini are paired with Cursor. Consulted after the profile's own failover and never written into a
|
|
200
|
-
* profile, where a pair whose rung is on no ladder would warn "never runs"; never a stand-in for a shipped Codex,
|
|
201
|
-
* Claude or opencode rung.
|
|
202
|
-
*/
|
|
203
|
-
export const PAIRED_FAILOVER: Record<string, string> = Object.fromEntries(
|
|
204
|
-
(
|
|
205
|
-
[
|
|
206
|
-
["grok", ["grok-4.7", "grok-4.6", "grok-4.5"], ["low", "medium", "high", "xhigh"]],
|
|
207
|
-
[
|
|
208
|
-
"antigravity",
|
|
209
|
-
["gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash"],
|
|
210
|
-
["low", "medium", "high"],
|
|
211
|
-
],
|
|
212
|
-
["antigravity", ["gemini-3.1-pro"], ["low", "high"]],
|
|
213
|
-
] as const
|
|
214
|
-
).flatMap(([backend, models, efforts]) =>
|
|
215
|
-
models.flatMap((m) => {
|
|
216
|
-
const cursor = CURSOR_PAIR[m] as { id: string; efforts: readonly string[] };
|
|
217
|
-
const back = ["default", ...efforts].map(
|
|
218
|
-
(e) => [`${backend}:${m}#${e}`, `cursor:${cursor.id}#${cursorPairEffort(m, e)}`] as const,
|
|
219
|
-
);
|
|
220
|
-
// `#default` too, as on the other side: a Cursor rung with no effort flag fails over the same way
|
|
221
|
-
const fore = [...new Set(["default", ...cursor.efforts])].map((e) => {
|
|
222
|
-
const there = e !== "default" && (efforts as readonly string[]).includes(e) ? e : "high";
|
|
223
|
-
return [`cursor:${cursor.id}#${e}`, `${backend}:${m}#${there}`] as const;
|
|
224
|
-
});
|
|
225
|
-
return [...back, ...fore];
|
|
226
|
-
}),
|
|
227
|
-
),
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
/** The five billing keys spec §7.1 writes out; cursor and grok arrive with their backends. */
|
|
254
|
+
/** The five billing keys spec §7.1 writes out; the rest default. */
|
|
231
255
|
const WRITTEN_BILLING: BillingKey[] = ["codex", "claude", "claude-code", "opencode-go", "opencode"];
|
|
232
256
|
|
|
233
257
|
/** Stored defaults for init/new; architect/verifier rungs resolve from the current host. */
|
|
@@ -245,6 +269,7 @@ export function defaultProfileDoc(name = "default"): ProfileDoc {
|
|
|
245
269
|
opencode: { isolated: false },
|
|
246
270
|
},
|
|
247
271
|
failover: { ...DEFAULT_FAILOVER },
|
|
272
|
+
maxEffort: { ...DEFAULT_MAX_EFFORT },
|
|
248
273
|
budget: {},
|
|
249
274
|
timeouts: { idleMin: 15, wallMin: 90 },
|
|
250
275
|
preflight: { confirm: false },
|
|
@@ -253,10 +278,10 @@ export function defaultProfileDoc(name = "default"): ProfileDoc {
|
|
|
253
278
|
};
|
|
254
279
|
}
|
|
255
280
|
|
|
256
|
-
/** Remove only the
|
|
281
|
+
/** Remove only the architect, verifier and judge execution choices, preserving other stored fields. */
|
|
257
282
|
export function hostDefaultsDoc(doc: ProfileDoc): ProfileDoc {
|
|
258
283
|
const next = structuredClone(doc);
|
|
259
|
-
for (const role of ["architect", "verifier"] as const) {
|
|
284
|
+
for (const role of ["architect", "verifier", "judge"] as const) {
|
|
260
285
|
const r = next.roles?.[role];
|
|
261
286
|
if (r) {
|
|
262
287
|
delete r.rungs;
|
|
@@ -285,6 +310,9 @@ export function resolveProfile(doc: ProfileDoc, name: string, host: Orchestratio
|
|
|
285
310
|
? []
|
|
286
311
|
: b.rungs;
|
|
287
312
|
}
|
|
313
|
+
// the judge is Opus by default; on a Codex host it follows the host like the architect and the verifier do
|
|
314
|
+
if (role === "judge" && d?.rungs === undefined && host === "codex")
|
|
315
|
+
omitted = ["codex:gpt-6.1-sol#medium"];
|
|
288
316
|
// a role that lists its own rungs never inherits the built-in default rung, which may not be among them
|
|
289
317
|
const defaultRung = d?.defaultRung ?? (d?.rungs === undefined ? b.defaultRung : undefined);
|
|
290
318
|
roles[role] = {
|
|
@@ -298,6 +326,15 @@ export function resolveProfile(doc: ProfileDoc, name: string, host: Orchestratio
|
|
|
298
326
|
rungs: [...(d?.rungs ?? omitted)],
|
|
299
327
|
...(defaultRung ? { defaultRung } : {}),
|
|
300
328
|
...(d?.network === false ? { network: false as const } : {}),
|
|
329
|
+
...(d?.timeouts?.idleMin !== undefined || d?.timeouts?.wallMin !== undefined
|
|
330
|
+
? {
|
|
331
|
+
timeouts: {
|
|
332
|
+
...(d.timeouts.idleMin !== undefined ? { idleMin: d.timeouts.idleMin } : {}),
|
|
333
|
+
...(d.timeouts.wallMin !== undefined ? { wallMin: d.timeouts.wallMin } : {}),
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
: {}),
|
|
337
|
+
...(Object.keys(readBars(d?.bars)).length > 0 ? { bars: readBars(d?.bars) } : {}),
|
|
301
338
|
};
|
|
302
339
|
}
|
|
303
340
|
const harness: Profile["harness"] = {};
|
|
@@ -320,11 +357,23 @@ export function resolveProfile(doc: ProfileDoc, name: string, host: Orchestratio
|
|
|
320
357
|
roles,
|
|
321
358
|
harness,
|
|
322
359
|
failover: { ...doc.failover },
|
|
360
|
+
maxEffort: Object.fromEntries(
|
|
361
|
+
Object.entries(doc.maxEffort ?? {}).filter((e): e is [string, EffortWord] => isEffort(e[1])),
|
|
362
|
+
),
|
|
363
|
+
bars: readBars(doc.bars),
|
|
323
364
|
budget,
|
|
324
365
|
timeouts: { idleMin: doc.timeouts?.idleMin ?? 15, wallMin: doc.timeouts?.wallMin ?? 90 },
|
|
325
366
|
preflight: { confirm: doc.preflight?.confirm ?? false },
|
|
326
367
|
lock: { heavy: doc.lock?.heavy ?? "cpus/2" },
|
|
327
368
|
notify: doc.notify === undefined ? [...NOTIFY] : doc.notify.filter((n) => known(NOTIFY, n)),
|
|
369
|
+
limits: {
|
|
370
|
+
memory: Object.fromEntries(
|
|
371
|
+
MEMORY_KEYS.flatMap((k) => {
|
|
372
|
+
const v = storedMemory(doc)[k];
|
|
373
|
+
return positiveInt(v) ? [[k, v]] : [];
|
|
374
|
+
}),
|
|
375
|
+
),
|
|
376
|
+
},
|
|
328
377
|
};
|
|
329
378
|
}
|
|
330
379
|
|
|
@@ -337,6 +386,10 @@ export function agentName(profile: string, role: Role, rung: string): string {
|
|
|
337
386
|
// Patches: what profile_set and `catherd profile set` accept. Strict at every level, so a misspelt key is
|
|
338
387
|
// refused with E_INPUT_INVALID instead of dropped (plan-2 review m9). `null` removes a map entry or a nullable
|
|
339
388
|
// field (a role's `network` among them: absent means granted).
|
|
389
|
+
const BarsPatchSchema = z
|
|
390
|
+
.partialRecord(z.enum(DIFFICULTIES), z.number().min(0).max(100).nullable())
|
|
391
|
+
.nullable();
|
|
392
|
+
|
|
340
393
|
const RolePatchSchema = z
|
|
341
394
|
.strictObject({
|
|
342
395
|
enabled: z.boolean(),
|
|
@@ -344,6 +397,11 @@ const RolePatchSchema = z
|
|
|
344
397
|
rungs: z.array(RungSchema),
|
|
345
398
|
defaultRung: RungSchema.nullable(),
|
|
346
399
|
network: z.boolean().nullable(),
|
|
400
|
+
timeouts: z
|
|
401
|
+
.strictObject({ idleMin: positive.nullable(), wallMin: positive.nullable() })
|
|
402
|
+
.partial()
|
|
403
|
+
.nullable(),
|
|
404
|
+
bars: BarsPatchSchema,
|
|
347
405
|
})
|
|
348
406
|
.partial();
|
|
349
407
|
|
|
@@ -354,6 +412,13 @@ export const ProfilePatchSchema = z.strictObject({
|
|
|
354
412
|
roles: z.partialRecord(z.enum(ROLES), RolePatchSchema).optional(),
|
|
355
413
|
harness: z.partialRecord(z.enum(HARNESS_KEYS), z.strictObject({ isolated: z.boolean() })).optional(),
|
|
356
414
|
failover: z.record(RungSchema, RungSchema.nullable()).optional(),
|
|
415
|
+
maxEffort: z
|
|
416
|
+
.record(
|
|
417
|
+
z.string().refine((k) => tryParseRung(`${k}#x`) !== null, "a key is <backend>:<model>"),
|
|
418
|
+
z.enum(EFFORT_ORDER).nullable(),
|
|
419
|
+
)
|
|
420
|
+
.optional(),
|
|
421
|
+
bars: BarsPatchSchema.optional(),
|
|
357
422
|
budget: z
|
|
358
423
|
.strictObject({ minutes: positive.nullable(), tokens: positive.nullable(), usd: positive.nullable() })
|
|
359
424
|
.partial()
|
|
@@ -362,6 +427,18 @@ export const ProfilePatchSchema = z.strictObject({
|
|
|
362
427
|
preflight: z.strictObject({ confirm: z.boolean() }).optional(),
|
|
363
428
|
lock: z.strictObject({ heavy: z.union([z.number().int().positive(), z.literal("cpus/2")]) }).optional(),
|
|
364
429
|
notify: z.array(z.enum(NOTIFY)).optional(),
|
|
430
|
+
limits: z
|
|
431
|
+
.strictObject({
|
|
432
|
+
memory: z
|
|
433
|
+
.strictObject({
|
|
434
|
+
reserveMb: z.number().int().positive().nullable(),
|
|
435
|
+
perMilestoneMb: z.number().int().positive().nullable(),
|
|
436
|
+
floorMb: z.number().int().positive().nullable(),
|
|
437
|
+
})
|
|
438
|
+
.partial(),
|
|
439
|
+
})
|
|
440
|
+
.partial()
|
|
441
|
+
.optional(),
|
|
365
442
|
});
|
|
366
443
|
export type ProfilePatch = z.infer<typeof ProfilePatchSchema>;
|
|
367
444
|
|
|
@@ -382,7 +459,7 @@ export function applyPatch(doc: ProfileDoc, patch: ProfilePatch): ProfileDoc {
|
|
|
382
459
|
}
|
|
383
460
|
|
|
384
461
|
/** Map fields whose keys are rungs or billing keys, which may hold dots: the rest of the path is one key. */
|
|
385
|
-
const MAP_FIELDS = new Set(["failover", "billing"]);
|
|
462
|
+
const MAP_FIELDS = new Set(["failover", "billing", "maxEffort"]);
|
|
386
463
|
const LIST_FIELDS = (keys: string[]) =>
|
|
387
464
|
(keys[0] === "roles" && keys[2] === "rungs") || (keys[0] === "notify" && keys.length === 1);
|
|
388
465
|
|
package/src/domain/record.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { z } from "zod";
|
|
|
3
3
|
export const ACCESS = ["read-only", "workspace-write", "full"] as const;
|
|
4
4
|
export type Access = (typeof ACCESS)[number];
|
|
5
5
|
|
|
6
|
-
const RUN_STATUSES = ["ok", "failed", "limit", "cli-too-old", "timeout", "cancelled"] as const;
|
|
6
|
+
export const RUN_STATUSES = ["ok", "failed", "limit", "cli-too-old", "timeout", "cancelled"] as const;
|
|
7
7
|
export type RunStatus = (typeof RUN_STATUSES)[number];
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/** plan 23: `flaky`, a worker's word for a check that failed, then passed when run alone */
|
|
10
|
+
export const REPLY_STATUSES = ["complete", "partial", "blocked", "refused", "flaky"] as const;
|
|
10
11
|
export type ReplyStatus = (typeof REPLY_STATUSES)[number];
|
|
11
12
|
|
|
12
13
|
export interface Tokens {
|
|
@@ -27,6 +28,8 @@ export const EXIT_REASONS = [
|
|
|
27
28
|
"wall-timeout",
|
|
28
29
|
"cancelled",
|
|
29
30
|
"lost",
|
|
31
|
+
/** plan 23: the provider kept failing (retries with no progress); failover treats it as a usage limit */
|
|
32
|
+
"provider-unavailable",
|
|
30
33
|
] as const;
|
|
31
34
|
export type ExitReason = (typeof EXIT_REASONS)[number];
|
|
32
35
|
export interface ExitInfo {
|
|
@@ -65,6 +68,19 @@ export const RunRecordSchema = z.looseObject({
|
|
|
65
68
|
gitUnavailable: z.boolean().optional(),
|
|
66
69
|
replyStatus: z.enum(REPLY_STATUSES).nullable(),
|
|
67
70
|
replyWhy: z.string().nullable(),
|
|
71
|
+
/** plan 23: the reply's ENV: line, when the environment stopped the role; absent otherwise */
|
|
72
|
+
environment: z.string().optional(),
|
|
73
|
+
/** a verifier's `HEAD: <sha>` line: the commit it checked (lower-case hex, 7-64 characters) */
|
|
74
|
+
head: z
|
|
75
|
+
.string()
|
|
76
|
+
.regex(/^[0-9a-f]{7,64}$/)
|
|
77
|
+
.optional(),
|
|
78
|
+
/** a worker's `EXTRA: <path> — <why>` lines: files outside its lane it had to change */
|
|
79
|
+
extras: z.array(z.object({ path: z.string(), why: z.string() })).optional(),
|
|
80
|
+
/** a fix-round worker's `REJECTED: <finding> — <why>` lines */
|
|
81
|
+
rejected: z.array(z.object({ finding: z.string(), why: z.string() })).optional(),
|
|
82
|
+
/** a worker's `DESIGN:` lines: where its plan task conflicts with the code */
|
|
83
|
+
design: z.array(z.string()).optional(),
|
|
68
84
|
threadHeavy: z.boolean(),
|
|
69
85
|
access: z.enum(ACCESS),
|
|
70
86
|
isolated: z.boolean(),
|
|
@@ -79,7 +95,16 @@ export const RunRecordSchema = z.looseObject({
|
|
|
79
95
|
});
|
|
80
96
|
export type RunRecord = z.infer<typeof RunRecordSchema>;
|
|
81
97
|
|
|
82
|
-
const STATUS_LINE = /^STATUS:\s*(complete|partial|blocked|refused)\s*(?:—|–|-)\s*(.*)$/;
|
|
98
|
+
const STATUS_LINE = /^STATUS:\s*(complete|partial|blocked|refused|flaky)\s*(?:—|–|-)\s*(.*)$/;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Plan 23: what stopped the role, when the environment did: an `ENV: <what>` line of its reply (a VPN, no
|
|
102
|
+
* Docker, a dead registry); null without one.
|
|
103
|
+
*/
|
|
104
|
+
export function parseReplyEnvironment(reply: string): string | null {
|
|
105
|
+
const m = /^[ \t]*ENV:[ \t]*(\S.*?)[ \t]*$/m.exec(reply);
|
|
106
|
+
return m ? (m[1] as string) : null;
|
|
107
|
+
}
|
|
83
108
|
|
|
84
109
|
/** The role's claim about its own work: the reply's last line, `STATUS: <s> — <why>`. */
|
|
85
110
|
export function parseReplyStatus(reply: string): { status: ReplyStatus | null; why: string | null } {
|
|
@@ -87,3 +112,43 @@ export function parseReplyStatus(reply: string): { status: ReplyStatus | null; w
|
|
|
87
112
|
const m = STATUS_LINE.exec(last);
|
|
88
113
|
return m ? { status: m[1] as ReplyStatus, why: (m[2] ?? "").trim() } : { status: null, why: null };
|
|
89
114
|
}
|
|
115
|
+
|
|
116
|
+
/** The last `HEAD: <hex>` line of a reply outside code fences, lower-cased: the commit a verifier says it checked. */
|
|
117
|
+
export function parseReplyHead(reply: string): string | null {
|
|
118
|
+
let fenced = false;
|
|
119
|
+
let head: string | null = null;
|
|
120
|
+
for (const line of reply.split("\n")) {
|
|
121
|
+
if (/^\s*(```|~~~)/.test(line)) fenced = !fenced;
|
|
122
|
+
else if (!fenced) {
|
|
123
|
+
const m = /^[ \t]*HEAD:[ \t]*([0-9a-fA-F]{7,64})[ \t]*$/.exec(line);
|
|
124
|
+
if (m) head = (m[1] as string).toLowerCase();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return head;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** `<LABEL>: <a> — <b>` lines (optional bullet; the separator is `—`, `–` or `-` with whitespace around it). */
|
|
131
|
+
function labelled(reply: string, label: string): [string, string][] {
|
|
132
|
+
const re = new RegExp(
|
|
133
|
+
`^[ \\t]*(?:[-*][ \\t]*)?${label}:[ \\t]*(\\S.*?)(?:[ \\t]+(?:—|–|-)[ \\t]+(.*?))?[ \\t]*$`,
|
|
134
|
+
"gm",
|
|
135
|
+
);
|
|
136
|
+
return [...reply.matchAll(re)].map((m) => [m[1] as string, m[2] ?? ""]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Every `EXTRA: <path> — <why>` line: a file outside the lane the worker had to change. */
|
|
140
|
+
export function parseReplyExtras(reply: string): { path: string; why: string }[] {
|
|
141
|
+
return labelled(reply, "EXTRA").map(([path, why]) => ({ path: path.replace(/`/g, ""), why }));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Every `REJECTED: <finding> — <why>` line of a fix round. */
|
|
145
|
+
export function parseReplyRejections(reply: string): { finding: string; why: string }[] {
|
|
146
|
+
return labelled(reply, "REJECTED").map(([finding, why]) => ({ finding, why }));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The trimmed value of every `DESIGN:` line: a conflict between a plan task and the code. */
|
|
150
|
+
export function parseReplyDesign(reply: string): string[] {
|
|
151
|
+
return [...reply.matchAll(/^[ \t]*(?:[-*][ \t]*)?DESIGN:[ \t]*(\S.*?)[ \t]*$/gm)].map(
|
|
152
|
+
(m) => m[1] as string,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parseReplyStatus } from "./record.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plan 22: a final reply never overwrites the report. `finals` are the messages that ended each of the run's
|
|
5
|
+
* turns, in order (a background command's notification can start another turn after the report); `last` is the
|
|
6
|
+
* reply the CLI kept, the last of them. With more than one, the reply is the longest STATUS-bearing message, the
|
|
7
|
+
* later ones after it under `later:`, and the report's STATUS line last, so the record's STATUS is the report's.
|
|
8
|
+
* Otherwise `last` is the reply, unchanged.
|
|
9
|
+
*/
|
|
10
|
+
export function composeReply(finals: string[], last: string): string {
|
|
11
|
+
if (finals.length < 2) return last;
|
|
12
|
+
let report = -1;
|
|
13
|
+
for (const [i, f] of finals.entries())
|
|
14
|
+
if (
|
|
15
|
+
parseReplyStatus(f).status !== null &&
|
|
16
|
+
(report < 0 || f.trim().length >= finals[report]!.trim().length)
|
|
17
|
+
)
|
|
18
|
+
report = i;
|
|
19
|
+
if (report < 0 || report === finals.length - 1) return last;
|
|
20
|
+
const lines = finals[report]!.trimEnd().split("\n");
|
|
21
|
+
const status = lines.pop() as string;
|
|
22
|
+
const later = finals
|
|
23
|
+
.slice(report + 1)
|
|
24
|
+
.map((f) => f.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
if (!later.length) return last;
|
|
27
|
+
const body = lines.join("\n").trimEnd();
|
|
28
|
+
return [...(body ? [body, ""] : []), "later:", later.join("\n\n"), "", status.trim(), ""].join("\n");
|
|
29
|
+
}
|