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
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CatherdError } from "../domain/errors.ts";
|
|
4
4
|
import type { RunRecord } from "../domain/record.ts";
|
|
5
|
-
import { git } from "../infra/git.ts";
|
|
5
|
+
import { commitExists, git } from "../infra/git.ts";
|
|
6
6
|
import { nonBlankLines } from "../infra/store.ts";
|
|
7
7
|
import { listDispatches } from "./dispatches.ts";
|
|
8
8
|
import { readAgentRuns, readRecords, readRoutes, type Run, runPaths } from "./run-store.ts";
|
|
@@ -50,27 +50,95 @@ export interface MilestoneReviewer {
|
|
|
50
50
|
record: RunRecord | null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
* ok; or a record_agent_run row with role reviewer and the same name, status ok. Null when there is none.
|
|
56
|
-
*/
|
|
57
|
-
export function milestoneReviewer(
|
|
53
|
+
/** Every reviewer attempt of the milestone since its lanes started that ended ok, oldest first, partial or not. */
|
|
54
|
+
function reviewerAttempts(
|
|
58
55
|
run: Run,
|
|
59
56
|
m: string,
|
|
60
|
-
start
|
|
61
|
-
): MilestoneReviewer
|
|
62
|
-
|
|
57
|
+
start: string | null,
|
|
58
|
+
): (MilestoneReviewer & { partial: boolean })[] {
|
|
59
|
+
return [
|
|
63
60
|
...readRecords(run)
|
|
64
61
|
.records.filter((r) => reviewsMilestone(r.name, m) && r.status === "ok" && since(r.endedAt, start))
|
|
65
|
-
.map((r) => ({ name: r.name, at: r.endedAt, record: r })),
|
|
62
|
+
.map((r) => ({ name: r.name, at: r.endedAt, record: r, partial: r.replyStatus === "partial" })),
|
|
66
63
|
...readAgentRuns(run)
|
|
67
64
|
.filter(
|
|
68
65
|
(a) =>
|
|
69
66
|
a.role === "reviewer" && reviewsMilestone(a.name, m) && a.status === "ok" && since(a.at, start),
|
|
70
67
|
)
|
|
71
|
-
.map((a) => ({ name: a.name, at: a.at, record: null })),
|
|
72
|
-
];
|
|
73
|
-
|
|
68
|
+
.map((a) => ({ name: a.name, at: a.at, record: null, partial: a.replyStatus === "partial" })),
|
|
69
|
+
].sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The milestone's latest passing reviewer since its lanes started: a dispatch named reviewer-<m>…, status
|
|
74
|
+
* ok; or a record_agent_run row with role reviewer and the same name, status ok. A reply of STATUS: partial
|
|
75
|
+
* read only part of the diff and is not the milestone's review (plan 23). Null when there is none.
|
|
76
|
+
*/
|
|
77
|
+
export function milestoneReviewer(
|
|
78
|
+
run: Run,
|
|
79
|
+
m: string,
|
|
80
|
+
start = milestoneStart(run, m),
|
|
81
|
+
): MilestoneReviewer | null {
|
|
82
|
+
const full = reviewerAttempts(run, m, start).filter((a) => !a.partial);
|
|
83
|
+
const last = full.at(-1);
|
|
84
|
+
return last ? { name: last.name, at: last.at, record: last.record } : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Plan 23: the milestone's latest reviewer that stopped at STATUS: partial, when no full review stands; else null. */
|
|
88
|
+
export function partialReviewer(
|
|
89
|
+
run: Run,
|
|
90
|
+
m: string,
|
|
91
|
+
start = milestoneStart(run, m),
|
|
92
|
+
): MilestoneReviewer | null {
|
|
93
|
+
if (milestoneReviewer(run, m, start)) return null;
|
|
94
|
+
const last = reviewerAttempts(run, m, start)
|
|
95
|
+
.filter((a) => a.partial)
|
|
96
|
+
.at(-1);
|
|
97
|
+
return last ? { name: last.name, at: last.at, record: last.record } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const FINDING = /^\s*(?:[-*]\s*)?(BLOCKER|BUG|NIT)\b/;
|
|
101
|
+
|
|
102
|
+
/** A reviewer reply's findings by severity; null when there is no reply to read. */
|
|
103
|
+
export function countFindings(run: Run, r: RunRecord): { BLOCKER: number; BUG: number; NIT: number } | null {
|
|
104
|
+
const file = join(run.dir, r.replyPath);
|
|
105
|
+
if (!r.replyPath || !existsSync(file)) return null;
|
|
106
|
+
const n = { BLOCKER: 0, BUG: 0, NIT: 0 };
|
|
107
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
108
|
+
const k = FINDING.exec(line)?.[1] as keyof typeof n | undefined;
|
|
109
|
+
if (k) n[k]++;
|
|
110
|
+
}
|
|
111
|
+
return n;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The roles a fix round runs as. */
|
|
115
|
+
const FIX_ROLES = new Set(["worker", "writer", "artist"]);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Plan 23: the milestone reviewer's BLOCKER and BUG lines while no fix round has ended since (a worker,
|
|
119
|
+
* writer or artist of the milestone, ended ok after the review); null when there are none, or for a native
|
|
120
|
+
* reviewer, whose reply catherd does not keep.
|
|
121
|
+
*/
|
|
122
|
+
export function openFindings(
|
|
123
|
+
run: Run,
|
|
124
|
+
m: string,
|
|
125
|
+
start = milestoneStart(run, m),
|
|
126
|
+
): { name: string; blocker: number; bug: number } | null {
|
|
127
|
+
const reviewer = milestoneReviewer(run, m, start);
|
|
128
|
+
if (!reviewer?.record) return null;
|
|
129
|
+
const n = countFindings(run, reviewer.record);
|
|
130
|
+
if (!n || n.BLOCKER + n.BUG === 0) return null;
|
|
131
|
+
const after = (at: string) => Date.parse(at) > Date.parse(reviewer.at);
|
|
132
|
+
const ofM = (name: string, lane: string | null | undefined) =>
|
|
133
|
+
inMilestone(lane, m) || namesMilestone(name, m);
|
|
134
|
+
const fixed =
|
|
135
|
+
readRecords(run).records.some(
|
|
136
|
+
(r) => FIX_ROLES.has(r.role) && r.status === "ok" && ofM(r.name, r.lane) && after(r.endedAt),
|
|
137
|
+
) ||
|
|
138
|
+
readAgentRuns(run).some(
|
|
139
|
+
(a) => FIX_ROLES.has(a.role) && a.status === "ok" && ofM(a.name, a.lane) && after(a.at),
|
|
140
|
+
);
|
|
141
|
+
return fixed ? null : { name: reviewer.name, blocker: n.BLOCKER, bug: n.BUG };
|
|
74
142
|
}
|
|
75
143
|
|
|
76
144
|
/** A reviewer record for the milestone since its lanes started (see milestoneReviewer). */
|
|
@@ -80,6 +148,15 @@ export const reviewerPassed = (run: Run, m: string, start = milestoneStart(run,
|
|
|
80
148
|
/** The verifier's contract puts `VERDICT: PASS` or `VERDICT: FAIL` on the reply's first line (role-prompts.ts). */
|
|
81
149
|
const VERDICT_PASS = /^VERDICT: PASS\b/;
|
|
82
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Plan 23: `VERDICT: BLOCKED: environment — <the probe that proves it>`, a verdict on the machine, not on the
|
|
153
|
+
* work. Its probe, or null when `line` is no such verdict ("" when it names none).
|
|
154
|
+
*/
|
|
155
|
+
export function blockedByEnvironment(line: string | null | undefined): string | null {
|
|
156
|
+
const m = /^VERDICT:\s*BLOCKED:\s*environment\b\s*(?:—|–|-|:)?\s*(.*)$/i.exec(line?.trim() ?? "");
|
|
157
|
+
return m ? (m[1] ?? "").trim() : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
83
160
|
/** A headless verifier's reply: its first non-blank line, trimmed; null when there is no reply to read. */
|
|
84
161
|
export function replyVerdict(run: Run, r: RunRecord): string | null {
|
|
85
162
|
const file = join(run.dir, r.replyPath);
|
|
@@ -106,11 +183,17 @@ export interface MilestoneVerifier {
|
|
|
106
183
|
passed: boolean;
|
|
107
184
|
/** what the attempt said: a native row's status, or a headless reply's first non-blank line ("no reply") */
|
|
108
185
|
verdict: string;
|
|
186
|
+
/** plan 23: the probe of a `VERDICT: BLOCKED: environment` attempt ("" when it named none); null otherwise */
|
|
187
|
+
blocked: string | null;
|
|
109
188
|
}
|
|
110
189
|
|
|
190
|
+
/** The one name a milestone's verifier is counted under (spec 1.5 "The ledger"): verifier-<m>, exactly. */
|
|
191
|
+
export const verifierName = (m: string): string => `verifier-${m}`;
|
|
192
|
+
|
|
111
193
|
/**
|
|
112
194
|
* The milestone's latest verifier attempt since its lanes started, whatever it said: a record_agent_run row
|
|
113
|
-
* with role verifier
|
|
195
|
+
* with role verifier named exactly verifier-<m> (the skill records a FAIL as failed; verifier-M2-pre or
|
|
196
|
+
* verifier-M2-gate1 is not M2's verifier), passing when its
|
|
114
197
|
* status is ok; or a headless verifier's dispatch record of the same shape, passing when it exited ok and its
|
|
115
198
|
* reply opens VERDICT: PASS (status ok means only the CLI exited). A later FAIL undoes an earlier PASS. Null
|
|
116
199
|
* when there is none.
|
|
@@ -122,16 +205,21 @@ export function milestoneVerifier(
|
|
|
122
205
|
): MilestoneVerifier | null {
|
|
123
206
|
const found: MilestoneVerifier[] = [
|
|
124
207
|
...readAgentRuns(run)
|
|
125
|
-
.filter((a) => a.role === "verifier" &&
|
|
126
|
-
.map((a) =>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
208
|
+
.filter((a) => a.role === "verifier" && a.name === verifierName(m) && since(a.at, start))
|
|
209
|
+
.map((a) => {
|
|
210
|
+
// plan 23: a native verifier's verdict line, when record_agent_run passed it
|
|
211
|
+
const blocked = a.status === "ok" ? null : blockedByEnvironment(a.verdict);
|
|
212
|
+
return {
|
|
213
|
+
name: a.name,
|
|
214
|
+
at: a.at,
|
|
215
|
+
headless: false,
|
|
216
|
+
passed: a.status === "ok",
|
|
217
|
+
verdict: blocked === null ? a.status : (a.verdict ?? a.status),
|
|
218
|
+
blocked,
|
|
219
|
+
};
|
|
220
|
+
}),
|
|
133
221
|
...readRecords(run)
|
|
134
|
-
.records.filter((r) => r.role === "verifier" &&
|
|
222
|
+
.records.filter((r) => r.role === "verifier" && r.name === verifierName(m) && since(r.endedAt, start))
|
|
135
223
|
.map((r) => {
|
|
136
224
|
const first = replyVerdict(run, r);
|
|
137
225
|
const passed = r.status === "ok" && first !== null && VERDICT_PASS.test(first);
|
|
@@ -142,6 +230,7 @@ export function milestoneVerifier(
|
|
|
142
230
|
headless: true,
|
|
143
231
|
passed,
|
|
144
232
|
verdict: r.status === "ok" ? said : `${r.status}, ${said}`,
|
|
233
|
+
blocked: r.status === "ok" ? blockedByEnvironment(first) : null,
|
|
145
234
|
};
|
|
146
235
|
}),
|
|
147
236
|
];
|
|
@@ -170,12 +259,16 @@ export function landedMilestones(run: Run): string[] {
|
|
|
170
259
|
}
|
|
171
260
|
|
|
172
261
|
/**
|
|
173
|
-
* The files the milestone's commit range changed: from the previous landed commit
|
|
174
|
-
*
|
|
262
|
+
* The files the milestone's commit range changed: from the previous landed commit, else the HEAD the run
|
|
263
|
+
* started on (meta.json `startHead`, when that commit is still there), else the commit's own parent, to
|
|
264
|
+
* `commit`. Throws E_IO_UNEXPECTED when git cannot say.
|
|
175
265
|
*/
|
|
176
266
|
export async function milestoneFiles(run: Run, commit: string): Promise<string[]> {
|
|
177
|
-
const base = landedCommits(run).at(-1);
|
|
178
267
|
const repo = run.meta.repo;
|
|
268
|
+
const start = run.meta.startHead;
|
|
269
|
+
const base =
|
|
270
|
+
landedCommits(run).at(-1) ??
|
|
271
|
+
(start && (await commitExists(repo, start).catch(() => false)) ? start : undefined);
|
|
179
272
|
const r = base
|
|
180
273
|
? await git(repo, ["diff", "--name-only", base, commit])
|
|
181
274
|
: await git(repo, ["show", "--name-only", "--format=", "--first-parent", commit]);
|
|
@@ -186,6 +279,24 @@ export async function milestoneFiles(run: Run, commit: string): Promise<string[]
|
|
|
186
279
|
return r.out.split("\n").filter(Boolean);
|
|
187
280
|
}
|
|
188
281
|
|
|
282
|
+
/** The most commits a digest lists; the rest are counted. */
|
|
283
|
+
const DIGEST_COMMITS = 20;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The commits the milestone lands, `<short hash> <subject>`, oldest first: from the previous landed commit
|
|
287
|
+
* (else the commit alone) to `commit`. Just the hash when git cannot say: a digest never fails a landing.
|
|
288
|
+
*/
|
|
289
|
+
export async function milestoneCommits(run: Run, commit: string): Promise<string[]> {
|
|
290
|
+
const base = landedCommits(run).at(-1);
|
|
291
|
+
const range = base ? [`${base}..${commit}`] : ["-1", commit];
|
|
292
|
+
const r = await git(run.meta.repo, ["log", "--reverse", "--format=%h %s", ...range]);
|
|
293
|
+
if (r.kind !== "ok") return [commit.slice(0, 7)];
|
|
294
|
+
const all = r.out.split("\n").filter(Boolean);
|
|
295
|
+
return all.length > DIGEST_COMMITS
|
|
296
|
+
? [...all.slice(-DIGEST_COMMITS), `and ${all.length - DIGEST_COMMITS} earlier`]
|
|
297
|
+
: all;
|
|
298
|
+
}
|
|
299
|
+
|
|
189
300
|
/** `rev`'s full commit hash in the run's repo. Throws E_IO_UNEXPECTED when git cannot say. */
|
|
190
301
|
export async function fullCommit(run: Run, rev: string): Promise<string> {
|
|
191
302
|
const r = await git(run.meta.repo, ["rev-parse", "--verify", "--quiet", `${rev}^{commit}`]);
|