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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The lines of a check's output that name a failure, stripped of colour, timings and counters so two runs compare
|
|
5
|
+
* equal: at most 60 lines of at most 300 characters (t3herd `failLines`).
|
|
6
|
+
*/
|
|
7
|
+
export function checkFailLines(out: string): string[] {
|
|
8
|
+
const seen = new Set<string>();
|
|
9
|
+
for (const raw of out.split("\n")) {
|
|
10
|
+
const line = raw
|
|
11
|
+
.replace(ANSI, "")
|
|
12
|
+
.replace(/\s*[[(]\s*\d+(\.\d+)?\s*(ms|s|m)\s*[\])]/g, "")
|
|
13
|
+
.replace(/\b\d+(\.\d+)?(ms|s)\b/g, "")
|
|
14
|
+
.trim();
|
|
15
|
+
const vuln = line.match(/^Vulnerability #\d+: (GO-\d{4}-\d+)/)?.[1];
|
|
16
|
+
if (vuln) seen.add(`vulnerability ${vuln}`);
|
|
17
|
+
else if (/(^|\s)(FAIL|FAILED|✗|✕|×)(\s|:|$)|^\(fail\)|^--- FAIL|^not ok\b|\bpanic:|^error:/.test(line))
|
|
18
|
+
seen.add(line.slice(0, 300));
|
|
19
|
+
if (seen.size >= 60) break;
|
|
20
|
+
}
|
|
21
|
+
return [...seen];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The lines of `fails` the base does not have; all of them when there is no base record. */
|
|
25
|
+
export const newFails = (fails: string[], base: string[] | null): string[] =>
|
|
26
|
+
fails.filter((f) => !base?.includes(f));
|
package/src/domain/failover.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { billingKeyOf, type Catalog,
|
|
1
|
+
import { billingKeyOf, type Catalog, pairOf, rungInfo } from "./catalog.ts";
|
|
2
2
|
import { type BillingMode, compareCost, type Cost, costOf, DEFAULT_BILLING } from "./cost.ts";
|
|
3
3
|
import { type Rung, tryParseRung } from "./ids.ts";
|
|
4
|
-
import { DIFFICULTIES, KINDS } from "./lane.ts";
|
|
5
|
-
import { EFFORT_ORDER } from "./sources.ts";
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* Spec §7.1 and §4.5: a stand-in on the same quota would be out of quota too. The billing key names the
|
|
@@ -16,74 +14,35 @@ export function claudeBilled(rung: string): boolean {
|
|
|
16
14
|
return r !== null && quotaOf(r) === "claude-code";
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
/** A rung's
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
return scoresOf(c, rungInfo(c, rung).canonical)?.values ?? null;
|
|
23
|
-
} catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
17
|
+
/** A rung's intelligence index; null when it is not a rung or not indexed. */
|
|
18
|
+
export const intelligenceOf = (c: Catalog, rung: string): number | null =>
|
|
19
|
+
pairOf(c, rung)?.intelligence ?? null;
|
|
27
20
|
|
|
28
|
-
/**
|
|
29
|
-
export function
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for (const kind of KINDS)
|
|
34
|
-
for (const d of DIFFICULTIES) {
|
|
35
|
-
const bar = Object.entries(c.bars[kind][d]).filter(([, min]) => min !== undefined) as [Dim, number][];
|
|
36
|
-
if (bar.every(([dim, min]) => (v[dim] ?? Number.NEGATIVE_INFINITY) >= min))
|
|
37
|
-
for (const [dim] of bar) used.add(dim);
|
|
38
|
-
}
|
|
39
|
-
return DIMS.filter((d) => used.has(d));
|
|
21
|
+
/** Spec 1.1 §11: `standIn` is a downgrade when the rung is indexed and the stand-in is not, or is less intelligent. */
|
|
22
|
+
export function downgrade(c: Catalog, rung: string, standIn: string): boolean {
|
|
23
|
+
const a = intelligenceOf(c, rung);
|
|
24
|
+
const b = intelligenceOf(c, standIn);
|
|
25
|
+
return a !== null && (b === null || b < a);
|
|
40
26
|
}
|
|
41
27
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export function downgradeDims(c: Catalog, rung: string, standIn: string): Dim[] {
|
|
48
|
-
return downgradeAgainst(valuesOf(c, rung), barDims(c, rung), valuesOf(c, standIn));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** downgradeDims with the rung's values and bar dims computed once, for a pool of stand-ins. */
|
|
52
|
-
function downgradeAgainst(
|
|
53
|
-
a: Partial<Record<Dim, number>> | null,
|
|
54
|
-
dims: Dim[],
|
|
55
|
-
standIn: Partial<Record<Dim, number>> | null,
|
|
56
|
-
): Dim[] {
|
|
57
|
-
if (!a) return [];
|
|
58
|
-
const b = standIn ?? {};
|
|
59
|
-
return dims.filter((d) => (b[d] ?? Number.NEGATIVE_INFINITY) < (a[d] as number));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Spec 1.1 §11 "the ladder goes down": the dims both rungs are scored on where `upper` (the later rung)
|
|
64
|
-
* scores below `lower`, when it scores above it on none of them. Empty when it does not go down.
|
|
65
|
-
*/
|
|
66
|
-
export function ladderDropDims(c: Catalog, lower: string, upper: string): Dim[] {
|
|
67
|
-
const a = valuesOf(c, lower);
|
|
68
|
-
const b = valuesOf(c, upper);
|
|
69
|
-
if (!a || !b) return [];
|
|
70
|
-
const shared = DIMS.filter((d) => a[d] !== undefined && b[d] !== undefined);
|
|
71
|
-
if (shared.some((d) => (b[d] as number) > (a[d] as number))) return [];
|
|
72
|
-
return shared.filter((d) => (b[d] as number) < (a[d] as number));
|
|
28
|
+
/** Spec 1.1 §11 "the ladder goes down": the later rung (`upper`) is less intelligent than `lower`. */
|
|
29
|
+
export function ladderDrop(c: Catalog, lower: string, upper: string): boolean {
|
|
30
|
+
const a = intelligenceOf(c, lower);
|
|
31
|
+
const b = intelligenceOf(c, upper);
|
|
32
|
+
return a !== null && b !== null && b < a;
|
|
73
33
|
}
|
|
74
34
|
|
|
75
35
|
const costFor = (c: Catalog, billing: Partial<Record<string, BillingMode>>, rung: string): Cost => {
|
|
76
36
|
const info = rungInfo(c, rung);
|
|
77
|
-
return costOf(info.family, info.parsed.effort, billing[info.key] ?? DEFAULT_BILLING[info.key]);
|
|
37
|
+
return costOf(info.family, info.pair, info.parsed.effort, billing[info.key] ?? DEFAULT_BILLING[info.key]);
|
|
78
38
|
};
|
|
79
39
|
|
|
80
40
|
/**
|
|
81
41
|
* Spec 1.1 §11: the rungs of `pool` that can stand in for `rung`, best first. A stand-in is on another
|
|
82
|
-
* quota,
|
|
83
|
-
* to), startable by `dispatch` (not a native `claude:` subagent), and
|
|
84
|
-
* Claude-billed stand-ins rank last, then the
|
|
85
|
-
*
|
|
86
|
-
* Ruling 4), then the cheapest.
|
|
42
|
+
* quota, indexed, paid from a plan or subscription under `billing` (a metered one spends money nobody chose
|
|
43
|
+
* to), startable by `dispatch` (not a native `claude:` subagent), and never less intelligent than the rung
|
|
44
|
+
* (a rung with no index of its own keeps every stand-in). Claude-billed stand-ins rank last, then the
|
|
45
|
+
* nearest intelligence above the rung's own, then the cheapest.
|
|
87
46
|
*/
|
|
88
47
|
export function rankStandIns(
|
|
89
48
|
c: Catalog,
|
|
@@ -93,17 +52,15 @@ export function rankStandIns(
|
|
|
93
52
|
): string[] {
|
|
94
53
|
const from = tryParseRung(rung);
|
|
95
54
|
if (!from) return [];
|
|
96
|
-
|
|
97
|
-
const own = valuesOf(c, rung);
|
|
98
|
-
const dims = barDims(c, rung);
|
|
55
|
+
const own = intelligenceOf(c, rung);
|
|
99
56
|
const fits = [...new Set(pool)].filter((x) => {
|
|
100
57
|
const r = tryParseRung(x);
|
|
101
|
-
const
|
|
102
|
-
if (!r || r.backend === "claude" || quotaOf(r) === quotaOf(from) ||
|
|
58
|
+
const i = r ? intelligenceOf(c, x) : null;
|
|
59
|
+
if (!r || r.backend === "claude" || quotaOf(r) === quotaOf(from) || i === null) return false;
|
|
103
60
|
if (costFor(c, billing, x).tier === 1) return false;
|
|
104
|
-
return
|
|
61
|
+
return own === null || i >= own;
|
|
105
62
|
});
|
|
106
|
-
const gap = (x: string) =>
|
|
63
|
+
const gap = (x: string) => (own === null ? 0 : (intelligenceOf(c, x) as number) - own);
|
|
107
64
|
return fits.sort(
|
|
108
65
|
(a, b) =>
|
|
109
66
|
Number(claudeBilled(a)) - Number(claudeBilled(b)) ||
|
|
@@ -113,35 +70,28 @@ export function rankStandIns(
|
|
|
113
70
|
);
|
|
114
71
|
}
|
|
115
72
|
|
|
116
|
-
/** How
|
|
117
|
-
function effortGap(a: string, b: string): number {
|
|
118
|
-
const i = (EFFORT_ORDER as readonly string[]).indexOf(a);
|
|
119
|
-
const j = (EFFORT_ORDER as readonly string[]).indexOf(b);
|
|
120
|
-
if (a === b) return 0;
|
|
121
|
-
return i < 0 || j < 0 ? EFFORT_ORDER.length : Math.abs(i - j);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** How a family's `on` key or a model id's prefix maps to the backend that runs it. */
|
|
73
|
+
/** How a family's `on` key maps to the backend that runs it. */
|
|
125
74
|
const BACKEND_OF_KEY: Record<string, string> = { "opencode-go": "opencode" };
|
|
126
75
|
|
|
127
76
|
/**
|
|
128
|
-
* Every rung
|
|
129
|
-
*
|
|
130
|
-
* each listed model at each effort, and each treat-like whose model is an opencode id (`opencode-go/…`,
|
|
131
|
-
* `opencode/…`), such as the shipped Kimi K3 stand-in. Sorted, without duplicates.
|
|
77
|
+
* Every indexed rung catherd can name: each shipped family's model on each backend at each effort the index
|
|
78
|
+
* (or the backend's listing) gives it, and each listed model at each listed effort. Sorted, without duplicates.
|
|
132
79
|
*/
|
|
133
|
-
export function
|
|
80
|
+
export function catalogPairs(c: Catalog): string[] {
|
|
134
81
|
const out = new Set<string>();
|
|
82
|
+
const add = (backend: string, model: string, efforts: readonly string[]) => {
|
|
83
|
+
for (const e of [...efforts, "default"]) {
|
|
84
|
+
const rung = `${backend}:${model}#${e}`;
|
|
85
|
+
if (pairOf(c, rung)) out.add(rung);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
135
88
|
for (const f of c.families)
|
|
136
89
|
for (const [key, m] of Object.entries(f.on)) {
|
|
137
90
|
if (!m) continue;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
out.add(`${BACKEND_OF_KEY[key] ?? key}:${m.id}#${e}`);
|
|
91
|
+
const backend = BACKEND_OF_KEY[key] ?? key;
|
|
92
|
+
add(backend, m.id, rungInfo(c, `${backend}:${m.id}#default`).efforts);
|
|
141
93
|
}
|
|
142
94
|
for (const [backend, l] of Object.entries(c.listed))
|
|
143
|
-
for (const m of l.models)
|
|
144
|
-
for (const canonical of Object.keys(c.treatLike))
|
|
145
|
-
if (/^opencode(-go)?\//.test(canonical)) out.add(`opencode:${canonical}`);
|
|
95
|
+
for (const m of l.models) add(backend, m.id, m.efforts);
|
|
146
96
|
return [...out].sort();
|
|
147
97
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Finding } from "./run-events.ts";
|
|
2
|
+
|
|
3
|
+
const LINE = /^\s*(?:[-*]\s+|\d+[.)]\s+)?\**(BLOCKER|BUG|NIT)\b\**\s*:?\s*(.*)$/;
|
|
4
|
+
/** What a reviewer writes where it has nothing to report: `BUG: none`, `NIT n/a`, a lone dash. */
|
|
5
|
+
const NOTHING = /^(?:none|n\/a|nil|no findings?|clean|[-—–]+)\.?\**$/i;
|
|
6
|
+
const PATH = /^[`'"(]*((?:\.{0,2}\/)?[\w@~.+-]+(?:\/[\w@~.+-]+)*)(?::\d+(?:[-:]\d+)?)?[`'"),;:]*$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The first `path:line` or `path` token of a finding's first words (a reviewer writes
|
|
10
|
+
* `BLOCKER file:line — problem — fix`), or null: a path has a `/` or an extension.
|
|
11
|
+
*/
|
|
12
|
+
export function fileOf(text: string): string | null {
|
|
13
|
+
for (const tok of text.trim().split(/\s+/).slice(0, 4)) {
|
|
14
|
+
if (/^[—–-]+$/.test(tok)) return null;
|
|
15
|
+
const p = PATH.exec(tok)?.[1];
|
|
16
|
+
if (p && (p.includes("/") || /\.[A-Za-z]\w*$/.test(p))) return p;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A reviewer's reply as findings, in order: its BLOCKER, BUG and NIT lines, as written; "none" lines left out. */
|
|
22
|
+
export function parseFindings(reply: string): Finding[] {
|
|
23
|
+
const out: Finding[] = [];
|
|
24
|
+
for (const line of reply.split("\n")) {
|
|
25
|
+
const m = LINE.exec(line);
|
|
26
|
+
if (!m) continue;
|
|
27
|
+
const rest = (m[2] ?? "").trim();
|
|
28
|
+
if (!rest || NOTHING.test(rest)) continue;
|
|
29
|
+
out.push({ label: m[1] as Finding["label"], text: line.trim(), file: fileOf(rest) });
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The lines a failing verifier lists below its VERDICT line, as BLOCKERs: what the next fix round starts from. */
|
|
35
|
+
export function parseFailLines(reply: string, max = 12): Finding[] {
|
|
36
|
+
const lines = reply.split("\n").map((l) => l.trim());
|
|
37
|
+
const at = lines.findIndex((l) => l !== "");
|
|
38
|
+
return lines
|
|
39
|
+
.slice(at + 1)
|
|
40
|
+
.filter((l) => l !== "" && !/^STATUS:/i.test(l))
|
|
41
|
+
.slice(0, max)
|
|
42
|
+
.map((l) => ({
|
|
43
|
+
label: "BLOCKER" as const,
|
|
44
|
+
text: l.replace(/^(?:[-*]\s+|\d+[.)]\s+)/, ""),
|
|
45
|
+
file: fileOf(l.replace(/^(?:[-*]\s+|\d+[.)]\s+)/, "").replace(/^\**FAIL\**:?\s*/i, "")),
|
|
46
|
+
}));
|
|
47
|
+
}
|
package/src/domain/gate-brief.ts
CHANGED
|
@@ -8,6 +8,7 @@ export const RECHECK_COMMAND_MIN = 10;
|
|
|
8
8
|
/** The rules of the gate, one paragraph each. */
|
|
9
9
|
export const VERIFIER_GATE_RULES = [
|
|
10
10
|
"Run the root gate (the milestone's full check) as one gate item and each per-service acceptance suite as its own item, never as one command: a failure then names its item, and a re-check reruns only that item. Acceptance that builds from HEAD (git archive, a commit's image) is yours: workers do not commit, so they never run it.",
|
|
11
|
+
"Run the milestone's full check with gate_run (or catherd gate run), on the committed HEAD, never by hand: catherd runs it detached under the verifier lock, reruns a failure with new lines once, scans the run's commits for secrets and records the result, and land reads that record. If it answers running, call it again. When it fails and no baseline exists, it says how to record the base's own failures from a checkout of the base.",
|
|
11
12
|
"Stay in the foreground until your verdict: no background watcher, and never end your turn while a command runs.",
|
|
12
13
|
`When the machine, not the work, stops an item (a VPN filter, a Docker client proxy, a dead registry, no DNS), probe it directly, twice, 5 s apart, and if it still fails, reply VERDICT: BLOCKED: environment — <the probe and its output>. That is a blocker for the owner, not a FAIL.`,
|
|
13
14
|
"Last, when you built Docker images, run docker image prune -f, so the gates of later milestones do not fill the disk.",
|
package/src/domain/ids.ts
CHANGED
|
@@ -11,8 +11,8 @@ export function assertId(kind: string, value: string): string {
|
|
|
11
11
|
return value;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/** The backends that run as a process through a `BackendAdapter
|
|
15
|
-
export const ADAPTER_IDS = ["codex", "claude-code", "opencode", "cursor"
|
|
14
|
+
/** The backends that run as a process through a `BackendAdapter`. */
|
|
15
|
+
export const ADAPTER_IDS = ["codex", "claude-code", "opencode", "cursor"] as const;
|
|
16
16
|
|
|
17
17
|
/** `claude` is the native Claude Code subagent path (no process); the rest are adapters. */
|
|
18
18
|
const RUNG_BACKENDS = [...ADAPTER_IDS, "claude"] as const;
|
|
@@ -56,8 +56,19 @@ export function tryParseRung(s: string): Rung | null {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** Reasoning efforts from least to most; a rung's effort that is none of these (`default`) cannot be ranked. */
|
|
60
|
+
export const EFFORT_ORDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"] as const;
|
|
61
|
+
export type EffortWord = (typeof EFFORT_ORDER)[number];
|
|
62
|
+
|
|
63
|
+
/** Index in `EFFORT_ORDER`; -1 when `e` is not an effort word. */
|
|
64
|
+
export const effortRank = (e: string): number => (EFFORT_ORDER as readonly string[]).indexOf(e);
|
|
65
|
+
|
|
59
66
|
export const formatRung = (r: Rung): string => `${r.backend}:${r.model}#${r.effort}`;
|
|
60
67
|
|
|
68
|
+
/** Spec 2.0 decision 12: the daemon runs every role headless, so a native `claude:` rung runs as `claude-code:`. */
|
|
69
|
+
export const headlessRung = (rung: string): string =>
|
|
70
|
+
rung.startsWith("claude:") ? `claude-code:${rung.slice("claude:".length)}` : rung;
|
|
71
|
+
|
|
61
72
|
const B32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
62
73
|
|
|
63
74
|
let lastTime = -1;
|
package/src/domain/jev.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { BLOCKED_REASONS } from "./blocked.ts";
|
|
2
3
|
import { type Difficulty, KINDS, type Kind, parseLaneHeader } from "./lane.ts";
|
|
3
4
|
import { scrubKeyShapes, scrubSecretAssignments } from "./secrets.ts";
|
|
4
5
|
|
|
@@ -29,6 +30,12 @@ const RouteRuleSchema = z.object({
|
|
|
29
30
|
/** exactly two levels each, in order: copy then build, logic then hard */
|
|
30
31
|
trackA: z.array(z.string()).length(2),
|
|
31
32
|
trackB: z.array(z.string()).length(2),
|
|
33
|
+
/** a noul answer at or above this is a yes */
|
|
34
|
+
noulMin: z.number().min(0).max(1),
|
|
35
|
+
/** a yes to any of these lifts the lane to at least logic */
|
|
36
|
+
floorLogic: z.array(z.string()),
|
|
37
|
+
/** a yes to any of these holds the lane at build; the floor wins */
|
|
38
|
+
capBuild: z.array(z.string()),
|
|
32
39
|
});
|
|
33
40
|
export type RouteRule = z.infer<typeof RouteRuleSchema>;
|
|
34
41
|
const VerdictRuleSchema = z.object({ min: z.number().min(0).max(1), fallback: z.string() });
|
|
@@ -38,9 +45,16 @@ export const JevFileSchema = z.looseObject({
|
|
|
38
45
|
schema: z.literal(1),
|
|
39
46
|
model: z.string(),
|
|
40
47
|
sets: z.object({
|
|
41
|
-
"route-v2": z
|
|
48
|
+
"route-v2": z
|
|
49
|
+
.object({ questions: z.record(z.string(), QuestionSchema), rule: RouteRuleSchema })
|
|
50
|
+
.superRefine((set, ctx) => {
|
|
51
|
+
for (const id of [...set.rule.floorLogic, ...set.rule.capBuild])
|
|
52
|
+
if (set.questions[id]?.type !== "noul")
|
|
53
|
+
ctx.addIssue({ code: "custom", message: `${id} is no noul question of route-v2` });
|
|
54
|
+
}),
|
|
42
55
|
finding: z.object({ questions: z.record(z.string(), QuestionSchema), rule: VerdictRuleSchema }),
|
|
43
56
|
"same-defect": z.object({ questions: z.record(z.string(), QuestionSchema), rule: VerdictRuleSchema }),
|
|
57
|
+
stop: z.object({ questions: z.record(z.string(), QuestionSchema), rule: VerdictRuleSchema }),
|
|
44
58
|
}),
|
|
45
59
|
});
|
|
46
60
|
export type JevFile = z.infer<typeof JevFileSchema>;
|
|
@@ -50,6 +64,7 @@ export type SetName = keyof JevFile["sets"];
|
|
|
50
64
|
export const VERDICT_OPTIONS = {
|
|
51
65
|
finding: ["design", "code", "unclear"],
|
|
52
66
|
"same-defect": ["yes", "no"],
|
|
67
|
+
stop: [...BLOCKED_REASONS, "other"],
|
|
53
68
|
} as const;
|
|
54
69
|
|
|
55
70
|
/** JSON with object keys sorted at every depth, so equal data always hashes the same. */
|
|
@@ -199,7 +214,7 @@ export interface RouteJudgement {
|
|
|
199
214
|
* different option counts; the rule reads probabilities instead: the kind at p_max ≥ kindMin, the track
|
|
200
215
|
* when the summed probability of its levels reaches trackMin, and neither in between (the dead band).
|
|
201
216
|
*/
|
|
202
|
-
|
|
217
|
+
function decideRoute(rule: RouteRule, answers: JevAnswers): RouteJudgement {
|
|
203
218
|
const k = answers.kind;
|
|
204
219
|
const d = answers.difficulty;
|
|
205
220
|
const nouls: Record<string, number> = {};
|
|
@@ -252,6 +267,36 @@ export function judgeRoute(rule: RouteRule, answers: JevAnswers): RouteJudgement
|
|
|
252
267
|
};
|
|
253
268
|
}
|
|
254
269
|
|
|
270
|
+
/**
|
|
271
|
+
* `decideRoute`, then Jev's yes/no answers (decision 11): a yes to a `floorLogic` question lifts the lane
|
|
272
|
+
* to logic (hard stays hard), else a yes to a `capBuild` question holds it at build (copy stays copy). The
|
|
273
|
+
* floor wins, and both apply in the dead band. The reason names the answer only when the difficulty moved.
|
|
274
|
+
*/
|
|
275
|
+
export function judgeRoute(rule: RouteRule, answers: JevAnswers): RouteJudgement {
|
|
276
|
+
const j = decideRoute(rule, answers);
|
|
277
|
+
const yes = (ids: string[]) => ids.find((id) => (j.nouls[id] ?? 0) >= rule.noulMin);
|
|
278
|
+
const lift = yes(rule.floorLogic);
|
|
279
|
+
const hold = lift === undefined ? yes(rule.capBuild) : undefined;
|
|
280
|
+
const now: Difficulty | null =
|
|
281
|
+
lift !== undefined
|
|
282
|
+
? j.difficulty === "hard"
|
|
283
|
+
? "hard"
|
|
284
|
+
: "logic"
|
|
285
|
+
: hold !== undefined
|
|
286
|
+
? j.difficulty === "copy"
|
|
287
|
+
? "copy"
|
|
288
|
+
: "build"
|
|
289
|
+
: j.difficulty;
|
|
290
|
+
if (now === j.difficulty) return j;
|
|
291
|
+
const [what, id] = lift !== undefined ? ["floor logic", lift] : ["cap build", hold ?? ""];
|
|
292
|
+
return {
|
|
293
|
+
...j,
|
|
294
|
+
track: now === "logic" || now === "hard" ? "B" : "A",
|
|
295
|
+
difficulty: now,
|
|
296
|
+
rule: `${j.rule}; ${what}: ${id} ${j.nouls[id]}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
255
300
|
export interface Verdict<T extends string> {
|
|
256
301
|
value: T;
|
|
257
302
|
/** the chosen answer's probability, when Jev answered */
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BLOCKED_LINE } from "./blocked.ts";
|
|
3
|
+
|
|
4
|
+
export const JUDGE_KINDS = ["triage", "question", "unexpected", "rules"] as const;
|
|
5
|
+
export type JudgeKind = (typeof JUDGE_KINDS)[number];
|
|
6
|
+
|
|
7
|
+
/** What a judge call is given: its kind, the unit it is about, a context text and the numbered findings. */
|
|
8
|
+
export interface JudgeInput {
|
|
9
|
+
id: string;
|
|
10
|
+
kind: JudgeKind;
|
|
11
|
+
unit: string;
|
|
12
|
+
context: string;
|
|
13
|
+
findings: string[];
|
|
14
|
+
lanes: { id: string; owns: string[] }[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const why = z.string();
|
|
18
|
+
|
|
19
|
+
/** A finished run's rules review proposes at most this many changes to the standing rules (plan 34). */
|
|
20
|
+
export const MAX_RULE_CHANGES = 5;
|
|
21
|
+
export const RuleChangeSchema = z.object({ op: z.enum(["add", "remove"]), text: z.string().min(1), why });
|
|
22
|
+
export type RuleChange = z.infer<typeof RuleChangeSchema>;
|
|
23
|
+
|
|
24
|
+
export const JudgeDecisionSchema = z.discriminatedUnion("verb", [
|
|
25
|
+
z.object({
|
|
26
|
+
verb: z.literal("route"),
|
|
27
|
+
routes: z.array(z.object({ finding: z.number().int().min(1), to: z.string(), why })),
|
|
28
|
+
}),
|
|
29
|
+
z.object({ verb: z.literal("answer"), answer: z.string() }),
|
|
30
|
+
z.object({ verb: z.literal("ask_owner"), question: z.string() }),
|
|
31
|
+
z.object({ verb: z.literal("park"), question: z.string() }),
|
|
32
|
+
z.object({ verb: z.literal("retry"), why }),
|
|
33
|
+
z.object({ verb: z.literal("climb"), why }),
|
|
34
|
+
z.object({ verb: z.literal("skip"), why }),
|
|
35
|
+
z.object({ verb: z.literal("rules"), changes: z.array(RuleChangeSchema) }),
|
|
36
|
+
]);
|
|
37
|
+
export type JudgeDecision = z.infer<typeof JudgeDecisionSchema>;
|
|
38
|
+
|
|
39
|
+
export const judgeName = (id: string): string => `judge-${id}`;
|
|
40
|
+
|
|
41
|
+
/** The judge's reply contract, appended to every judge brief (spec 2.0 decision 3). */
|
|
42
|
+
export const JUDGE_CONTRACT = [
|
|
43
|
+
"Your first line is DECISION: <verb>, one of the verbs your brief lists. Then, by verb:",
|
|
44
|
+
"- route: one line per finding, ROUTE <n> -> <lane id>|architect|dismiss — <why>",
|
|
45
|
+
"- answer: ANSWER: <the answer the worker needs, from the plan and the code>",
|
|
46
|
+
"- ask_owner or park: QUESTION: <one question only the owner can answer>",
|
|
47
|
+
"- retry, climb or skip: WHY: <one line>",
|
|
48
|
+
"- rules: up to five lines, ADD <a one-line rule, with a [role, …] tag when it is not for every role> — <the event it comes from>, or REMOVE <the text of one line of knowledge.md> — <why>; no line when nothing should change",
|
|
49
|
+
`Decide only from what the brief and the run's files show. The last line of your reply is: STATUS: complete — <one line why>. ${BLOCKED_LINE}`,
|
|
50
|
+
].join("\n");
|
|
51
|
+
|
|
52
|
+
/** The verbs each kind of call may answer with. */
|
|
53
|
+
export const JUDGE_VERBS: Record<JudgeKind, readonly JudgeDecision["verb"][]> = {
|
|
54
|
+
triage: ["route"],
|
|
55
|
+
question: ["answer", "ask_owner"],
|
|
56
|
+
unexpected: ["retry", "climb", "skip", "park"],
|
|
57
|
+
rules: ["rules"],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** A line without the emphasis a model puts around it (`**DECISION: retry**`, backticks). */
|
|
61
|
+
const plain = (l: string): string => l.trim().replace(/^[*_`\s]+|[*_`\s]+$/g, "");
|
|
62
|
+
const STATUS = /^STATUS:\s*(complete|partial|blocked|refused|flaky)\b/i;
|
|
63
|
+
const RULE_LINE = /^\s*(?:[-*]|\d+[.)])?\s*\**(ADD|REMOVE)\**:?\s+(.+)$/;
|
|
64
|
+
const ROUTE = /^[-*\s]*ROUTE\s+(\d+)\s*(?:->|→)\s*(\S+)\s*(?:—|–|-|:)?\s*(.*)$/;
|
|
65
|
+
|
|
66
|
+
/** The text after `label:` on its line and the lines under it, up to the STATUS line. */
|
|
67
|
+
function section(lines: string[], label: string): string | null {
|
|
68
|
+
const at = lines.findIndex((l) => new RegExp(`^${label}:`, "i").test(plain(l)));
|
|
69
|
+
if (at < 0) return null;
|
|
70
|
+
const rest = [plain(lines[at] as string).slice(label.length + 1)];
|
|
71
|
+
for (const l of lines.slice(at + 1)) {
|
|
72
|
+
if (STATUS.test(plain(l))) break;
|
|
73
|
+
rest.push(l.trim());
|
|
74
|
+
}
|
|
75
|
+
return rest.join("\n").trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The judge's decision, or null when the reply is not one its kind allows (the daemon asks once more). */
|
|
79
|
+
export function parseJudgeReply(reply: string, kind: JudgeKind): JudgeDecision | null {
|
|
80
|
+
const lines = reply.split("\n");
|
|
81
|
+
const first = lines.map(plain).find(Boolean) ?? "";
|
|
82
|
+
const verb = /^DECISION:\s*(\w+)\s*$/i.exec(first)?.[1]?.toLowerCase();
|
|
83
|
+
if (!verb || !(JUDGE_VERBS[kind] as readonly string[]).includes(verb)) return null;
|
|
84
|
+
const decision = ((): unknown => {
|
|
85
|
+
switch (verb) {
|
|
86
|
+
case "route": {
|
|
87
|
+
const routes = lines.flatMap((l) => {
|
|
88
|
+
const m = ROUTE.exec(plain(l));
|
|
89
|
+
return m ? [{ finding: Number(m[1]), to: m[2] as string, why: (m[3] ?? "").trim() }] : [];
|
|
90
|
+
});
|
|
91
|
+
return routes.length ? { verb, routes } : null;
|
|
92
|
+
}
|
|
93
|
+
case "answer": {
|
|
94
|
+
const answer = section(lines, "ANSWER");
|
|
95
|
+
return answer ? { verb, answer } : null;
|
|
96
|
+
}
|
|
97
|
+
case "ask_owner":
|
|
98
|
+
case "park": {
|
|
99
|
+
const question = section(lines, "QUESTION");
|
|
100
|
+
return question ? { verb, question } : null;
|
|
101
|
+
}
|
|
102
|
+
case "rules": {
|
|
103
|
+
const changes = lines.flatMap((l) => {
|
|
104
|
+
const m = RULE_LINE.exec(l);
|
|
105
|
+
if (!m) return [];
|
|
106
|
+
const rest = (m[2] as string).trim();
|
|
107
|
+
const at = rest.lastIndexOf(" — ");
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
op: (m[1] as string).toLowerCase(),
|
|
111
|
+
text: (at < 0 ? rest : rest.slice(0, at)).trim(),
|
|
112
|
+
why: at < 0 ? "" : rest.slice(at + 3).trim(),
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
});
|
|
116
|
+
return { verb, changes: changes.slice(0, MAX_RULE_CHANGES) };
|
|
117
|
+
}
|
|
118
|
+
default:
|
|
119
|
+
return { verb, why: section(lines, "WHY") ?? "" };
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
const parsed = JudgeDecisionSchema.safeParse(decision);
|
|
123
|
+
return parsed.success ? parsed.data : null;
|
|
124
|
+
}
|
package/src/domain/lane.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CatherdError } from "./errors.ts";
|
|
2
|
+
import { TASK_REF } from "./plan-doc.ts";
|
|
2
3
|
|
|
3
4
|
export const KINDS = ["repo_code", "terminal", "ui", "prose", "research"] as const;
|
|
4
5
|
export type Kind = (typeof KINDS)[number];
|
|
@@ -15,10 +16,12 @@ export interface LaneHeader {
|
|
|
15
16
|
after: string[];
|
|
16
17
|
/** `Allow: path[:line], …` under the check: hits of the lane's absence check that are allowed exceptions */
|
|
17
18
|
allow: string[];
|
|
19
|
+
/** `Tasks: 3, plan.md#4`: the plan tasks the lane implements (catherd inlines them into its worker's brief) */
|
|
20
|
+
tasks: string[];
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
/**
|
|
21
|
-
const LANE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]
|
|
23
|
+
/** An After: entry: a lane id (Mx.Ly, the shape lanes/<id>.md takes) or a whole milestone (Mx). */
|
|
24
|
+
const LANE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*(\.[A-Za-z0-9][A-Za-z0-9._-]*)?$/;
|
|
22
25
|
|
|
23
26
|
/** An Allow: entry: a repo-relative path, optionally `:<line>`. */
|
|
24
27
|
const ALLOW_ENTRY = /^([^\s:]+)(?::(\d+))?$/;
|
|
@@ -89,13 +92,29 @@ export function parseLaneHeader(text: string): LaneHeader {
|
|
|
89
92
|
difficulty: oneOf(field(text, "difficulty"), DIFFICULTIES),
|
|
90
93
|
after: list("after"),
|
|
91
94
|
allow: list("allow"),
|
|
95
|
+
tasks: list("tasks"),
|
|
92
96
|
};
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
/** The milestones, other than `m`, that `m`'s lanes wait on (After: entries, lane or milestone), in numeric order. */
|
|
100
|
+
export function milestoneAfter(lanes: { id: string; after: string[] }[], m: string): string[] {
|
|
101
|
+
const out = new Set<string>();
|
|
102
|
+
for (const l of lanes)
|
|
103
|
+
if (l.id.split(".")[0] === m)
|
|
104
|
+
for (const a of l.after) {
|
|
105
|
+
const dep = a.split(".")[0] as string;
|
|
106
|
+
if (dep !== m) out.add(dep);
|
|
107
|
+
}
|
|
108
|
+
return [...out].sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
/** What is wrong with the After: and Allow: entries, one line each; none when they are well formed. */
|
|
96
112
|
function orderProblems(h: LaneHeader): string[] {
|
|
97
113
|
const out: string[] = [];
|
|
98
|
-
for (const a of h.after)
|
|
114
|
+
for (const a of h.after)
|
|
115
|
+
if (!LANE_ID.test(a)) out.push(`After "${a}" is not a lane or milestone id like M1.L1 or M1`);
|
|
116
|
+
for (const t of h.tasks)
|
|
117
|
+
if (!TASK_REF.test(t)) out.push(`Tasks "${t}" is not a task number like 3 or plan.md#3`);
|
|
99
118
|
for (const a of h.allow) {
|
|
100
119
|
const m = ALLOW_ENTRY.exec(a);
|
|
101
120
|
try {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan 35 D6: how many milestones of a base run may work side by side. A new one starts when none is working,
|
|
3
|
+
* else when the memory the machine has free, less a reserve and less what the ramping milestones are still to take,
|
|
4
|
+
* stays above a floor. The three megabyte numbers are the run profile's `limits.memory`.
|
|
5
|
+
*/
|
|
6
|
+
export const MEMORY_LIMITS = {
|
|
7
|
+
reserveMb: 2048,
|
|
8
|
+
perMilestoneMb: 3072,
|
|
9
|
+
floorMb: 3072,
|
|
10
|
+
/** a milestone counts at `perMilestoneMb` for this long after its tree is ready: its lanes have not reached their memory yet */
|
|
11
|
+
rampMs: 300_000,
|
|
12
|
+
recheckMs: 120_000,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type MemoryLimits = {
|
|
16
|
+
reserveMb: number;
|
|
17
|
+
perMilestoneMb: number;
|
|
18
|
+
floorMb: number;
|
|
19
|
+
rampMs: number;
|
|
20
|
+
recheckMs: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** The profile's own memory limits over the defaults. */
|
|
24
|
+
export function memoryLimitsOf(p: {
|
|
25
|
+
limits?: { memory?: Partial<Pick<MemoryLimits, "reserveMb" | "perMilestoneMb" | "floorMb">> };
|
|
26
|
+
}): MemoryLimits {
|
|
27
|
+
return { ...MEMORY_LIMITS, ...p.limits?.memory };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Milestones working (a tree, not landed, merged or parked) and those of them still ramping up. */
|
|
31
|
+
export function memoryLoad(
|
|
32
|
+
ms: { tree: string | null; status: string; treeAt: string | null }[],
|
|
33
|
+
nowMs: number,
|
|
34
|
+
l: MemoryLimits = MEMORY_LIMITS,
|
|
35
|
+
): { working: number; ramping: number } {
|
|
36
|
+
const working = ms.filter((m) => m.tree !== null && !["landed", "merged", "parked"].includes(m.status));
|
|
37
|
+
return {
|
|
38
|
+
working: working.length,
|
|
39
|
+
ramping: working.filter((m) => m.treeAt !== null && Date.parse(m.treeAt) > nowMs - l.rampMs).length,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The memory a deferral waits for: the reserve, what the ramping milestones will take, and the floor. */
|
|
44
|
+
export const neededMb = (ramping: number, l: MemoryLimits = MEMORY_LIMITS): number =>
|
|
45
|
+
l.reserveMb + ramping * l.perMilestoneMb + l.floorMb;
|
|
46
|
+
|
|
47
|
+
/** Whether one more milestone may start: always when none is working, so a run always moves. */
|
|
48
|
+
export function mayStartMilestone(
|
|
49
|
+
i: { availableMb: number; working: number; ramping: number },
|
|
50
|
+
l: MemoryLimits = MEMORY_LIMITS,
|
|
51
|
+
): boolean {
|
|
52
|
+
return i.working === 0 || i.availableMb - l.reserveMb - i.ramping * l.perMilestoneMb >= l.floorMb;
|
|
53
|
+
}
|