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
package/src/services/protocol.ts
CHANGED
|
@@ -2,18 +2,24 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { RunRecord } from "../domain/record.ts";
|
|
4
4
|
import type { RouteRow } from "../domain/route.ts";
|
|
5
|
+
import { parseLaneHeader } from "../domain/lane.ts";
|
|
5
6
|
import { ensurePrivateDir, readJsonl, writeTextAtomic } from "../infra/store.ts";
|
|
6
7
|
import { type Dispatch, listDispatches, liveDispatches } from "./dispatches.ts";
|
|
7
|
-
import
|
|
8
|
+
import { RECHECK_COMMAND_MIN } from "../domain/gate-brief.ts";
|
|
9
|
+
import { failedItems, type VerifierStep } from "./gate-service.ts";
|
|
8
10
|
import {
|
|
11
|
+
countFindings,
|
|
12
|
+
FINDING,
|
|
9
13
|
landedMilestones,
|
|
10
14
|
milestoneReviewer,
|
|
11
15
|
milestoneVerifier,
|
|
12
16
|
milestoneStart,
|
|
13
17
|
namesMilestone,
|
|
18
|
+
openFindings,
|
|
19
|
+
partialReviewer,
|
|
14
20
|
reviewerPassed,
|
|
15
|
-
verifierPassed,
|
|
16
21
|
} from "./milestones.ts";
|
|
22
|
+
import { openQuestions } from "./questions.ts";
|
|
17
23
|
import { readAgentRuns, readRecords, readRoutes, type Run, runPaths } from "./run-store.ts";
|
|
18
24
|
|
|
19
25
|
// Spec 1.1 §10: the protocol's next step, derived from the run's own files so a session that lost its
|
|
@@ -44,30 +50,52 @@ function laneIds(run: Run): string[] {
|
|
|
44
50
|
.sort(byNumber);
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/** Plan 28: the milestones the run's lane files name, in number order. */
|
|
54
|
+
export const runMilestones = (run: Run): string[] =>
|
|
55
|
+
[...new Set(laneIds(run).map(milestoneOf))].sort(byNumber);
|
|
56
|
+
|
|
57
|
+
/** What laneDone reads of the run, read once for every lane of the milestone. */
|
|
58
|
+
interface LaneEvidence {
|
|
59
|
+
routes: RouteRow[];
|
|
60
|
+
live: Dispatch[];
|
|
61
|
+
records: Map<string, RunRecord>;
|
|
62
|
+
dispatches: Dispatch[];
|
|
63
|
+
agents: ReturnType<typeof readAgentRuns>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function laneEvidence(run: Run, routes: RouteRow[], live: Dispatch[]): LaneEvidence {
|
|
67
|
+
return {
|
|
68
|
+
routes,
|
|
69
|
+
live,
|
|
70
|
+
records: new Map(readRecords(run).records.map((r) => [r.dispatchId, r])),
|
|
71
|
+
dispatches: listDispatches(run),
|
|
72
|
+
agents: readAgentRuns(run),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
/**
|
|
48
77
|
* Whether `lane` needs no dispatch now: its latest dispatch or native agent run since its latest climb is
|
|
49
78
|
* still running, or ended ok with no blocked or refused reply. A lane whose last try failed, hit a limit,
|
|
50
79
|
* was blocked, or was climbed since goes back to dispatch; a plain re-route leaves an ok try standing.
|
|
51
80
|
*/
|
|
52
|
-
function laneDone(
|
|
81
|
+
function laneDone(lane: string, e: LaneEvidence): boolean {
|
|
53
82
|
const from = Math.max(
|
|
54
|
-
...routes.filter((r) => r.lane === lane && r.source === "climb").map((r) => Date.parse(r.at)),
|
|
83
|
+
...e.routes.filter((r) => r.lane === lane && r.source === "climb").map((r) => Date.parse(r.at)),
|
|
55
84
|
);
|
|
56
85
|
// a dispatch routes its lane first, so a try at the route's own time counts as after it
|
|
57
86
|
const after = (t: string) => !(Date.parse(t) < from);
|
|
58
|
-
const
|
|
59
|
-
const running = new Set(live.map((d) => d.admit.dispatchId));
|
|
87
|
+
const running = new Set(e.live.map((d) => d.admit.dispatchId));
|
|
60
88
|
const tries = [
|
|
61
|
-
...
|
|
89
|
+
...e.dispatches
|
|
62
90
|
.filter((d) => d.admit.lane === lane && after(d.admit.admittedAt))
|
|
63
91
|
.map((d) => {
|
|
64
|
-
const r = records.get(d.admit.dispatchId);
|
|
92
|
+
const r = e.records.get(d.admit.dispatchId);
|
|
65
93
|
const ok = r
|
|
66
94
|
? r.status === "ok" && r.replyStatus !== "blocked" && r.replyStatus !== "refused"
|
|
67
95
|
: running.has(d.admit.dispatchId);
|
|
68
96
|
return { at: Date.parse(d.admit.admittedAt), ok };
|
|
69
97
|
}),
|
|
70
|
-
...
|
|
98
|
+
...e.agents
|
|
71
99
|
.filter((a) => a.lane === lane && after(a.at))
|
|
72
100
|
.map((a) => ({ at: Date.parse(a.at), ok: a.status === "ok" })),
|
|
73
101
|
];
|
|
@@ -78,6 +106,27 @@ function laneDone(run: Run, lane: string, routes: RouteRow[], live: Dispatch[]):
|
|
|
78
106
|
return latest?.ok ?? false;
|
|
79
107
|
}
|
|
80
108
|
|
|
109
|
+
/** The lane's After: lanes, from its file; none when it has none or cannot be read. */
|
|
110
|
+
function afterOf(run: Run, lane: string): string[] {
|
|
111
|
+
try {
|
|
112
|
+
return parseLaneHeader(readFileSync(join(runPaths(run.dir).lanes, `${lane}.md`), "utf8")).after;
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Spec 1.5 "Lane editing": the lanes `lane`'s After: line names that have not finished: their milestone has
|
|
120
|
+
* not landed and their latest try since the latest climb is still running, or did not end ok.
|
|
121
|
+
*/
|
|
122
|
+
export function unfinishedAfter(run: Run, lane: string, routes: RouteRow[] = readRoutes(run)): string[] {
|
|
123
|
+
const landed = new Set(landedMilestones(run));
|
|
124
|
+
const after = afterOf(run, lane).filter((a) => !landed.has(milestoneOf(a)));
|
|
125
|
+
if (after.length === 0) return [];
|
|
126
|
+
const evidence = laneEvidence(run, routes, []);
|
|
127
|
+
return after.filter((a) => !laneDone(a, evidence));
|
|
128
|
+
}
|
|
129
|
+
|
|
81
130
|
/**
|
|
82
131
|
* The step the milestone loop is at: the first milestone neither landed nor parked, and within it the
|
|
83
132
|
* first of route, dispatch, collect, reviewer, verifier and land that is still to do.
|
|
@@ -98,20 +147,49 @@ export function protocolNext(run: Run, parked: string[], now = Date.now()): stri
|
|
|
98
147
|
const routed = new Set(routes.map((r) => r.lane));
|
|
99
148
|
if (mine.some((l) => !routed.has(l))) return `route and preflight ${m}'s lanes`;
|
|
100
149
|
const live = liveDispatches(run, now);
|
|
101
|
-
const
|
|
150
|
+
const evidence = laneEvidence(run, routes, live);
|
|
151
|
+
const notDone = mine.filter((l) => !laneDone(l, evidence));
|
|
102
152
|
// spec 1.1 §9: a climb past the top rung (from === rung) leaves nothing to climb to; that is a decision
|
|
103
153
|
const spent = notDone.filter((l) => {
|
|
104
154
|
const last = routes.findLast((r) => r.lane === l);
|
|
105
155
|
return last?.source === "climb" && last.from === last.rung;
|
|
106
156
|
});
|
|
107
157
|
const waiting = notDone.filter((l) => !spent.includes(l));
|
|
108
|
-
|
|
158
|
+
// an After: line keeps a lane back until the lanes it names have finished
|
|
159
|
+
const held = new Map(waiting.map((l) => [l, unfinishedAfter(run, l, routes)]));
|
|
160
|
+
const ready = waiting.filter((l) => held.get(l)?.length === 0);
|
|
161
|
+
const order = waiting
|
|
162
|
+
.filter((l) => !ready.includes(l))
|
|
163
|
+
.map((l) => `${l} after ${held.get(l)?.join(", ")}`)
|
|
164
|
+
.join("; ");
|
|
165
|
+
if (ready.length) return `dispatch ${ready.join(", ")}${order ? `; then ${order}` : ""}`;
|
|
109
166
|
if (spent.length) return `${spent.join(", ")}: out of rungs — ask finding, then the architect or park ${m}`;
|
|
110
167
|
const running = live.filter((d) => d.admit.lane !== null && mine.includes(d.admit.lane));
|
|
111
|
-
if (running.length)
|
|
168
|
+
if (running.length)
|
|
169
|
+
return `${m}: lanes running (${running.map((d) => d.admit.name).join(", ")})${order ? `; then ${order}` : ""}`;
|
|
170
|
+
if (order) return `${m}: ${order} (After:), not done: finish those lanes first`;
|
|
112
171
|
const start = milestoneStart(run, m);
|
|
113
|
-
if (!reviewerPassed(run, m, start))
|
|
114
|
-
|
|
172
|
+
if (!reviewerPassed(run, m, start)) {
|
|
173
|
+
// plan 23: a partial review is not the review: a second pass, scoped to what it did not read
|
|
174
|
+
const partial = partialReviewer(run, m, start);
|
|
175
|
+
return partial
|
|
176
|
+
? `${m}: ${partial.name} stopped partial: a scoped second pass (reviewer-${m}-2) over the files it did not read`
|
|
177
|
+
: `${m}: reviewer`;
|
|
178
|
+
}
|
|
179
|
+
// plan 23: open BLOCKER and BUG lines go to the fix round before the verifier
|
|
180
|
+
const open = openFindings(run, m, start);
|
|
181
|
+
if (open)
|
|
182
|
+
return `${m}: fix round for ${open.name}'s findings (${open.blocker} BLOCKER, ${open.bug} BUG), then the verifier`;
|
|
183
|
+
const verdict = milestoneVerifier(run, m, start);
|
|
184
|
+
if (!verdict) return `${m}: verifier`;
|
|
185
|
+
// plan 23: the machine, not the work: the owner hears of it, and no fix round runs
|
|
186
|
+
if (verdict.blocked !== null)
|
|
187
|
+
return `${m}: ${verdict.name} is blocked by the environment${verdict.blocked ? ` (${verdict.blocked})` : ""}: surface it to the owner (park ${m}), not a fix round`;
|
|
188
|
+
if (!verdict.passed) {
|
|
189
|
+
// plan 23: a resumed verifier can hang; a re-check goes to a fresh one, told what failed
|
|
190
|
+
const failed = failedItems(run, m);
|
|
191
|
+
return `${m}: ${verdict.name} failed: the owning lanes fix it, then a fresh verifier (not a resume) re-checks ${failed.length ? failed.join(", ") : "the failed items"}, each command capped at ${RECHECK_COMMAND_MIN} min`;
|
|
192
|
+
}
|
|
115
193
|
return `land ${m}`;
|
|
116
194
|
}
|
|
117
195
|
|
|
@@ -121,21 +199,24 @@ export const protocolView = (run: Run, parked: string[], now?: number) => ({
|
|
|
121
199
|
checklist: PROTOCOL_CHECKLIST,
|
|
122
200
|
});
|
|
123
201
|
|
|
124
|
-
const FINDING = /^\s*(?:[-*]\s*)?(BLOCKER|BUG|NIT)\b/;
|
|
125
|
-
|
|
126
202
|
function findingCounts(run: Run, r: RunRecord | undefined): string {
|
|
127
203
|
if (!r?.replyPath) return "no reply";
|
|
128
|
-
const
|
|
129
|
-
const text = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
130
|
-
const n = { BLOCKER: 0, BUG: 0, NIT: 0 };
|
|
131
|
-
for (const line of text.split("\n")) {
|
|
132
|
-
const k = FINDING.exec(line)?.[1] as keyof typeof n | undefined;
|
|
133
|
-
if (k) n[k]++;
|
|
134
|
-
}
|
|
204
|
+
const n = countFindings(run, r) ?? { BLOCKER: 0, BUG: 0, NIT: 0 };
|
|
135
205
|
const total = n.BLOCKER + n.BUG + n.NIT;
|
|
136
206
|
return `${total} finding(s): ${n.BLOCKER} BLOCKER, ${n.BUG} BUG, ${n.NIT} NIT`;
|
|
137
207
|
}
|
|
138
208
|
|
|
209
|
+
/** The reviewer's BLOCKER and BUG lines, as it wrote them: what the digest lists as findings. */
|
|
210
|
+
function findingLines(run: Run, r: RunRecord | null | undefined): string[] {
|
|
211
|
+
if (!r?.replyPath) return [];
|
|
212
|
+
const file = join(run.dir, r.replyPath);
|
|
213
|
+
const text = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
214
|
+
return text
|
|
215
|
+
.split("\n")
|
|
216
|
+
.filter((l) => /^(BLOCKER|BUG)$/.test(FINDING.exec(l)?.[1] ?? ""))
|
|
217
|
+
.map((l) => l.trim().replace(/^[-*]\s*/, ""));
|
|
218
|
+
}
|
|
219
|
+
|
|
139
220
|
const k = (x: number) => (x >= 1000 ? `${Math.round(x / 1000)}k` : String(x));
|
|
140
221
|
|
|
141
222
|
/**
|
|
@@ -145,7 +226,18 @@ const k = (x: number) => (x >= 1000 ? `${Math.round(x / 1000)}k` : String(x));
|
|
|
145
226
|
*/
|
|
146
227
|
export function writeDigest(
|
|
147
228
|
run: Run,
|
|
148
|
-
i: {
|
|
229
|
+
i: {
|
|
230
|
+
milestone: string;
|
|
231
|
+
what: string;
|
|
232
|
+
commit: string;
|
|
233
|
+
evidence: string;
|
|
234
|
+
minutes: number;
|
|
235
|
+
at: string;
|
|
236
|
+
/** spec 1.5: the commits the milestone landed, `<hash> <subject>`, oldest first */
|
|
237
|
+
commits?: string[];
|
|
238
|
+
/** spec 1.5: the parked and paused minutes `minutes` leaves out */
|
|
239
|
+
pausedMinutes?: number;
|
|
240
|
+
},
|
|
149
241
|
): string {
|
|
150
242
|
const m = i.milestone;
|
|
151
243
|
const start = milestoneStart(run, m);
|
|
@@ -163,6 +255,9 @@ export function writeDigest(
|
|
|
163
255
|
const records = readRecords(run).records;
|
|
164
256
|
// the reviewer the gate counted: since the milestone's lanes started, a dispatch or a native subagent
|
|
165
257
|
const reviewer = milestoneReviewer(run, m, start);
|
|
258
|
+
// spec 1.5: the review's BLOCKER and BUG lines, and the owner questions still open in the run
|
|
259
|
+
const findings = findingLines(run, reviewer?.record);
|
|
260
|
+
const open = openQuestions(run);
|
|
166
261
|
const agents = readAgentRuns(run);
|
|
167
262
|
// the verdict the gate counted: the latest attempt, native or headless, a FAIL shown as such
|
|
168
263
|
const verdict = milestoneVerifier(run, m, start);
|
|
@@ -193,7 +288,8 @@ export function writeDigest(
|
|
|
193
288
|
const text = [
|
|
194
289
|
`# ${m} — ${i.what}`,
|
|
195
290
|
"",
|
|
196
|
-
`Commit ${i.commit} · ${i.minutes} min · landed ${i.at}`,
|
|
291
|
+
`Commit ${i.commit} · ${i.minutes} min${i.pausedMinutes ? ` (${i.pausedMinutes} min parked or paused left out)` : ""} · landed ${i.at}`,
|
|
292
|
+
...(i.commits?.length ? [`Commits: ${i.commits.join("; ")}`] : []),
|
|
197
293
|
`A-lines: ${aLines.length ? aLines.join("; ") : "none named in what or evidence"}`,
|
|
198
294
|
"",
|
|
199
295
|
"Lanes:",
|
|
@@ -201,6 +297,8 @@ export function writeDigest(
|
|
|
201
297
|
"",
|
|
202
298
|
`Reviewer: ${reviewer ? `${reviewer.name} · ${reviewer.record ? findingCounts(run, reviewer.record) : "a Claude subagent (findings in its reply)"}` : "none"}`,
|
|
203
299
|
`Verifier: ${verdict ? `${verdict.passed ? "PASS" : `FAIL: ${verdict.verdict}`} (${verdict.name}${verdict.headless ? ", headless" : ""})` : "none"}${steps.length ? ` · carried: ${steps.map((s) => `${s.item}${s.commit ? ` from ${s.commit}` : ""}`).join(", ")}` : ""}`,
|
|
300
|
+
`Findings: ${findings.length ? findings.join(" · ") : "no BLOCKER or BUG lines"}`,
|
|
301
|
+
`Open: ${open.length ? open.map((q) => `${q.milestone} parked: ${q.question}`).join(" · ") : "none"}`,
|
|
204
302
|
`Evidence: ${i.evidence}`,
|
|
205
303
|
`Tokens: ${k(tokens.input)} in (${k(tokens.cached)} cached) · ${k(tokens.output)} out · Claude subagents ${k(reported)} (reported)`,
|
|
206
304
|
"",
|
|
@@ -1,115 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { barOf, BAR_PERCENTILE } from "../domain/bars.ts";
|
|
2
|
+
import { type Catalog, type Pair, rungInfo } from "../domain/catalog.ts";
|
|
2
3
|
import { type BillingMode, type Cost, costOf, DEFAULT_BILLING } from "../domain/cost.ts";
|
|
3
4
|
import type { Difficulty, Kind } from "../domain/lane.ts";
|
|
4
5
|
import { type EvidenceTable, evidenceLine, evidenceOf } from "./run-evidence.ts";
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
-
export interface ValueUsed {
|
|
8
|
-
dim: Dim;
|
|
9
|
-
value: number;
|
|
10
|
-
confidence: Confidence;
|
|
11
|
-
/** the source a synced value came from; `shipped` for a hand-typed one, `override` for the user's */
|
|
12
|
-
source: string;
|
|
13
|
-
benchmark: string;
|
|
14
|
-
date: string;
|
|
15
|
-
url: string;
|
|
16
|
-
/** catherd's guess for this rung: lent by a treat-like or an inferred stand-in (spec 1.2 §6.1) */
|
|
17
|
-
inferred: boolean;
|
|
18
|
-
/** the rung the value belongs to, when it is not this one */
|
|
19
|
-
from: string | null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Spec 1.2 §5.3: one threshold of the lane's bar, the value used against it, and whether it clears. */
|
|
23
|
-
export interface Threshold {
|
|
24
|
-
dim: Dim;
|
|
25
|
-
min: number;
|
|
26
|
-
used: ValueUsed | null;
|
|
27
|
-
clears: boolean;
|
|
28
|
-
/** where the default threshold came from (`barsWhy`) */
|
|
29
|
-
why: string | null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Spec 1.2 §5.3, §8: what `route` reports about the rung it chose. */
|
|
7
|
+
/** What `route` reports about the rung it chose: the one number it ranked on, the bar, and what it costs. */
|
|
33
8
|
export interface Provenance {
|
|
34
9
|
rung: string;
|
|
35
10
|
canonical: string;
|
|
36
|
-
/** the
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
11
|
+
/** the rung's Slopalytics intelligence index; null when it is not indexed */
|
|
12
|
+
intelligence: number | null;
|
|
13
|
+
/** the difficulty's bar, with the percentile it sits at; null when the route read no bar (the default rung) */
|
|
14
|
+
bar: { difficulty: Difficulty; percentile: number; min: number; clears: boolean } | null;
|
|
15
|
+
price: Pair["price"];
|
|
16
|
+
/** tokens per second; null when Slopalytics has none */
|
|
17
|
+
speed: number | null;
|
|
42
18
|
cost: Cost;
|
|
43
|
-
/**
|
|
44
|
-
|
|
19
|
+
/** when the proxy fetched the index the route read; null with no index */
|
|
20
|
+
indexedAt: string | null;
|
|
21
|
+
/** spec 1.2 §8: its runs for the lane's kind (or every kind) and over every kind; null when they could not be read */
|
|
22
|
+
evidence: { kind: string | null; all: string | null } | null;
|
|
45
23
|
}
|
|
46
24
|
|
|
47
|
-
/**
|
|
48
|
-
export function valuesUsed(c: Catalog, canonical: string): ValueUsed[] {
|
|
49
|
-
const s = scoresOf(c, canonical);
|
|
50
|
-
if (!s) return [];
|
|
51
|
-
return DIMS.flatMap((dim) => {
|
|
52
|
-
const r = s.records[dim];
|
|
53
|
-
if (!r) return [];
|
|
54
|
-
const from = s.borrowed.includes(dim) ? s.via : (s.standIns[dim] ?? null);
|
|
55
|
-
return [
|
|
56
|
-
{
|
|
57
|
-
dim,
|
|
58
|
-
value: r.value,
|
|
59
|
-
confidence: r.confidence,
|
|
60
|
-
source: r.source ?? "shipped",
|
|
61
|
-
benchmark: `${r.benchmark} ${r.version}`,
|
|
62
|
-
date: r.date,
|
|
63
|
-
url: r.url,
|
|
64
|
-
inferred: from !== null,
|
|
65
|
-
from,
|
|
66
|
-
},
|
|
67
|
-
];
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Spec 1.2 §5.3: the chosen rung's thresholds for the lane's kind and difficulty, its values and facts. */
|
|
25
|
+
/** The chosen rung's intelligence, the difficulty's bar and whether it clears it, its price, speed and cost. */
|
|
72
26
|
export function provenanceOf(
|
|
73
27
|
c: Catalog,
|
|
74
28
|
rung: string,
|
|
75
29
|
kind: Kind | null,
|
|
76
30
|
difficulty: Difficulty | null,
|
|
77
31
|
billing: Partial<Record<string, BillingMode>>,
|
|
78
|
-
evidence: EvidenceTable,
|
|
32
|
+
evidence: EvidenceTable | null,
|
|
33
|
+
at: Record<Difficulty, number> = BAR_PERCENTILE,
|
|
79
34
|
): Provenance {
|
|
80
35
|
const info = rungInfo(c, rung);
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const thresholds: Threshold[] = DIMS.flatMap((dim) => {
|
|
84
|
-
const min = bar[dim];
|
|
85
|
-
if (min === undefined || !kind || !difficulty) return [];
|
|
86
|
-
const used = values.find((v) => v.dim === dim) ?? null;
|
|
87
|
-
return [
|
|
88
|
-
{
|
|
89
|
-
dim,
|
|
90
|
-
min,
|
|
91
|
-
used,
|
|
92
|
-
clears: used !== null && used.value >= min,
|
|
93
|
-
why: c.barsWhy[dim]?.[difficulty] ?? null,
|
|
94
|
-
},
|
|
95
|
-
];
|
|
96
|
-
});
|
|
36
|
+
const intelligence = info.pair?.intelligence ?? null;
|
|
37
|
+
const min = difficulty ? barOf(c, difficulty, at) : null;
|
|
97
38
|
return {
|
|
98
39
|
rung,
|
|
99
40
|
canonical: info.canonical,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
41
|
+
intelligence,
|
|
42
|
+
bar:
|
|
43
|
+
difficulty && min !== null
|
|
44
|
+
? {
|
|
45
|
+
difficulty,
|
|
46
|
+
percentile: at[difficulty],
|
|
47
|
+
min,
|
|
48
|
+
clears: intelligence !== null && intelligence >= min,
|
|
49
|
+
}
|
|
50
|
+
: null,
|
|
51
|
+
price: info.pair?.price ?? null,
|
|
52
|
+
speed: info.pair?.speed ?? null,
|
|
53
|
+
cost: costOf(info.family, info.pair, info.parsed.effort, billing[info.key] ?? DEFAULT_BILLING[info.key]),
|
|
54
|
+
indexedAt: c.indexedAt,
|
|
55
|
+
evidence: evidence && {
|
|
105
56
|
kind: evidenceLine(evidenceOf(evidence, info.canonical, kind)),
|
|
106
57
|
all: evidenceLine(evidenceOf(evidence, info.canonical)),
|
|
107
58
|
},
|
|
108
59
|
};
|
|
109
60
|
}
|
|
110
|
-
|
|
111
|
-
/** A value as one line: `repo_code 66.6 (adjacent, shipped DeepSWE 1.1, 2026-09-22)`, marking a guess. */
|
|
112
|
-
export function valueWords(v: ValueUsed): string {
|
|
113
|
-
const lent = v.inferred ? `, inferred from ${v.from}` : "";
|
|
114
|
-
return `${v.dim} ${v.value} (${v.confidence}, ${v.source} ${v.benchmark}, ${v.date}${lent})`;
|
|
115
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { CatherdError } from "../domain/errors.ts";
|
|
3
3
|
import { assertId } from "../domain/ids.ts";
|
|
4
|
+
import type { Span } from "../domain/util.ts";
|
|
4
5
|
import { appendJsonl, ensureJsonlHeader, readJsonl } from "../infra/store.ts";
|
|
5
6
|
import type { Deps } from "./ports.ts";
|
|
6
7
|
import { findRun, type Run } from "./run-store.ts";
|
|
@@ -28,6 +29,15 @@ const rows = (run: Run): QuestionRow[] =>
|
|
|
28
29
|
(r) => typeof r?.milestone === "string" && typeof r.text === "string",
|
|
29
30
|
);
|
|
30
31
|
|
|
32
|
+
/** Plan 28: how many questions the run parked on and how many were answered. */
|
|
33
|
+
export const questionCounts = (run: Run): { questions: number; answers: number } => {
|
|
34
|
+
const all = rows(run);
|
|
35
|
+
return {
|
|
36
|
+
questions: all.filter((r) => r.kind === "question").length,
|
|
37
|
+
answers: all.filter((r) => r.kind === "answer").length,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
31
41
|
/** The questions not answered yet, oldest first; a milestone answered after its question is no longer open. */
|
|
32
42
|
export function openQuestions(run: Run): OpenQuestion[] {
|
|
33
43
|
const open = new Map<string, OpenQuestion>();
|
|
@@ -38,6 +48,20 @@ export function openQuestions(run: Run): OpenQuestion[] {
|
|
|
38
48
|
return [...open.values()];
|
|
39
49
|
}
|
|
40
50
|
|
|
51
|
+
/** Spec 1.5 "The ledger": when `milestone` was parked, question to answer; the last one open while unanswered. */
|
|
52
|
+
export function parkedSpans(run: Run, milestone: string): Span[] {
|
|
53
|
+
const out: Span[] = [];
|
|
54
|
+
for (const r of rows(run)) {
|
|
55
|
+
if (r.milestone !== milestone) continue;
|
|
56
|
+
const at = Date.parse(r.at);
|
|
57
|
+
if (Number.isNaN(at)) continue;
|
|
58
|
+
const open = out.at(-1);
|
|
59
|
+
if (r.kind === "question" && (!open || open.to !== null)) out.push({ from: at, to: null });
|
|
60
|
+
else if (r.kind === "answer" && open && open.to === null) open.to = at;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
function append(run: Run, row: QuestionRow): void {
|
|
42
66
|
const file = questionsFile(run);
|
|
43
67
|
ensureJsonlHeader(file, "questions");
|
|
@@ -3,10 +3,12 @@ import { errorMessage } from "../domain/errors.ts";
|
|
|
3
3
|
import { dispatchPaths } from "../infra/dispatch-dir.ts";
|
|
4
4
|
import { log } from "../infra/log.ts";
|
|
5
5
|
import { writeJsonAtomic } from "../infra/store.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { ensureDaemon } from "./daemon.ts";
|
|
7
|
+
import { recoverOwned, settle, stallPoll, unsettledLimits, watching } from "./dispatch-service.ts";
|
|
7
8
|
import { type Dispatch, listDispatches, pendingDispatches } from "./dispatches.ts";
|
|
8
9
|
import { finalizeDispatch, waitForFinish } from "./finalize.ts";
|
|
9
10
|
import type { Deps } from "./ports.ts";
|
|
11
|
+
import { isDaemonRun } from "./run-events-store.ts";
|
|
10
12
|
import { listRuns, type Run } from "./run-store.ts";
|
|
11
13
|
import { ownsRun } from "./sessions.ts";
|
|
12
14
|
|
|
@@ -57,6 +59,27 @@ async function watchAndFinalize(deps: Deps, run: Run, d: Dispatch): Promise<void
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Plan 22, final-review I2: a server that does not lead the boot still watches the live roles of the runs its
|
|
64
|
+
* own session owns, so a coordinator's server that restarts mid-run (an /mcp reconnect, a crash) while another
|
|
65
|
+
* session's server holds the boot lock still finalizes and announces them. It also records and announces the
|
|
66
|
+
* roles of those runs that finished while it was away (no live process, no record: PR #47 P1), as a claim
|
|
67
|
+
* does. A second watcher of a dispatch the lead also watches is harmless: finalize and failover run once, and
|
|
68
|
+
* a role another live process is finalizing is left to it. A run that cannot be read is skipped, logged.
|
|
69
|
+
*/
|
|
70
|
+
export function adoptOwned(deps: Deps): void {
|
|
71
|
+
for (const run of listRuns().runs) {
|
|
72
|
+
try {
|
|
73
|
+
if (!ownsRun(deps, run)) continue;
|
|
74
|
+
recoverOwned(deps, run, { background: true }).catch((e: unknown) =>
|
|
75
|
+
log("warn", "reconcile", { run: run.id, error: errorMessage(e) }),
|
|
76
|
+
);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
log("warn", "reconcile", { run: run.id, error: errorMessage(e) });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
/**
|
|
61
84
|
* Spec §4.7: on server start, finalize and settle each finished dispatch that has no record, settle each
|
|
62
85
|
* unread usage limit that was never failed over (its server died between the two), and watch each live one
|
|
@@ -79,6 +102,15 @@ export async function reconcileAll(deps: Deps): Promise<ReconcileReport> {
|
|
|
79
102
|
if (!report.warnings.includes(w)) report.warnings.push(w);
|
|
80
103
|
};
|
|
81
104
|
for (const run of runs) {
|
|
105
|
+
// a 2.0 run is its daemon's: an unfinished one gets its daemon back, and nothing here settles its roles
|
|
106
|
+
if (isDaemonRun(run)) {
|
|
107
|
+
try {
|
|
108
|
+
ensureDaemon(run);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
warn(run, e);
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
82
114
|
let pending;
|
|
83
115
|
try {
|
|
84
116
|
const scrubbed = scrubOldSpecs(run);
|
package/src/services/reentry.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { latestVerifierStep, type VerifierStep } from "./gate-service.ts";
|
|
2
1
|
import { protocolView } from "./protocol.ts";
|
|
3
2
|
import { type OpenQuestion, openQuestions } from "./questions.ts";
|
|
4
3
|
import type { Run } from "./run-store.ts";
|
|
5
|
-
import { readNotes } from "./state.ts";
|
|
4
|
+
import { type Notes, readNotes } from "./state.ts";
|
|
5
|
+
import { verifierStepView, type VerifierStepView } from "./verifier-step.ts";
|
|
6
6
|
|
|
7
7
|
// Spec 1.1 §8, §7 and §10: what a session re-entering a run needs first, as peek returns it: the owner questions still open, the protocol's next step with its checklist,
|
|
8
8
|
// and the verifier's latest step.
|
|
@@ -10,13 +10,14 @@ import { readNotes } from "./state.ts";
|
|
|
10
10
|
export interface Reentry {
|
|
11
11
|
questions: OpenQuestion[];
|
|
12
12
|
protocol: { next: string; checklist: string[] };
|
|
13
|
-
verifier:
|
|
13
|
+
verifier: VerifierStepView | null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** `notes`: state.json as the caller already read it (peek), so it is read once. */
|
|
17
|
+
export function reentry(run: Run, now?: number, notes: Notes = readNotes(run)): Reentry {
|
|
17
18
|
return {
|
|
18
19
|
questions: openQuestions(run),
|
|
19
|
-
protocol: protocolView(run,
|
|
20
|
-
verifier:
|
|
20
|
+
protocol: protocolView(run, notes.parked ?? [], now),
|
|
21
|
+
verifier: verifierStepView(run, now ?? Date.now()),
|
|
21
22
|
};
|
|
22
23
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CatherdError } from "../domain/errors.ts";
|
|
2
|
+
import type { RoleScope } from "../domain/role-scope.ts";
|
|
3
|
+
import { roleMcpTools } from "../domain/role-tools.ts";
|
|
4
|
+
import { latestDispatch } from "./dispatches.ts";
|
|
5
|
+
import { findRun } from "./run-store.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Spec 1.5 plan 21: what the role server lets a role do, for the CLI forms any role can run
|
|
9
|
+
* (`catherd run-file`, `catherd gate`). Outside a role (`scope` null: the user's own terminal) everything is
|
|
10
|
+
* allowed. In a role, only its own run, and only the operations its role server would list (`tool`).
|
|
11
|
+
*/
|
|
12
|
+
export function assertRoleMay(scope: RoleScope | null, run: string, tool: string): void {
|
|
13
|
+
if (!scope) return;
|
|
14
|
+
if (scope.run !== run)
|
|
15
|
+
throw new CatherdError(
|
|
16
|
+
"E_ROLE_SCOPE",
|
|
17
|
+
`this process runs ${scope.name} of run ${scope.run}, not of run ${run}`,
|
|
18
|
+
{ fix: `use run ${scope.run}` },
|
|
19
|
+
);
|
|
20
|
+
const role = latestDispatch(findRun(run), scope.name)?.admit.role;
|
|
21
|
+
if (!role || !roleMcpTools(role).includes(tool))
|
|
22
|
+
throw new CatherdError(
|
|
23
|
+
"E_ROLE_SCOPE",
|
|
24
|
+
`${scope.name} ${role ? `is a ${role}, which has no ${tool}` : `is no dispatch of run ${run}`}`,
|
|
25
|
+
{ fix: "report it in your reply; the orchestrator does it" },
|
|
26
|
+
);
|
|
27
|
+
}
|