catherd-cli 1.3.0 → 1.5.0
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 +144 -42
- package/catalog/ATTRIBUTION.md +12 -4
- package/catalog/models.json +8 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +11 -0
- package/plugin/.mcp-codex.json +10 -0
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/skills/catherd/SKILL.md +150 -79
- package/plugin/skills/catherd-setup/SKILL.md +24 -8
- package/src/adapters/access.ts +7 -1
- package/src/adapters/antigravity/index.ts +18 -3
- package/src/adapters/backend.ts +18 -2
- package/src/adapters/claude-code/index.ts +126 -14
- package/src/adapters/cli.ts +1 -64
- package/src/adapters/codex/events.ts +10 -0
- package/src/adapters/codex/index.ts +17 -5
- package/src/adapters/cursor/index.ts +7 -1
- package/src/adapters/grok/home.ts +75 -20
- package/src/adapters/grok/index.ts +27 -8
- package/src/adapters/opencode/index.ts +38 -1
- package/src/cli.ts +53 -19
- package/src/domain/agents.ts +3 -1
- package/src/domain/brief.ts +91 -0
- package/src/domain/calibration.ts +7 -0
- package/src/domain/catalog.ts +12 -1
- package/src/domain/changes.ts +23 -1
- package/src/domain/cost.ts +8 -3
- package/src/domain/errors.ts +4 -0
- package/src/domain/failover.ts +17 -5
- package/src/domain/gate-brief.ts +52 -0
- package/src/domain/hints.ts +13 -2
- package/src/domain/host.ts +17 -0
- package/src/domain/lane.ts +99 -3
- package/src/domain/notice.ts +14 -4
- package/src/domain/profile-rules.ts +129 -16
- package/src/domain/profile.ts +56 -5
- package/src/domain/record.ts +17 -2
- package/src/domain/reply.ts +29 -0
- package/src/domain/role-prompts.ts +56 -16
- package/src/domain/role-scope.ts +106 -0
- package/src/domain/role-tools.ts +34 -0
- package/src/domain/route.ts +73 -0
- package/src/domain/select.ts +177 -25
- package/src/domain/state.ts +5 -0
- package/src/domain/util.ts +22 -0
- package/src/domain/workspace.ts +130 -0
- package/src/entry/catalog-command.ts +18 -8
- package/src/entry/cli-kit.ts +17 -1
- package/src/entry/deps.ts +8 -3
- package/src/entry/doctor-command.ts +29 -2
- package/src/entry/host-arg.ts +56 -0
- package/src/entry/init-command.ts +20 -8
- package/src/entry/knowledge-command.ts +78 -3
- package/src/entry/lock-command.ts +111 -10
- package/src/entry/mcp/dispatch-tools.ts +3 -3
- package/src/entry/mcp/handshake.ts +8 -2
- package/src/entry/mcp/lane-tools.ts +55 -8
- package/src/entry/mcp/protocol-tools.ts +10 -4
- package/src/entry/mcp/push-tools.ts +16 -0
- package/src/entry/mcp/role-bin.ts +20 -0
- package/src/entry/mcp/role-server.ts +116 -0
- package/src/entry/mcp/run-tools.ts +25 -6
- package/src/entry/mcp/server.ts +224 -44
- package/src/entry/mcp/setup-tools.ts +11 -5
- package/src/entry/mcp/workspace-tools.ts +142 -0
- package/src/entry/pause-command.ts +67 -0
- package/src/entry/profile-command.ts +131 -16
- package/src/entry/role-cli-command.ts +115 -0
- package/src/entry/runs-command.ts +157 -11
- package/src/entry/supervise-command.ts +30 -3
- package/src/entry/tui/effects.ts +23 -8
- package/src/entry/tui/fixtures.ts +33 -10
- package/src/entry/tui/run.tsx +10 -4
- package/src/entry/tui/state.ts +56 -16
- package/src/entry/tui/stories.ts +11 -4
- package/src/entry/tui/views/profile-actions.ts +4 -2
- package/src/entry/tui/views/profiles.tsx +3 -3
- package/src/entry/tui/views/runs.tsx +66 -20
- package/src/entry/tui/views/save-dialog.tsx +8 -4
- package/src/entry/tui/views/status.tsx +6 -2
- package/src/entry/workspace-command.ts +44 -0
- package/src/infra/boot-lock.ts +25 -0
- package/src/infra/cli.ts +65 -0
- package/src/infra/codex-queue.ts +165 -0
- package/src/infra/delivery.ts +86 -0
- package/src/infra/dispatch-dir.ts +3 -0
- package/src/infra/env.ts +15 -2
- package/src/infra/git.ts +6 -0
- package/src/infra/heavy-lock.ts +93 -4
- package/src/infra/host-context.ts +58 -0
- package/src/infra/host-probe.ts +16 -0
- package/src/infra/lock-activity.ts +88 -0
- package/src/infra/login-env.ts +48 -0
- package/src/infra/paths.ts +35 -0
- package/src/infra/peer-inbox.ts +4 -1
- package/src/infra/role-mcp.ts +69 -0
- package/src/infra/sources/arena.ts +11 -3
- package/src/infra/sources/artificial-analysis.ts +8 -1
- package/src/infra/sources/cache.ts +2 -0
- package/src/infra/supervisor.ts +110 -18
- package/src/services/admission.ts +117 -26
- package/src/services/agent-links.ts +30 -5
- package/src/services/backends.ts +7 -0
- package/src/services/budget.ts +26 -6
- package/src/services/catalog-service.ts +20 -12
- package/src/services/credentials.ts +11 -6
- package/src/services/dispatch-service.ts +220 -22
- package/src/services/dispatches.ts +36 -3
- package/src/services/doctor-access.ts +8 -3
- package/src/services/doctor-backends.ts +30 -7
- package/src/services/doctor-checks.ts +20 -5
- package/src/services/doctor-docker.ts +221 -0
- package/src/services/doctor-push.ts +69 -90
- package/src/services/doctor-role-mcp.ts +65 -0
- package/src/services/doctor.ts +172 -38
- package/src/services/end-push.ts +157 -0
- package/src/services/finalize.ts +13 -3
- package/src/services/gate-env.ts +218 -0
- package/src/services/gate-service.ts +216 -45
- package/src/services/global-install.ts +46 -13
- package/src/services/lane-edit.ts +154 -0
- package/src/services/lane-service.ts +258 -52
- package/src/services/milestones.ts +137 -26
- package/src/services/notifier.ts +377 -72
- package/src/services/orchestrator-wait.ts +82 -0
- package/src/services/pause.ts +161 -0
- package/src/services/peek.ts +122 -23
- package/src/services/ports.ts +39 -4
- package/src/services/preflight.ts +153 -19
- package/src/services/profile-service.ts +117 -30
- package/src/services/profile-store.ts +31 -7
- package/src/services/protocol.ts +119 -25
- package/src/services/provenance.ts +16 -12
- package/src/services/questions.ts +15 -0
- package/src/services/reconcile.ts +22 -1
- package/src/services/reentry.ts +7 -6
- package/src/services/role-access.ts +27 -0
- package/src/services/routing-service.ts +150 -37
- package/src/services/run-debug.ts +98 -4
- package/src/services/run-pin.ts +125 -0
- package/src/services/run-service.ts +153 -19
- package/src/services/run-store.ts +119 -19
- package/src/services/runs-page.ts +45 -9
- package/src/services/scratch.ts +60 -0
- package/src/services/session-view.ts +18 -13
- package/src/services/sessions.ts +34 -16
- package/src/services/setup.ts +31 -10
- package/src/services/source-derive.ts +28 -5
- package/src/services/source-sync.ts +9 -2
- package/src/services/standins.ts +40 -6
- package/src/services/state.ts +21 -0
- package/src/services/summary.ts +86 -11
- package/src/services/treat-likes.ts +41 -12
- package/src/services/verifier-brief.ts +23 -0
- package/src/services/verifier-step.ts +48 -0
- package/src/services/workspace-admission.ts +267 -0
- package/src/services/workspace-service.ts +254 -0
- package/src/services/workspace-store.ts +95 -0
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,48 @@ function laneIds(run: Run): string[] {
|
|
|
44
50
|
.sort(byNumber);
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/** What laneDone reads of the run, read once for every lane of the milestone. */
|
|
54
|
+
interface LaneEvidence {
|
|
55
|
+
routes: RouteRow[];
|
|
56
|
+
live: Dispatch[];
|
|
57
|
+
records: Map<string, RunRecord>;
|
|
58
|
+
dispatches: Dispatch[];
|
|
59
|
+
agents: ReturnType<typeof readAgentRuns>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function laneEvidence(run: Run, routes: RouteRow[], live: Dispatch[]): LaneEvidence {
|
|
63
|
+
return {
|
|
64
|
+
routes,
|
|
65
|
+
live,
|
|
66
|
+
records: new Map(readRecords(run).records.map((r) => [r.dispatchId, r])),
|
|
67
|
+
dispatches: listDispatches(run),
|
|
68
|
+
agents: readAgentRuns(run),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
/**
|
|
48
73
|
* Whether `lane` needs no dispatch now: its latest dispatch or native agent run since its latest climb is
|
|
49
74
|
* still running, or ended ok with no blocked or refused reply. A lane whose last try failed, hit a limit,
|
|
50
75
|
* was blocked, or was climbed since goes back to dispatch; a plain re-route leaves an ok try standing.
|
|
51
76
|
*/
|
|
52
|
-
function laneDone(
|
|
77
|
+
function laneDone(lane: string, e: LaneEvidence): boolean {
|
|
53
78
|
const from = Math.max(
|
|
54
|
-
...routes.filter((r) => r.lane === lane && r.source === "climb").map((r) => Date.parse(r.at)),
|
|
79
|
+
...e.routes.filter((r) => r.lane === lane && r.source === "climb").map((r) => Date.parse(r.at)),
|
|
55
80
|
);
|
|
56
81
|
// a dispatch routes its lane first, so a try at the route's own time counts as after it
|
|
57
82
|
const after = (t: string) => !(Date.parse(t) < from);
|
|
58
|
-
const
|
|
59
|
-
const running = new Set(live.map((d) => d.admit.dispatchId));
|
|
83
|
+
const running = new Set(e.live.map((d) => d.admit.dispatchId));
|
|
60
84
|
const tries = [
|
|
61
|
-
...
|
|
85
|
+
...e.dispatches
|
|
62
86
|
.filter((d) => d.admit.lane === lane && after(d.admit.admittedAt))
|
|
63
87
|
.map((d) => {
|
|
64
|
-
const r = records.get(d.admit.dispatchId);
|
|
88
|
+
const r = e.records.get(d.admit.dispatchId);
|
|
65
89
|
const ok = r
|
|
66
90
|
? r.status === "ok" && r.replyStatus !== "blocked" && r.replyStatus !== "refused"
|
|
67
91
|
: running.has(d.admit.dispatchId);
|
|
68
92
|
return { at: Date.parse(d.admit.admittedAt), ok };
|
|
69
93
|
}),
|
|
70
|
-
...
|
|
94
|
+
...e.agents
|
|
71
95
|
.filter((a) => a.lane === lane && after(a.at))
|
|
72
96
|
.map((a) => ({ at: Date.parse(a.at), ok: a.status === "ok" })),
|
|
73
97
|
];
|
|
@@ -78,6 +102,27 @@ function laneDone(run: Run, lane: string, routes: RouteRow[], live: Dispatch[]):
|
|
|
78
102
|
return latest?.ok ?? false;
|
|
79
103
|
}
|
|
80
104
|
|
|
105
|
+
/** The lane's After: lanes, from its file; none when it has none or cannot be read. */
|
|
106
|
+
function afterOf(run: Run, lane: string): string[] {
|
|
107
|
+
try {
|
|
108
|
+
return parseLaneHeader(readFileSync(join(runPaths(run.dir).lanes, `${lane}.md`), "utf8")).after;
|
|
109
|
+
} catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Spec 1.5 "Lane editing": the lanes `lane`'s After: line names that have not finished: their milestone has
|
|
116
|
+
* not landed and their latest try since the latest climb is still running, or did not end ok.
|
|
117
|
+
*/
|
|
118
|
+
export function unfinishedAfter(run: Run, lane: string, routes: RouteRow[] = readRoutes(run)): string[] {
|
|
119
|
+
const landed = new Set(landedMilestones(run));
|
|
120
|
+
const after = afterOf(run, lane).filter((a) => !landed.has(milestoneOf(a)));
|
|
121
|
+
if (after.length === 0) return [];
|
|
122
|
+
const evidence = laneEvidence(run, routes, []);
|
|
123
|
+
return after.filter((a) => !laneDone(a, evidence));
|
|
124
|
+
}
|
|
125
|
+
|
|
81
126
|
/**
|
|
82
127
|
* The step the milestone loop is at: the first milestone neither landed nor parked, and within it the
|
|
83
128
|
* first of route, dispatch, collect, reviewer, verifier and land that is still to do.
|
|
@@ -98,20 +143,49 @@ export function protocolNext(run: Run, parked: string[], now = Date.now()): stri
|
|
|
98
143
|
const routed = new Set(routes.map((r) => r.lane));
|
|
99
144
|
if (mine.some((l) => !routed.has(l))) return `route and preflight ${m}'s lanes`;
|
|
100
145
|
const live = liveDispatches(run, now);
|
|
101
|
-
const
|
|
146
|
+
const evidence = laneEvidence(run, routes, live);
|
|
147
|
+
const notDone = mine.filter((l) => !laneDone(l, evidence));
|
|
102
148
|
// spec 1.1 §9: a climb past the top rung (from === rung) leaves nothing to climb to; that is a decision
|
|
103
149
|
const spent = notDone.filter((l) => {
|
|
104
150
|
const last = routes.findLast((r) => r.lane === l);
|
|
105
151
|
return last?.source === "climb" && last.from === last.rung;
|
|
106
152
|
});
|
|
107
153
|
const waiting = notDone.filter((l) => !spent.includes(l));
|
|
108
|
-
|
|
154
|
+
// an After: line keeps a lane back until the lanes it names have finished
|
|
155
|
+
const held = new Map(waiting.map((l) => [l, unfinishedAfter(run, l, routes)]));
|
|
156
|
+
const ready = waiting.filter((l) => held.get(l)?.length === 0);
|
|
157
|
+
const order = waiting
|
|
158
|
+
.filter((l) => !ready.includes(l))
|
|
159
|
+
.map((l) => `${l} after ${held.get(l)?.join(", ")}`)
|
|
160
|
+
.join("; ");
|
|
161
|
+
if (ready.length) return `dispatch ${ready.join(", ")}${order ? `; then ${order}` : ""}`;
|
|
109
162
|
if (spent.length) return `${spent.join(", ")}: out of rungs — ask finding, then the architect or park ${m}`;
|
|
110
163
|
const running = live.filter((d) => d.admit.lane !== null && mine.includes(d.admit.lane));
|
|
111
|
-
if (running.length)
|
|
164
|
+
if (running.length)
|
|
165
|
+
return `${m}: lanes running (${running.map((d) => d.admit.name).join(", ")})${order ? `; then ${order}` : ""}`;
|
|
166
|
+
if (order) return `${m}: ${order} (After:), not done: finish those lanes first`;
|
|
112
167
|
const start = milestoneStart(run, m);
|
|
113
|
-
if (!reviewerPassed(run, m, start))
|
|
114
|
-
|
|
168
|
+
if (!reviewerPassed(run, m, start)) {
|
|
169
|
+
// plan 23: a partial review is not the review: a second pass, scoped to what it did not read
|
|
170
|
+
const partial = partialReviewer(run, m, start);
|
|
171
|
+
return partial
|
|
172
|
+
? `${m}: ${partial.name} stopped partial: a scoped second pass (reviewer-${m}-2) over the files it did not read`
|
|
173
|
+
: `${m}: reviewer`;
|
|
174
|
+
}
|
|
175
|
+
// plan 23: open BLOCKER and BUG lines go to the fix round before the verifier
|
|
176
|
+
const open = openFindings(run, m, start);
|
|
177
|
+
if (open)
|
|
178
|
+
return `${m}: fix round for ${open.name}'s findings (${open.blocker} BLOCKER, ${open.bug} BUG), then the verifier`;
|
|
179
|
+
const verdict = milestoneVerifier(run, m, start);
|
|
180
|
+
if (!verdict) return `${m}: verifier`;
|
|
181
|
+
// plan 23: the machine, not the work: the owner hears of it, and no fix round runs
|
|
182
|
+
if (verdict.blocked !== null)
|
|
183
|
+
return `${m}: ${verdict.name} is blocked by the environment${verdict.blocked ? ` (${verdict.blocked})` : ""}: surface it to the owner (park ${m}), not a fix round`;
|
|
184
|
+
if (!verdict.passed) {
|
|
185
|
+
// plan 23: a resumed verifier can hang; a re-check goes to a fresh one, told what failed
|
|
186
|
+
const failed = failedItems(run, m);
|
|
187
|
+
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`;
|
|
188
|
+
}
|
|
115
189
|
return `land ${m}`;
|
|
116
190
|
}
|
|
117
191
|
|
|
@@ -121,21 +195,24 @@ export const protocolView = (run: Run, parked: string[], now?: number) => ({
|
|
|
121
195
|
checklist: PROTOCOL_CHECKLIST,
|
|
122
196
|
});
|
|
123
197
|
|
|
124
|
-
const FINDING = /^\s*(?:[-*]\s*)?(BLOCKER|BUG|NIT)\b/;
|
|
125
|
-
|
|
126
198
|
function findingCounts(run: Run, r: RunRecord | undefined): string {
|
|
127
199
|
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
|
-
}
|
|
200
|
+
const n = countFindings(run, r) ?? { BLOCKER: 0, BUG: 0, NIT: 0 };
|
|
135
201
|
const total = n.BLOCKER + n.BUG + n.NIT;
|
|
136
202
|
return `${total} finding(s): ${n.BLOCKER} BLOCKER, ${n.BUG} BUG, ${n.NIT} NIT`;
|
|
137
203
|
}
|
|
138
204
|
|
|
205
|
+
/** The reviewer's BLOCKER and BUG lines, as it wrote them: what the digest lists as findings. */
|
|
206
|
+
function findingLines(run: Run, r: RunRecord | null | undefined): string[] {
|
|
207
|
+
if (!r?.replyPath) return [];
|
|
208
|
+
const file = join(run.dir, r.replyPath);
|
|
209
|
+
const text = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
210
|
+
return text
|
|
211
|
+
.split("\n")
|
|
212
|
+
.filter((l) => /^(BLOCKER|BUG)$/.test(FINDING.exec(l)?.[1] ?? ""))
|
|
213
|
+
.map((l) => l.trim().replace(/^[-*]\s*/, ""));
|
|
214
|
+
}
|
|
215
|
+
|
|
139
216
|
const k = (x: number) => (x >= 1000 ? `${Math.round(x / 1000)}k` : String(x));
|
|
140
217
|
|
|
141
218
|
/**
|
|
@@ -145,7 +222,18 @@ const k = (x: number) => (x >= 1000 ? `${Math.round(x / 1000)}k` : String(x));
|
|
|
145
222
|
*/
|
|
146
223
|
export function writeDigest(
|
|
147
224
|
run: Run,
|
|
148
|
-
i: {
|
|
225
|
+
i: {
|
|
226
|
+
milestone: string;
|
|
227
|
+
what: string;
|
|
228
|
+
commit: string;
|
|
229
|
+
evidence: string;
|
|
230
|
+
minutes: number;
|
|
231
|
+
at: string;
|
|
232
|
+
/** spec 1.5: the commits the milestone landed, `<hash> <subject>`, oldest first */
|
|
233
|
+
commits?: string[];
|
|
234
|
+
/** spec 1.5: the parked and paused minutes `minutes` leaves out */
|
|
235
|
+
pausedMinutes?: number;
|
|
236
|
+
},
|
|
149
237
|
): string {
|
|
150
238
|
const m = i.milestone;
|
|
151
239
|
const start = milestoneStart(run, m);
|
|
@@ -163,6 +251,9 @@ export function writeDigest(
|
|
|
163
251
|
const records = readRecords(run).records;
|
|
164
252
|
// the reviewer the gate counted: since the milestone's lanes started, a dispatch or a native subagent
|
|
165
253
|
const reviewer = milestoneReviewer(run, m, start);
|
|
254
|
+
// spec 1.5: the review's BLOCKER and BUG lines, and the owner questions still open in the run
|
|
255
|
+
const findings = findingLines(run, reviewer?.record);
|
|
256
|
+
const open = openQuestions(run);
|
|
166
257
|
const agents = readAgentRuns(run);
|
|
167
258
|
// the verdict the gate counted: the latest attempt, native or headless, a FAIL shown as such
|
|
168
259
|
const verdict = milestoneVerifier(run, m, start);
|
|
@@ -193,7 +284,8 @@ export function writeDigest(
|
|
|
193
284
|
const text = [
|
|
194
285
|
`# ${m} — ${i.what}`,
|
|
195
286
|
"",
|
|
196
|
-
`Commit ${i.commit} · ${i.minutes} min · landed ${i.at}`,
|
|
287
|
+
`Commit ${i.commit} · ${i.minutes} min${i.pausedMinutes ? ` (${i.pausedMinutes} min parked or paused left out)` : ""} · landed ${i.at}`,
|
|
288
|
+
...(i.commits?.length ? [`Commits: ${i.commits.join("; ")}`] : []),
|
|
197
289
|
`A-lines: ${aLines.length ? aLines.join("; ") : "none named in what or evidence"}`,
|
|
198
290
|
"",
|
|
199
291
|
"Lanes:",
|
|
@@ -201,6 +293,8 @@ export function writeDigest(
|
|
|
201
293
|
"",
|
|
202
294
|
`Reviewer: ${reviewer ? `${reviewer.name} · ${reviewer.record ? findingCounts(run, reviewer.record) : "a Claude subagent (findings in its reply)"}` : "none"}`,
|
|
203
295
|
`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(", ")}` : ""}`,
|
|
296
|
+
`Findings: ${findings.length ? findings.join(" · ") : "no BLOCKER or BUG lines"}`,
|
|
297
|
+
`Open: ${open.length ? open.map((q) => `${q.milestone} parked: ${q.question}`).join(" · ") : "none"}`,
|
|
204
298
|
`Evidence: ${i.evidence}`,
|
|
205
299
|
`Tokens: ${k(tokens.input)} in (${k(tokens.cached)} cached) · ${k(tokens.output)} out · Claude subagents ${k(reported)} (reported)`,
|
|
206
300
|
"",
|
|
@@ -17,6 +17,8 @@ export interface ValueUsed {
|
|
|
17
17
|
inferred: boolean;
|
|
18
18
|
/** the rung the value belongs to, when it is not this one */
|
|
19
19
|
from: string | null;
|
|
20
|
+
/** what lent it: a treat-like (a mapping, `like X`) or an inferred stand-in (a guess); null for its own */
|
|
21
|
+
lent: "treat-like" | "stand-in" | null;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** Spec 1.2 §5.3: one threshold of the lane's bar, the value used against it, and whether it clears. */
|
|
@@ -40,8 +42,11 @@ export interface Provenance {
|
|
|
40
42
|
/** facts that never carry a bar (spec 1.2 §4.1): `<source>.<field>` → value */
|
|
41
43
|
speed: Record<string, number>;
|
|
42
44
|
cost: Cost;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
+
/**
|
|
46
|
+
* spec 1.2 §8: its runs for the lane's kind (or every kind) and over every kind; never used for routing;
|
|
47
|
+
* null when the runs could not be read
|
|
48
|
+
*/
|
|
49
|
+
evidence: { kind: string | null; all: string | null } | null;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/** Spec 1.2 §5.3: each value a canonical rung has, with its confidence and source, borrowed ones marked. */
|
|
@@ -51,7 +56,8 @@ export function valuesUsed(c: Catalog, canonical: string): ValueUsed[] {
|
|
|
51
56
|
return DIMS.flatMap((dim) => {
|
|
52
57
|
const r = s.records[dim];
|
|
53
58
|
if (!r) return [];
|
|
54
|
-
const
|
|
59
|
+
const borrowed = s.borrowed.includes(dim);
|
|
60
|
+
const from = borrowed ? s.via : (s.standIns[dim] ?? null);
|
|
55
61
|
return [
|
|
56
62
|
{
|
|
57
63
|
dim,
|
|
@@ -63,6 +69,7 @@ export function valuesUsed(c: Catalog, canonical: string): ValueUsed[] {
|
|
|
63
69
|
url: r.url,
|
|
64
70
|
inferred: from !== null,
|
|
65
71
|
from,
|
|
72
|
+
lent: from === null ? null : borrowed ? "treat-like" : "stand-in",
|
|
66
73
|
},
|
|
67
74
|
];
|
|
68
75
|
});
|
|
@@ -75,7 +82,7 @@ export function provenanceOf(
|
|
|
75
82
|
kind: Kind | null,
|
|
76
83
|
difficulty: Difficulty | null,
|
|
77
84
|
billing: Partial<Record<string, BillingMode>>,
|
|
78
|
-
evidence: EvidenceTable,
|
|
85
|
+
evidence: EvidenceTable | null,
|
|
79
86
|
): Provenance {
|
|
80
87
|
const info = rungInfo(c, rung);
|
|
81
88
|
const values = valuesUsed(c, info.canonical);
|
|
@@ -90,7 +97,10 @@ export function provenanceOf(
|
|
|
90
97
|
min,
|
|
91
98
|
used,
|
|
92
99
|
clears: used !== null && used.value >= min,
|
|
93
|
-
why
|
|
100
|
+
// a threshold the user's override set is theirs, whatever the default's why says (1.2 minor)
|
|
101
|
+
why: c.userBars?.includes(`${kind}/${difficulty}/${dim}`)
|
|
102
|
+
? "your override"
|
|
103
|
+
: (c.barsWhy[dim]?.[difficulty] ?? null),
|
|
94
104
|
},
|
|
95
105
|
];
|
|
96
106
|
});
|
|
@@ -101,15 +111,9 @@ export function provenanceOf(
|
|
|
101
111
|
values,
|
|
102
112
|
speed: info.family?.speed ?? {},
|
|
103
113
|
cost: costOf(info.family, info.parsed.effort, billing[info.key] ?? DEFAULT_BILLING[info.key]),
|
|
104
|
-
evidence: {
|
|
114
|
+
evidence: evidence && {
|
|
105
115
|
kind: evidenceLine(evidenceOf(evidence, info.canonical, kind)),
|
|
106
116
|
all: evidenceLine(evidenceOf(evidence, info.canonical)),
|
|
107
117
|
},
|
|
108
118
|
};
|
|
109
119
|
}
|
|
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";
|
|
@@ -38,6 +39,20 @@ export function openQuestions(run: Run): OpenQuestion[] {
|
|
|
38
39
|
return [...open.values()];
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/** Spec 1.5 "The ledger": when `milestone` was parked, question to answer; the last one open while unanswered. */
|
|
43
|
+
export function parkedSpans(run: Run, milestone: string): Span[] {
|
|
44
|
+
const out: Span[] = [];
|
|
45
|
+
for (const r of rows(run)) {
|
|
46
|
+
if (r.milestone !== milestone) continue;
|
|
47
|
+
const at = Date.parse(r.at);
|
|
48
|
+
if (Number.isNaN(at)) continue;
|
|
49
|
+
const open = out.at(-1);
|
|
50
|
+
if (r.kind === "question" && (!open || open.to !== null)) out.push({ from: at, to: null });
|
|
51
|
+
else if (r.kind === "answer" && open && open.to === null) open.to = at;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
function append(run: Run, row: QuestionRow): void {
|
|
42
57
|
const file = questionsFile(run);
|
|
43
58
|
ensureJsonlHeader(file, "questions");
|
|
@@ -3,7 +3,7 @@ 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 { settle, stallPoll, unsettledLimits, watching } from "./dispatch-service.ts";
|
|
6
|
+
import { recoverOwned, settle, stallPoll, unsettledLimits, watching } from "./dispatch-service.ts";
|
|
7
7
|
import { type Dispatch, listDispatches, pendingDispatches } from "./dispatches.ts";
|
|
8
8
|
import { finalizeDispatch, waitForFinish } from "./finalize.ts";
|
|
9
9
|
import type { Deps } from "./ports.ts";
|
|
@@ -57,6 +57,27 @@ async function watchAndFinalize(deps: Deps, run: Run, d: Dispatch): Promise<void
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Plan 22, final-review I2: a server that does not lead the boot still watches the live roles of the runs its
|
|
62
|
+
* own session owns, so a coordinator's server that restarts mid-run (an /mcp reconnect, a crash) while another
|
|
63
|
+
* session's server holds the boot lock still finalizes and announces them. It also records and announces the
|
|
64
|
+
* roles of those runs that finished while it was away (no live process, no record: PR #47 P1), as a claim
|
|
65
|
+
* does. A second watcher of a dispatch the lead also watches is harmless: finalize and failover run once, and
|
|
66
|
+
* a role another live process is finalizing is left to it. A run that cannot be read is skipped, logged.
|
|
67
|
+
*/
|
|
68
|
+
export function adoptOwned(deps: Deps): void {
|
|
69
|
+
for (const run of listRuns().runs) {
|
|
70
|
+
try {
|
|
71
|
+
if (!ownsRun(deps, run)) continue;
|
|
72
|
+
recoverOwned(deps, run, { background: true }).catch((e: unknown) =>
|
|
73
|
+
log("warn", "reconcile", { run: run.id, error: errorMessage(e) }),
|
|
74
|
+
);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
log("warn", "reconcile", { run: run.id, error: errorMessage(e) });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
/**
|
|
61
82
|
* Spec §4.7: on server start, finalize and settle each finished dispatch that has no record, settle each
|
|
62
83
|
* unread usage limit that was never failed over (its server died between the two), and watch each live one
|
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
|
+
}
|