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
|
@@ -4,7 +4,8 @@ import { CatherdError } from "../domain/errors.ts";
|
|
|
4
4
|
import { assertId, parseRung } from "../domain/ids.ts";
|
|
5
5
|
import { assertLaneValues } from "../domain/lane.ts";
|
|
6
6
|
import type { RunRecord } from "../domain/record.ts";
|
|
7
|
-
import type
|
|
7
|
+
import { ROLES, type Role } from "../domain/roles.ts";
|
|
8
|
+
import { rulesFor, tagNames } from "../domain/rules.ts";
|
|
8
9
|
import { awaitsCollect, dispatchPaths, endCollect, tryCollect } from "../infra/dispatch-dir.ts";
|
|
9
10
|
import { withFileLock } from "../infra/filelock.ts";
|
|
10
11
|
import { git, gitToplevel } from "../infra/git.ts";
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "./dispatches.ts";
|
|
20
21
|
import type { Deps } from "./ports.ts";
|
|
21
22
|
import { assertNativeHost } from "./backends.ts";
|
|
23
|
+
import { writeRunIndex } from "./catalog-service.ts";
|
|
22
24
|
import {
|
|
23
25
|
type AgentRun,
|
|
24
26
|
appendAgentRun,
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
findRun,
|
|
28
30
|
knowledgeFor,
|
|
29
31
|
readRecords,
|
|
32
|
+
removeKnowledgeLine,
|
|
30
33
|
runFile,
|
|
31
34
|
type Run,
|
|
32
35
|
runPaths,
|
|
@@ -109,7 +112,7 @@ async function supersededHints(run: Run, by: string): Promise<{ hints?: string[]
|
|
|
109
112
|
|
|
110
113
|
export async function startRun(
|
|
111
114
|
deps: Deps,
|
|
112
|
-
i: { repo: string; title: string; aLines: string[]; from?: string },
|
|
115
|
+
i: { repo: string; title: string; aLines: string[]; from?: string; base?: string },
|
|
113
116
|
): Promise<{
|
|
114
117
|
run: string;
|
|
115
118
|
dir: string;
|
|
@@ -121,6 +124,18 @@ export async function startRun(
|
|
|
121
124
|
throw new CatherdError("E_IO_PATH", `${i.repo} is not inside a git repository`, {
|
|
122
125
|
fix: "pass the path of the repository to work in",
|
|
123
126
|
});
|
|
127
|
+
// a run starts only when every enabled role has an indexed rung; the index it checked is the one it pins
|
|
128
|
+
const start = await deps.routing.checkStart(deps.profiles.forRepo(top), top);
|
|
129
|
+
if (start.unusable.length > 0) {
|
|
130
|
+
const roles = start.unusable.join(", ");
|
|
131
|
+
throw new CatherdError(
|
|
132
|
+
"E_CONFIG_INVALID",
|
|
133
|
+
`the ${roles} role${start.unusable.length === 1 ? " has" : "s have"} no usable rung on the Slopalytics index`,
|
|
134
|
+
{
|
|
135
|
+
fix: `catherd profile validate, then pick indexed pairs: catherd catalog list --role ${start.unusable[0]}`,
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
}
|
|
124
139
|
// where the first milestone's commit range starts; a repo with no commit yet has none
|
|
125
140
|
const head = await git(top, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
|
|
126
141
|
const open = async (): Promise<Run> => {
|
|
@@ -130,11 +145,13 @@ export async function startRun(
|
|
|
130
145
|
aLines: i.aLines,
|
|
131
146
|
version: deps.version,
|
|
132
147
|
startHead: head.kind === "ok" ? head.out.trim() || null : null,
|
|
148
|
+
...(i.base ? { base: i.base } : {}),
|
|
133
149
|
now: new Date(deps.now()),
|
|
134
150
|
startedBy: currentSession(deps),
|
|
135
151
|
});
|
|
136
152
|
// spec 1.5 "Pinned per run": what the run starts on stays what it dispatches on
|
|
137
153
|
writePin(deps, run);
|
|
154
|
+
writeRunIndex(run.dir, start.index);
|
|
138
155
|
await claimRun(deps, run);
|
|
139
156
|
return run;
|
|
140
157
|
};
|
|
@@ -164,8 +181,19 @@ export function writeRunFile(i: { run: string; path: string; content: string }):
|
|
|
164
181
|
} {
|
|
165
182
|
const run = findRun(i.run);
|
|
166
183
|
const file = runFile(run, i.path, "write");
|
|
184
|
+
const rel = relative(run.dir, file).split(sep).join("/");
|
|
185
|
+
// macOS disks ignore case, so Plan-Input/ is the same folder
|
|
186
|
+
const low = rel.toLowerCase();
|
|
187
|
+
if (low === "plan-approved.md" || low === "plan-input" || low.startsWith("plan-input/"))
|
|
188
|
+
throw new CatherdError(
|
|
189
|
+
"E_INPUT_INVALID",
|
|
190
|
+
`${rel} holds the owner's approved plan; roles never write it`,
|
|
191
|
+
{
|
|
192
|
+
fix: "write plan.md or a lane file",
|
|
193
|
+
},
|
|
194
|
+
);
|
|
167
195
|
// spec 1.5 "Lane editing": a lane's header values are checked when it is written, not first at preflight
|
|
168
|
-
const lane = /^lanes\/([^/]+)\.md$/.exec(
|
|
196
|
+
const lane = /^lanes\/([^/]+)\.md$/.exec(rel);
|
|
169
197
|
if (lane) assertLaneValues(i.content, `lanes/${lane[1]}.md`);
|
|
170
198
|
writeTextAtomic(file, i.content);
|
|
171
199
|
return { path: file, bytes: Buffer.byteLength(i.content) };
|
|
@@ -350,12 +378,33 @@ export async function knowledgeLines(repo: string): Promise<{ repo: string; path
|
|
|
350
378
|
/** The source a line added by hand carries, where a landing puts its run's title and milestone. */
|
|
351
379
|
const BY_HAND = "by hand";
|
|
352
380
|
|
|
353
|
-
/**
|
|
354
|
-
export async function
|
|
381
|
+
/** The repo's standing rules for `role`: its knowledge.md as `rulesFor` gives it, "" when missing or blank. */
|
|
382
|
+
export async function standingRules(repo: string, role: Role): Promise<string> {
|
|
383
|
+
const { lines } = await knowledgeLines(repo);
|
|
384
|
+
return rulesFor(lines.join("\n"), role);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Appends one line to the repo's knowledge.md (a hand-written one by default), as `land`'s `learned` does; returns it. */
|
|
388
|
+
export async function addKnowledge(
|
|
389
|
+
repo: string,
|
|
390
|
+
text: string,
|
|
391
|
+
now: Date = new Date(),
|
|
392
|
+
source = BY_HAND,
|
|
393
|
+
): Promise<string> {
|
|
355
394
|
const top = await repoTop(repo);
|
|
356
395
|
if (!text.trim())
|
|
357
396
|
throw new CatherdError("E_INPUT_INVALID", "the knowledge line is empty", {
|
|
358
397
|
fix: 'pass one line of text, e.g. catherd knowledge add "the targeted test is bun test <file>"',
|
|
359
398
|
});
|
|
360
|
-
|
|
399
|
+
const bad = (tagNames(text) ?? []).find((n) => !(ROLES as readonly string[]).includes(n));
|
|
400
|
+
if (bad !== undefined)
|
|
401
|
+
throw new CatherdError("E_INPUT_INVALID", `"${bad}" is not a role: tag a rule with ${ROLES.join(", ")}`, {
|
|
402
|
+
fix: 'catherd knowledge add "[worker, reviewer] <rule>"',
|
|
403
|
+
});
|
|
404
|
+
return appendKnowledge(top, now, source, text);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Removes the one line of the repo's knowledge.md that contains `match`; returns it. */
|
|
408
|
+
export async function removeKnowledge(repo: string, match: string): Promise<string> {
|
|
409
|
+
return removeKnowledgeLine(await repoTop(repo), match);
|
|
361
410
|
}
|
|
@@ -2,10 +2,17 @@ import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync
|
|
|
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
17
|
import { gitOrigin } from "../infra/git.ts";
|
|
11
18
|
import { dataDir, originDir, repoDir, runsDir } from "../infra/paths.ts";
|
|
@@ -14,6 +21,7 @@ import {
|
|
|
14
21
|
ensureJsonlHeader,
|
|
15
22
|
ensurePrivateDir,
|
|
16
23
|
PRIVATE_DIR,
|
|
24
|
+
PRIVATE_FILE,
|
|
17
25
|
appendPrivate,
|
|
18
26
|
nonBlankLines,
|
|
19
27
|
readJsonl,
|
|
@@ -43,6 +51,8 @@ const RunMetaSchema = z.looseObject({
|
|
|
43
51
|
workspace: z.strictObject({ id: z.string(), step: z.string() }).optional(),
|
|
44
52
|
/** the repo's HEAD when run_start ran: where the first milestone's commit range starts (absent before 1.5) */
|
|
45
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(),
|
|
46
56
|
});
|
|
47
57
|
type RunMeta = z.infer<typeof RunMetaSchema>;
|
|
48
58
|
|
|
@@ -80,7 +90,6 @@ export function runPaths(dir: string) {
|
|
|
80
90
|
|
|
81
91
|
export const LEDGER_HEADER = "milestone | what | commit | minutes | evidence";
|
|
82
92
|
|
|
83
|
-
const runSlug = (title: string) => slug(title).slice(0, 40) || "run";
|
|
84
93
|
const stamp = (d: Date) => d.toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
|
|
85
94
|
|
|
86
95
|
/** Creates a run folder under the repo's runs dir. `repo` is a git toplevel; meta.json is written last. */
|
|
@@ -93,6 +102,8 @@ export function createRun(o: {
|
|
|
93
102
|
workspace?: { id: string; step: string };
|
|
94
103
|
/** the repo's full HEAD at the start, when it has a commit */
|
|
95
104
|
startHead?: string | null;
|
|
105
|
+
/** plan 35: the branch on origin each milestone's branch starts from */
|
|
106
|
+
base?: string;
|
|
96
107
|
startedBy?: {
|
|
97
108
|
host?: StartedBy["host"];
|
|
98
109
|
sessionId: string;
|
|
@@ -145,6 +156,7 @@ export function createRun(o: {
|
|
|
145
156
|
...(o.startedBy ? { startedBy: StartedBySchema.parse(o.startedBy) } : {}),
|
|
146
157
|
...(workspace ? { workspace } : {}),
|
|
147
158
|
...(o.startHead ? { startHead: o.startHead } : {}),
|
|
159
|
+
...(o.base ? { base: o.base } : {}),
|
|
148
160
|
};
|
|
149
161
|
writeJsonAtomic(p.meta, meta);
|
|
150
162
|
return { id, dir, meta };
|
|
@@ -260,11 +272,18 @@ export function readOutcomeRoutes(run: Run): OutcomeRouteRow[] {
|
|
|
260
272
|
/** Spec 1.5 plan 24: the run's role decisions outside a lane, oldest first. */
|
|
261
273
|
export function readRoleRoutes(run: Run): RoleRouteRow[] {
|
|
262
274
|
return readJsonl<RoleRouteRow>(runPaths(run.dir).routes).rows.filter(
|
|
263
|
-
(r) =>
|
|
275
|
+
(r) =>
|
|
276
|
+
r?.lane === null &&
|
|
277
|
+
(r.source === "route" || r.source === "dispatch") &&
|
|
278
|
+
typeof r.role === "string" &&
|
|
279
|
+
typeof r.rung === "string",
|
|
264
280
|
);
|
|
265
281
|
}
|
|
266
282
|
|
|
267
|
-
export function appendRoute(
|
|
283
|
+
export function appendRoute(
|
|
284
|
+
run: Run,
|
|
285
|
+
row: RouteRow | RoleRouteRow | OutcomeRouteRow | FailoverRouteRow,
|
|
286
|
+
): void {
|
|
268
287
|
const file = runPaths(run.dir).routes;
|
|
269
288
|
ensureJsonlHeader(file, "routes");
|
|
270
289
|
appendJsonl(file, row);
|
|
@@ -350,7 +369,7 @@ export async function knowledgeFor(toplevel: string): Promise<string> {
|
|
|
350
369
|
|
|
351
370
|
/**
|
|
352
371
|
* Appends one line to the repo's knowledge.md, `- <date> <source>: <text>` with `text` on one line, and returns
|
|
353
|
-
* it.
|
|
372
|
+
* it. `land`'s `learned` and `catherd knowledge add` both come through here; `removeKnowledgeLine` is the other writer.
|
|
354
373
|
*/
|
|
355
374
|
export async function appendKnowledge(
|
|
356
375
|
toplevel: string,
|
|
@@ -359,10 +378,38 @@ export async function appendKnowledge(
|
|
|
359
378
|
text: string,
|
|
360
379
|
): Promise<string> {
|
|
361
380
|
const line = `- ${at.toISOString().slice(0, 10)} ${source}: ${cell(text)}`;
|
|
362
|
-
|
|
381
|
+
const file = await knowledgeFor(toplevel);
|
|
382
|
+
ensurePrivateDir(dirname(file));
|
|
383
|
+
await withFileLock(file, () => appendPrivate(file, `${line}\n`));
|
|
363
384
|
return line;
|
|
364
385
|
}
|
|
365
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
|
+
|
|
366
413
|
/** Spec 1.5 "runs supersede": the run that took over a closed one, and when. */
|
|
367
414
|
const SupersededSchema = z.looseObject({ schema: z.literal(1), by: z.string(), at: z.string() });
|
|
368
415
|
export type Superseded = z.infer<typeof SupersededSchema>;
|
|
@@ -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
|
+
);
|