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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { closeSync, existsSync, fstatSync, ftruncateSync, openSync, readFileSync, readSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CatherdError, UPGRADE } from "../domain/errors.ts";
|
|
4
|
+
import { EVENTS_SCHEMA, RunEventSchema, type RunEvent, type RunEventInput } from "../domain/run-events.ts";
|
|
5
|
+
import { type RunState, replay } from "../domain/run-state.ts";
|
|
6
|
+
import { withFileLock } from "../infra/filelock.ts";
|
|
7
|
+
import { log } from "../infra/log.ts";
|
|
8
|
+
import { appendJsonlBatch } from "../infra/store.ts";
|
|
9
|
+
import type { Run } from "./run-store.ts";
|
|
10
|
+
|
|
11
|
+
/** `<run>/events.jsonl`: one fact per line, dense `seq`, the only state a 2.0 run keeps. */
|
|
12
|
+
export const eventsFile = (run: Run): string => join(run.dir, "events.jsonl");
|
|
13
|
+
|
|
14
|
+
/** A 2.0 run has an events.jsonl; a run without one is a 1.5 run, and the daemon never drives it. */
|
|
15
|
+
export const isDaemonRun = (run: Run): boolean => existsSync(eventsFile(run));
|
|
16
|
+
|
|
17
|
+
const corrupt = (run: Run, after: number, why: string): CatherdError =>
|
|
18
|
+
new CatherdError("E_RUN_CORRUPT", `${eventsFile(run)} is corrupt after seq ${after}: ${why}`, {
|
|
19
|
+
fix: `restore events.jsonl from a backup, or finish run ${run.id} with catherd 1.x`,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const tooOld = (run: Run, v: unknown): CatherdError =>
|
|
23
|
+
new CatherdError(
|
|
24
|
+
"E_RUNTIME_TOO_OLD",
|
|
25
|
+
`${eventsFile(run)} holds events of version ${String(v)}, newer than this catherd (${EVENTS_SCHEMA})`,
|
|
26
|
+
{ fix: `upgrade catherd: ${UPGRADE}` },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/** One line as an event: a newer `v` is too old a catherd, anything else invalid is corrupt (null here). */
|
|
30
|
+
function decode(run: Run, line: string): RunEvent | null {
|
|
31
|
+
let raw: unknown;
|
|
32
|
+
try {
|
|
33
|
+
raw = JSON.parse(line);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const v = (raw as { v?: unknown } | null)?.v;
|
|
38
|
+
if (typeof v === "number" && v > EVENTS_SCHEMA) throw tooOld(run, v);
|
|
39
|
+
const r = RunEventSchema.safeParse(raw);
|
|
40
|
+
return r.success ? r.data : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The run's events from `fromSeq` (1 reads them all and checks every line). A last line without its newline
|
|
45
|
+
* that does not parse is a torn append: skipped and logged. Any other line that does not parse, or a seq that
|
|
46
|
+
* is not the line's number, refuses the read (E_RUN_CORRUPT, naming the seq before it); a newer `v`, too.
|
|
47
|
+
* Lines before `fromSeq` are skipped unread: the caller replayed them already.
|
|
48
|
+
*/
|
|
49
|
+
export function readEvents(run: Run, fromSeq = 1): RunEvent[] {
|
|
50
|
+
let text: string;
|
|
51
|
+
try {
|
|
52
|
+
text = readFileSync(eventsFile(run), "utf8");
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const lines = text.split("\n");
|
|
57
|
+
const tail = lines.pop() ?? "";
|
|
58
|
+
if (tail) lines.push(tail);
|
|
59
|
+
const out: RunEvent[] = [];
|
|
60
|
+
for (const [i, line] of lines.entries()) {
|
|
61
|
+
if (i + 1 < fromSeq || !line.trim()) continue;
|
|
62
|
+
const e = decode(run, line);
|
|
63
|
+
if (e === null) {
|
|
64
|
+
if (i === lines.length - 1 && line === tail) {
|
|
65
|
+
log("warn", "events", { run: run.id, torn: i + 1 });
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
throw corrupt(run, i, "a line is not an event");
|
|
69
|
+
}
|
|
70
|
+
if (e.seq !== i + 1) throw corrupt(run, i, `seq ${e.seq} stands where ${i + 1} belongs`);
|
|
71
|
+
out.push(e);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const replayRun = (run: Run): RunState => replay(readEvents(run), run.id);
|
|
77
|
+
|
|
78
|
+
/** The last seq of the file; a torn tail is cut off first, so the next line starts clean. Under the file's lock. */
|
|
79
|
+
function lastSeq(run: Run): number {
|
|
80
|
+
const file = eventsFile(run);
|
|
81
|
+
if (!existsSync(file)) return 0;
|
|
82
|
+
const fd = openSync(file, "r+");
|
|
83
|
+
try {
|
|
84
|
+
const size = fstatSync(fd).size;
|
|
85
|
+
let chunk = 64 * 1024;
|
|
86
|
+
for (;;) {
|
|
87
|
+
const start = Math.max(0, size - chunk);
|
|
88
|
+
const buf = Buffer.alloc(size - start);
|
|
89
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
90
|
+
const text = buf.toString("utf8");
|
|
91
|
+
const cut = text.lastIndexOf("\n");
|
|
92
|
+
const tail = text.slice(cut + 1);
|
|
93
|
+
const body = text.slice(0, cut + 1).trimEnd();
|
|
94
|
+
// the whole last complete line is in the chunk, or the chunk is the whole file
|
|
95
|
+
if (start > 0 && !body.includes("\n")) {
|
|
96
|
+
chunk *= 4;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (tail && decode(run, tail) === null) {
|
|
100
|
+
// a torn append: not an event, never will be one
|
|
101
|
+
ftruncateSync(fd, size - Buffer.byteLength(tail));
|
|
102
|
+
log("warn", "events", { run: run.id, tornTail: Buffer.byteLength(tail) });
|
|
103
|
+
}
|
|
104
|
+
const line = tail && decode(run, tail) !== null ? tail : body.slice(body.lastIndexOf("\n") + 1);
|
|
105
|
+
if (!line) return 0;
|
|
106
|
+
const last = decode(run, line);
|
|
107
|
+
if (!last) throw corrupt(run, 0, "its last line is not an event");
|
|
108
|
+
return last.seq;
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
closeSync(fd);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Runs `fn` under the events file's lock, handing it an append that does not take the lock again. */
|
|
116
|
+
export function withEventsLock<T>(
|
|
117
|
+
run: Run,
|
|
118
|
+
fn: (add: (e: RunEventInput, now?: Date) => RunEvent) => T | Promise<T>,
|
|
119
|
+
): Promise<T> {
|
|
120
|
+
return withFileLock(eventsFile(run), () => fn((e, now) => writeEvents(run, [e], now)[0] as RunEvent));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The events with the next dense seqs, validated all before the one write that appends them. */
|
|
124
|
+
function writeEvents(run: Run, es: RunEventInput[], now: Date = new Date()): RunEvent[] {
|
|
125
|
+
const first = lastSeq(run) + 1;
|
|
126
|
+
const out = es.map((e, i) =>
|
|
127
|
+
RunEventSchema.parse({ v: EVENTS_SCHEMA, seq: first + i, at: now.toISOString(), ...e }),
|
|
128
|
+
);
|
|
129
|
+
appendJsonlBatch(eventsFile(run), out);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Appends one event under the file's lock, with the next dense `seq`, and returns it as written. */
|
|
134
|
+
export async function appendEvent(run: Run, e: RunEventInput, now: Date = new Date()): Promise<RunEvent> {
|
|
135
|
+
return withEventsLock(run, (add) => add(e, now));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Appends events that belong together in one write under the file's lock: all of them or, on a crash, a torn tail. */
|
|
139
|
+
export function appendEvents(run: Run, es: RunEventInput[], now: Date = new Date()): Promise<RunEvent[]> {
|
|
140
|
+
return withFileLock(eventsFile(run), () => writeEvents(run, es, now));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Calls `onEvent` with each event after `fromSeq - 1`, in order, as they are appended, until the returned stop
|
|
145
|
+
* is called. A poll that finds the file unreadable waits for the next one.
|
|
146
|
+
*/
|
|
147
|
+
export function tailEvents(
|
|
148
|
+
run: Run,
|
|
149
|
+
fromSeq: number,
|
|
150
|
+
onEvent: (e: RunEvent) => void,
|
|
151
|
+
o: { pollMs?: number } = {},
|
|
152
|
+
): () => void {
|
|
153
|
+
let next = fromSeq;
|
|
154
|
+
let stopped = false;
|
|
155
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
156
|
+
const poll = (): void => {
|
|
157
|
+
if (stopped) return;
|
|
158
|
+
try {
|
|
159
|
+
for (const e of readEvents(run, next)) {
|
|
160
|
+
if (stopped) return;
|
|
161
|
+
next = e.seq + 1;
|
|
162
|
+
onEvent(e);
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
log("warn", "events", { run: run.id, tail: err instanceof Error ? err.message : String(err) });
|
|
166
|
+
}
|
|
167
|
+
if (!stopped) timer = setTimeout(poll, o.pollMs ?? 500);
|
|
168
|
+
};
|
|
169
|
+
poll();
|
|
170
|
+
return () => {
|
|
171
|
+
stopped = true;
|
|
172
|
+
if (timer) clearTimeout(timer);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -17,7 +17,7 @@ export interface Evidence {
|
|
|
17
17
|
fails: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** `<canonical rung>|<kind or *>` → its evidence. Shown by route
|
|
20
|
+
/** `<canonical rung>|<kind or *>` → its evidence. Shown by route and catalog list; never routes. */
|
|
21
21
|
export type EvidenceTable = Record<string, Evidence>;
|
|
22
22
|
|
|
23
23
|
const VERDICT_FAIL = /^VERDICT: FAIL\b/;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { errorMessage } from "../domain/errors.ts";
|
|
5
|
+
import { ACCESS } from "../domain/record.ts";
|
|
6
|
+
import { ROLES } from "../domain/roles.ts";
|
|
7
|
+
import { readVersioned, writeJsonAtomic } from "../infra/store.ts";
|
|
8
|
+
import type { Deps, ProfileView } from "./ports.ts";
|
|
9
|
+
import { findRun, type Run } from "./run-store.ts";
|
|
10
|
+
import { refreshState } from "./state.ts";
|
|
11
|
+
|
|
12
|
+
// Spec 1.5 "Pinned per run": run_start records the profile the run started on, each role's access and each
|
|
13
|
+
// backend's isolation. A later change (another active profile, a toggled isolation, a version that changed a
|
|
14
|
+
// default) is logged in state.md and status, and dispatch keeps the pinned values until the owner re-pins.
|
|
15
|
+
|
|
16
|
+
const PinSchema = z.looseObject({
|
|
17
|
+
schema: z.literal(1),
|
|
18
|
+
at: z.string(),
|
|
19
|
+
profile: z.string(),
|
|
20
|
+
access: z.partialRecord(z.enum(ROLES), z.enum(ACCESS)),
|
|
21
|
+
isolated: z.record(z.string(), z.boolean()),
|
|
22
|
+
});
|
|
23
|
+
export type Pin = z.infer<typeof PinSchema>;
|
|
24
|
+
|
|
25
|
+
export const pinFile = (run: Run): string => join(run.dir, "pin.json");
|
|
26
|
+
|
|
27
|
+
/** The run's pin, or null: a run from before 1.5, or a pin that cannot be read (the profile then rules). */
|
|
28
|
+
export function readPin(run: Run): Pin | null {
|
|
29
|
+
if (!existsSync(pinFile(run))) return null;
|
|
30
|
+
try {
|
|
31
|
+
return readVersioned(pinFile(run), PinSchema, 1);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const pinOf = (view: ProfileView, at: string): Pin => ({
|
|
38
|
+
schema: 1,
|
|
39
|
+
at,
|
|
40
|
+
profile: view.name,
|
|
41
|
+
access: Object.fromEntries(
|
|
42
|
+
Object.entries(view.roles).flatMap(([role, rc]) => (rc ? [[role, rc.access]] : [])),
|
|
43
|
+
),
|
|
44
|
+
isolated: Object.fromEntries(Object.entries(view.isolated).map(([k, v]) => [k, v === true])),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Records what the run runs on now: the profile its repo runs on, each role's access, each backend's isolation. */
|
|
48
|
+
export function writePin(deps: Deps, run: Run): Pin {
|
|
49
|
+
const pin = pinOf(deps.profiles.forRepo(run.meta.repo), new Date(deps.now()).toISOString());
|
|
50
|
+
writeJsonAtomic(pinFile(run), pin);
|
|
51
|
+
return pin;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The pinned profile's view, or null with why when it can no longer be read (deleted, invalid). */
|
|
55
|
+
function pinnedView(deps: Deps, run: Run, pin: Pin): { view: ProfileView | null; why: string | null } {
|
|
56
|
+
const current = deps.profiles.forRepo(run.meta.repo);
|
|
57
|
+
if (current.name === pin.profile) return { view: current, why: null };
|
|
58
|
+
try {
|
|
59
|
+
return { view: deps.profiles.get(pin.profile, run.meta.repo).profile, why: null };
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return { view: null, why: errorMessage(e) };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The profile a run's dispatch, routing and failover read: its pinned profile, with its pinned access and
|
|
67
|
+
* isolation laid over it. A run with no pin, or whose pinned profile is gone, reads its repo's profile.
|
|
68
|
+
*/
|
|
69
|
+
export function runProfile(deps: Deps, run: Run): ProfileView {
|
|
70
|
+
const pin = readPin(run);
|
|
71
|
+
if (!pin) return deps.profiles.forRepo(run.meta.repo);
|
|
72
|
+
const view = pinnedView(deps, run, pin).view ?? deps.profiles.forRepo(run.meta.repo);
|
|
73
|
+
const roles: ProfileView["roles"] = {};
|
|
74
|
+
for (const [role, rc] of Object.entries(view.roles) as [
|
|
75
|
+
keyof ProfileView["roles"],
|
|
76
|
+
NonNullable<ProfileView["roles"][keyof ProfileView["roles"]]>,
|
|
77
|
+
][])
|
|
78
|
+
roles[role] = { ...rc, access: pin.access[role] ?? rc.access };
|
|
79
|
+
// a backend the pin does not name ran with no isolation when the run started
|
|
80
|
+
const backends = new Set([...Object.keys(view.isolated), ...Object.keys(pin.isolated)]);
|
|
81
|
+
const isolated = Object.fromEntries([...backends].map((k) => [k, pin.isolated[k] === true]));
|
|
82
|
+
return { ...view, roles, isolated };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What changed since the pin, one line each, in words; none while the run's repo still runs as pinned. */
|
|
86
|
+
export function pinChanges(deps: Deps, run: Run): string[] {
|
|
87
|
+
const pin = readPin(run);
|
|
88
|
+
if (!pin) return [];
|
|
89
|
+
let now: ProfileView;
|
|
90
|
+
try {
|
|
91
|
+
now = deps.profiles.forRepo(run.meta.repo);
|
|
92
|
+
} catch {
|
|
93
|
+
// a profile that cannot be read is doctor's and profile validate's to report, not a pin change
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const out: string[] = [];
|
|
97
|
+
if (now.name !== pin.profile) {
|
|
98
|
+
const { why } = pinnedView(deps, run, pin);
|
|
99
|
+
out.push(
|
|
100
|
+
`profile: pinned ${pin.profile}, the repo now runs on ${now.name}${why ? ` (${pin.profile} cannot be read: ${why}; dispatch reads ${now.name})` : ""}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
for (const [role, access] of Object.entries(pin.access)) {
|
|
104
|
+
const was = now.roles[role as keyof ProfileView["roles"]]?.access;
|
|
105
|
+
if (was !== undefined && was !== access) out.push(`${role} access: pinned ${access}, now ${was}`);
|
|
106
|
+
}
|
|
107
|
+
for (const backend of new Set([...Object.keys(pin.isolated), ...Object.keys(now.isolated)])) {
|
|
108
|
+
const isolated = pin.isolated[backend] === true;
|
|
109
|
+
const was = now.isolated[backend] === true;
|
|
110
|
+
if (was !== isolated) out.push(`${backend} isolated: pinned ${isolated}, now ${was}`);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** `run_pin`: the owner re-pins the run to what its repo runs on now. Returns the new pin and what it changed. */
|
|
116
|
+
export async function repin(
|
|
117
|
+
deps: Deps,
|
|
118
|
+
i: { run: string },
|
|
119
|
+
): Promise<{ pin: Pin; changed: string[]; hints?: string[] }> {
|
|
120
|
+
const run = findRun(i.run);
|
|
121
|
+
const changed = pinChanges(deps, run);
|
|
122
|
+
const pin = writePin(deps, run);
|
|
123
|
+
const { hints } = await refreshState(run, { pinChanges: [] });
|
|
124
|
+
return { pin, changed, ...(hints.length ? { hints } : {}) };
|
|
125
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { errorMessage } from "../domain/errors.ts";
|
|
3
|
+
import { PARK_REASONS, describeEvent } from "../domain/run-events.ts";
|
|
4
|
+
import { type RunState, liveDispatches } from "../domain/run-state.ts";
|
|
5
|
+
import { log } from "../infra/log.ts";
|
|
6
|
+
import { daemonAlive, ensureDaemon, readHeartbeat } from "./daemon.ts";
|
|
7
|
+
import { liveDispatches as liveDispatchesOf } from "./dispatches.ts";
|
|
8
|
+
import { landedMilestones } from "./milestones.ts";
|
|
9
|
+
import type { Deps } from "./ports.ts";
|
|
10
|
+
import { openQuestions } from "./questions.ts";
|
|
11
|
+
import { isDaemonRun, readEvents, replayRun, tailEvents } from "./run-events-store.ts";
|
|
12
|
+
import { findRun, listRuns, type Run } from "./run-store.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The read side of a run, for the panel (plan 31) and the session's `status`: each shape has a zod
|
|
16
|
+
* schema, which is its contract. A 1.5 run reads as `engine: "1.5"`, with what its folder says.
|
|
17
|
+
*/
|
|
18
|
+
export const ParkedViewSchema = z.object({
|
|
19
|
+
run: z.string(),
|
|
20
|
+
runTitle: z.string(),
|
|
21
|
+
unit: z.string(),
|
|
22
|
+
question: z.string(),
|
|
23
|
+
reason: z.enum(PARK_REASONS),
|
|
24
|
+
at: z.string(),
|
|
25
|
+
/** the command that answers it */
|
|
26
|
+
command: z.string(),
|
|
27
|
+
});
|
|
28
|
+
export type ParkedView = z.infer<typeof ParkedViewSchema>;
|
|
29
|
+
|
|
30
|
+
export const TimelineEntrySchema = z.object({
|
|
31
|
+
seq: z.number().int(),
|
|
32
|
+
at: z.string(),
|
|
33
|
+
type: z.string(),
|
|
34
|
+
text: z.string(),
|
|
35
|
+
});
|
|
36
|
+
export type TimelineEntry = z.infer<typeof TimelineEntrySchema>;
|
|
37
|
+
|
|
38
|
+
export const RunViewSchema = z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
title: z.string(),
|
|
41
|
+
repo: z.string(),
|
|
42
|
+
engine: z.enum(["2.0", "1.5"]),
|
|
43
|
+
phase: z.enum(["planning", "approval", "running", "ended"]).nullable(),
|
|
44
|
+
outcome: z.enum(["done", "cancelled", "failed"]).nullable(),
|
|
45
|
+
daemon: z.object({
|
|
46
|
+
alive: z.boolean(),
|
|
47
|
+
pid: z.number().int().nullable(),
|
|
48
|
+
beatAgeSec: z.number().nullable(),
|
|
49
|
+
}),
|
|
50
|
+
milestones: z.array(
|
|
51
|
+
z.object({
|
|
52
|
+
id: z.string(),
|
|
53
|
+
title: z.string(),
|
|
54
|
+
status: z.string(),
|
|
55
|
+
round: z.number().int(),
|
|
56
|
+
commit: z.string().nullable(),
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
live: z.array(
|
|
60
|
+
z.object({ name: z.string(), role: z.string(), rung: z.string(), unit: z.string(), since: z.string() }),
|
|
61
|
+
),
|
|
62
|
+
parked: z.array(ParkedViewSchema),
|
|
63
|
+
holds: z.array(z.object({ quota: z.string(), until: z.string() })),
|
|
64
|
+
/** the latest event, in one line */
|
|
65
|
+
last: z.string().nullable(),
|
|
66
|
+
startedAt: z.string(),
|
|
67
|
+
updatedAt: z.string(),
|
|
68
|
+
});
|
|
69
|
+
export type RunView = z.infer<typeof RunViewSchema>;
|
|
70
|
+
|
|
71
|
+
export { tailEvents };
|
|
72
|
+
|
|
73
|
+
const answerCommand = (run: string, unit: string): string => `catherd answer ${run} ${unit} "<answer>"`;
|
|
74
|
+
|
|
75
|
+
function parkedOf(run: Run, s: RunState): ParkedView[] {
|
|
76
|
+
return Object.values(s.questions).map((q) => ({
|
|
77
|
+
run: run.id,
|
|
78
|
+
runTitle: run.meta.title,
|
|
79
|
+
unit: q.unit,
|
|
80
|
+
question: q.question,
|
|
81
|
+
reason: q.reason,
|
|
82
|
+
at: q.at,
|
|
83
|
+
command: answerCommand(run.id, q.unit),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A daemon of an unfinished 2.0 run is started again when it is dead; a failure to start never fails a read. */
|
|
88
|
+
function revive(run: Run): void {
|
|
89
|
+
try {
|
|
90
|
+
ensureDaemon(run);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
log("warn", "daemon", { run: run.id, revive: errorMessage(e) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function view15(deps: Deps, run: Run): RunView {
|
|
97
|
+
const now = deps.now();
|
|
98
|
+
return {
|
|
99
|
+
id: run.id,
|
|
100
|
+
title: run.meta.title,
|
|
101
|
+
repo: run.meta.repo,
|
|
102
|
+
engine: "1.5",
|
|
103
|
+
phase: null,
|
|
104
|
+
outcome: null,
|
|
105
|
+
daemon: { alive: false, pid: null, beatAgeSec: null },
|
|
106
|
+
milestones: landedMilestones(run).map((id) => ({
|
|
107
|
+
id,
|
|
108
|
+
title: id,
|
|
109
|
+
status: "landed",
|
|
110
|
+
round: 0,
|
|
111
|
+
commit: null,
|
|
112
|
+
})),
|
|
113
|
+
live: liveDispatchesOf(run, now).map((d) => ({
|
|
114
|
+
name: d.admit.name,
|
|
115
|
+
role: d.admit.role,
|
|
116
|
+
rung: d.admit.rung,
|
|
117
|
+
unit: d.admit.lane ?? d.admit.name,
|
|
118
|
+
since: d.admit.admittedAt,
|
|
119
|
+
})),
|
|
120
|
+
parked: openQuestions(run).map((q) => ({
|
|
121
|
+
run: run.id,
|
|
122
|
+
runTitle: run.meta.title,
|
|
123
|
+
unit: q.milestone,
|
|
124
|
+
question: q.question,
|
|
125
|
+
reason: "needs_user" as const,
|
|
126
|
+
at: q.at,
|
|
127
|
+
command: `answer(run: "${run.id}", milestone: "${q.milestone}", answer: "<answer>")`,
|
|
128
|
+
})),
|
|
129
|
+
holds: [],
|
|
130
|
+
last: null,
|
|
131
|
+
startedAt: run.meta.createdAt,
|
|
132
|
+
updatedAt: run.meta.createdAt,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** One run at a glance. Reading an unfinished 2.0 run starts its daemon again if it died. */
|
|
137
|
+
export function runView(deps: Deps, runId: string): RunView {
|
|
138
|
+
const run = findRun(runId);
|
|
139
|
+
if (!isDaemonRun(run)) return view15(deps, run);
|
|
140
|
+
revive(run);
|
|
141
|
+
const s = replayRun(run);
|
|
142
|
+
const beat = readHeartbeat(run);
|
|
143
|
+
const events = readEvents(run);
|
|
144
|
+
const last = events.at(-1);
|
|
145
|
+
return {
|
|
146
|
+
id: run.id,
|
|
147
|
+
title: run.meta.title,
|
|
148
|
+
repo: run.meta.repo,
|
|
149
|
+
engine: "2.0",
|
|
150
|
+
phase: s.phase,
|
|
151
|
+
outcome: s.outcome,
|
|
152
|
+
daemon: {
|
|
153
|
+
alive: daemonAlive(run),
|
|
154
|
+
pid: beat?.pid ?? null,
|
|
155
|
+
beatAgeSec: beat ? Math.max(0, Math.round((deps.now() - Date.parse(beat.beatAt)) / 1000)) : null,
|
|
156
|
+
},
|
|
157
|
+
milestones: s.milestones.map((m) => ({
|
|
158
|
+
id: m.id,
|
|
159
|
+
title: m.title,
|
|
160
|
+
status: m.status,
|
|
161
|
+
round: m.round,
|
|
162
|
+
commit: m.commit,
|
|
163
|
+
})),
|
|
164
|
+
live: liveDispatches(s).map((d) => ({
|
|
165
|
+
name: d.name,
|
|
166
|
+
role: d.role,
|
|
167
|
+
rung: d.rung,
|
|
168
|
+
unit: d.unit,
|
|
169
|
+
since: d.startedAt,
|
|
170
|
+
})),
|
|
171
|
+
parked: parkedOf(run, s),
|
|
172
|
+
holds: Object.entries(s.holds).map(([quota, until]) => ({ quota, until })),
|
|
173
|
+
last: last ? describeEvent(last, (id) => s.dispatches[id]?.rung) : null,
|
|
174
|
+
startedAt: run.meta.createdAt,
|
|
175
|
+
updatedAt: last?.at ?? run.meta.createdAt,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Every run, newest first (one repo's with `repo`), at most `limit` (default 50). */
|
|
180
|
+
export function listRunViews(deps: Deps, o: { repo?: string; limit?: number } = {}): RunView[] {
|
|
181
|
+
const runs = listRuns(o.repo ? [o.repo] : undefined).runs.slice(0, o.limit ?? 50);
|
|
182
|
+
const out: RunView[] = [];
|
|
183
|
+
for (const r of runs) {
|
|
184
|
+
try {
|
|
185
|
+
out.push(runView(deps, r.id));
|
|
186
|
+
} catch (e) {
|
|
187
|
+
log("warn", "read", { run: r.id, error: errorMessage(e) });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The run's events from `fromSeq`, one line each (never a reply's body), at most `limit` (default 200). */
|
|
194
|
+
export function runTimeline(runId: string, fromSeq = 1, limit = 200): TimelineEntry[] {
|
|
195
|
+
const run = findRun(runId);
|
|
196
|
+
if (!isDaemonRun(run)) return [];
|
|
197
|
+
const s = replayRun(run);
|
|
198
|
+
return readEvents(run, fromSeq)
|
|
199
|
+
.slice(0, limit)
|
|
200
|
+
.map((e) => ({
|
|
201
|
+
seq: e.seq,
|
|
202
|
+
at: e.at,
|
|
203
|
+
type: e.type,
|
|
204
|
+
text: describeEvent(e, (id) => s.dispatches[id]?.rung),
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Every unfinished 2.0 run's open questions (one run's with `run`), each with the command that answers it. */
|
|
209
|
+
export function parkedQuestions(o: { run?: string } = {}): ParkedView[] {
|
|
210
|
+
const runs = o.run ? [findRun(o.run)] : listRuns().runs;
|
|
211
|
+
return runs.flatMap((run) => {
|
|
212
|
+
try {
|
|
213
|
+
if (!isDaemonRun(run)) return [];
|
|
214
|
+
const s = replayRun(run);
|
|
215
|
+
return s.phase === "ended" ? [] : parkedOf(run, s);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
log("warn", "read", { run: run.id, error: errorMessage(e) });
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|