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/core/codex-events.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { Tokens } from "../types.ts";
|
|
2
|
-
|
|
3
|
-
export interface CodexParse {
|
|
4
|
-
thread: string | null;
|
|
5
|
-
tokens: Tokens;
|
|
6
|
-
turnFailed: boolean;
|
|
7
|
-
failure: string | null;
|
|
8
|
-
limit: boolean;
|
|
9
|
-
cliTooOld: boolean;
|
|
10
|
-
lastEvent: string | null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const LIMIT = /usage limit|rate limit reached|quota exceeded|try again later/i;
|
|
14
|
-
const TOO_OLD = /not supported when using Codex with a ChatGPT account/i;
|
|
15
|
-
|
|
16
|
-
export function parseCodexEvents(lines: string[]): CodexParse {
|
|
17
|
-
const p: CodexParse = {
|
|
18
|
-
thread: null,
|
|
19
|
-
tokens: { input: 0, cached: 0, output: 0 },
|
|
20
|
-
turnFailed: false,
|
|
21
|
-
failure: null,
|
|
22
|
-
limit: false,
|
|
23
|
-
cliTooOld: false,
|
|
24
|
-
lastEvent: null,
|
|
25
|
-
};
|
|
26
|
-
for (const line of lines) {
|
|
27
|
-
if (!line.trim().startsWith("{")) continue;
|
|
28
|
-
let e: Record<string, any>;
|
|
29
|
-
try {
|
|
30
|
-
e = JSON.parse(line);
|
|
31
|
-
} catch {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
p.lastEvent = e.item?.type ? `${e.type}/${e.item.type}` : e.type;
|
|
35
|
-
const msg: string = e.message ?? e.error?.message ?? e.item?.message ?? "";
|
|
36
|
-
if (TOO_OLD.test(msg)) p.cliTooOld = true;
|
|
37
|
-
if (e.type === "thread.started") p.thread = e.thread_id ?? null;
|
|
38
|
-
else if (e.type === "turn.completed" && e.usage) {
|
|
39
|
-
p.tokens.input += e.usage.input_tokens ?? 0;
|
|
40
|
-
p.tokens.cached += e.usage.cached_input_tokens ?? 0;
|
|
41
|
-
p.tokens.output += e.usage.output_tokens ?? 0;
|
|
42
|
-
} else if (e.type === "turn.failed") {
|
|
43
|
-
p.turnFailed = true;
|
|
44
|
-
p.failure = msg || "turn failed";
|
|
45
|
-
if (LIMIT.test(msg)) p.limit = true;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return p;
|
|
49
|
-
}
|
package/src/core/codex.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, symlinkSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { dataDir } from "../paths.ts";
|
|
5
|
-
import {
|
|
6
|
-
ROLE_SANDBOX,
|
|
7
|
-
THREAD_HEAVY_INPUT,
|
|
8
|
-
splitRung,
|
|
9
|
-
type Role,
|
|
10
|
-
type RungId,
|
|
11
|
-
type RunRecord,
|
|
12
|
-
type RunStatus,
|
|
13
|
-
} from "../types.ts";
|
|
14
|
-
import { parseCodexEvents } from "./codex-events.ts";
|
|
15
|
-
import { changedSince, parseReplyStatus, snapshotOwned } from "./reply.ts";
|
|
16
|
-
import {
|
|
17
|
-
appendRunRecord,
|
|
18
|
-
clearLive,
|
|
19
|
-
type LiveMarker,
|
|
20
|
-
rolePaths,
|
|
21
|
-
rotateRound,
|
|
22
|
-
writeLive,
|
|
23
|
-
} from "./runstore.ts";
|
|
24
|
-
import { readLines, runChild } from "./spawn.ts";
|
|
25
|
-
|
|
26
|
-
export interface DispatchOpts {
|
|
27
|
-
runDir: string;
|
|
28
|
-
name: string;
|
|
29
|
-
role: Role;
|
|
30
|
-
rung: RungId;
|
|
31
|
-
cwd: string;
|
|
32
|
-
thread?: string;
|
|
33
|
-
ownedFiles: string[];
|
|
34
|
-
isolated?: boolean;
|
|
35
|
-
idleSecs?: number;
|
|
36
|
-
onProgress?: (p: { secs: number; lastEvent: string | null }) => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function userCodexHome(): string {
|
|
40
|
-
return process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Isolated runs only. `--ignore-user-config` alone still loads the global AGENTS.md, so the home goes too. */
|
|
44
|
-
export function codexHome(): string {
|
|
45
|
-
const home = join(dataDir(), "codex-home");
|
|
46
|
-
mkdirSync(home, { recursive: true });
|
|
47
|
-
const auth = join(userCodexHome(), "auth.json");
|
|
48
|
-
const link = join(home, "auth.json");
|
|
49
|
-
if (!existsSync(link) && existsSync(auth)) symlinkSync(auth, link);
|
|
50
|
-
return home;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function codexArgs(o: DispatchOpts, replyPath: string): string[] {
|
|
54
|
-
const { model, effort } = splitRung(o.rung);
|
|
55
|
-
const sandbox = ROLE_SANDBOX[o.role];
|
|
56
|
-
const common = [
|
|
57
|
-
...(o.isolated ? ["--ignore-user-config"] : []),
|
|
58
|
-
"-m",
|
|
59
|
-
model,
|
|
60
|
-
"-c",
|
|
61
|
-
`model_reasoning_effort=${effort}`,
|
|
62
|
-
"--json",
|
|
63
|
-
"-o",
|
|
64
|
-
replyPath,
|
|
65
|
-
];
|
|
66
|
-
return o.thread
|
|
67
|
-
? ["exec", "resume", ...common, "-c", `sandbox_mode=${sandbox}`, o.thread, "-"]
|
|
68
|
-
: ["exec", ...common, "-s", sandbox, "-"];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function imagesFor(home: string, thread: string | null, since: number): string[] {
|
|
72
|
-
if (!thread) return [];
|
|
73
|
-
const dir = join(home, "generated_images", thread);
|
|
74
|
-
if (!existsSync(dir)) return [];
|
|
75
|
-
return readdirSync(dir)
|
|
76
|
-
.filter((f) => /\.(png|webp|jpe?g)$/i.test(f))
|
|
77
|
-
.map((f) => join(dir, f))
|
|
78
|
-
.filter((f) => statSync(f).mtimeMs >= since - 1000)
|
|
79
|
-
.sort();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Builds and appends the record from the files a finished run left. `code` is null after a reconcile. */
|
|
83
|
-
export function finalizeCodex(runDir: string, live: LiveMarker, code: number | null): RunRecord {
|
|
84
|
-
const p = rolePaths(runDir, live.name);
|
|
85
|
-
const ev = parseCodexEvents(readLines(p.jsonl));
|
|
86
|
-
const reply = existsSync(p.out) ? readFileSync(p.out, "utf8") : "";
|
|
87
|
-
const err = existsSync(p.err) ? readFileSync(p.err, "utf8") : "";
|
|
88
|
-
const status: RunStatus =
|
|
89
|
-
ev.cliTooOld || /not supported when using Codex with a ChatGPT account/i.test(err)
|
|
90
|
-
? "cli-too-old"
|
|
91
|
-
: ev.limit
|
|
92
|
-
? "limit"
|
|
93
|
-
: ev.turnFailed
|
|
94
|
-
? "failed"
|
|
95
|
-
: code !== 0 && !reply.trim()
|
|
96
|
-
? "failed"
|
|
97
|
-
: "ok";
|
|
98
|
-
const rs = parseReplyStatus(reply);
|
|
99
|
-
const started = Date.parse(live.startedAt);
|
|
100
|
-
const thread = ev.thread ?? live.thread;
|
|
101
|
-
const record: RunRecord = {
|
|
102
|
-
name: live.name,
|
|
103
|
-
role: live.role,
|
|
104
|
-
backend: "codex",
|
|
105
|
-
rung: live.rung,
|
|
106
|
-
thread,
|
|
107
|
-
status,
|
|
108
|
-
startedAt: live.startedAt,
|
|
109
|
-
secs: Math.round((Date.now() - started) / 1000),
|
|
110
|
-
pid: live.pid,
|
|
111
|
-
tokens: ev.tokens,
|
|
112
|
-
costUsd: null,
|
|
113
|
-
changedOwned: changedSince(live.cwd, new Map(Object.entries(live.before)), live.ownedFiles),
|
|
114
|
-
replyStatus: rs.status,
|
|
115
|
-
replyWhy: rs.why,
|
|
116
|
-
threadHeavy: ev.tokens.input > THREAD_HEAVY_INPUT,
|
|
117
|
-
isolated: live.isolated,
|
|
118
|
-
images: imagesFor(live.isolated ? codexHome() : userCodexHome(), thread, started),
|
|
119
|
-
error: status === "ok" ? null : (ev.failure ?? (err.trim().split("\n").at(-1) || `exit ${code}`)),
|
|
120
|
-
replyPath: p.out,
|
|
121
|
-
};
|
|
122
|
-
appendRunRecord(runDir, record);
|
|
123
|
-
clearLive(runDir, live.name);
|
|
124
|
-
return record;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export async function runCodex(o: DispatchOpts): Promise<RunRecord> {
|
|
128
|
-
const p = rolePaths(o.runDir, o.name);
|
|
129
|
-
rotateRound(o.runDir, o.name);
|
|
130
|
-
const isolated = o.isolated ?? false;
|
|
131
|
-
const before = Object.fromEntries(snapshotOwned(o.cwd, o.ownedFiles));
|
|
132
|
-
const startedAt = new Date().toISOString();
|
|
133
|
-
let live: LiveMarker | null = null;
|
|
134
|
-
let lastEvent: string | null = null;
|
|
135
|
-
const res = await runChild("codex", codexArgs(o, p.out), {
|
|
136
|
-
cwd: o.cwd,
|
|
137
|
-
env: isolated ? { ...process.env, CODEX_HOME: codexHome() } : process.env,
|
|
138
|
-
stdinFile: o.thread ? p.fix : p.brief,
|
|
139
|
-
stdoutFile: p.jsonl,
|
|
140
|
-
stderrFile: p.err,
|
|
141
|
-
onSpawn: ({ pid }) => {
|
|
142
|
-
live = {
|
|
143
|
-
name: o.name,
|
|
144
|
-
role: o.role,
|
|
145
|
-
backend: "codex",
|
|
146
|
-
rung: o.rung,
|
|
147
|
-
pid,
|
|
148
|
-
thread: o.thread ?? null,
|
|
149
|
-
startedAt,
|
|
150
|
-
cwd: o.cwd,
|
|
151
|
-
ownedFiles: o.ownedFiles,
|
|
152
|
-
isolated,
|
|
153
|
-
before,
|
|
154
|
-
};
|
|
155
|
-
writeLive(o.runDir, live);
|
|
156
|
-
},
|
|
157
|
-
onLines: (lines) => {
|
|
158
|
-
lastEvent = parseCodexEvents(lines).lastEvent ?? lastEvent;
|
|
159
|
-
},
|
|
160
|
-
tickMs: Number(process.env.CATHERD_TICK_MS ?? 60_000),
|
|
161
|
-
onTick: () =>
|
|
162
|
-
o.onProgress?.({ secs: Math.round((Date.now() - Date.parse(startedAt)) / 1000), lastEvent }),
|
|
163
|
-
});
|
|
164
|
-
if (!live) throw new Error("catherd: codex did not start");
|
|
165
|
-
return finalizeCodex(o.runDir, live, res.code);
|
|
166
|
-
}
|
package/src/core/harness.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { RunRecord } from "../types.ts";
|
|
4
|
-
import { appendJsonl, readJsonl, rolePaths } from "./runstore.ts";
|
|
5
|
-
|
|
6
|
-
export interface HarnessRow {
|
|
7
|
-
at: string;
|
|
8
|
-
name: string;
|
|
9
|
-
backend: "codex" | "opencode";
|
|
10
|
-
isolated: boolean;
|
|
11
|
-
firstTurnInput: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface HarnessCost {
|
|
15
|
-
backend: "codex" | "opencode";
|
|
16
|
-
nativeRuns: number;
|
|
17
|
-
isolatedRuns: number;
|
|
18
|
-
nativeMedian: number | null;
|
|
19
|
-
isolatedMedian: number | null;
|
|
20
|
-
extraPerRun: number | null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function firstTurnInput(jsonlFile: string): number | null {
|
|
24
|
-
if (!existsSync(jsonlFile)) return null;
|
|
25
|
-
for (const line of readFileSync(jsonlFile, "utf8").split("\n")) {
|
|
26
|
-
if (!line.includes('"turn.completed"')) continue;
|
|
27
|
-
try {
|
|
28
|
-
return (JSON.parse(line) as { usage?: { input_tokens?: number } }).usage?.input_tokens ?? null;
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function recordHarness(runDir: string, r: RunRecord): void {
|
|
37
|
-
const n = firstTurnInput(rolePaths(runDir, r.name).jsonl);
|
|
38
|
-
// ponytail: opencode's stream has no token usage, so only Codex runs feed the estimate
|
|
39
|
-
if (n === null) return;
|
|
40
|
-
const row: HarnessRow = {
|
|
41
|
-
at: new Date().toISOString(),
|
|
42
|
-
name: r.name,
|
|
43
|
-
backend: r.backend,
|
|
44
|
-
isolated: r.isolated,
|
|
45
|
-
firstTurnInput: n,
|
|
46
|
-
};
|
|
47
|
-
appendJsonl(join(runDir, "harness.jsonl"), row);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function median(xs: number[]): number | null {
|
|
51
|
-
if (xs.length === 0) return null;
|
|
52
|
-
const s = [...xs].sort((a, b) => a - b);
|
|
53
|
-
const m = s.length >> 1;
|
|
54
|
-
return s.length % 2 ? (s[m] as number) : Math.round(((s[m - 1] as number) + (s[m] as number)) / 2);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function harnessCosts(runDirs: string[]): HarnessCost[] {
|
|
58
|
-
const rows = runDirs.flatMap((d) => readJsonl<HarnessRow>(join(d, "harness.jsonl")));
|
|
59
|
-
return (["codex", "opencode"] as const).flatMap((backend) => {
|
|
60
|
-
const native = rows.filter((r) => r.backend === backend && !r.isolated).map((r) => r.firstTurnInput);
|
|
61
|
-
const isolated = rows.filter((r) => r.backend === backend && r.isolated).map((r) => r.firstTurnInput);
|
|
62
|
-
if (native.length === 0 && isolated.length === 0) return [];
|
|
63
|
-
const n = median(native);
|
|
64
|
-
const i = median(isolated);
|
|
65
|
-
return [
|
|
66
|
-
{
|
|
67
|
-
backend,
|
|
68
|
-
nativeRuns: native.length,
|
|
69
|
-
isolatedRuns: isolated.length,
|
|
70
|
-
nativeMedian: n,
|
|
71
|
-
isolatedMedian: i,
|
|
72
|
-
extraPerRun: n !== null && i !== null ? n - i : null,
|
|
73
|
-
},
|
|
74
|
-
];
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const k = (n: number) => `~${Math.round(n / 1000)}k`;
|
|
79
|
-
const runs = (n: number) => `${n} run${n === 1 ? "" : "s"}`;
|
|
80
|
-
|
|
81
|
-
export function formatHarness(h: HarnessCost): string {
|
|
82
|
-
if (h.extraPerRun !== null && h.nativeMedian !== null && h.isolatedMedian !== null) {
|
|
83
|
-
return `your ${h.backend} customizations add ${k(h.extraPerRun)} input tokens per run (first turn: native ${k(h.nativeMedian)} over ${runs(h.nativeRuns)}, isolated ${k(h.isolatedMedian)} over ${runs(h.isolatedRuns)})`;
|
|
84
|
-
}
|
|
85
|
-
const native = h.nativeMedian !== null;
|
|
86
|
-
const med = (native ? h.nativeMedian : h.isolatedMedian) as number;
|
|
87
|
-
const n = native ? h.nativeRuns : h.isolatedRuns;
|
|
88
|
-
return `${h.backend} ${native ? "native" : "isolated"}: median first-turn input ${k(med)} tokens over ${runs(n)}; no ${native ? "isolated" : "native"} runs to compare`;
|
|
89
|
-
}
|
package/src/core/lanes.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { Difficulty, Kind, Role, RungId } from "../types.ts";
|
|
4
|
-
import { appendJsonl, readJsonl } from "./runstore.ts";
|
|
5
|
-
|
|
6
|
-
/** Ids of runs, roles and lanes end up in file names, so they never hold a separator or start with a dot. */
|
|
7
|
-
export const ID = /^[A-Za-z0-9][\w.-]*$/;
|
|
8
|
-
|
|
9
|
-
export function ownedFilesOf(laneText: string): string[] {
|
|
10
|
-
for (const line of laneText.split("\n")) {
|
|
11
|
-
const m = /^\s*[*_]*owns[*_]*\s*:[*_]*\s*(.*)$/i.exec(line);
|
|
12
|
-
if (m) {
|
|
13
|
-
return (m[1] ?? "")
|
|
14
|
-
.split(",")
|
|
15
|
-
.map((s) => s.trim().replace(/^`+|`+$/g, ""))
|
|
16
|
-
.filter(Boolean);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function fastCheckOf(laneText: string): string | null {
|
|
23
|
-
for (const line of laneText.split("\n")) {
|
|
24
|
-
const m = /^\s*[*_]*fast check[*_]*\s*:[*_]*\s*(.*)$/i.exec(line);
|
|
25
|
-
if (m) return (m[1] ?? "").trim().replace(/^`+|`+$/g, "") || null;
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function readLane(runDir: string, lane: string): string {
|
|
31
|
-
if (!ID.test(lane)) throw new Error(`catherd: bad lane id "${lane}"`);
|
|
32
|
-
const file = join(runDir, "lanes", `${lane}.md`);
|
|
33
|
-
if (!existsSync(file))
|
|
34
|
-
throw new Error(`catherd: no lane file lanes/${lane}.md; write it with write_run_file first`);
|
|
35
|
-
return readFileSync(file, "utf8");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const CLIMB_REASONS = [
|
|
39
|
-
"check-failed-twice",
|
|
40
|
-
"blocker",
|
|
41
|
-
"same-defect",
|
|
42
|
-
"refused",
|
|
43
|
-
"blocked",
|
|
44
|
-
"unchanged",
|
|
45
|
-
] as const;
|
|
46
|
-
export type ClimbReason = (typeof CLIMB_REASONS)[number];
|
|
47
|
-
|
|
48
|
-
export interface LaneRoute {
|
|
49
|
-
at: string;
|
|
50
|
-
lane: string;
|
|
51
|
-
role: Role;
|
|
52
|
-
rung: RungId;
|
|
53
|
-
ladder: RungId[];
|
|
54
|
-
source: "route" | "climb";
|
|
55
|
-
from: RungId | null;
|
|
56
|
-
reason: string | null;
|
|
57
|
-
kind: Kind | null;
|
|
58
|
-
difficulty: Difficulty | null;
|
|
59
|
-
jev: "jev" | "default";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const routesFile = (runDir: string) => join(runDir, "routes.jsonl");
|
|
63
|
-
|
|
64
|
-
export const appendRoute = (runDir: string, r: LaneRoute): void => appendJsonl(routesFile(runDir), r);
|
|
65
|
-
export const readRoutes = (runDir: string): LaneRoute[] => readJsonl<LaneRoute>(routesFile(runDir));
|
|
66
|
-
export const currentRoute = (runDir: string, lane: string): LaneRoute | undefined =>
|
|
67
|
-
readRoutes(runDir).findLast((r) => r.lane === lane);
|
package/src/core/lock.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeSync } from "node:fs";
|
|
2
|
-
import { availableParallelism } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { defineCommand } from "citty";
|
|
5
|
-
import { dataDir } from "../paths.ts";
|
|
6
|
-
import { loadProfile } from "../profile/profile.ts";
|
|
7
|
-
|
|
8
|
-
/** Kept for interface parity across plans; reclaim here is immediate (pid liveness), not a stale-mtime window. */
|
|
9
|
-
export const LOCK_STALE_MS = 5_000;
|
|
10
|
-
|
|
11
|
-
export function heavySlots(setting: number | "cpus/2" | undefined): number {
|
|
12
|
-
if (typeof setting === "number") return Math.max(1, Math.floor(setting));
|
|
13
|
-
return Math.max(1, Math.floor(availableParallelism() / 2));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** --slots, then CATHERD_LOCK_SLOTS, then the active profile's lock.heavy, then half the cores. */
|
|
17
|
-
export function resolveSlots(flag?: string): number {
|
|
18
|
-
const raw = flag ?? process.env.CATHERD_LOCK_SLOTS;
|
|
19
|
-
if (raw) {
|
|
20
|
-
if (raw === "cpus/2") return heavySlots("cpus/2");
|
|
21
|
-
const n = Number(raw);
|
|
22
|
-
if (!Number.isFinite(n)) throw new Error(`catherd lock: slots must be a number or cpus/2, got "${raw}"`);
|
|
23
|
-
return heavySlots(n);
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
return heavySlots(loadProfile().lock.heavy);
|
|
27
|
-
} catch (e) {
|
|
28
|
-
console.error(`catherd lock: ${(e as Error).message}; using half the CPU cores`);
|
|
29
|
-
return heavySlots("cpus/2");
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function pidAlive(pid: number): boolean {
|
|
34
|
-
try {
|
|
35
|
-
process.kill(pid, 0);
|
|
36
|
-
return true;
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return (e as NodeJS.ErrnoException).code === "EPERM";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Exclusive create (`wx`): fails EEXIST when another process already holds the slot. A held slot
|
|
43
|
-
* whose pid is dead is reclaimed by removing the file and retrying, with no mtime bookkeeping. */
|
|
44
|
-
function tryTake(file: string, label: string): boolean {
|
|
45
|
-
try {
|
|
46
|
-
const fd = openSync(file, "wx");
|
|
47
|
-
writeSync(fd, JSON.stringify({ pid: process.pid, label }));
|
|
48
|
-
closeSync(fd);
|
|
49
|
-
return true;
|
|
50
|
-
} catch (e) {
|
|
51
|
-
if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e;
|
|
52
|
-
try {
|
|
53
|
-
const { pid } = JSON.parse(readFileSync(file, "utf8")) as { pid: number };
|
|
54
|
-
if (!pidAlive(pid)) {
|
|
55
|
-
rmSync(file, { force: true });
|
|
56
|
-
return tryTake(file, label);
|
|
57
|
-
}
|
|
58
|
-
} catch {
|
|
59
|
-
// corrupt or mid-write: leave it for its holder, try again on the next poll
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function acquire(
|
|
66
|
-
slots: number,
|
|
67
|
-
label: string,
|
|
68
|
-
pollMs = 2000,
|
|
69
|
-
): Promise<{ slot: number; release: () => Promise<void> }> {
|
|
70
|
-
const dir = join(dataDir(), "locks");
|
|
71
|
-
mkdirSync(dir, { recursive: true });
|
|
72
|
-
for (;;) {
|
|
73
|
-
for (let i = 0; i < slots; i++) {
|
|
74
|
-
const file = join(dir, `slot-${i}`);
|
|
75
|
-
if (!tryTake(file, label)) continue;
|
|
76
|
-
return {
|
|
77
|
-
slot: i,
|
|
78
|
-
release: async () => {
|
|
79
|
-
rmSync(file, { force: true });
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export const lockCommand = defineCommand({
|
|
88
|
-
meta: { name: "lock", description: "Run a heavy command behind the machine-wide semaphore" },
|
|
89
|
-
args: {
|
|
90
|
-
slots: { type: "string", description: "Slots (default: CATHERD_LOCK_SLOTS, else half the CPU cores)" },
|
|
91
|
-
},
|
|
92
|
-
async run({ args, rawArgs }) {
|
|
93
|
-
const sep = rawArgs.indexOf("--");
|
|
94
|
-
const [cmd, ...rest] = sep < 0 ? [] : rawArgs.slice(sep + 1);
|
|
95
|
-
if (!cmd) {
|
|
96
|
-
console.error("usage: catherd lock [--slots N] -- <command> [args...]");
|
|
97
|
-
process.exitCode = 2;
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const held = await acquire(resolveSlots(args.slots), cmd);
|
|
101
|
-
try {
|
|
102
|
-
const proc = Bun.spawn([cmd, ...rest], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
103
|
-
process.exitCode = await proc.exited;
|
|
104
|
-
} catch (e) {
|
|
105
|
-
console.error(`catherd lock: ${(e as Error).message}`);
|
|
106
|
-
process.exitCode = 1;
|
|
107
|
-
} finally {
|
|
108
|
-
await held.release();
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
});
|
package/src/core/opencode.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { dataDir } from "../paths.ts";
|
|
4
|
-
import { ROLE_SANDBOX, splitRung, type RunRecord, type RunStatus } from "../types.ts";
|
|
5
|
-
import type { DispatchOpts } from "./codex.ts";
|
|
6
|
-
import { changedSince, parseReplyStatus, snapshotOwned } from "./reply.ts";
|
|
7
|
-
import {
|
|
8
|
-
appendRunRecord,
|
|
9
|
-
clearLive,
|
|
10
|
-
type LiveMarker,
|
|
11
|
-
rolePaths,
|
|
12
|
-
rotateRound,
|
|
13
|
-
writeLive,
|
|
14
|
-
} from "./runstore.ts";
|
|
15
|
-
import { readLines, runChild } from "./spawn.ts";
|
|
16
|
-
|
|
17
|
-
/** An empty config home for isolated runs; opencode's credentials and session db live in its data dir, which stays. */
|
|
18
|
-
export function opencodeConfigHome(): string {
|
|
19
|
-
const dir = join(dataDir(), "opencode-home", "config");
|
|
20
|
-
mkdirSync(dir, { recursive: true });
|
|
21
|
-
return dir;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function opencodeArgs(o: DispatchOpts, brief: string): string[] {
|
|
25
|
-
const { model, effort } = splitRung(o.rung);
|
|
26
|
-
const spec = effort === "default" ? model : `${model}#${effort}`;
|
|
27
|
-
const sandbox = ROLE_SANDBOX[o.role];
|
|
28
|
-
const mode =
|
|
29
|
-
sandbox === "read-only"
|
|
30
|
-
? ["--agent", "explore", "--auto"]
|
|
31
|
-
: sandbox === "danger-full-access"
|
|
32
|
-
? ["--auto"]
|
|
33
|
-
: [];
|
|
34
|
-
// The background service resolves config, so only a private server sees the isolated XDG_CONFIG_HOME.
|
|
35
|
-
const server = o.isolated ? ["--standalone"] : [];
|
|
36
|
-
return [
|
|
37
|
-
"run",
|
|
38
|
-
...server,
|
|
39
|
-
"--format",
|
|
40
|
-
"json",
|
|
41
|
-
"-m",
|
|
42
|
-
spec,
|
|
43
|
-
...mode,
|
|
44
|
-
...(o.thread ? ["-s", o.thread] : []),
|
|
45
|
-
brief,
|
|
46
|
-
];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function api(cwd: string, ...a: string[]): string {
|
|
50
|
-
// env must be explicit: Bun's default env is a snapshot from when this process launched, not
|
|
51
|
-
// the current process.env, so a runtime PATH/FAKE_* override would otherwise be invisible here.
|
|
52
|
-
const proc = Bun.spawnSync(["opencode", "api", ...a], { cwd, env: process.env, stdout: "pipe" });
|
|
53
|
-
return proc.stdout?.toString("utf8") ?? "";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Verified live on opencode 2.0.15: no "status":"running" field exists. `.data` is newest-first;
|
|
57
|
-
* the session is idle iff the newest entry has `type: "idle"`, and busy otherwise (an in-flight
|
|
58
|
-
* assistant message, still streaming a tool). */
|
|
59
|
-
function sessionBusy(cwd: string, session: string): boolean {
|
|
60
|
-
try {
|
|
61
|
-
const res = JSON.parse(api(cwd, "GET", `/api/session/${session}/message`) || "{}");
|
|
62
|
-
return res?.data?.[0]?.type !== "idle";
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function parse(lines: string[]) {
|
|
69
|
-
let session: string | null = null;
|
|
70
|
-
const texts: string[] = [];
|
|
71
|
-
const errors: string[] = [];
|
|
72
|
-
for (const l of lines) {
|
|
73
|
-
let e: Record<string, any>;
|
|
74
|
-
try {
|
|
75
|
-
e = JSON.parse(l);
|
|
76
|
-
} catch {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
session ??= e.sessionID ?? null;
|
|
80
|
-
if (e.type === "text" && e.part?.text) texts.push(e.part.text);
|
|
81
|
-
if (e.type === "error") errors.push(e.error?.message ?? "error");
|
|
82
|
-
}
|
|
83
|
-
return { session, texts, errors };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function finalizeOpencode(
|
|
87
|
-
runDir: string,
|
|
88
|
-
live: LiveMarker,
|
|
89
|
-
code: number | null,
|
|
90
|
-
timedOut = false,
|
|
91
|
-
): RunRecord {
|
|
92
|
-
const p = rolePaths(runDir, live.name);
|
|
93
|
-
const ev = parse(readLines(p.jsonl));
|
|
94
|
-
const session = ev.session ?? live.thread;
|
|
95
|
-
const reply = ev.texts.join("\n");
|
|
96
|
-
writeFileSync(p.out, reply);
|
|
97
|
-
let cost = 0;
|
|
98
|
-
if (session) {
|
|
99
|
-
try {
|
|
100
|
-
cost = Number(JSON.parse(api(live.cwd, "GET", `/api/session/${session}`) || "{}")?.data?.cost ?? 0);
|
|
101
|
-
} catch {
|
|
102
|
-
cost = 0;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
const status: RunStatus = timedOut
|
|
106
|
-
? "timeout"
|
|
107
|
-
: ev.errors.length
|
|
108
|
-
? "failed"
|
|
109
|
-
: code !== 0 && !reply
|
|
110
|
-
? "failed"
|
|
111
|
-
: "ok";
|
|
112
|
-
const rs = parseReplyStatus(reply);
|
|
113
|
-
const record: RunRecord = {
|
|
114
|
-
name: live.name,
|
|
115
|
-
role: live.role,
|
|
116
|
-
backend: "opencode",
|
|
117
|
-
rung: live.rung,
|
|
118
|
-
thread: session,
|
|
119
|
-
status,
|
|
120
|
-
startedAt: live.startedAt,
|
|
121
|
-
secs: Math.round((Date.now() - Date.parse(live.startedAt)) / 1000),
|
|
122
|
-
pid: live.pid,
|
|
123
|
-
tokens: { input: 0, cached: 0, output: 0 },
|
|
124
|
-
costUsd: cost,
|
|
125
|
-
changedOwned: changedSince(live.cwd, new Map(Object.entries(live.before)), live.ownedFiles),
|
|
126
|
-
replyStatus: rs.status,
|
|
127
|
-
replyWhy: rs.why,
|
|
128
|
-
threadHeavy: false,
|
|
129
|
-
isolated: live.isolated,
|
|
130
|
-
images: [],
|
|
131
|
-
error:
|
|
132
|
-
status === "ok"
|
|
133
|
-
? null
|
|
134
|
-
: timedOut
|
|
135
|
-
? "no event within the idle limit and not busy"
|
|
136
|
-
: (ev.errors.at(-1) ?? `exit ${code}`),
|
|
137
|
-
replyPath: p.out,
|
|
138
|
-
};
|
|
139
|
-
appendRunRecord(runDir, record);
|
|
140
|
-
clearLive(runDir, live.name);
|
|
141
|
-
return record;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export async function runOpencode(o: DispatchOpts): Promise<RunRecord> {
|
|
145
|
-
const p = rolePaths(o.runDir, o.name);
|
|
146
|
-
rotateRound(o.runDir, o.name);
|
|
147
|
-
const brief = readFileSync(o.thread ? p.fix : p.brief, "utf8");
|
|
148
|
-
const isolated = o.isolated ?? false;
|
|
149
|
-
const before = Object.fromEntries(snapshotOwned(o.cwd, o.ownedFiles));
|
|
150
|
-
const startedAt = new Date().toISOString();
|
|
151
|
-
const idleMs = (o.idleSecs ?? 600) * 1000;
|
|
152
|
-
let session: string | null = o.thread ?? null;
|
|
153
|
-
let lastEventAt = Date.now();
|
|
154
|
-
let timedOut = false;
|
|
155
|
-
let kill: (() => void) | null = null;
|
|
156
|
-
let live: LiveMarker | null = null;
|
|
157
|
-
|
|
158
|
-
// A quiet session that the server says is not running a tool is stuck: interrupt it server-side
|
|
159
|
-
// (killing the client alone leaves it running and editing files), then stop the client.
|
|
160
|
-
const watchdog = setInterval(
|
|
161
|
-
() => {
|
|
162
|
-
if (Date.now() - lastEventAt < idleMs) return;
|
|
163
|
-
if (session && sessionBusy(o.cwd, session)) {
|
|
164
|
-
lastEventAt = Date.now();
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
timedOut = true;
|
|
168
|
-
if (session) api(o.cwd, "POST", `/api/session/${session}/interrupt`);
|
|
169
|
-
kill?.();
|
|
170
|
-
},
|
|
171
|
-
Math.min(1000, idleMs),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
const res = await runChild("opencode", opencodeArgs(o, brief), {
|
|
175
|
-
cwd: o.cwd,
|
|
176
|
-
env: isolated ? { ...process.env, XDG_CONFIG_HOME: opencodeConfigHome() } : process.env,
|
|
177
|
-
stdoutFile: p.jsonl,
|
|
178
|
-
stderrFile: p.err,
|
|
179
|
-
pollMs: 250,
|
|
180
|
-
onSpawn: ({ pid, kill: k }) => {
|
|
181
|
-
kill = k;
|
|
182
|
-
live = {
|
|
183
|
-
name: o.name,
|
|
184
|
-
role: o.role,
|
|
185
|
-
backend: "opencode",
|
|
186
|
-
rung: o.rung,
|
|
187
|
-
pid,
|
|
188
|
-
thread: o.thread ?? null,
|
|
189
|
-
startedAt,
|
|
190
|
-
cwd: o.cwd,
|
|
191
|
-
ownedFiles: o.ownedFiles,
|
|
192
|
-
isolated,
|
|
193
|
-
before,
|
|
194
|
-
};
|
|
195
|
-
writeLive(o.runDir, live);
|
|
196
|
-
},
|
|
197
|
-
onLines: (lines) => {
|
|
198
|
-
lastEventAt = Date.now();
|
|
199
|
-
session ??= parse(lines).session;
|
|
200
|
-
},
|
|
201
|
-
tickMs: Number(process.env.CATHERD_TICK_MS ?? 60_000),
|
|
202
|
-
onTick: () =>
|
|
203
|
-
o.onProgress?.({ secs: Math.round((Date.now() - Date.parse(startedAt)) / 1000), lastEvent: null }),
|
|
204
|
-
}).finally(() => clearInterval(watchdog));
|
|
205
|
-
if (!live) throw new Error("catherd: opencode did not start");
|
|
206
|
-
return finalizeOpencode(o.runDir, live, res.code, timedOut);
|
|
207
|
-
}
|