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
|
@@ -2,15 +2,20 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CatherdError, errorMessage } from "../domain/errors.ts";
|
|
4
4
|
import { assertLaneHeader, LANE_HEADER_FIX, onlyAllowedHits, parseLaneHeader } from "../domain/lane.ts";
|
|
5
|
+
import { coverageProblems } from "../domain/plan-doc.ts";
|
|
6
|
+
import { lintLanes } from "../domain/plan-lint.ts";
|
|
5
7
|
import { checkEnv } from "../infra/env.ts";
|
|
6
8
|
import { heavySlots, withHeavySlotWithin } from "../infra/heavy-lock.ts";
|
|
7
9
|
import { loginEnv } from "../infra/login-env.ts";
|
|
8
10
|
import { killGroup } from "../infra/proc.ts";
|
|
9
11
|
import { readGateEnv, resolveGateEnv } from "./gate-env.ts";
|
|
10
12
|
import { landedMilestones } from "./milestones.ts";
|
|
13
|
+
import { planDocs } from "./plan-input.ts";
|
|
11
14
|
import type { Deps } from "./ports.ts";
|
|
15
|
+
import { isDaemonRun, replayRun } from "./run-events-store.ts";
|
|
12
16
|
import { TAIL_LINES } from "./run-debug.ts";
|
|
13
17
|
import { findRun, type Run, runPaths } from "./run-store.ts";
|
|
18
|
+
import { treeOf } from "./tree-path.ts";
|
|
14
19
|
|
|
15
20
|
/** Spec §4.7. Only `cannot-start` blocks the run; `lock-busy` (plan 23) means: run preflight again. */
|
|
16
21
|
export type PreflightOutcome = "pass" | "fails-as-expected" | "skipped" | "cannot-start" | "lock-busy";
|
|
@@ -25,7 +30,12 @@ interface PreflightResult {
|
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export type PreflightReport =
|
|
28
|
-
| {
|
|
33
|
+
| {
|
|
34
|
+
needsConfirmation: true;
|
|
35
|
+
commands: { lane: string; check: string | null }[];
|
|
36
|
+
/** plan 34: the plan lint's warnings */
|
|
37
|
+
lint: string[];
|
|
38
|
+
}
|
|
29
39
|
| {
|
|
30
40
|
needsConfirmation: false;
|
|
31
41
|
commands: { lane: string; check: string | null }[];
|
|
@@ -33,6 +43,8 @@ export type PreflightReport =
|
|
|
33
43
|
blocked: boolean;
|
|
34
44
|
/** plan 23: lanes whose fast check runs no linter while the repo has one */
|
|
35
45
|
warnings: string[];
|
|
46
|
+
/** plan 34: the plan lint's warnings */
|
|
47
|
+
lint: string[];
|
|
36
48
|
};
|
|
37
49
|
|
|
38
50
|
const CHECK_TIMEOUT_MS = 120_000;
|
|
@@ -57,6 +69,10 @@ interface LaneCheck {
|
|
|
57
69
|
problem: string | null;
|
|
58
70
|
/** spec 1.1 §6: why its Kind: or Difficulty: line is refused */
|
|
59
71
|
invalid: string | null;
|
|
72
|
+
/** plan 32: the plan tasks it names on its `Tasks:` line */
|
|
73
|
+
tasks: string[];
|
|
74
|
+
/** plan 34: the lane file's text; null when it cannot be read */
|
|
75
|
+
text: string | null;
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
function laneChecks(run: Run): LaneCheck[] {
|
|
@@ -83,9 +99,20 @@ function laneChecks(run: Run): LaneCheck[] {
|
|
|
83
99
|
allow: h.allow,
|
|
84
100
|
problem: h.fastCheck ? null : `lanes/${f} has no Fast check: line`,
|
|
85
101
|
invalid,
|
|
102
|
+
tasks: h.tasks,
|
|
103
|
+
text,
|
|
86
104
|
};
|
|
87
105
|
} catch (e) {
|
|
88
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
lane,
|
|
108
|
+
check: null,
|
|
109
|
+
owns: [],
|
|
110
|
+
allow: [],
|
|
111
|
+
problem: (e as Error).message,
|
|
112
|
+
invalid,
|
|
113
|
+
tasks: [],
|
|
114
|
+
text: null,
|
|
115
|
+
};
|
|
89
116
|
}
|
|
90
117
|
});
|
|
91
118
|
}
|
|
@@ -280,8 +307,43 @@ export async function preflight(
|
|
|
280
307
|
const invalid = lanes.flatMap((l) => (l.invalid ? [l.invalid] : []));
|
|
281
308
|
if (invalid.length) throw new CatherdError("E_LANE_INVALID", invalid.join("; "), { fix: LANE_HEADER_FIX });
|
|
282
309
|
const commands = lanes.map((l) => ({ lane: l.lane, check: l.check }));
|
|
283
|
-
|
|
310
|
+
const { warnings: lint, stuck } = lintLanes(
|
|
311
|
+
lanes.flatMap((l) => (l.text === null ? [] : [{ id: l.lane, text: l.text }])),
|
|
312
|
+
laneChecks(run).map((l) => l.lane),
|
|
313
|
+
// a base run's milestone starts after the ones it names are merged, not only landed (plan 35)
|
|
314
|
+
run.meta.base
|
|
315
|
+
? replayRun(run)
|
|
316
|
+
.milestones.filter((m) => m.status === "merged")
|
|
317
|
+
.map((m) => m.id)
|
|
318
|
+
: [...landed],
|
|
319
|
+
);
|
|
320
|
+
// plan 32: a daemon run starts no lane the plan does not back, before the confirm list and before any check
|
|
321
|
+
if (isDaemonRun(run)) {
|
|
322
|
+
const problems = coverageProblems(
|
|
323
|
+
planDocs(run),
|
|
324
|
+
lanes.map((l) => ({ id: l.lane, tasks: l.tasks })),
|
|
325
|
+
);
|
|
326
|
+
if (problems.length)
|
|
327
|
+
return {
|
|
328
|
+
needsConfirmation: false,
|
|
329
|
+
commands,
|
|
330
|
+
results: problems.map((p) => ({
|
|
331
|
+
lane: p.lane || (i.milestone ?? "plan"),
|
|
332
|
+
check: null,
|
|
333
|
+
outcome: "cannot-start" as const,
|
|
334
|
+
exitCode: null,
|
|
335
|
+
tail: [],
|
|
336
|
+
note: p.note,
|
|
337
|
+
})),
|
|
338
|
+
blocked: true,
|
|
339
|
+
warnings: [],
|
|
340
|
+
lint,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (profile.preflight.confirm && !i.confirmed) return { needsConfirmation: true, commands, lint };
|
|
284
344
|
const timeoutMs = i.timeoutMs ?? CHECK_TIMEOUT_MS;
|
|
345
|
+
// plan 35: a base run's milestone is checked in its own worktree
|
|
346
|
+
const cwd = treeOf(run, i.milestone ?? null);
|
|
285
347
|
const results: PreflightResult[] = [];
|
|
286
348
|
// the login env is captured once per server; the gate env is read once per preflight
|
|
287
349
|
let env: Record<string, string> | undefined;
|
|
@@ -302,9 +364,7 @@ export async function preflight(
|
|
|
302
364
|
results.push({ lane: l.lane, check, outcome: "skipped", exitCode: null, tail: [], note: AS_ROOT });
|
|
303
365
|
continue;
|
|
304
366
|
}
|
|
305
|
-
const unborn = l.owns.filter(
|
|
306
|
-
(p) => check.includes(p.replace(/\/$/, "")) && !existsSync(join(run.meta.repo, p)),
|
|
307
|
-
);
|
|
367
|
+
const unborn = l.owns.filter((p) => check.includes(p.replace(/\/$/, "")) && !existsSync(join(cwd, p)));
|
|
308
368
|
if (unborn.length) {
|
|
309
369
|
const note = `${unborn.join(", ")} does not exist yet; the lane creates it`;
|
|
310
370
|
results.push({ lane: l.lane, check, outcome: "skipped", exitCode: null, tail: [], note });
|
|
@@ -313,8 +373,11 @@ export async function preflight(
|
|
|
313
373
|
env ??= await checkEnvFor(run.meta.repo);
|
|
314
374
|
const checkEnvironment = env;
|
|
315
375
|
// plan 23: each check waits for its slot within its own budget, and says lock-busy past it
|
|
316
|
-
const slot = await withHeavySlotWithin(
|
|
317
|
-
|
|
376
|
+
const slot = await withHeavySlotWithin(
|
|
377
|
+
heavySlots(profile.heavy),
|
|
378
|
+
preflightLimits.lockWaitMs,
|
|
379
|
+
(_slot, tmp) => runCheck(cwd, check, timeoutMs, { ...checkEnvironment, TMPDIR: tmp, GOTMPDIR: tmp }),
|
|
380
|
+
{ tmpBase: process.env.TMPDIR },
|
|
318
381
|
);
|
|
319
382
|
if (slot.busy) {
|
|
320
383
|
const note = `every heavy slot stayed busy for ${preflightLimits.lockWaitMs / 1000} s (other lanes' checks); run preflight again when they finish`;
|
|
@@ -335,6 +398,16 @@ export async function preflight(
|
|
|
335
398
|
note: allowed ? `every hit is an Allow: exception (${r.lines.length})` : noteOf(r, outcome, timeoutMs),
|
|
336
399
|
});
|
|
337
400
|
}
|
|
401
|
+
// plan 34: an After: that can never finish stops the milestone, so the daemon parks it instead of waiting forever
|
|
402
|
+
for (const s of stuck)
|
|
403
|
+
results.push({
|
|
404
|
+
lane: s.lane,
|
|
405
|
+
check: null,
|
|
406
|
+
outcome: "cannot-start",
|
|
407
|
+
exitCode: null,
|
|
408
|
+
tail: [],
|
|
409
|
+
note: s.note,
|
|
410
|
+
});
|
|
338
411
|
// plan 23: lint reaches the worker only through its fast check
|
|
339
412
|
const linter = repoLinter(run.meta.repo);
|
|
340
413
|
const warnings = linter
|
|
@@ -351,5 +424,6 @@ export async function preflight(
|
|
|
351
424
|
results,
|
|
352
425
|
blocked: results.some((r) => r.outcome === "cannot-start"),
|
|
353
426
|
warnings,
|
|
427
|
+
lint,
|
|
354
428
|
};
|
|
355
429
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HostContext, OrchestrationHost } from "../domain/host.ts";
|
|
2
2
|
import { agentFiles } from "../domain/agents.ts";
|
|
3
|
+
import { type MemoryLimits, memoryLimitsOf } from "../domain/milestone-memory.ts";
|
|
3
4
|
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -78,7 +79,7 @@ function saveAndLink(name: string, doc: ProfileDoc, host: OrchestrationHost, rep
|
|
|
78
79
|
|
|
79
80
|
const validate = (doc: ProfileDoc, name: string, host: OrchestrationHost): Validation => {
|
|
80
81
|
try {
|
|
81
|
-
return validateHere(resolveProfile(doc, name, host), loadCatalog(
|
|
82
|
+
return validateHere(resolveProfile(doc, name, host), loadCatalog(), doc, host);
|
|
82
83
|
} catch (e) {
|
|
83
84
|
if (!(e instanceof CatherdError)) throw e;
|
|
84
85
|
return { errors: [{ path: "roles", message: e.message, fix: e.fix }], warnings: [] };
|
|
@@ -105,7 +106,7 @@ function syncFor(
|
|
|
105
106
|
return allowPrune ? pruneProfileAgents(name) : { linked: [], pruned: [], newSessionNeededFor: [] };
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
/** What a save returns: the port's ProfileSaved (one type for the CLI, the
|
|
109
|
+
/** What a save returns: the port's ProfileSaved (one type for the CLI, the panel and the MCP tools). */
|
|
109
110
|
export type Saved = ProfileSaved;
|
|
110
111
|
|
|
111
112
|
const unsaved = (v: Validation): Saved => ({
|
|
@@ -121,7 +122,7 @@ const unsaved = (v: Validation): Saved => ({
|
|
|
121
122
|
export const CHANGED_ON_DISK = "profile";
|
|
122
123
|
|
|
123
124
|
/**
|
|
124
|
-
* Spec §7.3 `patch` (profile_set, `catherd profile set`, the
|
|
125
|
+
* Spec §7.3 `patch` (profile_set, `catherd profile set`, the panel's save): validates first and writes
|
|
125
126
|
* nothing when invalid, unless the save repairs the profile (spec 1.2 §6.2): it removes at least one error
|
|
126
127
|
* the stored profile had and adds none. Then it saves, and the result's `errors` are those still open. A
|
|
127
128
|
* profile that does not exist yet starts from the default profile. With `expect` (the profile as a preview
|
|
@@ -301,14 +302,23 @@ function viewOf(p: Profile): ProfileView {
|
|
|
301
302
|
jev: p.jev,
|
|
302
303
|
isolated: Object.fromEntries(Object.entries(p.harness).map(([k, h]) => [k, h.isolated])),
|
|
303
304
|
failover: p.failover,
|
|
305
|
+
maxEffort: p.maxEffort,
|
|
306
|
+
bars: p.bars,
|
|
304
307
|
budget: p.budget,
|
|
305
308
|
timeouts: p.timeouts,
|
|
306
309
|
preflight: p.preflight,
|
|
307
310
|
heavy: p.lock.heavy,
|
|
308
311
|
notify: p.notify,
|
|
312
|
+
limits: { memory: pickMemory(memoryLimitsOf(p)) },
|
|
309
313
|
};
|
|
310
314
|
}
|
|
311
315
|
|
|
316
|
+
const pickMemory = ({ reserveMb, perMilestoneMb, floorMb }: MemoryLimits) => ({
|
|
317
|
+
reserveMb,
|
|
318
|
+
perMilestoneMb,
|
|
319
|
+
floorMb,
|
|
320
|
+
});
|
|
321
|
+
|
|
312
322
|
/** The ProfilePort the run engine and the MCP tools use (spec §7.3: the single writer). */
|
|
313
323
|
export function profileService(host: () => HostContext): ProfilePort {
|
|
314
324
|
// without a name, each tool acts on the profile `repo` runs on (the active one outside a repo)
|
|
@@ -341,6 +351,23 @@ export function profileService(host: () => HostContext): ProfilePort {
|
|
|
341
351
|
set: (name, patch, repo = null) => patchProfile(name ?? activeName(repo), patch, { host: host().host }),
|
|
342
352
|
agentFor: (repo, role, rung) =>
|
|
343
353
|
parseRung(rung).backend === "claude" ? agentName(activeName(repo), role, rung) : null,
|
|
354
|
+
notifyUrl: () =>
|
|
355
|
+
locked(() => {
|
|
356
|
+
const config = readConfig();
|
|
357
|
+
if (config.notify?.url) return config.notify.url;
|
|
358
|
+
const url = `https://ntfy.sh/catherd-${Buffer.from(crypto.getRandomValues(new Uint8Array(10))).toString("hex")}`;
|
|
359
|
+
writeJsonAtomic(configFile(), { ...config, schema: 1, notify: { ...config.notify, url } });
|
|
360
|
+
return url;
|
|
361
|
+
}),
|
|
362
|
+
setNotifyUrl: (url) =>
|
|
363
|
+
locked(() => {
|
|
364
|
+
if (!/^https?:\/\//.test(url) || !URL.canParse(url))
|
|
365
|
+
throw new CatherdError("E_INPUT_INVALID", `"${url}" is not an http(s) url`, {
|
|
366
|
+
fix: "give the ntfy topic's url, like https://ntfy.sh/<topic>",
|
|
367
|
+
});
|
|
368
|
+
const config = readConfig();
|
|
369
|
+
writeJsonAtomic(configFile(), { ...config, schema: 1, notify: { ...config.notify, url } });
|
|
370
|
+
}),
|
|
344
371
|
};
|
|
345
372
|
}
|
|
346
373
|
|
|
@@ -39,7 +39,12 @@ export const projectsFile = (): string => join(configDir(), "projects.json");
|
|
|
39
39
|
export const agentsRoot = (): string => join(configDir(), "agents");
|
|
40
40
|
export const profileFile = (name: string): string => join(profilesDir(), `${name}.json`);
|
|
41
41
|
|
|
42
|
-
const ConfigSchema = z.looseObject({
|
|
42
|
+
const ConfigSchema = z.looseObject({
|
|
43
|
+
schema: z.literal(1),
|
|
44
|
+
activeProfile: z.string().optional(),
|
|
45
|
+
/** where the phone messages go (an ntfy topic), made at the first run */
|
|
46
|
+
notify: z.looseObject({ url: z.string().optional() }).optional(),
|
|
47
|
+
});
|
|
43
48
|
const ProjectsSchema = z.looseObject({
|
|
44
49
|
schema: z.literal(1),
|
|
45
50
|
bindings: z.record(z.string(), z.string()).default({}),
|
|
@@ -149,7 +154,7 @@ export function isolationKeyErrors(
|
|
|
149
154
|
|
|
150
155
|
/**
|
|
151
156
|
* Spec 1.3 §9 Q2: each enabled role whose rung, or a failover stand-in of one, runs natively on a backend that
|
|
152
|
-
* holds the role's access only when isolated (
|
|
157
|
+
* holds the role's access only when isolated (one with no read-only flag).
|
|
153
158
|
*/
|
|
154
159
|
export function isolatedOnlyErrors(p: Profile): Issue[] {
|
|
155
160
|
const out: Issue[] = [];
|
|
@@ -243,7 +248,7 @@ export function validateNamed(
|
|
|
243
248
|
): Validation {
|
|
244
249
|
const n = name ?? activeName(repo);
|
|
245
250
|
const doc = readProfileDoc(n);
|
|
246
|
-
const catalog = loadCatalog(
|
|
251
|
+
const catalog = loadCatalog(repo === null ? {} : { repo });
|
|
247
252
|
try {
|
|
248
253
|
return validateHere(resolveProfile(doc, n, host), catalog, doc, host, o);
|
|
249
254
|
} catch (e) {
|
package/src/services/protocol.ts
CHANGED
|
@@ -50,6 +50,10 @@ function laneIds(run: Run): string[] {
|
|
|
50
50
|
.sort(byNumber);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Plan 28: the milestones the run's lane files name, in number order. */
|
|
54
|
+
export const runMilestones = (run: Run): string[] =>
|
|
55
|
+
[...new Set(laneIds(run).map(milestoneOf))].sort(byNumber);
|
|
56
|
+
|
|
53
57
|
/** What laneDone reads of the run, read once for every lane of the milestone. */
|
|
54
58
|
interface LaneEvidence {
|
|
55
59
|
routes: RouteRow[];
|
|
@@ -1,81 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { barOf, BAR_PERCENTILE } from "../domain/bars.ts";
|
|
2
|
+
import { type Catalog, type Pair, rungInfo } from "../domain/catalog.ts";
|
|
2
3
|
import { type BillingMode, type Cost, costOf, DEFAULT_BILLING } from "../domain/cost.ts";
|
|
3
4
|
import type { Difficulty, Kind } from "../domain/lane.ts";
|
|
4
5
|
import { type EvidenceTable, evidenceLine, evidenceOf } from "./run-evidence.ts";
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
-
export interface ValueUsed {
|
|
8
|
-
dim: Dim;
|
|
9
|
-
value: number;
|
|
10
|
-
confidence: Confidence;
|
|
11
|
-
/** the source a synced value came from; `shipped` for a hand-typed one, `override` for the user's */
|
|
12
|
-
source: string;
|
|
13
|
-
benchmark: string;
|
|
14
|
-
date: string;
|
|
15
|
-
url: string;
|
|
16
|
-
/** catherd's guess for this rung: lent by a treat-like or an inferred stand-in (spec 1.2 §6.1) */
|
|
17
|
-
inferred: boolean;
|
|
18
|
-
/** the rung the value belongs to, when it is not this one */
|
|
19
|
-
from: string | null;
|
|
20
|
-
/** what lent it: a treat-like (a mapping, `like X`) or an inferred stand-in (a guess); null for its own */
|
|
21
|
-
lent: "treat-like" | "stand-in" | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Spec 1.2 §5.3: one threshold of the lane's bar, the value used against it, and whether it clears. */
|
|
25
|
-
export interface Threshold {
|
|
26
|
-
dim: Dim;
|
|
27
|
-
min: number;
|
|
28
|
-
used: ValueUsed | null;
|
|
29
|
-
clears: boolean;
|
|
30
|
-
/** where the default threshold came from (`barsWhy`) */
|
|
31
|
-
why: string | null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Spec 1.2 §5.3, §8: what `route` reports about the rung it chose. */
|
|
7
|
+
/** What `route` reports about the rung it chose: the one number it ranked on, the bar, and what it costs. */
|
|
35
8
|
export interface Provenance {
|
|
36
9
|
rung: string;
|
|
37
10
|
canonical: string;
|
|
38
|
-
/** the
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
11
|
+
/** the rung's Slopalytics intelligence index; null when it is not indexed */
|
|
12
|
+
intelligence: number | null;
|
|
13
|
+
/** the difficulty's bar, with the percentile it sits at; null when the route read no bar (the default rung) */
|
|
14
|
+
bar: { difficulty: Difficulty; percentile: number; min: number; clears: boolean } | null;
|
|
15
|
+
price: Pair["price"];
|
|
16
|
+
/** tokens per second; null when Slopalytics has none */
|
|
17
|
+
speed: number | null;
|
|
44
18
|
cost: Cost;
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*/
|
|
19
|
+
/** when the proxy fetched the index the route read; null with no index */
|
|
20
|
+
indexedAt: string | null;
|
|
21
|
+
/** spec 1.2 §8: its runs for the lane's kind (or every kind) and over every kind; null when they could not be read */
|
|
49
22
|
evidence: { kind: string | null; all: string | null } | null;
|
|
50
23
|
}
|
|
51
24
|
|
|
52
|
-
/**
|
|
53
|
-
export function valuesUsed(c: Catalog, canonical: string): ValueUsed[] {
|
|
54
|
-
const s = scoresOf(c, canonical);
|
|
55
|
-
if (!s) return [];
|
|
56
|
-
return DIMS.flatMap((dim) => {
|
|
57
|
-
const r = s.records[dim];
|
|
58
|
-
if (!r) return [];
|
|
59
|
-
const borrowed = s.borrowed.includes(dim);
|
|
60
|
-
const from = borrowed ? s.via : (s.standIns[dim] ?? null);
|
|
61
|
-
return [
|
|
62
|
-
{
|
|
63
|
-
dim,
|
|
64
|
-
value: r.value,
|
|
65
|
-
confidence: r.confidence,
|
|
66
|
-
source: r.source ?? "shipped",
|
|
67
|
-
benchmark: `${r.benchmark} ${r.version}`,
|
|
68
|
-
date: r.date,
|
|
69
|
-
url: r.url,
|
|
70
|
-
inferred: from !== null,
|
|
71
|
-
from,
|
|
72
|
-
lent: from === null ? null : borrowed ? "treat-like" : "stand-in",
|
|
73
|
-
},
|
|
74
|
-
];
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Spec 1.2 §5.3: the chosen rung's thresholds for the lane's kind and difficulty, its values and facts. */
|
|
25
|
+
/** The chosen rung's intelligence, the difficulty's bar and whether it clears it, its price, speed and cost. */
|
|
79
26
|
export function provenanceOf(
|
|
80
27
|
c: Catalog,
|
|
81
28
|
rung: string,
|
|
@@ -83,34 +30,28 @@ export function provenanceOf(
|
|
|
83
30
|
difficulty: Difficulty | null,
|
|
84
31
|
billing: Partial<Record<string, BillingMode>>,
|
|
85
32
|
evidence: EvidenceTable | null,
|
|
33
|
+
at: Record<Difficulty, number> = BAR_PERCENTILE,
|
|
86
34
|
): Provenance {
|
|
87
35
|
const info = rungInfo(c, rung);
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const thresholds: Threshold[] = DIMS.flatMap((dim) => {
|
|
91
|
-
const min = bar[dim];
|
|
92
|
-
if (min === undefined || !kind || !difficulty) return [];
|
|
93
|
-
const used = values.find((v) => v.dim === dim) ?? null;
|
|
94
|
-
return [
|
|
95
|
-
{
|
|
96
|
-
dim,
|
|
97
|
-
min,
|
|
98
|
-
used,
|
|
99
|
-
clears: used !== null && used.value >= min,
|
|
100
|
-
// a threshold the user's override set is theirs, whatever the default's why says (1.2 minor)
|
|
101
|
-
why: c.userBars?.includes(`${kind}/${difficulty}/${dim}`)
|
|
102
|
-
? "your override"
|
|
103
|
-
: (c.barsWhy[dim]?.[difficulty] ?? null),
|
|
104
|
-
},
|
|
105
|
-
];
|
|
106
|
-
});
|
|
36
|
+
const intelligence = info.pair?.intelligence ?? null;
|
|
37
|
+
const min = difficulty ? barOf(c, difficulty, at) : null;
|
|
107
38
|
return {
|
|
108
39
|
rung,
|
|
109
40
|
canonical: info.canonical,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
41
|
+
intelligence,
|
|
42
|
+
bar:
|
|
43
|
+
difficulty && min !== null
|
|
44
|
+
? {
|
|
45
|
+
difficulty,
|
|
46
|
+
percentile: at[difficulty],
|
|
47
|
+
min,
|
|
48
|
+
clears: intelligence !== null && intelligence >= min,
|
|
49
|
+
}
|
|
50
|
+
: null,
|
|
51
|
+
price: info.pair?.price ?? null,
|
|
52
|
+
speed: info.pair?.speed ?? null,
|
|
53
|
+
cost: costOf(info.family, info.pair, info.parsed.effort, billing[info.key] ?? DEFAULT_BILLING[info.key]),
|
|
54
|
+
indexedAt: c.indexedAt,
|
|
114
55
|
evidence: evidence && {
|
|
115
56
|
kind: evidenceLine(evidenceOf(evidence, info.canonical, kind)),
|
|
116
57
|
all: evidenceLine(evidenceOf(evidence, info.canonical)),
|
|
@@ -29,6 +29,15 @@ const rows = (run: Run): QuestionRow[] =>
|
|
|
29
29
|
(r) => typeof r?.milestone === "string" && typeof r.text === "string",
|
|
30
30
|
);
|
|
31
31
|
|
|
32
|
+
/** Plan 28: how many questions the run parked on and how many were answered. */
|
|
33
|
+
export const questionCounts = (run: Run): { questions: number; answers: number } => {
|
|
34
|
+
const all = rows(run);
|
|
35
|
+
return {
|
|
36
|
+
questions: all.filter((r) => r.kind === "question").length,
|
|
37
|
+
answers: all.filter((r) => r.kind === "answer").length,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
32
41
|
/** The questions not answered yet, oldest first; a milestone answered after its question is no longer open. */
|
|
33
42
|
export function openQuestions(run: Run): OpenQuestion[] {
|
|
34
43
|
const open = new Map<string, OpenQuestion>();
|
|
@@ -3,10 +3,12 @@ import { errorMessage } from "../domain/errors.ts";
|
|
|
3
3
|
import { dispatchPaths } from "../infra/dispatch-dir.ts";
|
|
4
4
|
import { log } from "../infra/log.ts";
|
|
5
5
|
import { writeJsonAtomic } from "../infra/store.ts";
|
|
6
|
+
import { ensureDaemon } from "./daemon.ts";
|
|
6
7
|
import { recoverOwned, settle, stallPoll, unsettledLimits, watching } from "./dispatch-service.ts";
|
|
7
8
|
import { type Dispatch, listDispatches, pendingDispatches } from "./dispatches.ts";
|
|
8
9
|
import { finalizeDispatch, waitForFinish } from "./finalize.ts";
|
|
9
10
|
import type { Deps } from "./ports.ts";
|
|
11
|
+
import { isDaemonRun } from "./run-events-store.ts";
|
|
10
12
|
import { listRuns, type Run } from "./run-store.ts";
|
|
11
13
|
import { ownsRun } from "./sessions.ts";
|
|
12
14
|
|
|
@@ -100,6 +102,15 @@ export async function reconcileAll(deps: Deps): Promise<ReconcileReport> {
|
|
|
100
102
|
if (!report.warnings.includes(w)) report.warnings.push(w);
|
|
101
103
|
};
|
|
102
104
|
for (const run of runs) {
|
|
105
|
+
// a 2.0 run is its daemon's: an unfinished one gets its daemon back, and nothing here settles its roles
|
|
106
|
+
if (isDaemonRun(run)) {
|
|
107
|
+
try {
|
|
108
|
+
ensureDaemon(run);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
warn(run, e);
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
103
114
|
let pending;
|
|
104
115
|
try {
|
|
105
116
|
const scrubbed = scrubOldSpecs(run);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertNativeHost } from "./backends.ts";
|
|
2
|
+
import { percentilesOf } from "../domain/bars.ts";
|
|
2
3
|
import { BUDGET_CHEAP_AT } from "../domain/budget.ts";
|
|
3
4
|
import { errorMessage } from "../domain/errors.ts";
|
|
4
5
|
import {
|
|
@@ -21,10 +22,11 @@ import {
|
|
|
21
22
|
type Pick,
|
|
22
23
|
quotaUsage,
|
|
23
24
|
type RoutingProfile,
|
|
25
|
+
rolesWithoutRung,
|
|
24
26
|
select,
|
|
25
27
|
} from "../domain/select.ts";
|
|
26
28
|
import { log } from "../infra/log.ts";
|
|
27
|
-
import { catalogQuery, freshenDiscovery, loadCatalog } from "./catalog-service.ts";
|
|
29
|
+
import { catalogQuery, freshenDiscovery, loadCatalog, startIndex } from "./catalog-service.ts";
|
|
28
30
|
import { type Asked, askJev, type JevOpts, jevQuestions, logJev } from "./jev-service.ts";
|
|
29
31
|
import type { ProfileView, RouteAnswer, RouteRequest, RoutingPort, Verdict } from "./ports.ts";
|
|
30
32
|
import { provenanceOf } from "./provenance.ts";
|
|
@@ -42,6 +44,8 @@ function routingProfile(
|
|
|
42
44
|
// spec §4.6: from 80 % of the budget on, start at the cheapest rung that clears the bar
|
|
43
45
|
objective: spentFraction >= BUDGET_CHEAP_AT ? "cost" : v.objective,
|
|
44
46
|
billing: v.billing,
|
|
47
|
+
maxEffort: v.maxEffort ?? {},
|
|
48
|
+
bars: { ...v.bars, ...rc?.bars },
|
|
45
49
|
role: {
|
|
46
50
|
enabled: rc?.enabled ?? false,
|
|
47
51
|
rungs: rc?.rungs ?? [],
|
|
@@ -142,7 +146,7 @@ interface Prepared {
|
|
|
142
146
|
async function prepare(req: RouteRequest, o: RoutingOpts): Promise<Prepared> {
|
|
143
147
|
const p = routingProfile(req.profile, req.role, req.spentFraction, req.usage);
|
|
144
148
|
await freshenWithin(p.role.rungs, req.repo, o.discoveryBudgetMs ?? DISCOVERY_BUDGET_MS);
|
|
145
|
-
return { p, c: loadCatalog({ repo: req.repo }) };
|
|
149
|
+
return { p, c: loadCatalog({ repo: req.repo, runDir: req.runDir }) };
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
/** Jev's answer about a lane; null when the route reads no lane, empty when it asks nothing (Jev off, one usable rung). */
|
|
@@ -172,7 +176,15 @@ function decide(req: RouteRequest, { p, c }: Prepared, j: Judged | null): RouteA
|
|
|
172
176
|
const fallback = () => defaultLadder(c, p, req.role);
|
|
173
177
|
const finish = (out: RouteAnswer): RouteAnswer => {
|
|
174
178
|
out.why = whyOf(out, req.laneText !== null);
|
|
175
|
-
out.provenance = provenanceOf(
|
|
179
|
+
out.provenance = provenanceOf(
|
|
180
|
+
c,
|
|
181
|
+
out.rung,
|
|
182
|
+
out.kind,
|
|
183
|
+
out.difficulty,
|
|
184
|
+
p.billing,
|
|
185
|
+
evidenceOrNone(c),
|
|
186
|
+
percentilesOf(p),
|
|
187
|
+
);
|
|
176
188
|
return out;
|
|
177
189
|
};
|
|
178
190
|
if (req.laneText === null || j === null)
|
|
@@ -184,21 +196,14 @@ function decide(req: RouteRequest, { p, c }: Prepared, j: Judged | null): RouteA
|
|
|
184
196
|
: null;
|
|
185
197
|
let out: RouteAnswer;
|
|
186
198
|
if (lane.kind && lane.difficulty) {
|
|
187
|
-
out = answer(
|
|
188
|
-
select(c, p, req.role, lane.kind, lane.difficulty),
|
|
189
|
-
"lane",
|
|
190
|
-
lane.kind,
|
|
191
|
-
lane.difficulty,
|
|
192
|
-
asked,
|
|
193
|
-
jev,
|
|
194
|
-
);
|
|
199
|
+
out = answer(select(c, p, req.role, lane.difficulty), "lane", lane.kind, lane.difficulty, asked, jev);
|
|
195
200
|
} else if (judged?.track && judged.difficulty) {
|
|
196
201
|
const kind = lane.kind ?? judged.kind ?? "repo_code";
|
|
197
|
-
out = answer(select(c, p, req.role,
|
|
202
|
+
out = answer(select(c, p, req.role, judged.difficulty), "jev", kind, judged.difficulty, asked, jev);
|
|
198
203
|
} else if (judged?.kind || (judged && lane.kind)) {
|
|
199
204
|
const kind = (lane.kind ?? judged.kind) as Kind;
|
|
200
|
-
const difficulty = lane.difficulty ?? defaultDifficulty(c, p, req.role
|
|
201
|
-
out = answer(select(c, p, req.role,
|
|
205
|
+
const difficulty = lane.difficulty ?? defaultDifficulty(c, p, req.role);
|
|
206
|
+
out = answer(select(c, p, req.role, difficulty), "jev-kind", kind, difficulty, asked, jev);
|
|
202
207
|
} else {
|
|
203
208
|
out = answer(fallback(), "default", null, null, asked, jev);
|
|
204
209
|
}
|
|
@@ -248,7 +253,7 @@ async function routeMany(reqs: RouteRequest[], o: RoutingOpts): Promise<RouteAns
|
|
|
248
253
|
|
|
249
254
|
async function verdict<T extends string>(
|
|
250
255
|
runDir: string,
|
|
251
|
-
set: Extract<SetName, "finding" | "same-defect">,
|
|
256
|
+
set: Extract<SetName, "finding" | "same-defect" | "stop">,
|
|
252
257
|
state: Record<string, unknown>,
|
|
253
258
|
options: readonly T[],
|
|
254
259
|
use: "auto" | "off",
|
|
@@ -303,6 +308,12 @@ export function routingService(o: RoutingOpts = {}): RoutingPort {
|
|
|
303
308
|
use,
|
|
304
309
|
o,
|
|
305
310
|
),
|
|
311
|
+
stopCause: (runDir, status, reply, use) =>
|
|
312
|
+
verdict(runDir, "stop", { status, reply: scrubFree(reply.slice(-4000)) }, VERDICT_OPTIONS.stop, use, o),
|
|
313
|
+
checkStart: async (view, repo) => {
|
|
314
|
+
const { catalog, index } = await startIndex(repo);
|
|
315
|
+
return { unusable: rolesWithoutRung(catalog, view), index };
|
|
316
|
+
},
|
|
306
317
|
catalog: (filter, billing) => catalogQuery(filter, billing),
|
|
307
318
|
};
|
|
308
319
|
}
|