catherd-cli 1.4.0 → 2.0.0-beta.20261022113309
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -169
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -239
- package/package.json +6 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +97 -356
- package/plugin/skills/catherd-setup/SKILL.md +21 -20
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +19 -4
- package/src/adapters/claude-code/index.ts +126 -14
- package/src/adapters/codex/events.ts +10 -0
- package/src/adapters/codex/index.ts +17 -5
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +10 -4
- package/src/adapters/opencode/index.ts +38 -1
- package/src/cli.ts +27 -27
- package/src/domain/agents.ts +3 -1
- package/src/domain/bars.ts +33 -107
- package/src/domain/blocked.ts +72 -0
- package/src/domain/branch.ts +51 -0
- package/src/domain/brief.ts +106 -0
- package/src/domain/catalog.ts +69 -313
- package/src/domain/changes.ts +23 -1
- package/src/domain/cost.ts +19 -29
- package/src/domain/errors.ts +6 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -75
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +53 -0
- package/src/domain/hints.ts +13 -2
- package/src/domain/ids.ts +13 -2
- package/src/domain/jev.ts +47 -2
- package/src/domain/judge.ts +124 -0
- package/src/domain/lane.ts +118 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +32 -8
- package/src/domain/notify.ts +32 -0
- package/src/domain/plan-doc.ts +198 -0
- package/src/domain/plan-lint.ts +141 -0
- package/src/domain/profile-rules.ts +175 -138
- package/src/domain/profile.ts +140 -63
- package/src/domain/record.ts +68 -3
- package/src/domain/reply.ts +29 -0
- package/src/domain/role-prompts.ts +182 -56
- package/src/domain/role-scope.ts +76 -0
- package/src/domain/role-tools.ts +34 -0
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +90 -0
- package/src/domain/rules.ts +33 -0
- package/src/domain/run-events.ts +298 -0
- package/src/domain/run-machine.ts +668 -0
- package/src/domain/run-state.ts +1310 -0
- package/src/domain/scorecard.ts +197 -0
- package/src/domain/select.ts +229 -72
- package/src/domain/slopalytics.ts +72 -0
- package/src/domain/state.ts +5 -0
- package/src/domain/util.ts +22 -0
- package/src/domain/workspace.ts +130 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +24 -165
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +18 -2
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/deps.ts +2 -0
- package/src/entry/doctor-command.ts +20 -2
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/host-arg.ts +28 -0
- package/src/entry/init-command.ts +20 -69
- package/src/entry/knowledge-command.ts +121 -5
- package/src/entry/lock-command.ts +103 -8
- package/src/entry/mcp/handshake.ts +6 -2
- package/src/entry/mcp/role-bin.ts +20 -0
- package/src/entry/mcp/role-server.ts +134 -0
- package/src/entry/mcp/server.ts +85 -21
- package/src/entry/mcp/session-tools.ts +149 -0
- package/src/entry/panel/openapi.ts +189 -0
- package/src/entry/panel/server.ts +410 -0
- package/src/entry/panel-command.ts +68 -0
- package/src/entry/pause-command.ts +67 -0
- package/src/entry/profile-command.ts +33 -18
- package/src/entry/role-cli-command.ts +159 -0
- package/src/entry/runs-command.ts +81 -27
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/entry/supervise-command.ts +30 -3
- package/src/entry/workspace-command.ts +44 -0
- package/src/infra/boot-lock.ts +25 -0
- package/src/infra/delivery.ts +3 -0
- package/src/infra/dispatch-dir.ts +2 -0
- package/src/infra/env.ts +11 -3
- package/src/infra/git.ts +70 -0
- package/src/infra/heavy-lock.ts +133 -6
- package/src/infra/host-probe.ts +16 -0
- package/src/infra/launch.ts +63 -7
- package/src/infra/lock-activity.ts +88 -0
- package/src/infra/login-env.ts +48 -0
- package/src/infra/memory.ts +45 -0
- package/src/infra/ntfy.ts +40 -0
- package/src/infra/open-url.ts +42 -0
- package/src/infra/paths.ts +37 -2
- package/src/infra/role-mcp.ts +69 -0
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/infra/supervisor.ts +107 -16
- package/src/services/admission.ts +133 -31
- package/src/services/backends.ts +4 -6
- package/src/services/budget.ts +26 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -254
- package/src/services/credentials.ts +15 -26
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +209 -17
- package/src/services/dispatches.ts +30 -2
- package/src/services/doctor-access.ts +8 -3
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor-docker.ts +221 -0
- package/src/services/doctor-role-mcp.ts +65 -0
- package/src/services/doctor.ts +72 -15
- package/src/services/end-push.ts +157 -0
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +24 -2
- package/src/services/gate-env.ts +217 -0
- package/src/services/gate-service.ts +231 -45
- package/src/services/global-install.ts +46 -13
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-edit.ts +154 -0
- package/src/services/lane-service.ts +291 -55
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +167 -29
- package/src/services/notifier.ts +124 -4
- package/src/services/notify-service.ts +26 -0
- package/src/services/orchestrator-wait.ts +82 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/pause.ts +161 -0
- package/src/services/peek.ts +79 -17
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +60 -5
- package/src/services/preflight.ts +232 -24
- package/src/services/profile-service.ts +31 -3
- package/src/services/profile-store.ts +18 -6
- package/src/services/protocol.ts +123 -25
- package/src/services/provenance.ts +34 -89
- package/src/services/questions.ts +24 -0
- package/src/services/reconcile.ts +33 -1
- package/src/services/reentry.ts +7 -6
- package/src/services/role-access.ts +27 -0
- package/src/services/routing-service.ts +161 -42
- package/src/services/run-briefs.ts +477 -0
- package/src/services/run-client.ts +217 -0
- package/src/services/run-effects.ts +837 -0
- package/src/services/run-events-store.ts +174 -0
- package/src/services/run-evidence.ts +1 -1
- package/src/services/run-pin.ts +125 -0
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +204 -23
- package/src/services/run-store.ts +159 -18
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/scratch.ts +60 -0
- package/src/services/sessions.ts +3 -1
- package/src/services/state.ts +21 -0
- package/src/services/summary.ts +76 -12
- package/src/services/tree-path.ts +27 -0
- package/src/services/verifier-brief.ts +23 -0
- package/src/services/verifier-step.ts +48 -0
- package/src/services/workspace-admission.ts +259 -0
- package/src/services/workspace-service.ts +259 -0
- package/src/services/workspace-store.ts +95 -0
- package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js +1 -0
- package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
- package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js +1 -0
- package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-B6GiwDkh.js +11 -0
- package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
- package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
- package/web/dist/assets/index-C5IbylvM.css +2 -0
- package/web/dist/assets/index-C5IbylvM.css.br +0 -0
- package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
- package/web/dist/assets/label-DFSYBc62.js +1 -0
- package/web/dist/assets/label-DFSYBc62.js.br +0 -0
- package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
- package/web/dist/assets/link-D8EcKZUt.js +1 -0
- package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
- package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
- package/web/dist/assets/login-DaN5HPPW.js +1 -0
- package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
- package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
- package/web/dist/assets/query-Cu--ccT3.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
- package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
- package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
- package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
- package/web/dist/assets/table-BOxXhqS4.js +1 -0
- package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
- package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
- package/web/dist/assets/time--9kSQbbp.js +1 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
- package/web/dist/index.html +15 -0
- package/catalog/ATTRIBUTION.md +0 -36
- package/catalog/scores.json +0 -3202
- package/catalog/sources.json +0 -97
- package/src/adapters/antigravity/events.ts +0 -131
- package/src/adapters/antigravity/home.ts +0 -46
- package/src/adapters/antigravity/index.ts +0 -320
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -132
- package/src/adapters/grok/index.ts +0 -339
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -125
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -84
- package/src/entry/mcp/protocol-tools.ts +0 -57
- package/src/entry/mcp/run-tools.ts +0 -148
- package/src/entry/mcp/setup-tools.ts +0 -108
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -381
- package/src/entry/tui/fixtures.ts +0 -637
- package/src/entry/tui/profile-edits.ts +0 -146
- package/src/entry/tui/profile-tree.ts +0 -429
- package/src/entry/tui/providers/app.tsx +0 -162
- package/src/entry/tui/providers/boundary.tsx +0 -57
- package/src/entry/tui/providers/data.tsx +0 -160
- package/src/entry/tui/providers/keymap.tsx +0 -166
- package/src/entry/tui/providers/root.tsx +0 -45
- package/src/entry/tui/providers/theme.tsx +0 -17
- package/src/entry/tui/providers/toast.tsx +0 -77
- package/src/entry/tui/run.tsx +0 -114
- package/src/entry/tui/state.ts +0 -373
- package/src/entry/tui/stories.ts +0 -114
- package/src/entry/tui/text.ts +0 -130
- package/src/entry/tui/theme.ts +0 -140
- package/src/entry/tui/views/app.tsx +0 -208
- package/src/entry/tui/views/dialogs.tsx +0 -41
- package/src/entry/tui/views/profile-actions.ts +0 -331
- package/src/entry/tui/views/profiles.tsx +0 -520
- package/src/entry/tui/views/runs.tsx +0 -573
- package/src/entry/tui/views/save-dialog.tsx +0 -340
- package/src/entry/tui/views/status.tsx +0 -209
- package/src/entry/tui/widgets/chrome.tsx +0 -114
- package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
- package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
- package/src/entry/tui/widgets/dialog-select.tsx +0 -212
- package/src/entry/tui/widgets/dialog.tsx +0 -68
- package/src/entry/tui/widgets/line.tsx +0 -92
- package/src/entry/tui/widgets/list.tsx +0 -197
- package/src/entry/tui/widgets/spinner.tsx +0 -49
- package/src/entry/tui/widgets/toast.tsx +0 -62
- package/src/infra/sources/arena.ts +0 -87
- package/src/infra/sources/artificial-analysis.ts +0 -112
- package/src/infra/sources/cache.ts +0 -124
- package/src/infra/sources/csv.ts +0 -38
- package/src/infra/sources/epoch.ts +0 -82
- package/src/infra/sources/http.ts +0 -75
- package/src/infra/sources/litellm.ts +0 -58
- package/src/infra/sources/models-dev.ts +0 -59
- package/src/infra/sources/openrouter-endpoints.ts +0 -52
- package/src/infra/sources/openrouter-models.ts +0 -60
- package/src/infra/sources/rows.ts +0 -38
- package/src/infra/sources/vectara.ts +0 -69
- package/src/infra/sources/zip.ts +0 -48
- package/src/services/catalog-refresh.ts +0 -201
- package/src/services/doctor-sources.ts +0 -76
- package/src/services/runs-page.ts +0 -255
- package/src/services/source-derive.ts +0 -374
- package/src/services/source-sync.ts +0 -305
- package/src/services/standins.ts +0 -204
- package/src/services/treat-likes.ts +0 -97
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync } from "node:fs";
|
|
2
2
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { CatherdError } from "../domain/errors.ts";
|
|
5
|
+
import { runSlug } from "../domain/branch.ts";
|
|
5
6
|
import { assertId } from "../domain/ids.ts";
|
|
6
7
|
import { type RunRecord, RunRecordSchema } from "../domain/record.ts";
|
|
7
|
-
import type {
|
|
8
|
-
|
|
8
|
+
import type {
|
|
9
|
+
FailoverRouteRow,
|
|
10
|
+
OutcomeRouteRow,
|
|
11
|
+
OutcomeRow,
|
|
12
|
+
RoleRouteRow,
|
|
13
|
+
RouteRow,
|
|
14
|
+
} from "../domain/route.ts";
|
|
15
|
+
import { cell } from "../domain/util.ts";
|
|
9
16
|
import { withFileLock } from "../infra/filelock.ts";
|
|
10
|
-
import {
|
|
17
|
+
import { gitOrigin } from "../infra/git.ts";
|
|
18
|
+
import { dataDir, originDir, repoDir, runsDir } from "../infra/paths.ts";
|
|
11
19
|
import {
|
|
12
20
|
appendJsonl,
|
|
13
21
|
ensureJsonlHeader,
|
|
14
22
|
ensurePrivateDir,
|
|
15
23
|
PRIVATE_DIR,
|
|
24
|
+
PRIVATE_FILE,
|
|
16
25
|
appendPrivate,
|
|
26
|
+
nonBlankLines,
|
|
17
27
|
readJsonl,
|
|
18
28
|
readVersioned,
|
|
19
29
|
writeJsonAtomic,
|
|
@@ -38,6 +48,11 @@ const RunMetaSchema = z.looseObject({
|
|
|
38
48
|
createdAt: z.string(),
|
|
39
49
|
catherdVersion: z.string(),
|
|
40
50
|
startedBy: StartedBySchema.nullable().optional(),
|
|
51
|
+
workspace: z.strictObject({ id: z.string(), step: z.string() }).optional(),
|
|
52
|
+
/** the repo's HEAD when run_start ran: where the first milestone's commit range starts (absent before 1.5) */
|
|
53
|
+
startHead: z.string().optional(),
|
|
54
|
+
/** plan 35: the branch on origin each milestone's own branch starts from; absent for a run in the repo itself */
|
|
55
|
+
base: z.string().optional(),
|
|
41
56
|
});
|
|
42
57
|
type RunMeta = z.infer<typeof RunMetaSchema>;
|
|
43
58
|
|
|
@@ -66,6 +81,8 @@ export function runPaths(dir: string) {
|
|
|
66
81
|
sessions: join(dir, "sessions.jsonl"),
|
|
67
82
|
roles: join(dir, "roles"),
|
|
68
83
|
shots: join(dir, "shots"),
|
|
84
|
+
/** per dispatch name, the role's TMPDIR (spec 1.5 plan 21): `scratch/<name>/` */
|
|
85
|
+
scratch: join(dir, "scratch"),
|
|
69
86
|
/** the admission lock's target: `admission.lock` guards dispatch admission */
|
|
70
87
|
admission: join(dir, "admission"),
|
|
71
88
|
};
|
|
@@ -73,7 +90,6 @@ export function runPaths(dir: string) {
|
|
|
73
90
|
|
|
74
91
|
export const LEDGER_HEADER = "milestone | what | commit | minutes | evidence";
|
|
75
92
|
|
|
76
|
-
const runSlug = (title: string) => slug(title).slice(0, 40) || "run";
|
|
77
93
|
const stamp = (d: Date) => d.toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
|
|
78
94
|
|
|
79
95
|
/** Creates a run folder under the repo's runs dir. `repo` is a git toplevel; meta.json is written last. */
|
|
@@ -83,6 +99,11 @@ export function createRun(o: {
|
|
|
83
99
|
aLines: string[];
|
|
84
100
|
version: string;
|
|
85
101
|
now?: Date;
|
|
102
|
+
workspace?: { id: string; step: string };
|
|
103
|
+
/** the repo's full HEAD at the start, when it has a commit */
|
|
104
|
+
startHead?: string | null;
|
|
105
|
+
/** plan 35: the branch on origin each milestone's branch starts from */
|
|
106
|
+
base?: string;
|
|
86
107
|
startedBy?: {
|
|
87
108
|
host?: StartedBy["host"];
|
|
88
109
|
sessionId: string;
|
|
@@ -90,6 +111,9 @@ export function createRun(o: {
|
|
|
90
111
|
name: string | null;
|
|
91
112
|
} | null;
|
|
92
113
|
}): Run {
|
|
114
|
+
const workspace = o.workspace
|
|
115
|
+
? { id: assertId("workspace", o.workspace.id), step: assertId("workspace step", o.workspace.step) }
|
|
116
|
+
: undefined;
|
|
93
117
|
const now = o.now ?? new Date();
|
|
94
118
|
const root = runsDir(o.repo);
|
|
95
119
|
ensurePrivateDir(root);
|
|
@@ -130,6 +154,9 @@ export function createRun(o: {
|
|
|
130
154
|
createdAt: now.toISOString(),
|
|
131
155
|
catherdVersion: o.version,
|
|
132
156
|
...(o.startedBy ? { startedBy: StartedBySchema.parse(o.startedBy) } : {}),
|
|
157
|
+
...(workspace ? { workspace } : {}),
|
|
158
|
+
...(o.startHead ? { startHead: o.startHead } : {}),
|
|
159
|
+
...(o.base ? { base: o.base } : {}),
|
|
133
160
|
};
|
|
134
161
|
writeJsonAtomic(p.meta, meta);
|
|
135
162
|
return { id, dir, meta };
|
|
@@ -141,14 +168,18 @@ export interface RunListing {
|
|
|
141
168
|
corrupt: { id: string; dir: string; reason: string }[];
|
|
142
169
|
}
|
|
143
170
|
|
|
144
|
-
/** Every run
|
|
145
|
-
export function listRuns(): RunListing {
|
|
171
|
+
/** Every run, newest first; an explicit repo scope avoids reading unrelated histories. */
|
|
172
|
+
export function listRuns(repos?: readonly string[]): RunListing {
|
|
146
173
|
const root = join(dataDir(), "repos");
|
|
147
174
|
const out: RunListing = { runs: [], corrupt: [] };
|
|
148
175
|
if (!existsSync(root)) return out;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
176
|
+
const directories = repos
|
|
177
|
+
? [...new Set(repos.map(runsDir))]
|
|
178
|
+
: readdirSync(root, { withFileTypes: true })
|
|
179
|
+
.filter((repo) => repo.isDirectory())
|
|
180
|
+
.map((repo) => join(root, repo.name, "runs"));
|
|
181
|
+
for (const runs of directories) {
|
|
182
|
+
if (!existsSync(runs)) continue;
|
|
152
183
|
for (const e of readdirSync(runs, { withFileTypes: true })) {
|
|
153
184
|
if (!e.isDirectory()) continue;
|
|
154
185
|
const dir = join(runs, e.name);
|
|
@@ -220,13 +251,39 @@ export function appendRecord(run: Run, r: RunRecord): Promise<RunRecord> {
|
|
|
220
251
|
});
|
|
221
252
|
}
|
|
222
253
|
|
|
254
|
+
/** The run's lane rows (routes and climbs); a role's rows outside a lane are left out (`readRoleRoutes`). */
|
|
223
255
|
export function readRoutes(run: Run): RouteRow[] {
|
|
224
256
|
return readJsonl<RouteRow>(runPaths(run.dir).routes).rows.filter(
|
|
225
|
-
(r) =>
|
|
257
|
+
(r) =>
|
|
258
|
+
typeof r?.lane === "string" &&
|
|
259
|
+
(r.source === "route" || r.source === "climb") &&
|
|
260
|
+
Array.isArray(r.ladder) &&
|
|
261
|
+
typeof r.rung === "string",
|
|
226
262
|
);
|
|
227
263
|
}
|
|
228
264
|
|
|
229
|
-
|
|
265
|
+
/** Spec 1.5 plan 24: the lanes' outcome rows of routes.jsonl, oldest first (the last per lane wins). */
|
|
266
|
+
export function readOutcomeRoutes(run: Run): OutcomeRouteRow[] {
|
|
267
|
+
return readJsonl<OutcomeRouteRow>(runPaths(run.dir).routes).rows.filter(
|
|
268
|
+
(r) => typeof r?.lane === "string" && r.source === "outcome",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Spec 1.5 plan 24: the run's role decisions outside a lane, oldest first. */
|
|
273
|
+
export function readRoleRoutes(run: Run): RoleRouteRow[] {
|
|
274
|
+
return readJsonl<RoleRouteRow>(runPaths(run.dir).routes).rows.filter(
|
|
275
|
+
(r) =>
|
|
276
|
+
r?.lane === null &&
|
|
277
|
+
(r.source === "route" || r.source === "dispatch") &&
|
|
278
|
+
typeof r.role === "string" &&
|
|
279
|
+
typeof r.rung === "string",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function appendRoute(
|
|
284
|
+
run: Run,
|
|
285
|
+
row: RouteRow | RoleRouteRow | OutcomeRouteRow | FailoverRouteRow,
|
|
286
|
+
): void {
|
|
230
287
|
const file = runPaths(run.dir).routes;
|
|
231
288
|
ensureJsonlHeader(file, "routes");
|
|
232
289
|
appendJsonl(file, row);
|
|
@@ -255,13 +312,22 @@ const AgentRunSchema = z.looseObject({
|
|
|
255
312
|
status: z.enum(["ok", "failed", "cancelled"]),
|
|
256
313
|
/** the lane the subagent worked, so catalog timings count it under that lane's kind; rows before it lack it */
|
|
257
314
|
lane: z.string().nullable().optional(),
|
|
315
|
+
/** plan 23: a native verifier's verdict line (VERDICT: BLOCKED: environment — <probe>); rows before it lack it */
|
|
316
|
+
verdict: z.string().optional(),
|
|
317
|
+
/** plan 23: the STATUS word of the subagent's reply (a reviewer's partial is no review); rows before it lack it */
|
|
318
|
+
replyStatus: z.enum(["complete", "partial", "blocked", "refused", "flaky"]).optional(),
|
|
258
319
|
});
|
|
259
320
|
export type AgentRun = z.infer<typeof AgentRunSchema>;
|
|
260
321
|
|
|
261
322
|
/** Native Claude subagent runs, as the orchestrator reported them through record_agent_run. */
|
|
262
|
-
export function readAgentRuns(run: Run): AgentRun[] {
|
|
263
|
-
|
|
323
|
+
export function readAgentRuns(run: Run, strict = false): AgentRun[] {
|
|
324
|
+
const { rows, corrupt } = readJsonl<unknown>(runPaths(run.dir).agents);
|
|
325
|
+
if (strict && corrupt)
|
|
326
|
+
throw new CatherdError("E_RUN_CORRUPT", `run ${run.id} has unreadable native cost records`);
|
|
327
|
+
return rows.flatMap((row) => {
|
|
264
328
|
const r = AgentRunSchema.safeParse(row);
|
|
329
|
+
if (strict && !r.success)
|
|
330
|
+
throw new CatherdError("E_RUN_CORRUPT", `run ${run.id} has invalid native cost records`);
|
|
265
331
|
return r.success ? [r.data] : [];
|
|
266
332
|
});
|
|
267
333
|
}
|
|
@@ -276,20 +342,95 @@ export function appendLedger(run: Run, row: string): void {
|
|
|
276
342
|
appendPrivate(runPaths(run.dir).ledger, `${row}\n`);
|
|
277
343
|
}
|
|
278
344
|
|
|
279
|
-
/** Spec §4.7: what past runs of a repo learned, keyed by its git toplevel. */
|
|
345
|
+
/** Spec §4.7: what past runs of a repo learned, keyed by its git toplevel (before 1.5, and with no origin). */
|
|
280
346
|
export const knowledgeFile = (toplevel: string): string => join(repoDir(toplevel), "knowledge.md");
|
|
281
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Spec 1.5 "Knowledge keyed by git origin": the knowledge.md every worktree and clone of one repository
|
|
350
|
+
* shares, keyed by its `origin` remote, else by its toplevel. A toplevel-keyed file this worktree left before
|
|
351
|
+
* is migrated on read: its lines not there yet are appended, and it is renamed `knowledge.md.migrated`.
|
|
352
|
+
*/
|
|
353
|
+
export async function knowledgeFor(toplevel: string): Promise<string> {
|
|
354
|
+
const origin = await gitOrigin(toplevel);
|
|
355
|
+
const legacy = knowledgeFile(toplevel);
|
|
356
|
+
if (!origin) return legacy;
|
|
357
|
+
const file = join(originDir(origin), "knowledge.md");
|
|
358
|
+
if (!existsSync(legacy)) return file;
|
|
359
|
+
ensurePrivateDir(dirname(file));
|
|
360
|
+
await withFileLock(file, () => {
|
|
361
|
+
if (!existsSync(legacy)) return;
|
|
362
|
+
const have = new Set(existsSync(file) ? nonBlankLines(file) : []);
|
|
363
|
+
const add = nonBlankLines(legacy).filter((l) => !have.has(l));
|
|
364
|
+
if (add.length) appendPrivate(file, `${add.join("\n")}\n`);
|
|
365
|
+
renameSync(legacy, `${legacy}.migrated`);
|
|
366
|
+
});
|
|
367
|
+
return file;
|
|
368
|
+
}
|
|
369
|
+
|
|
282
370
|
/**
|
|
283
371
|
* Appends one line to the repo's knowledge.md, `- <date> <source>: <text>` with `text` on one line, and returns
|
|
284
|
-
* it.
|
|
372
|
+
* it. `land`'s `learned` and `catherd knowledge add` both come through here; `removeKnowledgeLine` is the other writer.
|
|
285
373
|
*/
|
|
286
|
-
export function appendKnowledge(
|
|
374
|
+
export async function appendKnowledge(
|
|
375
|
+
toplevel: string,
|
|
376
|
+
at: Date,
|
|
377
|
+
source: string,
|
|
378
|
+
text: string,
|
|
379
|
+
): Promise<string> {
|
|
287
380
|
const line = `- ${at.toISOString().slice(0, 10)} ${source}: ${cell(text)}`;
|
|
288
|
-
|
|
381
|
+
const file = await knowledgeFor(toplevel);
|
|
382
|
+
ensurePrivateDir(dirname(file));
|
|
383
|
+
await withFileLock(file, () => appendPrivate(file, `${line}\n`));
|
|
289
384
|
return line;
|
|
290
385
|
}
|
|
291
386
|
|
|
387
|
+
/** Removes the one line of the repo's knowledge.md that contains `match`, and returns it. */
|
|
388
|
+
export async function removeKnowledgeLine(toplevel: string, match: string): Promise<string> {
|
|
389
|
+
const file = await knowledgeFor(toplevel);
|
|
390
|
+
const needle = match.trim();
|
|
391
|
+
ensurePrivateDir(dirname(file));
|
|
392
|
+
return withFileLock(file, () => {
|
|
393
|
+
const lines = existsSync(file) ? nonBlankLines(file) : [];
|
|
394
|
+
const hits = needle ? lines.filter((l) => l.includes(needle)) : [];
|
|
395
|
+
if (hits.length === 0)
|
|
396
|
+
throw new CatherdError("E_INPUT_INVALID", `no line of knowledge.md contains "${match}"`, {
|
|
397
|
+
fix: "catherd knowledge show",
|
|
398
|
+
});
|
|
399
|
+
if (hits.length > 1)
|
|
400
|
+
throw new CatherdError(
|
|
401
|
+
"E_INPUT_INVALID",
|
|
402
|
+
`${hits.length} lines contain "${match}": quote more of the one to remove`,
|
|
403
|
+
{
|
|
404
|
+
fix: "catherd knowledge show",
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
const rest = lines.filter((l) => l !== hits[0]);
|
|
408
|
+
writeTextAtomic(file, rest.length ? `${rest.join("\n")}\n` : "", { mode: PRIVATE_FILE });
|
|
409
|
+
return hits[0] as string;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Spec 1.5 "runs supersede": the run that took over a closed one, and when. */
|
|
414
|
+
const SupersededSchema = z.looseObject({ schema: z.literal(1), by: z.string(), at: z.string() });
|
|
415
|
+
export type Superseded = z.infer<typeof SupersededSchema>;
|
|
416
|
+
|
|
417
|
+
export const supersededFile = (run: Run): string => join(run.dir, "superseded.json");
|
|
418
|
+
|
|
419
|
+
/** The pointer `runs supersede` left, or null: an open run, or a pointer that cannot be read. */
|
|
420
|
+
export function supersededBy(run: Run): Superseded | null {
|
|
421
|
+
const file = supersededFile(run);
|
|
422
|
+
if (!existsSync(file)) return null;
|
|
423
|
+
try {
|
|
424
|
+
return readVersioned(file, SupersededSchema, 1);
|
|
425
|
+
} catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
292
430
|
const SERVER_OWNED = new Set([
|
|
431
|
+
"superseded.json",
|
|
432
|
+
"pin.json",
|
|
433
|
+
"workspace-contract.md",
|
|
293
434
|
"meta.json",
|
|
294
435
|
"state.md",
|
|
295
436
|
"state.json",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sessionKey } from "../domain/host.ts";
|
|
4
|
+
import { type Interventions, interventionTotal } from "../domain/scorecard.ts";
|
|
5
|
+
import type { RunEvent } from "../domain/run-events.ts";
|
|
6
|
+
import { coveredMs } from "../domain/util.ts";
|
|
7
|
+
import { landedMilestones } from "./milestones.ts";
|
|
8
|
+
import { pauseSpans } from "./pause.ts";
|
|
9
|
+
import { runMilestones } from "./protocol.ts";
|
|
10
|
+
import { parkedSpans, questionCounts } from "./questions.ts";
|
|
11
|
+
import { replay } from "../domain/run-state.ts";
|
|
12
|
+
import { isDaemonRun, readEvents } from "./run-events-store.ts";
|
|
13
|
+
import { listRuns, readAgentRuns, readRecords, type Run, supersededBy } from "./run-store.ts";
|
|
14
|
+
import { readSessionRows } from "./sessions.ts";
|
|
15
|
+
|
|
16
|
+
// Plan 28: what a finished run's own files say about how much it needed a person. Read-only.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A daemon run's own events (plan 36, ruling 12): the owner's answers and the questions still open are parks (a
|
|
20
|
+
* judge's answer is not), every answer is shown, and an owner's cancel or plan approval each count.
|
|
21
|
+
*/
|
|
22
|
+
function eventCounts(run: Run): { parks: number; answers: number; cancels: number; approvals: number } {
|
|
23
|
+
const events = readEvents(run);
|
|
24
|
+
const n = (f: (e: RunEvent) => boolean) => events.filter(f).length;
|
|
25
|
+
// one snapshot: an answer written between two reads would otherwise count nowhere
|
|
26
|
+
const open = Object.keys(replay(events, run.id).questions).length;
|
|
27
|
+
return {
|
|
28
|
+
parks: n((e) => e.type === "question.answered" && e.by === "owner") + open,
|
|
29
|
+
answers: n((e) => e.type === "question.answered"),
|
|
30
|
+
cancels: n((e) => e.type === "cancel.requested"),
|
|
31
|
+
approvals: n((e) => e.type === "plan.approved" && e.by === "owner"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A 1.5 run keeps its questions in questions.jsonl and has no cancel or plan approval to count. */
|
|
36
|
+
const oldCounts = (run: Run) => {
|
|
37
|
+
const { questions, answers } = questionCounts(run);
|
|
38
|
+
return { parks: questions, answers, cancels: 0, approvals: 0 };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** The interventions a run needed by `end` (epoch ms), counted from its records (plan 28, ruling 4). */
|
|
42
|
+
export function interventionsOf(run: Run, end: number, chat: number): Interventions {
|
|
43
|
+
const daemon = isDaemonRun(run);
|
|
44
|
+
const { parks, answers, cancels, approvals } = daemon ? eventCounts(run) : oldCounts(run);
|
|
45
|
+
const keys = [...(run.meta.startedBy ? [run.meta.startedBy] : []), ...readSessionRows(run)].map(sessionKey);
|
|
46
|
+
const takeovers = keys.filter((k, i) => i > 0 && k !== keys[i - 1]).length;
|
|
47
|
+
// a daemon run admits every dispatch without a session
|
|
48
|
+
const manualDispatches = daemon ? readRecords(run).records.filter((r) => r.sessionId).length : 0;
|
|
49
|
+
const from = Date.parse(run.meta.createdAt);
|
|
50
|
+
const pauses = pauseSpans(run).filter((s) => s.from >= from && s.from <= end).length;
|
|
51
|
+
const superseded = listRuns([run.meta.repo]).runs.filter((r) => supersededBy(r)?.by === run.id).length;
|
|
52
|
+
const i = { parks, answers, takeovers, manualDispatches, pauses, superseded, chat, cancels, approvals };
|
|
53
|
+
return { ...i, total: interventionTotal(i) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** When the run ended, in epoch ms: its last landing, else its last record or native agent run, else its start. */
|
|
57
|
+
export function runEnd(run: Run): number {
|
|
58
|
+
const last = landedMilestones(run).at(-1);
|
|
59
|
+
if (last) {
|
|
60
|
+
const file = join(run.dir, "digests", `${last}.md`);
|
|
61
|
+
const at = existsSync(file) ? / · landed (\S+)\s*$/m.exec(readFileSync(file, "utf8"))?.[1] : undefined;
|
|
62
|
+
const ms = at ? Date.parse(at) : NaN;
|
|
63
|
+
if (!Number.isNaN(ms)) return ms;
|
|
64
|
+
}
|
|
65
|
+
const times = [...readRecords(run).records.map((r) => r.endedAt), ...readAgentRuns(run).map((a) => a.at)]
|
|
66
|
+
.map((t) => Date.parse(t))
|
|
67
|
+
.filter((t) => !Number.isNaN(t));
|
|
68
|
+
return times.length ? Math.max(...times) : Date.parse(run.meta.createdAt);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The ms of the run, up to `end`, that it spent parked on the owner or paused; overlaps once. */
|
|
72
|
+
export const pausedMs = (run: Run, end: number): number =>
|
|
73
|
+
coveredMs(Date.parse(run.meta.createdAt), end, [
|
|
74
|
+
...runMilestones(run).flatMap((m) => parkedSpans(run, m)),
|
|
75
|
+
...pauseSpans(run),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/** A failover stand-in (failoverFrom set) ended ok and was not blocked or refused. */
|
|
79
|
+
export const failoverRecovered = (run: Run): boolean =>
|
|
80
|
+
readRecords(run).records.some(
|
|
81
|
+
(r) =>
|
|
82
|
+
r.failoverFrom !== null &&
|
|
83
|
+
r.status === "ok" &&
|
|
84
|
+
r.replyStatus !== "blocked" &&
|
|
85
|
+
r.replyStatus !== "refused",
|
|
86
|
+
);
|