catherd-cli 0.2.1 → 1.1.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 +141 -33
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/catalog/jev.json +89 -0
- package/catalog/models.json +236 -0
- package/catalog/scores.json +366 -0
- package/package.json +24 -8
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.mcp.json +2 -2
- package/plugin/bin/catherd-mcp +30 -0
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +160 -97
- package/plugin/skills/catherd-setup/SKILL.md +34 -24
- package/src/adapters/access.ts +63 -0
- package/src/adapters/all.ts +8 -0
- package/src/adapters/backend.ts +159 -0
- package/src/adapters/claude-code/events.ts +86 -0
- package/src/adapters/claude-code/index.ts +317 -0
- package/src/adapters/claude-code/models-api.ts +65 -0
- package/src/adapters/claude-code/models.ts +26 -0
- package/src/adapters/cli.ts +74 -0
- package/src/adapters/codex/events.ts +65 -0
- package/src/adapters/codex/home.ts +47 -0
- package/src/adapters/codex/index.ts +282 -0
- package/src/adapters/discovery.ts +87 -0
- package/src/adapters/opencode/agents.ts +84 -0
- package/src/adapters/opencode/events.ts +109 -0
- package/src/adapters/opencode/index.ts +333 -0
- package/src/adapters/registry.ts +14 -0
- package/src/cli.ts +159 -19
- package/src/domain/agents.ts +60 -0
- package/src/domain/budget.ts +45 -0
- package/src/domain/catalog.ts +262 -0
- package/src/domain/changes.ts +52 -0
- package/src/domain/cost.ts +89 -0
- package/src/domain/errors.ts +60 -0
- package/src/domain/failover.ts +119 -0
- package/src/domain/hints.ts +22 -0
- package/src/domain/ids.ts +97 -0
- package/src/domain/jev.ts +283 -0
- package/src/domain/lane.ts +92 -0
- package/src/domain/notice.ts +95 -0
- package/src/domain/profile-rules.ts +239 -0
- package/src/domain/profile.ts +389 -0
- package/src/domain/record.ts +86 -0
- package/src/{profile → domain}/role-prompts.ts +75 -27
- package/src/domain/roles.ts +25 -0
- package/src/domain/route.ts +126 -0
- package/src/domain/runtime.ts +21 -0
- package/src/domain/sanitize.ts +29 -0
- package/src/domain/secrets.ts +57 -0
- package/src/domain/select.ts +136 -0
- package/src/domain/state.ts +49 -0
- package/src/domain/util.ts +18 -0
- package/src/entry/capture-fixtures-command.ts +57 -0
- package/src/entry/catalog-command.ts +106 -0
- package/src/entry/cli-kit.ts +36 -0
- package/src/entry/deps.ts +17 -0
- package/src/entry/doctor-command.ts +51 -0
- package/src/entry/glyphs.ts +34 -0
- package/src/entry/init-command.ts +182 -0
- package/src/entry/lock-command.ts +123 -0
- package/src/{mcp/commands.ts → entry/mcp/command.ts} +5 -1
- package/src/entry/mcp/dispatch-tools.ts +52 -0
- package/src/entry/mcp/handshake.ts +58 -0
- package/src/entry/mcp/lane-tools.ts +84 -0
- package/src/entry/mcp/protocol-tools.ts +57 -0
- package/src/entry/mcp/result.ts +51 -0
- package/src/entry/mcp/run-tools.ts +145 -0
- package/src/entry/mcp/server.ts +88 -0
- package/src/entry/mcp/setup-tools.ts +81 -0
- package/src/entry/profile-command.ts +318 -0
- package/src/entry/prompt.ts +103 -0
- package/src/entry/runs-command.ts +260 -0
- package/src/entry/supervise-bin.ts +18 -0
- package/src/entry/supervise-command.ts +37 -0
- package/src/entry/tui/commands.ts +545 -0
- package/src/entry/tui/effects.ts +312 -0
- package/src/entry/tui/fixtures.ts +585 -0
- package/src/entry/tui/profile-edits.ts +138 -0
- package/src/entry/tui/profile-tree.ts +420 -0
- package/src/entry/tui/providers/app.tsx +162 -0
- package/src/entry/tui/providers/boundary.tsx +57 -0
- package/src/entry/tui/providers/data.tsx +160 -0
- package/src/entry/tui/providers/keymap.tsx +166 -0
- package/src/entry/tui/providers/root.tsx +45 -0
- package/src/entry/tui/providers/theme.tsx +17 -0
- package/src/entry/tui/providers/toast.tsx +77 -0
- package/src/entry/tui/run.tsx +98 -0
- package/src/entry/tui/state.ts +356 -0
- package/src/entry/tui/stories.ts +107 -0
- package/src/entry/tui/text.ts +130 -0
- package/src/entry/tui/theme.ts +140 -0
- package/src/entry/tui/views/app.tsx +208 -0
- package/src/entry/tui/views/dialogs.tsx +41 -0
- package/src/entry/tui/views/profile-actions.ts +321 -0
- package/src/entry/tui/views/profiles.tsx +440 -0
- package/src/entry/tui/views/runs.tsx +573 -0
- package/src/entry/tui/views/save-dialog.tsx +332 -0
- package/src/entry/tui/views/status.tsx +209 -0
- package/src/entry/tui/widgets/chrome.tsx +114 -0
- package/src/entry/tui/widgets/dialog-confirm.tsx +58 -0
- package/src/entry/tui/widgets/dialog-prompt.tsx +52 -0
- package/src/entry/tui/widgets/dialog-select.tsx +212 -0
- package/src/entry/tui/widgets/dialog.tsx +68 -0
- package/src/entry/tui/widgets/line.tsx +92 -0
- package/src/entry/tui/widgets/list.tsx +197 -0
- package/src/entry/tui/widgets/spinner.tsx +49 -0
- package/src/entry/tui/widgets/toast.tsx +62 -0
- package/src/infra/assets.ts +4 -0
- package/src/infra/claude-session.ts +98 -0
- package/src/infra/dispatch-dir.ts +286 -0
- package/src/infra/env.ts +67 -0
- package/src/infra/filelock.ts +159 -0
- package/src/infra/git.ts +91 -0
- package/src/infra/heavy-lock.ts +36 -0
- package/src/infra/jev-client.ts +162 -0
- package/src/infra/launch.ts +25 -0
- package/src/infra/log.ts +84 -0
- package/src/infra/paths.ts +38 -0
- package/src/infra/peer-inbox.ts +92 -0
- package/src/infra/proc.ts +110 -0
- package/src/infra/store.ts +229 -0
- package/src/infra/supervisor.ts +287 -0
- package/src/infra/version.ts +6 -0
- package/src/services/admission.ts +303 -0
- package/src/services/agent-links.ts +139 -0
- package/src/services/backends.ts +44 -0
- package/src/services/budget.ts +47 -0
- package/src/services/capture.ts +233 -0
- package/src/services/catalog-service.ts +382 -0
- package/src/services/dispatch-service.ts +620 -0
- package/src/services/dispatches.ts +174 -0
- package/src/services/doctor-access.ts +231 -0
- package/src/services/doctor-backends.ts +170 -0
- package/src/services/doctor-checks.ts +148 -0
- package/src/services/doctor-push.ts +109 -0
- package/src/services/doctor.ts +338 -0
- package/src/services/finalize.ts +377 -0
- package/src/services/gate-service.ts +288 -0
- package/src/services/global-install.ts +99 -0
- package/src/services/jev-service.ts +183 -0
- package/src/services/lane-service.ts +390 -0
- package/src/services/milestones.ts +208 -0
- package/src/services/notifier.ts +296 -0
- package/src/services/peek.ts +103 -0
- package/src/services/ports.ts +135 -0
- package/src/services/preflight.ts +221 -0
- package/src/services/profile-service.ts +292 -0
- package/src/services/profile-store.ts +141 -0
- package/src/services/protocol.ts +212 -0
- package/src/services/questions.ts +102 -0
- package/src/services/reconcile.ts +130 -0
- package/src/services/reentry.ts +22 -0
- package/src/services/routing-service.ts +190 -0
- package/src/services/run-debug.ts +79 -0
- package/src/services/run-service.ts +194 -0
- package/src/services/run-store.ts +334 -0
- package/src/services/runs-page.ts +254 -0
- package/src/services/session-view.ts +152 -0
- package/src/services/sessions.ts +92 -0
- package/src/services/setup.ts +87 -0
- package/src/services/state.ts +111 -0
- package/src/services/summary.ts +241 -0
- package/catalog/catalog.json +0 -112
- package/catalog/models-dev.json +0 -56453
- package/src/core/codex-events.ts +0 -49
- package/src/core/codex.ts +0 -166
- package/src/core/harness.ts +0 -89
- package/src/core/lanes.ts +0 -67
- package/src/core/lock.ts +0 -111
- package/src/core/opencode.ts +0 -207
- package/src/core/reconcile.ts +0 -26
- package/src/core/reply.ts +0 -54
- package/src/core/runstore.ts +0 -163
- package/src/core/spawn.ts +0 -89
- package/src/core/status.ts +0 -193
- package/src/files.ts +0 -19
- package/src/mcp/dispatch.ts +0 -154
- package/src/mcp/lane-tools.ts +0 -200
- package/src/mcp/out.ts +0 -6
- package/src/mcp/preflight.ts +0 -84
- package/src/mcp/run-tools.ts +0 -121
- package/src/mcp/runs.ts +0 -118
- package/src/mcp/server.ts +0 -26
- package/src/mcp/setup-tools.ts +0 -175
- package/src/paths.ts +0 -20
- package/src/profile/agents.ts +0 -130
- package/src/profile/profile.ts +0 -228
- package/src/routing/catalog.ts +0 -227
- package/src/routing/commands.ts +0 -22
- package/src/routing/jev.ts +0 -185
- package/src/routing/route.ts +0 -155
- package/src/routing/select.ts +0 -90
- package/src/tui/backends.ts +0 -57
- package/src/tui/commands.ts +0 -53
- package/src/tui/dashboard.tsx +0 -245
- package/src/tui/editor.tsx +0 -214
- package/src/tui/init.tsx +0 -208
- package/src/tui/matrix-model.ts +0 -279
- package/src/tui/matrix.tsx +0 -512
- package/src/tui/profiles.ts +0 -38
- package/src/tui/theme.ts +0 -177
- package/src/tui/ui.tsx +0 -267
- package/src/tui/watch-model.ts +0 -83
- package/src/tui/watch.tsx +0 -236
- package/src/types.ts +0 -184
- package/src/version.ts +0 -7
package/src/mcp/dispatch.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { type DispatchOpts, runCodex } from "../core/codex.ts";
|
|
5
|
-
import { recordHarness } from "../core/harness.ts";
|
|
6
|
-
import { ID, ownedFilesOf, readLane } from "../core/lanes.ts";
|
|
7
|
-
import { runOpencode } from "../core/opencode.ts";
|
|
8
|
-
import { reconcileLive } from "../core/reconcile.ts";
|
|
9
|
-
import { readLive, rolePaths } from "../core/runstore.ts";
|
|
10
|
-
import { budgetStatus, formatBudget, summarizeRun } from "../core/status.ts";
|
|
11
|
-
import { agentName } from "../profile/agents.ts";
|
|
12
|
-
import { activeProfileName, loadProfile } from "../profile/profile.ts";
|
|
13
|
-
import { loadCatalog } from "../routing/catalog.ts";
|
|
14
|
-
import { ROLES, type RunRecord } from "../types.ts";
|
|
15
|
-
import { json } from "./out.ts";
|
|
16
|
-
import { backendOf, findRun, refreshState } from "./runs.ts";
|
|
17
|
-
|
|
18
|
-
export function dispatchHints(r: RunRecord, owned: string[]): string[] {
|
|
19
|
-
const h: string[] = [];
|
|
20
|
-
if (r.status === "limit")
|
|
21
|
-
h.push(`limit: ${r.backend} hit a usage limit; the run is paused, report it and push`);
|
|
22
|
-
if (r.status === "cli-too-old")
|
|
23
|
-
h.push(`cli-too-old: tell the user to upgrade ${r.backend}: ${r.error ?? ""}`.trim());
|
|
24
|
-
if (r.status === "failed") h.push(`failed: read roles/${r.name}.err before a retry`);
|
|
25
|
-
if (r.replyStatus === "refused" || r.replyStatus === "blocked") h.push(`climb: ${r.replyStatus}`);
|
|
26
|
-
else if (r.status === "ok" && owned.length > 0 && r.changedOwned.length === 0) h.push("climb: unchanged");
|
|
27
|
-
if (r.threadHeavy) h.push("thread-heavy: its next piece starts on a fresh thread");
|
|
28
|
-
return h;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function registerDispatch(server: McpServer): void {
|
|
32
|
-
server.registerTool(
|
|
33
|
-
"dispatch",
|
|
34
|
-
{
|
|
35
|
-
description:
|
|
36
|
-
"Run one Codex or opencode role and return its RESULT record and hints when it finishes. The brief is the text itself; catherd writes it to roles/<name>.md, or roles/<name>.fix.md when resuming a thread. With lane, the lane file's Owns: line gives the owned files, and a lane overlapping a running one is refused. Call it from the main thread.",
|
|
37
|
-
inputSchema: {
|
|
38
|
-
run: z.string(),
|
|
39
|
-
role: z.enum(ROLES),
|
|
40
|
-
name: z.string().regex(ID),
|
|
41
|
-
brief: z.string().min(1),
|
|
42
|
-
rung: z.string().min(3),
|
|
43
|
-
thread: z.string().optional(),
|
|
44
|
-
lane: z.string().regex(ID).optional(),
|
|
45
|
-
next: z.string().optional(),
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
async (a, extra) => {
|
|
49
|
-
const run = findRun(a.run);
|
|
50
|
-
reconcileLive(run.dir);
|
|
51
|
-
const catalog = loadCatalog();
|
|
52
|
-
const backend = backendOf(catalog, a.rung);
|
|
53
|
-
if (backend === "claude") {
|
|
54
|
-
throw new Error(
|
|
55
|
-
`catherd: ${a.rung} is a Claude rung. Run it as Agent(subagent_type: "${agentName(a.role, a.rung)}") instead.`,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
const profile = loadProfile(activeProfileName(run.meta.repo));
|
|
59
|
-
// spec §11b: at 100% of the run budget, refuse rather than start another role that might overrun it.
|
|
60
|
-
const budget = budgetStatus(summarizeRun(run).totals, profile.budget);
|
|
61
|
-
if (budget && budget.fraction >= 1) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`catherd: run budget exhausted (${formatBudget(budget)}); ask the user before continuing`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
const owned = a.lane ? ownedFilesOf(readLane(run.dir, a.lane)) : [];
|
|
67
|
-
if (a.lane && owned.length === 0) throw new Error(`catherd: lanes/${a.lane}.md has no "Owns:" line`);
|
|
68
|
-
const live = readLive(run.dir);
|
|
69
|
-
if (live.some((m) => m.name === a.name)) throw new Error(`catherd: ${a.name} is already running`);
|
|
70
|
-
for (const m of live) {
|
|
71
|
-
const shared = m.ownedFiles.filter((f) => owned.includes(f));
|
|
72
|
-
if (shared.length) {
|
|
73
|
-
throw new Error(
|
|
74
|
-
`catherd: ${a.name} owns ${shared.join(", ")}, which running ${m.name} owns; dispatch it after that one`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const p = rolePaths(run.dir, a.name);
|
|
80
|
-
writeFileSync(a.thread ? p.fix : p.brief, a.brief);
|
|
81
|
-
await refreshState(run, {
|
|
82
|
-
starting: {
|
|
83
|
-
name: a.name,
|
|
84
|
-
rung: a.rung,
|
|
85
|
-
thread: a.thread ?? null,
|
|
86
|
-
brief: `roles/${a.name}${a.thread ? ".fix" : ""}.md`,
|
|
87
|
-
since: new Date().toISOString().slice(11, 16),
|
|
88
|
-
},
|
|
89
|
-
...(a.next ? { next: a.next } : {}),
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const token = extra._meta?.progressToken;
|
|
93
|
-
let tick = 0;
|
|
94
|
-
const runOne = (
|
|
95
|
-
rung: string,
|
|
96
|
-
thread: string | undefined,
|
|
97
|
-
rb: "codex" | "opencode",
|
|
98
|
-
): Promise<RunRecord> => {
|
|
99
|
-
const o: DispatchOpts = {
|
|
100
|
-
runDir: run.dir,
|
|
101
|
-
name: a.name,
|
|
102
|
-
role: a.role,
|
|
103
|
-
rung,
|
|
104
|
-
cwd: run.meta.repo,
|
|
105
|
-
thread,
|
|
106
|
-
ownedFiles: owned,
|
|
107
|
-
isolated: profile.harness[rb].isolated,
|
|
108
|
-
onProgress:
|
|
109
|
-
token === undefined
|
|
110
|
-
? undefined
|
|
111
|
-
: (pr) => {
|
|
112
|
-
void extra.sendNotification({
|
|
113
|
-
method: "notifications/progress",
|
|
114
|
-
params: {
|
|
115
|
-
progressToken: token,
|
|
116
|
-
progress: ++tick,
|
|
117
|
-
message: `${a.name} · ${rung} · ${pr.secs}s${pr.lastEvent ? ` · ${pr.lastEvent}` : ""}`,
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
return rb === "codex" ? runCodex(o) : runOpencode(o);
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
let record = await runOne(a.rung, a.thread, backend);
|
|
126
|
-
if (!a.thread) recordHarness(run.dir, record);
|
|
127
|
-
|
|
128
|
-
// spec §11b: a limit result re-dispatches on the profile's failover stand-in for this rung.
|
|
129
|
-
const hints: string[] = [];
|
|
130
|
-
const failoverTo = profile.failover?.[a.rung];
|
|
131
|
-
if (record.status === "limit" && failoverTo) {
|
|
132
|
-
const toBackend = backendOf(catalog, failoverTo);
|
|
133
|
-
if (toBackend === "claude") {
|
|
134
|
-
hints.push(
|
|
135
|
-
`limit: ${a.rung} hit a usage limit; its failover ${failoverTo} is a Claude rung — run it as Agent(subagent_type: "${agentName(a.role, failoverTo)}")`,
|
|
136
|
-
);
|
|
137
|
-
} else {
|
|
138
|
-
const failedRung = record.rung;
|
|
139
|
-
record = await runOne(failoverTo, undefined, toBackend);
|
|
140
|
-
if (!a.thread) recordHarness(run.dir, record);
|
|
141
|
-
hints.push(`limit: ${a.role} ${failedRung} hit a usage limit; failed over to ${failoverTo}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
await refreshState(
|
|
146
|
-
run,
|
|
147
|
-
record.status === "limit"
|
|
148
|
-
? { next: `paused: ${record.backend} usage limit; resume when the user says so` }
|
|
149
|
-
: {},
|
|
150
|
-
);
|
|
151
|
-
return json({ record, hints: [...hints, ...dispatchHints(record, owned)] });
|
|
152
|
-
},
|
|
153
|
-
);
|
|
154
|
-
}
|
package/src/mcp/lane-tools.ts
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { appendRoute, CLIMB_REASONS, currentRoute, ID } from "../core/lanes.ts";
|
|
6
|
-
import { appendLedger } from "../core/runstore.ts";
|
|
7
|
-
import { budgetStatus, summarizeRun } from "../core/status.ts";
|
|
8
|
-
import { runsRoot } from "../paths.ts";
|
|
9
|
-
import { agentName } from "../profile/agents.ts";
|
|
10
|
-
import { activeProfileName, loadProfile } from "../profile/profile.ts";
|
|
11
|
-
import { loadCatalog } from "../routing/catalog.ts";
|
|
12
|
-
import { askFinding, askSameDefect, nextRung, route as routeLane } from "../routing/route.ts";
|
|
13
|
-
import { defaultLadder } from "../routing/select.ts";
|
|
14
|
-
import { type Backend, type Catalog, ROLES, type Role, type RungId } from "../types.ts";
|
|
15
|
-
import { json, text } from "./out.ts";
|
|
16
|
-
import { backendOf, findRun, refreshState, runFile } from "./runs.ts";
|
|
17
|
-
|
|
18
|
-
export function targetOf(c: Catalog, role: Role, rung: RungId): { backend: Backend; agent?: string } {
|
|
19
|
-
const backend = backendOf(c, rung);
|
|
20
|
-
return backend === "claude" ? { backend, agent: agentName(role, rung) } : { backend };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const cell = (s: string) => s.replace(/[|\n]/g, "/").replace(/\s+/g, " ").trim();
|
|
24
|
-
|
|
25
|
-
/** git's short commit exists check: does `<commit>^{commit}` resolve in `repo`? */
|
|
26
|
-
function commitExists(repo: string, commit: string): boolean {
|
|
27
|
-
return Bun.spawnSync(["git", "-C", repo, "cat-file", "-e", `${commit}^{commit}`]).success;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** spec §11b: what past runs of this repo learned, one line per landed milestone that had something to say. */
|
|
31
|
-
const knowledgeFile = (repo: string): string => join(runsRoot(repo), "knowledge.md");
|
|
32
|
-
|
|
33
|
-
function appendKnowledge(repo: string, line: string): void {
|
|
34
|
-
const file = knowledgeFile(repo);
|
|
35
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
36
|
-
appendFileSync(file, `${line}\n`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function registerLaneTools(server: McpServer): void {
|
|
40
|
-
server.registerTool(
|
|
41
|
-
"route",
|
|
42
|
-
{
|
|
43
|
-
description:
|
|
44
|
-
"The rung for a lane (decided by Jev from its lane file, lanes/<id>.md) or, without a lane file, for a role (the profile's default), with the ladder above it. A Claude rung comes with the agent to run it as.",
|
|
45
|
-
inputSchema: {
|
|
46
|
-
run: z.string(),
|
|
47
|
-
lane_file: z.string().optional(),
|
|
48
|
-
role: z.enum(ROLES).default("worker"),
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
async (a) => {
|
|
52
|
-
const run = findRun(a.run);
|
|
53
|
-
const profile = loadProfile(activeProfileName(run.meta.repo));
|
|
54
|
-
const catalog = loadCatalog();
|
|
55
|
-
if (!a.lane_file) {
|
|
56
|
-
const d = defaultLadder(profile, catalog, a.role);
|
|
57
|
-
return json({
|
|
58
|
-
role: a.role,
|
|
59
|
-
rung: d.rung,
|
|
60
|
-
ladder: d.ladder,
|
|
61
|
-
source: "default",
|
|
62
|
-
...targetOf(catalog, a.role, d.rung),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
const file = runFile(run, a.lane_file, "read");
|
|
66
|
-
const lane = basename(file, ".md");
|
|
67
|
-
if (!ID.test(lane) || relative(run.dir, file) !== join("lanes", `${lane}.md`)) {
|
|
68
|
-
throw new Error(`catherd: a lane file lives at lanes/<id>.md, not ${a.lane_file}`);
|
|
69
|
-
}
|
|
70
|
-
const d = await routeLane({
|
|
71
|
-
runDir: run.dir,
|
|
72
|
-
profile,
|
|
73
|
-
catalog,
|
|
74
|
-
role: a.role,
|
|
75
|
-
laneText: readFileSync(file, "utf8"),
|
|
76
|
-
budget: { spentFraction: budgetStatus(summarizeRun(run).totals, profile.budget)?.fraction ?? 0 },
|
|
77
|
-
});
|
|
78
|
-
appendRoute(run.dir, {
|
|
79
|
-
at: new Date().toISOString(),
|
|
80
|
-
lane,
|
|
81
|
-
role: a.role,
|
|
82
|
-
rung: d.rung,
|
|
83
|
-
ladder: d.ladder,
|
|
84
|
-
source: "route",
|
|
85
|
-
from: null,
|
|
86
|
-
reason: null,
|
|
87
|
-
kind: d.kind,
|
|
88
|
-
difficulty: d.difficulty,
|
|
89
|
-
jev: d.source,
|
|
90
|
-
});
|
|
91
|
-
return json({ lane, role: a.role, ...d, ...targetOf(catalog, a.role, d.rung) });
|
|
92
|
-
},
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
server.registerTool(
|
|
96
|
-
"climb",
|
|
97
|
-
{
|
|
98
|
-
description:
|
|
99
|
-
"Move a routed lane one rung up its ladder and record why. Returns the next rung, or top: true when there is none. Dispatch the lane again on a fresh thread.",
|
|
100
|
-
inputSchema: {
|
|
101
|
-
run: z.string(),
|
|
102
|
-
lane: z.string().regex(ID),
|
|
103
|
-
reason: z.enum(CLIMB_REASONS),
|
|
104
|
-
evidence: z.string().optional(),
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
async (a) => {
|
|
108
|
-
const run = findRun(a.run);
|
|
109
|
-
const cur = currentRoute(run.dir, a.lane);
|
|
110
|
-
if (!cur) throw new Error(`catherd: lane ${a.lane} was never routed; call route first`);
|
|
111
|
-
const next = nextRung(cur.ladder, cur.rung);
|
|
112
|
-
appendRoute(run.dir, {
|
|
113
|
-
...cur,
|
|
114
|
-
at: new Date().toISOString(),
|
|
115
|
-
source: "climb",
|
|
116
|
-
from: cur.rung,
|
|
117
|
-
rung: next ?? cur.rung,
|
|
118
|
-
reason: a.evidence ? `${a.reason}: ${a.evidence}` : a.reason,
|
|
119
|
-
});
|
|
120
|
-
await refreshState(run, {
|
|
121
|
-
next: next
|
|
122
|
-
? `dispatch ${a.lane} at ${next} on a fresh thread`
|
|
123
|
-
: `${a.lane} failed on its top rung: ask finding, then the architect or the report`,
|
|
124
|
-
});
|
|
125
|
-
if (!next) return json({ lane: a.lane, rung: cur.rung, top: true });
|
|
126
|
-
return json({ lane: a.lane, rung: next, top: false, ...targetOf(loadCatalog(), cur.role, next) });
|
|
127
|
-
},
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
server.registerTool(
|
|
131
|
-
"ask",
|
|
132
|
-
{
|
|
133
|
-
description:
|
|
134
|
-
"Ask Jev one fixed question. finding: state { lane_file, finding } → design | code | unclear. same-defect: state { before, after } → yes | no. Falls back to the default when Jev is unsure or down. Keep the state short, in English, and free of secrets.",
|
|
135
|
-
inputSchema: {
|
|
136
|
-
run: z.string(),
|
|
137
|
-
question: z.enum(["finding", "same-defect"]),
|
|
138
|
-
state: z.record(z.string(), z.string()),
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
async (a) => {
|
|
142
|
-
const run = findRun(a.run);
|
|
143
|
-
const need = (k: string) => {
|
|
144
|
-
const v = a.state[k];
|
|
145
|
-
if (!v) throw new Error(`catherd: ${a.question} needs state.${k}`);
|
|
146
|
-
return v;
|
|
147
|
-
};
|
|
148
|
-
if (a.question === "finding") {
|
|
149
|
-
const laneText = readFileSync(runFile(run, need("lane_file"), "read"), "utf8");
|
|
150
|
-
return json(await askFinding(run.dir, laneText, need("finding")));
|
|
151
|
-
}
|
|
152
|
-
return json(await askSameDefect(run.dir, need("before"), need("after")));
|
|
153
|
-
},
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
server.registerTool(
|
|
157
|
-
"land",
|
|
158
|
-
{
|
|
159
|
-
description:
|
|
160
|
-
"Record a landed milestone after you commit it: appends its ledger row and rewrites state.md with its evidence as the last check and the next step. learned, when given, is appended to this repo's knowledge.md (read_knowledge returns it) for the next run's architect to read.",
|
|
161
|
-
inputSchema: {
|
|
162
|
-
run: z.string(),
|
|
163
|
-
milestone: z.string().min(1),
|
|
164
|
-
what: z.string().min(1),
|
|
165
|
-
commit: z.string().regex(/^[0-9a-f]{7,40}$/),
|
|
166
|
-
evidence: z.string().min(1),
|
|
167
|
-
next: z.string().min(1),
|
|
168
|
-
learned: z.string().min(1).optional(),
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
async (a) => {
|
|
172
|
-
const run = findRun(a.run);
|
|
173
|
-
if (!commitExists(run.meta.repo, a.commit))
|
|
174
|
-
throw new Error(`catherd: no commit ${a.commit} in ${run.meta.repo}`);
|
|
175
|
-
const row = [a.milestone, a.what, a.commit, a.evidence].map(cell).join(" | ");
|
|
176
|
-
appendLedger(run.dir, row);
|
|
177
|
-
await refreshState(run, { lastCheck: cell(a.evidence), next: a.next });
|
|
178
|
-
if (a.learned) {
|
|
179
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
180
|
-
appendKnowledge(run.meta.repo, `- ${date} ${run.meta.title} ${a.milestone}: ${cell(a.learned)}`);
|
|
181
|
-
}
|
|
182
|
-
return json({ ledger: row });
|
|
183
|
-
},
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
server.registerTool(
|
|
187
|
-
"read_knowledge",
|
|
188
|
-
{
|
|
189
|
-
description:
|
|
190
|
-
"What past runs of this repo learned, one line per landed milestone that said something (land's learned). Read it before an architect plans a new run.",
|
|
191
|
-
inputSchema: { repo: z.string().min(1) },
|
|
192
|
-
},
|
|
193
|
-
async (a) => {
|
|
194
|
-
const file = knowledgeFile(a.repo);
|
|
195
|
-
return text(
|
|
196
|
-
existsSync(file) ? readFileSync(file, "utf8") : "catherd: no knowledge recorded yet for this repo",
|
|
197
|
-
);
|
|
198
|
-
},
|
|
199
|
-
);
|
|
200
|
-
}
|
package/src/mcp/out.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
|
|
3
|
-
export const json = (v: unknown): CallToolResult => ({
|
|
4
|
-
content: [{ type: "text", text: JSON.stringify(v, null, 2) }],
|
|
5
|
-
});
|
|
6
|
-
export const text = (t: string): CallToolResult => ({ content: [{ type: "text", text: t }] });
|
package/src/mcp/preflight.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { fastCheckOf, ownedFilesOf } from "../core/lanes.ts";
|
|
6
|
-
import { acquire, resolveSlots } from "../core/lock.ts";
|
|
7
|
-
import { json } from "./out.ts";
|
|
8
|
-
import { findRun } from "./runs.ts";
|
|
9
|
-
|
|
10
|
-
const TAIL_LINES = 20;
|
|
11
|
-
|
|
12
|
-
interface PreflightResult {
|
|
13
|
-
lane: string;
|
|
14
|
-
check: string;
|
|
15
|
-
pass: boolean;
|
|
16
|
-
skipped?: boolean;
|
|
17
|
-
tail: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function runCheck(cwd: string, check: string): Promise<{ pass: boolean; tail: string[] }> {
|
|
21
|
-
const proc = Bun.spawn(["sh", "-c", check], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
22
|
-
const [out, err, code] = await Promise.all([
|
|
23
|
-
new Response(proc.stdout).text(),
|
|
24
|
-
new Response(proc.stderr).text(),
|
|
25
|
-
proc.exited,
|
|
26
|
-
]);
|
|
27
|
-
const tail = `${out}${err}`
|
|
28
|
-
.split("\n")
|
|
29
|
-
.filter((l) => l.trim())
|
|
30
|
-
.slice(-TAIL_LINES);
|
|
31
|
-
return { pass: code === 0, tail };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function registerPreflight(server: McpServer): void {
|
|
35
|
-
server.registerTool(
|
|
36
|
-
"preflight",
|
|
37
|
-
{
|
|
38
|
-
description:
|
|
39
|
-
"Runs each lane's fast check once, on the base tree, behind the machine-wide heavy-command lock, and reports pass/fail per lane with the tail of its output. Call it after the lane files exist and before dispatching any lane.",
|
|
40
|
-
inputSchema: { run: z.string() },
|
|
41
|
-
},
|
|
42
|
-
async (a) => {
|
|
43
|
-
const run = findRun(a.run);
|
|
44
|
-
const dir = join(run.dir, "lanes");
|
|
45
|
-
const files = existsSync(dir)
|
|
46
|
-
? readdirSync(dir)
|
|
47
|
-
.filter((f) => f.endsWith(".md"))
|
|
48
|
-
.sort()
|
|
49
|
-
: [];
|
|
50
|
-
const held = await acquire(resolveSlots(), `preflight ${run.id}`);
|
|
51
|
-
try {
|
|
52
|
-
const results: PreflightResult[] = [];
|
|
53
|
-
for (const f of files) {
|
|
54
|
-
const lane = f.slice(0, -".md".length);
|
|
55
|
-
const text = readFileSync(join(dir, f), "utf8");
|
|
56
|
-
const check = fastCheckOf(text);
|
|
57
|
-
if (!check) {
|
|
58
|
-
results.push({ lane, check: "", pass: false, tail: [`lanes/${f} has no "Fast check:" line`] });
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
// A check on a file the lane itself creates cannot run on the base tree; that is expected.
|
|
62
|
-
const unborn = ownedFilesOf(text).filter(
|
|
63
|
-
(p) => check.includes(p) && !existsSync(join(run.meta.repo, p)),
|
|
64
|
-
);
|
|
65
|
-
if (unborn.length > 0) {
|
|
66
|
-
results.push({
|
|
67
|
-
lane,
|
|
68
|
-
check,
|
|
69
|
-
pass: true,
|
|
70
|
-
skipped: true,
|
|
71
|
-
tail: [`skipped: ${unborn.join(", ")} does not exist yet; the lane creates it`],
|
|
72
|
-
});
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const r = await runCheck(run.meta.repo, check);
|
|
76
|
-
results.push({ lane, check, ...r });
|
|
77
|
-
}
|
|
78
|
-
return json({ results, allPass: results.every((r) => r.pass) });
|
|
79
|
-
} finally {
|
|
80
|
-
await held.release();
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
);
|
|
84
|
-
}
|
package/src/mcp/run-tools.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { ID } from "../core/lanes.ts";
|
|
6
|
-
import { createRun, listRuns, readRunRecords, rolePaths } from "../core/runstore.ts";
|
|
7
|
-
import { formatSummary, summarizeRun } from "../core/status.ts";
|
|
8
|
-
import { jevKey } from "../routing/jev.ts";
|
|
9
|
-
import { json, text } from "./out.ts";
|
|
10
|
-
import { findRun, refreshState, runFile } from "./runs.ts";
|
|
11
|
-
|
|
12
|
-
const CAP_LINES = 250;
|
|
13
|
-
const CAP_CHARS = 20_000;
|
|
14
|
-
|
|
15
|
-
export function capReply(reply: string, path: string): string {
|
|
16
|
-
const lines = reply.split("\n");
|
|
17
|
-
const head = lines.length > CAP_LINES ? lines.slice(0, CAP_LINES).join("\n") : reply;
|
|
18
|
-
if (head === reply && reply.length <= CAP_CHARS) return reply;
|
|
19
|
-
return `${head.slice(0, CAP_CHARS)}\n[capped: the full reply is ${path}]`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** git's toplevel for `repo`, or null when it is not inside a git repository. */
|
|
23
|
-
function gitToplevel(repo: string): string | null {
|
|
24
|
-
const proc = Bun.spawnSync(["git", "-C", repo, "rev-parse", "--show-toplevel"], {
|
|
25
|
-
stdout: "pipe",
|
|
26
|
-
stderr: "pipe",
|
|
27
|
-
});
|
|
28
|
-
return proc.success ? proc.stdout.toString("utf8").trim() : null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function registerRunTools(server: McpServer): void {
|
|
32
|
-
server.registerTool(
|
|
33
|
-
"run_start",
|
|
34
|
-
{
|
|
35
|
-
description:
|
|
36
|
-
"Start a catherd run for a git repository: creates its run folder (outside the repo) and state.md. Returns the run id and the folder.",
|
|
37
|
-
inputSchema: {
|
|
38
|
-
repo: z.string().min(1),
|
|
39
|
-
title: z.string().min(1),
|
|
40
|
-
a_lines: z.array(z.string().min(1)).min(1),
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
async ({ repo, title, a_lines }) => {
|
|
44
|
-
if (!jevKey()) throw new Error("catherd: no Jev key. Run `bunx catherd-cli init` in a terminal first.");
|
|
45
|
-
const top = gitToplevel(repo);
|
|
46
|
-
if (!top) throw new Error(`catherd: ${repo} is not inside a git repository`);
|
|
47
|
-
const run = createRun(top, title, a_lines);
|
|
48
|
-
return json({ run: run.id, dir: run.dir });
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
server.registerTool(
|
|
53
|
-
"write_run_file",
|
|
54
|
-
{
|
|
55
|
-
description:
|
|
56
|
-
"Write a file inside a run folder: plan.md, lanes/Mx.Ly.md, a dossier or notes. The path is relative to the run folder; catherd's own files are refused.",
|
|
57
|
-
inputSchema: { run: z.string(), path: z.string().min(1), content: z.string() },
|
|
58
|
-
},
|
|
59
|
-
async (a) => {
|
|
60
|
-
const file = runFile(findRun(a.run), a.path, "write");
|
|
61
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
62
|
-
writeFileSync(file, a.content);
|
|
63
|
-
return json({ path: file, bytes: Buffer.byteLength(a.content) });
|
|
64
|
-
},
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
server.registerTool(
|
|
68
|
-
"read_run_file",
|
|
69
|
-
{
|
|
70
|
-
description: "Read a file inside a run folder. The path is relative to the run folder.",
|
|
71
|
-
inputSchema: { run: z.string(), path: z.string().min(1) },
|
|
72
|
-
},
|
|
73
|
-
async (a) => text(readFileSync(runFile(findRun(a.run), a.path, "read"), "utf8")),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
server.registerTool(
|
|
77
|
-
"status",
|
|
78
|
-
{
|
|
79
|
-
description:
|
|
80
|
-
"One screen on a run: the state.md tail, live roles with their age, totals, Jev fallbacks and the harness cost. Without a run: every run with live roles, else the newest run.",
|
|
81
|
-
inputSchema: { run: z.string().optional() },
|
|
82
|
-
},
|
|
83
|
-
async (a) => {
|
|
84
|
-
if (a.run) return text(formatSummary(summarizeRun(findRun(a.run))));
|
|
85
|
-
const runs = listRuns();
|
|
86
|
-
if (runs.length === 0) return text("catherd: no runs yet");
|
|
87
|
-
const all = runs.map(summarizeRun);
|
|
88
|
-
const live = all.filter((s) => s.live.length > 0);
|
|
89
|
-
return text((live.length ? live : all.slice(0, 1)).map(formatSummary).join("\n\n"));
|
|
90
|
-
},
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
server.registerTool(
|
|
94
|
-
"result",
|
|
95
|
-
{
|
|
96
|
-
description: "A role's latest reply, capped, and its RESULT record.",
|
|
97
|
-
inputSchema: { run: z.string(), name: z.string().regex(ID) },
|
|
98
|
-
},
|
|
99
|
-
async (a) => {
|
|
100
|
-
const run = findRun(a.run);
|
|
101
|
-
const p = rolePaths(run.dir, a.name);
|
|
102
|
-
const reply = existsSync(p.out) ? readFileSync(p.out, "utf8") : "";
|
|
103
|
-
const record = readRunRecords(run.dir).findLast((r) => r.name === a.name) ?? null;
|
|
104
|
-
return json({ reply: capReply(reply, p.out), record });
|
|
105
|
-
},
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
server.registerTool(
|
|
109
|
-
"set_next",
|
|
110
|
-
{
|
|
111
|
-
description:
|
|
112
|
-
"Record the run's next step, the last line of state.md: when pausing, or when the plan changes. dispatch, climb and land write state.md themselves.",
|
|
113
|
-
inputSchema: { run: z.string(), next: z.string().min(1) },
|
|
114
|
-
},
|
|
115
|
-
async (a) => {
|
|
116
|
-
const run = findRun(a.run);
|
|
117
|
-
await refreshState(run, { next: a.next });
|
|
118
|
-
return text(readFileSync(join(run.dir, "state.md"), "utf8"));
|
|
119
|
-
},
|
|
120
|
-
);
|
|
121
|
-
}
|
package/src/mcp/runs.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { ID } from "../core/lanes.ts";
|
|
4
|
-
import { listRuns, readLive, type Run, writeState } from "../core/runstore.ts";
|
|
5
|
-
import { modelOf } from "../routing/catalog.ts";
|
|
6
|
-
import { type Backend, type Catalog, type RungId, splitRung, type StateSnapshot } from "../types.ts";
|
|
7
|
-
|
|
8
|
-
export function findRun(id: string): Run {
|
|
9
|
-
if (!ID.test(id)) throw new Error(`catherd: bad run id "${id}"`);
|
|
10
|
-
const run = listRuns().find((r) => r.id === id);
|
|
11
|
-
if (!run) throw new Error(`catherd: no run "${id}"; status() lists the runs`);
|
|
12
|
-
return run;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const SERVER_OWNED = [
|
|
16
|
-
"meta.json",
|
|
17
|
-
"state.md",
|
|
18
|
-
"state.json",
|
|
19
|
-
"ledger.md",
|
|
20
|
-
"runs.jsonl",
|
|
21
|
-
"jev.jsonl",
|
|
22
|
-
"routes.jsonl",
|
|
23
|
-
"harness.jsonl",
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const present = (p: string) => {
|
|
27
|
-
try {
|
|
28
|
-
lstatSync(p);
|
|
29
|
-
return true;
|
|
30
|
-
} catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/** The absolute path of `path` inside the run folder. Throws when it, or a symlink on the way, leaves the folder. */
|
|
36
|
-
export function runFile(run: Run, path: string, mode: "read" | "write"): string {
|
|
37
|
-
const full = resolve(run.dir, path);
|
|
38
|
-
const rel = relative(run.dir, full);
|
|
39
|
-
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || resolve(run.dir, rel) !== full) {
|
|
40
|
-
throw new Error(`catherd: "${path}" is outside the run folder`);
|
|
41
|
-
}
|
|
42
|
-
if (mode === "write" && (SERVER_OWNED.includes(rel) || rel.startsWith(`roles${sep}`))) {
|
|
43
|
-
throw new Error(`catherd: ${rel} is written by catherd itself`);
|
|
44
|
-
}
|
|
45
|
-
let probe = full;
|
|
46
|
-
while (!present(probe)) probe = dirname(probe);
|
|
47
|
-
const root = realpathSync(run.dir);
|
|
48
|
-
const real = realpathSync(probe);
|
|
49
|
-
if (real !== root && !real.startsWith(`${root}${sep}`)) {
|
|
50
|
-
throw new Error(`catherd: "${path}" resolves outside the run folder`);
|
|
51
|
-
}
|
|
52
|
-
return full;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface StateNotes {
|
|
56
|
-
next: string;
|
|
57
|
-
lastCheck: string | null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const notesFile = (run: Run) => join(run.dir, "state.json");
|
|
61
|
-
|
|
62
|
-
function readNotes(run: Run): StateNotes {
|
|
63
|
-
if (!existsSync(notesFile(run))) return { next: "plan the milestones", lastCheck: null };
|
|
64
|
-
return JSON.parse(readFileSync(notesFile(run), "utf8")) as StateNotes;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** git output, or "" on any failure (no repo, git missing, non-zero exit). */
|
|
68
|
-
function git(cwd: string, ...args: string[]): string {
|
|
69
|
-
try {
|
|
70
|
-
const proc = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
71
|
-
return proc.success ? (proc.stdout?.toString("utf8") ?? "") : "";
|
|
72
|
-
} catch {
|
|
73
|
-
return "";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function refreshState(
|
|
78
|
-
run: Run,
|
|
79
|
-
o: { starting?: StateSnapshot["running"][number]; next?: string; lastCheck?: string } = {},
|
|
80
|
-
): Promise<void> {
|
|
81
|
-
const notes = readNotes(run);
|
|
82
|
-
if (o.next !== undefined) notes.next = o.next;
|
|
83
|
-
if (o.lastCheck !== undefined) notes.lastCheck = o.lastCheck;
|
|
84
|
-
writeFileSync(notesFile(run), `${JSON.stringify(notes)}\n`);
|
|
85
|
-
|
|
86
|
-
const live = readLive(run.dir);
|
|
87
|
-
const running: StateSnapshot["running"] = live.map((m) => ({
|
|
88
|
-
name: m.name,
|
|
89
|
-
rung: m.rung,
|
|
90
|
-
thread: m.thread,
|
|
91
|
-
brief: `roles/${m.name}${m.thread ? ".fix" : ""}.md`,
|
|
92
|
-
since: m.startedAt.slice(11, 16),
|
|
93
|
-
}));
|
|
94
|
-
if (o.starting && !running.some((r) => r.name === o.starting?.name)) running.push(o.starting);
|
|
95
|
-
|
|
96
|
-
const head = git(run.meta.repo, "rev-parse", "--short", "HEAD").trim() || "none";
|
|
97
|
-
const porcelain = git(run.meta.repo, "status", "--porcelain", "--untracked-files=all");
|
|
98
|
-
const dirty = porcelain
|
|
99
|
-
.split("\n")
|
|
100
|
-
.filter((l) => l.trim())
|
|
101
|
-
.map((l) => l.slice(3))
|
|
102
|
-
.map((path) => ({ path, owner: live.find((m) => m.ownedFiles.includes(path))?.name ?? null }));
|
|
103
|
-
|
|
104
|
-
const waiting = running.map((r) => r.name);
|
|
105
|
-
writeState(run.dir, {
|
|
106
|
-
head,
|
|
107
|
-
dirty,
|
|
108
|
-
running,
|
|
109
|
-
lastCheck: notes.lastCheck,
|
|
110
|
-
next: waiting.length ? `wait for ${waiting.join(", ")}; then ${notes.next}` : notes.next,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function backendOf(c: Catalog, rung: RungId): Backend {
|
|
115
|
-
const { model } = splitRung(rung);
|
|
116
|
-
// ponytail: an opencode model the catalog does not list is recognised by its provider/ prefix
|
|
117
|
-
return modelOf(c, model)?.backend ?? (model.includes("/") ? "opencode" : "codex");
|
|
118
|
-
}
|