catherd-cli 1.4.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 +93 -30
- 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 +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/skills/catherd/SKILL.md +110 -45
- package/plugin/skills/catherd-setup/SKILL.md +2 -2
- 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/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 +5 -0
- 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/lane.ts +99 -3
- package/src/domain/notice.ts +9 -2
- package/src/domain/profile-rules.ts +112 -16
- package/src/domain/profile.ts +24 -0
- package/src/domain/record.ts +16 -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 +14 -6
- package/src/entry/cli-kit.ts +17 -1
- package/src/entry/deps.ts +2 -0
- package/src/entry/doctor-command.ts +20 -2
- package/src/entry/host-arg.ts +28 -0
- package/src/entry/init-command.ts +2 -1
- package/src/entry/knowledge-command.ts +78 -3
- package/src/entry/lock-command.ts +104 -8
- package/src/entry/mcp/dispatch-tools.ts +3 -3
- package/src/entry/mcp/handshake.ts +6 -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 +20 -4
- package/src/entry/mcp/server.ts +98 -7
- package/src/entry/mcp/setup-tools.ts +4 -3
- package/src/entry/mcp/workspace-tools.ts +142 -0
- package/src/entry/pause-command.ts +67 -0
- package/src/entry/profile-command.ts +23 -4
- package/src/entry/role-cli-command.ts +115 -0
- package/src/entry/runs-command.ts +78 -8
- package/src/entry/supervise-command.ts +30 -3
- package/src/entry/tui/effects.ts +4 -0
- package/src/entry/tui/state.ts +37 -10
- package/src/entry/tui/views/profiles.tsx +1 -1
- package/src/entry/tui/views/runs.tsx +66 -20
- package/src/entry/tui/views/save-dialog.tsx +6 -2
- 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/delivery.ts +3 -0
- package/src/infra/dispatch-dir.ts +2 -0
- package/src/infra/env.ts +11 -1
- package/src/infra/git.ts +6 -0
- package/src/infra/heavy-lock.ts +93 -4
- 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/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 +107 -16
- package/src/services/admission.ts +110 -23
- 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 +204 -16
- package/src/services/dispatches.ts +30 -2
- package/src/services/doctor-access.ts +8 -3
- package/src/services/doctor-docker.ts +221 -0
- package/src/services/doctor-role-mcp.ts +65 -0
- package/src/services/doctor.ts +68 -6
- package/src/services/end-push.ts +157 -0
- package/src/services/finalize.ts +12 -2
- 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 +257 -52
- package/src/services/milestones.ts +137 -26
- package/src/services/notifier.ts +56 -3
- package/src/services/orchestrator-wait.ts +82 -0
- package/src/services/pause.ts +161 -0
- package/src/services/peek.ts +79 -17
- package/src/services/ports.ts +31 -2
- package/src/services/preflight.ts +153 -19
- package/src/services/profile-service.ts +1 -0
- package/src/services/profile-store.ts +10 -3
- 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 +144 -36
- package/src/services/run-pin.ts +125 -0
- package/src/services/run-service.ts +151 -19
- package/src/services/run-store.ts +109 -15
- package/src/services/runs-page.ts +42 -7
- package/src/services/scratch.ts +60 -0
- package/src/services/sessions.ts +3 -1
- 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 +49 -9
- package/src/services/treat-likes.ts +19 -3
- 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
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { claudeHome } from "../../infra/paths.ts";
|
|
19
|
+
import { ROLE_MCP_SERVER, roleMcpTools } from "../../domain/role-tools.ts";
|
|
20
|
+
import { roleMcpConfig } from "../../infra/role-mcp.ts";
|
|
19
21
|
import { dockerSocket, scratchShell, writableRoots } from "../access.ts";
|
|
20
22
|
import { jsonOf, runCli } from "../cli.ts";
|
|
21
23
|
import {
|
|
@@ -74,15 +76,21 @@ export const CLAUDE_ACCESS: Record<Access, string[]> = {
|
|
|
74
76
|
* them Docker: the socket and `docker *` are network grants) and sets `allowLocalBinding: false`, so the
|
|
75
77
|
* user's own `true` does not carry over (their allowedDomains and other array grants merge in and stay;
|
|
76
78
|
* doctor says `network: false` is not enforced by claude-code's shell). With the user's sandbox on, `enabled: true`
|
|
77
|
-
* goes in too, so a merge that replaces the whole `sandbox` object cannot turn it off.
|
|
79
|
+
* goes in too, so a merge that replaces the whole `sandbox` object cannot turn it off. A `repo` of null reads the
|
|
80
|
+
* user's settings only: a run that drops the project's settings files (`--setting-sources ""`).
|
|
78
81
|
*/
|
|
79
|
-
export function claudeAccessArgs(
|
|
82
|
+
export function claudeAccessArgs(
|
|
83
|
+
access: Access,
|
|
84
|
+
network = true,
|
|
85
|
+
repo?: string | null,
|
|
86
|
+
extra: string[] = [],
|
|
87
|
+
): string[] {
|
|
80
88
|
const base = CLAUDE_ACCESS[access];
|
|
81
89
|
if (access !== "workspace-write") return base;
|
|
82
90
|
const sock = dockerSocket();
|
|
83
91
|
const sandbox = {
|
|
84
92
|
...(claudeSandboxOn(repo) ? { enabled: true } : {}),
|
|
85
|
-
filesystem: { allowWrite: writableRoots() },
|
|
93
|
+
filesystem: { allowWrite: [...new Set([...writableRoots(), ...extra])] },
|
|
86
94
|
...(network
|
|
87
95
|
? {
|
|
88
96
|
network: { allowLocalBinding: true, ...(sock ? { allowUnixSockets: [sock] } : {}) },
|
|
@@ -113,12 +121,14 @@ function sandboxEnabledIn(file: string): boolean | undefined {
|
|
|
113
121
|
* Whether Claude Code's Bash sandbox is on for a worker in `repo` (`sandbox.enabled`): the most specific
|
|
114
122
|
* settings file that says wins, as Claude Code layers them: the user's settings.json, then the project's
|
|
115
123
|
* .claude/settings.json and .claude/settings.local.json (Claude Code has no user-level settings.local.json).
|
|
124
|
+
* A `repo` of null reads the user's settings.json only.
|
|
116
125
|
*/
|
|
117
|
-
export function claudeSandboxOn(repo: string = process.cwd()): boolean {
|
|
126
|
+
export function claudeSandboxOn(repo: string | null = process.cwd()): boolean {
|
|
118
127
|
const files = [
|
|
119
128
|
join(claudeHome(), "settings.json"),
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
...(repo === null
|
|
130
|
+
? []
|
|
131
|
+
: [join(repo, ".claude", "settings.json"), join(repo, ".claude", "settings.local.json")]),
|
|
122
132
|
];
|
|
123
133
|
return files.map(sandboxEnabledIn).findLast((on) => on !== undefined) ?? false;
|
|
124
134
|
}
|
|
@@ -128,17 +138,114 @@ export function claudeSandboxOn(repo: string = process.cwd()): boolean {
|
|
|
128
138
|
* shell, which doctor probes as is. With it on, only a model turn runs inside it, so doctor says what to
|
|
129
139
|
* check instead of spending one.
|
|
130
140
|
*/
|
|
131
|
-
async function accessShell(): Promise<AccessShell | string> {
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
async function accessShell(o: { repos?: string[] } = {}): Promise<AccessShell | string> {
|
|
142
|
+
// the setting is per repo (its .claude/settings*.json over the user's): each repo a worker runs in counts,
|
|
143
|
+
// not only the directory doctor runs in
|
|
144
|
+
const repos = o.repos?.length ? o.repos : [process.cwd()];
|
|
145
|
+
const on = repos.filter((r) => claudeSandboxOn(r));
|
|
146
|
+
if (on.length)
|
|
147
|
+
return `Claude Code's own sandbox is on (sandbox.enabled)${o.repos?.length ? ` in ${on.join(", ")}` : ""}: catherd passes the lock, temp, loopback and Docker grants in --settings; outbound HTTPS reaches only sandbox.network.allowedDomains, so add registry.npmjs.org and the hosts your checks need there`;
|
|
134
148
|
return scratchShell("an unsandboxed shell (Claude Code's sandbox is off)", []);
|
|
135
149
|
}
|
|
136
150
|
|
|
151
|
+
/**
|
|
152
|
+
* An isolated run without the user's customizations. --bare would also drop OAuth, so a Claude plan could not
|
|
153
|
+
* log in; --safe-mode keeps auth, but drops every --mcp-config server too (Claude Code 2.1.287: "--mcp-config:
|
|
154
|
+
* … ignored (safe mode)"). A run with the role server (spec 1.5 plan 21, #42 finding 1) gets the same isolation
|
|
155
|
+
* piece by piece instead: only the --mcp-config servers, no settings file (so no hooks, no enabled plugins), no
|
|
156
|
+
* skills, CLAUDE.md and auto-memory off through the env, and the user's auth, provider, model, deny rules and
|
|
157
|
+
* allowed domains carried into --settings (carriedUserSettings).
|
|
158
|
+
*/
|
|
159
|
+
/** The user settings keys --setting-sources "" would drop that a run needs: auth, provider and model. */
|
|
160
|
+
const CARRIED_SETTINGS = [
|
|
161
|
+
"apiKeyHelper",
|
|
162
|
+
"env",
|
|
163
|
+
"awsAuthRefresh",
|
|
164
|
+
"awsCredentialExport",
|
|
165
|
+
"gcpAuthRefresh",
|
|
166
|
+
"model",
|
|
167
|
+
] as const;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* What an isolated run with the role server keeps of the user's settings.json (plan 21 review, finding 1):
|
|
171
|
+
* --safe-mode keeps the settings files and drops only customizations, while `--setting-sources ""` drops the
|
|
172
|
+
* files, so a Bedrock, Vertex, gateway or apiKeyHelper login would fail. Carried: the auth and provider keys,
|
|
173
|
+
* `model`, `permissions.deny` and `sandbox.network`; never hooks, plugins, allow rules or the rest.
|
|
174
|
+
*/
|
|
175
|
+
function carriedUserSettings(): Record<string, unknown> {
|
|
176
|
+
let user: Record<string, any>;
|
|
177
|
+
try {
|
|
178
|
+
user = JSON.parse(readFileSync(join(claudeHome(), "settings.json"), "utf8"));
|
|
179
|
+
} catch {
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
if (!user || typeof user !== "object") return {};
|
|
183
|
+
const out: Record<string, unknown> = {};
|
|
184
|
+
for (const k of CARRIED_SETTINGS) if (user[k] !== undefined) out[k] = user[k];
|
|
185
|
+
const deny = user.permissions?.deny;
|
|
186
|
+
if (Array.isArray(deny) && deny.length) out.permissions = { deny };
|
|
187
|
+
const network = user.sandbox?.network;
|
|
188
|
+
if (network && typeof network === "object") out.sandbox = { network };
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `--settings` with the user's carried keys merged under catherd's own: catherd's sandbox grants win. */
|
|
193
|
+
function withCarriedSettings(args: string[]): string[] {
|
|
194
|
+
const carried = carriedUserSettings();
|
|
195
|
+
if (!Object.keys(carried).length) return args;
|
|
196
|
+
const at = args.indexOf("--settings");
|
|
197
|
+
const own: Record<string, any> = at >= 0 ? JSON.parse(args[at + 1] as string) : {};
|
|
198
|
+
const userNet: Record<string, unknown> = (carried.sandbox as any)?.network ?? {};
|
|
199
|
+
const ownNet: Record<string, unknown> = own.sandbox?.network ?? {};
|
|
200
|
+
const sockets = [
|
|
201
|
+
...((userNet.allowUnixSockets as string[] | undefined) ?? []),
|
|
202
|
+
...((ownNet.allowUnixSockets as string[] | undefined) ?? []),
|
|
203
|
+
];
|
|
204
|
+
const sandbox =
|
|
205
|
+
carried.sandbox || own.sandbox
|
|
206
|
+
? {
|
|
207
|
+
sandbox: {
|
|
208
|
+
...own.sandbox,
|
|
209
|
+
network: {
|
|
210
|
+
...userNet,
|
|
211
|
+
...ownNet,
|
|
212
|
+
...(sockets.length ? { allowUnixSockets: [...new Set(sockets)] } : {}),
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
: {};
|
|
217
|
+
const json = JSON.stringify({ ...carried, ...own, ...sandbox });
|
|
218
|
+
if (at < 0) return [...args, "--settings", json];
|
|
219
|
+
const out = [...args];
|
|
220
|
+
out[at + 1] = json;
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isolationArgs(r: RunRequest): string[] {
|
|
225
|
+
if (!r.roleMcp) return ["--safe-mode"];
|
|
226
|
+
return ["--strict-mcp-config", "--setting-sources", "", "--disable-slash-commands"];
|
|
227
|
+
}
|
|
228
|
+
|
|
137
229
|
function plan(r: RunRequest): SpawnPlan {
|
|
138
230
|
if (r.thread !== null && !THREAD.test(r.thread))
|
|
139
231
|
throw new CatherdError("E_ADMIT_THREAD", `"${r.thread}" is not a Claude Code session id`, {
|
|
140
232
|
fix: "pass the thread from the earlier RunRecord",
|
|
141
233
|
});
|
|
234
|
+
// isolated with the role server, `--setting-sources ""` drops the project's settings (its network grants too),
|
|
235
|
+
// so only the user's carried settings.json decides whether the sandbox is on (PR #46)
|
|
236
|
+
const carried = r.isolated && !!r.roleMcp;
|
|
237
|
+
const access = claudeAccessArgs(r.access, r.network, carried ? null : r.repo, r.scratch ? [r.scratch] : []);
|
|
238
|
+
const accessArgs = carried ? withCarriedSettings(access) : [...access];
|
|
239
|
+
if (r.roleMcp) {
|
|
240
|
+
const tools = roleMcpTools(r.roleMcp.role).map((tool) => `mcp__${ROLE_MCP_SERVER}__${tool}`);
|
|
241
|
+
const allowed = accessArgs.indexOf("--allowedTools");
|
|
242
|
+
if (allowed >= 0) accessArgs[allowed + 1] = [accessArgs[allowed + 1], ...tools].join(",");
|
|
243
|
+
else accessArgs.push("--allowedTools", tools.join(","));
|
|
244
|
+
accessArgs.push(
|
|
245
|
+
"--mcp-config",
|
|
246
|
+
JSON.stringify({ mcpServers: { [ROLE_MCP_SERVER]: roleMcpConfig(r.roleMcp) } }),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
142
249
|
return {
|
|
143
250
|
cmd: "claude",
|
|
144
251
|
args: [
|
|
@@ -154,11 +261,14 @@ function plan(r: RunRequest): SpawnPlan {
|
|
|
154
261
|
...(r.thread === null ? ["--session-id", crypto.randomUUID()] : ["--resume", r.thread]),
|
|
155
262
|
"--permission-prompts",
|
|
156
263
|
"none",
|
|
157
|
-
...
|
|
158
|
-
|
|
159
|
-
...(r.isolated ? ["--safe-mode"] : []),
|
|
264
|
+
...accessArgs,
|
|
265
|
+
...(r.isolated ? isolationArgs(r) : []),
|
|
160
266
|
],
|
|
161
|
-
|
|
267
|
+
// CLAUDE.md and auto-memory off, as --safe-mode turns them off (spec 1.5 plan 21)
|
|
268
|
+
env:
|
|
269
|
+
r.isolated && r.roleMcp
|
|
270
|
+
? { CLAUDE_CODE_DISABLE_CLAUDE_MDS: "1", CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1" }
|
|
271
|
+
: {},
|
|
162
272
|
cwd: r.repo,
|
|
163
273
|
stdinPath: r.briefPath,
|
|
164
274
|
};
|
|
@@ -232,7 +342,9 @@ function claudeActivity(e: Record<string, any>): string | undefined {
|
|
|
232
342
|
if (typeof input.command === "string") return `$ ${input.command}`;
|
|
233
343
|
if (typeof input.file_path === "string")
|
|
234
344
|
return `${EDIT_TOOLS.has(c.name) ? "edit" : String(c.name)} ${input.file_path}`;
|
|
235
|
-
|
|
345
|
+
// another tool shows its first string argument, as opencode's activity does
|
|
346
|
+
const first = Object.values(input).find((v) => typeof v === "string");
|
|
347
|
+
return `${String(c.name ?? "tool")}${typeof first === "string" ? ` ${first}` : ""}`;
|
|
236
348
|
}
|
|
237
349
|
if (c?.type === "text" && typeof c.text === "string") return c.text;
|
|
238
350
|
return undefined;
|
|
@@ -8,6 +8,8 @@ export interface CodexFold {
|
|
|
8
8
|
limit: boolean;
|
|
9
9
|
tooOld: boolean;
|
|
10
10
|
lastEvent: string | null;
|
|
11
|
+
/** plan 22: the agent message that ended each completed turn, in order */
|
|
12
|
+
finals: string[];
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -43,10 +45,18 @@ export function foldCodexEvents(lines: string[]): CodexFold {
|
|
|
43
45
|
limit: false,
|
|
44
46
|
tooOld: false,
|
|
45
47
|
lastEvent: null,
|
|
48
|
+
finals: [],
|
|
46
49
|
};
|
|
50
|
+
let said: string | null = null;
|
|
47
51
|
for (const line of lines) {
|
|
48
52
|
const e = parseCodexLine(line);
|
|
49
53
|
if (!e) continue;
|
|
54
|
+
if (e.type === "item.completed" && e.item?.type === "agent_message" && typeof e.item.text === "string")
|
|
55
|
+
said = e.item.text;
|
|
56
|
+
if (e.type === "turn.completed" && said !== null) {
|
|
57
|
+
f.finals.push(said);
|
|
58
|
+
said = null;
|
|
59
|
+
}
|
|
50
60
|
f.lastEvent = e.item?.type ? `${e.type}/${e.item.type}` : e.type;
|
|
51
61
|
const msg: string = e.message ?? e.error?.message ?? e.item?.message ?? "";
|
|
52
62
|
if (CODEX_TOO_OLD.some((r) => r.test(msg))) f.tooOld = true;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { codexRoleMcpArgs } from "../../infra/role-mcp.ts";
|
|
3
4
|
import { CatherdError } from "../../domain/errors.ts";
|
|
4
5
|
import type { Access, RunStatus } from "../../domain/record.ts";
|
|
6
|
+
import { composeReply } from "../../domain/reply.ts";
|
|
5
7
|
import {
|
|
6
8
|
type AccessShell,
|
|
7
9
|
type BackendAdapter,
|
|
@@ -57,9 +59,14 @@ export function userWritableRoots(): string[] {
|
|
|
57
59
|
* The same `-c` overrides go to `codex sandbox` in doctor's probes, so doctor tests exactly what a worker gets. `-c …writable_roots=` replaces the
|
|
58
60
|
* user's own roots, so theirs go in too; an isolated run ignores the user's config, so it has none.
|
|
59
61
|
*/
|
|
60
|
-
export function codexGrants(
|
|
62
|
+
export function codexGrants(
|
|
63
|
+
access: Access,
|
|
64
|
+
network = true,
|
|
65
|
+
isolated = false,
|
|
66
|
+
extra: string[] = [],
|
|
67
|
+
): string[] {
|
|
61
68
|
if (access !== "workspace-write") return [];
|
|
62
|
-
const roots = [...new Set([...(isolated ? [] : userWritableRoots()), ...writableRoots()])];
|
|
69
|
+
const roots = [...new Set([...(isolated ? [] : userWritableRoots()), ...writableRoots(), ...extra])];
|
|
63
70
|
return [
|
|
64
71
|
"-c",
|
|
65
72
|
`sandbox_workspace_write.network_access=${network}`,
|
|
@@ -89,7 +96,8 @@ function plan(r: RunRequest): SpawnPlan {
|
|
|
89
96
|
"--json",
|
|
90
97
|
"-o",
|
|
91
98
|
r.replyPath,
|
|
92
|
-
...codexGrants(r.access, r.network, r.isolated),
|
|
99
|
+
...codexGrants(r.access, r.network, r.isolated, r.scratch ? [r.scratch] : []),
|
|
100
|
+
...(r.roleMcp ? codexRoleMcpArgs(r.roleMcp) : []),
|
|
93
101
|
];
|
|
94
102
|
const sandbox = SANDBOX[r.access];
|
|
95
103
|
const args =
|
|
@@ -125,6 +133,7 @@ function finalize(run: FinishedRun): Outcome {
|
|
|
125
133
|
: "ok";
|
|
126
134
|
const lastErr = run.stderr.trim().split("\n").at(-1) ?? "";
|
|
127
135
|
const home = run.request.isolated ? isolatedCodexHomePath() : userCodexHome();
|
|
136
|
+
const reply = composeReply(f.finals, run.reply);
|
|
128
137
|
return {
|
|
129
138
|
status,
|
|
130
139
|
thread: f.thread ?? run.request.thread,
|
|
@@ -138,6 +147,8 @@ function finalize(run: FinishedRun): Outcome {
|
|
|
138
147
|
code: status,
|
|
139
148
|
message: f.failure ?? (lastErr || `${stopped}, exit ${run.exit.code ?? run.exit.signal}`),
|
|
140
149
|
},
|
|
150
|
+
// plan 22: -o keeps the last turn's message; a later turn never overwrites the report
|
|
151
|
+
...(reply !== run.reply ? { reply } : {}),
|
|
141
152
|
};
|
|
142
153
|
}
|
|
143
154
|
|
|
@@ -243,7 +254,7 @@ function codexActivity(e: Record<string, any>): string | undefined {
|
|
|
243
254
|
return `edit ${it.changes
|
|
244
255
|
.map((c: { path?: unknown }) => c.path)
|
|
245
256
|
.filter((p: unknown) => typeof p === "string")
|
|
246
|
-
.join(", ")}
|
|
257
|
+
.join(", ")}`.trimEnd();
|
|
247
258
|
if (it.type === "agent_message" && typeof it.text === "string") return it.text;
|
|
248
259
|
return undefined;
|
|
249
260
|
}
|
|
@@ -263,12 +274,13 @@ export const codexAdapter: BackendAdapter = {
|
|
|
263
274
|
// Not a todo_list: update_plan opens one that only completes with the turn, and would mute the watchdog
|
|
264
275
|
const id = typeof e.item?.id === "string" && CODEX_TOOL_ITEMS.has(e.item.type) ? e.item.id : null;
|
|
265
276
|
const open = e.type === "item.started" ? true : e.type === "item.completed" ? false : null;
|
|
277
|
+
const activity = codexActivity(e);
|
|
266
278
|
return {
|
|
267
279
|
...(f.thread ? { thread: f.thread } : {}),
|
|
268
280
|
...(id !== null && open !== null ? { item: { id, open } } : {}),
|
|
269
281
|
...(e.type === "turn.completed" ? { tokens: f.tokens } : {}),
|
|
270
282
|
lastEvent: f.lastEvent ?? undefined,
|
|
271
|
-
...(
|
|
283
|
+
...(activity ? { activity } : {}),
|
|
272
284
|
...(f.turnFailed ? { failure: f.failure ?? "turn failed" } : {}),
|
|
273
285
|
...(f.limit ? { limit: true } : {}),
|
|
274
286
|
...(f.tooOld ? { tooOld: true } : {}),
|
|
@@ -100,7 +100,13 @@ function plan(r: RunRequest): SpawnPlan {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
async function listModels(): Promise<DiscoveredModel[]> {
|
|
103
|
-
const
|
|
103
|
+
const bin = cursorBin();
|
|
104
|
+
// an `agent` that is not Cursor's (grok's, say) is never asked for models: nothing to list, as when absent
|
|
105
|
+
if (bin === "agent") {
|
|
106
|
+
const v = await runCli(bin, ["--version"], cursorShell).catch(() => null);
|
|
107
|
+
if (!v || !CURSOR_VERSION.test(v.out)) return [];
|
|
108
|
+
}
|
|
109
|
+
const r = await runCli(bin, ["models"], { ...cursorShell, env: { NO_OPEN_BROWSER: "1" } });
|
|
104
110
|
return r?.ok ? parseCursorModels(r.out) : [];
|
|
105
111
|
}
|
|
106
112
|
|
|
@@ -21,9 +21,9 @@ const COMPAT = ["CLAUDE", "CURSOR"].flatMap((tool) =>
|
|
|
21
21
|
/**
|
|
22
22
|
* Spec 1.3 §5.4: an isolated run's env. GROK_HOME and the toggles alone still load the user's Claude Code
|
|
23
23
|
* plugins, agents and permission rules (research §3.8 [run]), so HOME moves too, and GROK_HOME lives in it.
|
|
24
|
+
* `home`: another root than the isolated runs' (doctor's sandbox check runs in a scratch one).
|
|
24
25
|
*/
|
|
25
|
-
export function grokHomeEnv(): Record<string, string> {
|
|
26
|
-
const home = isolatedGrokRoot();
|
|
26
|
+
export function grokHomeEnv(home = isolatedGrokRoot()): Record<string, string> {
|
|
27
27
|
return {
|
|
28
28
|
...movedHomeEnv(home),
|
|
29
29
|
GROK_HOME: join(home, ".grok"),
|
|
@@ -82,10 +82,47 @@ const tomlError = (text: string): string | null => {
|
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
/** The `read_write` roots of each profile in catherd's block as it stands, by profile name. */
|
|
86
|
+
function blockRoots(block: string): Record<string, string[]> {
|
|
87
|
+
try {
|
|
88
|
+
const p =
|
|
89
|
+
(Bun.TOML.parse(block) as { profiles?: Record<string, { read_write?: unknown }> }).profiles ?? {};
|
|
90
|
+
return Object.fromEntries(
|
|
91
|
+
Object.entries(p).map(([k, v]) => [
|
|
92
|
+
k,
|
|
93
|
+
Array.isArray(v.read_write) ? v.read_write.filter((r): r is string => typeof r === "string") : [],
|
|
94
|
+
]),
|
|
95
|
+
);
|
|
96
|
+
} catch {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `profiles` with each one's `read_write` merged into the block's current roots: those that still exist, in
|
|
103
|
+
* their order, then the new ones. Two catherd homes on one machine (another CATHERD_HOME, other lock dirs) then
|
|
104
|
+
* converge on one block instead of rewriting each other's.
|
|
105
|
+
*/
|
|
106
|
+
function mergedProfiles(
|
|
107
|
+
profiles: Record<string, Record<string, unknown>>,
|
|
108
|
+
was: Record<string, string[]>,
|
|
109
|
+
): Record<string, Record<string, unknown>> {
|
|
110
|
+
return Object.fromEntries(
|
|
111
|
+
Object.entries(profiles).map(([name, p]) => {
|
|
112
|
+
const mine = Array.isArray(p.read_write) ? (p.read_write as string[]) : [];
|
|
113
|
+
const kept = (was[name] ?? []).filter((r) => existsSync(r));
|
|
114
|
+
const roots = [...new Set([...kept, ...mine])];
|
|
115
|
+
return [name, { ...p, read_write: roots }];
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
85
120
|
/**
|
|
86
|
-
* `text` (a sandbox.toml) with catherd's marked block set to `profiles`:
|
|
87
|
-
*
|
|
88
|
-
*
|
|
121
|
+
* `text` (a sandbox.toml) with catherd's marked block set to `profiles`: rewritten where it stands, or appended
|
|
122
|
+
* when there is none. A rewrite in place keeps every line outside the block, and what each line belongs to: a
|
|
123
|
+
* bare key the user put after the block stays in the table it was in. The block's existing roots that still
|
|
124
|
+
* exist stay (mergedProfiles). `why` when catherd must not write: the file is not TOML, defines a `catherd-*`
|
|
125
|
+
* profile of its own, or has lost the block's end marker.
|
|
89
126
|
*/
|
|
90
127
|
export function withCatherdProfiles(
|
|
91
128
|
text: string,
|
|
@@ -95,21 +132,22 @@ export function withCatherdProfiles(
|
|
|
95
132
|
const end = start === -1 ? -1 : text.indexOf(END, start);
|
|
96
133
|
if (start !== -1 && end === -1)
|
|
97
134
|
return { why: `catherd's block has lost its end marker (${END}): delete the block` };
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
? text
|
|
102
|
-
: text.slice(0, start).replace(/\n\n$/, "\n") + text.slice(end + END.length).replace(/^\r?\n/, "");
|
|
135
|
+
const after = start === -1 ? "" : text.slice(end + END.length).replace(/^\r?\n/, "");
|
|
136
|
+
// the file without the block, for the checks: the block goes with the blank line catherd put before it
|
|
137
|
+
const rest = start === -1 ? text : text.slice(0, start).replace(/\n\n$/, "\n") + after;
|
|
103
138
|
const bad = tomlError(rest);
|
|
104
139
|
if (bad) return { why: `it is not valid TOML (${bad})` };
|
|
105
140
|
const own = Object.keys((Bun.TOML.parse(rest) as { profiles?: object }).profiles ?? {}).find((k) =>
|
|
106
141
|
k.startsWith("catherd-"),
|
|
107
142
|
);
|
|
108
143
|
if (own) return { why: `it has a [profiles.${own}] of its own: rename it, catherd's tables are catherd-*` };
|
|
144
|
+
const was = start === -1 ? {} : blockRoots(text.slice(start, end));
|
|
145
|
+
const block = `${HEADER}\n${Bun.TOML.stringify({ profiles: mergedProfiles(profiles, was) })}${END}\n`;
|
|
109
146
|
const head = rest.trimEnd();
|
|
110
|
-
const out =
|
|
111
|
-
|
|
112
|
-
|
|
147
|
+
const out =
|
|
148
|
+
start === -1 ? `${head ? `${head}\n\n` : ""}${block}` : `${text.slice(0, start)}${block}${after}`;
|
|
149
|
+
const broken = tomlError(out);
|
|
150
|
+
return broken ? { why: `it is not valid TOML with catherd's tables added (${broken})` } : { text: out };
|
|
113
151
|
}
|
|
114
152
|
|
|
115
153
|
/**
|
|
@@ -119,14 +157,31 @@ export function withCatherdProfiles(
|
|
|
119
157
|
*/
|
|
120
158
|
export function writeGrokProfiles(home: string): void {
|
|
121
159
|
const path = join(home, "sandbox.toml");
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const was = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
125
|
-
const next = withCatherdProfiles(was);
|
|
126
|
-
if ("why" in next)
|
|
127
|
-
throw new CatherdError("E_CONFIG_INVALID", `catherd will not edit ${file}: ${next.why}`, {
|
|
160
|
+
const refuse = (file: string, why: string) =>
|
|
161
|
+
new CatherdError("E_CONFIG_INVALID", `catherd will not edit ${file}: ${why}`, {
|
|
128
162
|
fix: `fix ${file}, or isolate grok (catherd profile set harness.grok.isolated true)`,
|
|
129
163
|
});
|
|
164
|
+
// a link (a dotfiles repo) stays a link: the atomic rename goes to the file it points at
|
|
165
|
+
let file = path;
|
|
166
|
+
if (lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
167
|
+
try {
|
|
168
|
+
file = realpathSync(path);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
throw refuse(path, `it is a link catherd cannot follow (${(e as NodeJS.ErrnoException).code ?? e})`);
|
|
171
|
+
}
|
|
172
|
+
let was = "";
|
|
173
|
+
try {
|
|
174
|
+
was = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
175
|
+
} catch (e) {
|
|
176
|
+
throw refuse(file, `catherd cannot read it (${(e as NodeJS.ErrnoException).code ?? e})`);
|
|
177
|
+
}
|
|
178
|
+
const next = withCatherdProfiles(was);
|
|
179
|
+
if ("why" in next) throw refuse(file, next.why);
|
|
130
180
|
if (next.text === was) return;
|
|
131
|
-
|
|
181
|
+
try {
|
|
182
|
+
writeTextAtomic(file, next.text, { mode: existsSync(file) ? statSync(file).mode & 0o777 : 0o600 });
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// a read-only store (a Nix home, say) or a folder catherd may not write
|
|
185
|
+
throw refuse(file, `catherd cannot write it (${(e as NodeJS.ErrnoException).code ?? e})`);
|
|
186
|
+
}
|
|
132
187
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { CatherdError } from "../../domain/errors.ts";
|
|
4
4
|
import type { Rung } from "../../domain/ids.ts";
|
|
5
5
|
import type { Access, RunStatus } from "../../domain/record.ts";
|
|
@@ -57,11 +57,13 @@ const NO_UPDATE = { GROK_DISABLE_AUTOUPDATER: "1" };
|
|
|
57
57
|
export const grokShell = { timeoutMs: 15_000 };
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* A fresh run's session id (`-s`), derived from its dispatch
|
|
61
|
-
* killed before grok's `end` (a timeout, a cancel) still records the session grok
|
|
60
|
+
* A fresh run's session id (`-s`), derived from its dispatch id (the dispatch dir's last part) so finalize knows
|
|
61
|
+
* it without the stream: a run killed before grok's `end` (a timeout, a cancel) still records the session grok
|
|
62
|
+
* saved (plan 16 final review). The id alone, not the full path: a data dir reached by another path (a link,
|
|
63
|
+
* `/private/var` for `/var`) gives the same session.
|
|
62
64
|
*/
|
|
63
65
|
export function sessionFor(r: Pick<RunRequest, "dispatchDir">): string {
|
|
64
|
-
const h = new Bun.CryptoHasher("sha256").update(r.dispatchDir).digest("hex");
|
|
66
|
+
const h = new Bun.CryptoHasher("sha256").update(basename(r.dispatchDir)).digest("hex");
|
|
65
67
|
const variant = ((Number.parseInt(h[16] as string, 16) & 0x3) | 0x8).toString(16);
|
|
66
68
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${variant}${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
67
69
|
}
|
|
@@ -125,11 +127,20 @@ function shippedOnGrok(): ShippedGrok {
|
|
|
125
127
|
return shipped;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
/** `grok models` under `env`: the user's GROK_HOME (its login) by default, catherd's isolated one with the key. */
|
|
131
|
+
async function listAs(env: Record<string, string>): Promise<DiscoveredModel[]> {
|
|
132
|
+
const r = await runCli("grok", ["models"], { ...grokShell, env });
|
|
130
133
|
return r?.ok ? parseGrokModels(r.out, shippedOnGrok()).models : [];
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
const listModels = (): Promise<DiscoveredModel[]> => listAs(NO_UPDATE);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The discovery cache an isolated run is judged by: the listing under catherd's own GROK_HOME, where only
|
|
140
|
+
* XAI_API_KEY signs grok in, apart from the native login's (1.3 follow-ups: the two accounts may differ).
|
|
141
|
+
*/
|
|
142
|
+
export const ISOLATED_LISTING = "grok-isolated";
|
|
143
|
+
|
|
133
144
|
/**
|
|
134
145
|
* Spec 1.3 §5.3–§5.4: an isolated run needs XAI_API_KEY (its home holds no login, and the user's auth.json is
|
|
135
146
|
* never copied: its refresh token rotates, research §3.9); a workspace-write run needs catherd-ws in the
|
|
@@ -150,7 +161,13 @@ async function prepare(req: {
|
|
|
150
161
|
if (req.isolated) ensurePrivateDir(home);
|
|
151
162
|
if (req.access === "workspace-write") writeGrokProfiles(home);
|
|
152
163
|
const { model, effort } = req.rung;
|
|
153
|
-
|
|
164
|
+
// an isolated run signs in with the key, so its model is checked against the key's own listing
|
|
165
|
+
const models = req.isolated
|
|
166
|
+
? await discovered(ISOLATED_LISTING, () => listAs({ ...NO_UPDATE, ...grokHomeEnv() }), {
|
|
167
|
+
maxAgeMs: DAY_MS,
|
|
168
|
+
need: model,
|
|
169
|
+
})
|
|
170
|
+
: await discovered("grok", listModels, { maxAgeMs: DAY_MS, need: model });
|
|
154
171
|
if (models.length === 0) return; // grok listed nothing: let the run itself say what is wrong
|
|
155
172
|
const m = models.find((x) => x.id === model);
|
|
156
173
|
if (!m)
|
|
@@ -241,7 +258,9 @@ async function sandboxInfo(): Promise<{ id: string; label: string; detail: strin
|
|
|
241
258
|
const r = await runCli(
|
|
242
259
|
"grok",
|
|
243
260
|
["-p", "hi", "--output-format", "streaming-json", "--sandbox", "read-only", "--no-auto-update"],
|
|
244
|
-
|
|
261
|
+
// a scratch HOME as well as GROK_HOME, with memory and the compat features off: nothing of the user's
|
|
262
|
+
// own Claude Code or Cursor setup loads into the check (1.3 follow-ups)
|
|
263
|
+
{ ...grokShell, env: { ...NO_UPDATE, ...grokHomeEnv(home), XAI_API_KEY: "" } },
|
|
245
264
|
);
|
|
246
265
|
if (!r) return null;
|
|
247
266
|
const refused = /could not apply the '?read-only'? sandbox profile/.test(r.err);
|
|
@@ -155,6 +155,20 @@ function finalize(run: FinishedRun): Outcome {
|
|
|
155
155
|
const f = foldOpencodeEvents(run.eventLines);
|
|
156
156
|
const stopped = run.exit.reason;
|
|
157
157
|
const tooOld = f.tooOld || OPENCODE_TOO_OLD.some((r) => r.test(run.stderr));
|
|
158
|
+
// plan 23: the provider kept failing; failover takes it as a usage limit, onto the next backend
|
|
159
|
+
if (stopped === "provider-unavailable")
|
|
160
|
+
return {
|
|
161
|
+
status: "limit",
|
|
162
|
+
thread: f.thread ?? run.request.thread,
|
|
163
|
+
tokens: f.tokens,
|
|
164
|
+
costUsd: f.costUsd,
|
|
165
|
+
images: [],
|
|
166
|
+
error: {
|
|
167
|
+
code: "provider-unavailable",
|
|
168
|
+
message: f.error?.message ?? "the provider retried with no progress",
|
|
169
|
+
},
|
|
170
|
+
reply: f.reply,
|
|
171
|
+
};
|
|
158
172
|
const status: RunStatus =
|
|
159
173
|
stopped === "cancelled"
|
|
160
174
|
? "cancelled"
|
|
@@ -201,6 +215,8 @@ function parse(line: string): EventDelta {
|
|
|
201
215
|
const activity = opencodeActivity(e);
|
|
202
216
|
if (activity) d.activity = activity;
|
|
203
217
|
if (typeof e.sessionID === "string") d.thread = e.sessionID;
|
|
218
|
+
// plan 23: each provider retry starts a step again with nothing between; the supervisor counts them
|
|
219
|
+
if (e.type === "step_start") d.step = true;
|
|
204
220
|
if (e.type === "step_finish") {
|
|
205
221
|
d.tokens = opencodeTokens(e.part?.tokens);
|
|
206
222
|
d.requestInput = d.tokens.input;
|
|
@@ -220,6 +236,21 @@ function parse(line: string): EventDelta {
|
|
|
220
236
|
* older messages keep their errors, so only the newest assistant message counts, and with `sinceMs` only
|
|
221
237
|
* if this run wrote it.
|
|
222
238
|
*/
|
|
239
|
+
/**
|
|
240
|
+
* Plan 23: the provider retry the newest assistant message this run wrote is waiting out (a 503, an
|
|
241
|
+
* overloaded backend), by its attempt number; null when it is not retrying.
|
|
242
|
+
*/
|
|
243
|
+
function providerRetryOf(messages: unknown, sinceMs: number): number | null {
|
|
244
|
+
if (!Array.isArray(messages)) return null;
|
|
245
|
+
const m = messages.find((x) => x?.type === "assistant") as Record<string, any> | undefined;
|
|
246
|
+
if (!m?.retry || !(m.time?.created >= sinceMs)) return null;
|
|
247
|
+
return typeof m.retry.attempt === "number" ? m.retry.attempt : 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function providerRetry(thread: string, sinceMs: number): Promise<number | null> {
|
|
251
|
+
return providerRetryOf((await opencodeApi("GET", `/api/session/${thread}/message`))?.data, sinceMs);
|
|
252
|
+
}
|
|
253
|
+
|
|
223
254
|
function limitRetry(messages: unknown, sinceMs?: number): string | null {
|
|
224
255
|
if (!Array.isArray(messages)) return null;
|
|
225
256
|
const m = messages.find((x) => x?.type === "assistant") as Record<string, any> | undefined;
|
|
@@ -265,7 +296,12 @@ async function isBusy(thread: string, sinceMs?: number): Promise<boolean> {
|
|
|
265
296
|
const active = (await opencodeApi("GET", "/api/session/active"))?.data;
|
|
266
297
|
if (!active || typeof active !== "object" || !(thread in active)) return false;
|
|
267
298
|
const messages = (await opencodeApi("GET", `/api/session/${thread}/message`))?.data;
|
|
268
|
-
|
|
299
|
+
// plan 23: a session only retrying its provider is not busy, so the idle timeout still runs
|
|
300
|
+
return (
|
|
301
|
+
Array.isArray(messages) &&
|
|
302
|
+
limitRetry(messages, sinceMs) === null &&
|
|
303
|
+
providerRetryOf(messages, sinceMs ?? 0) === null
|
|
304
|
+
);
|
|
269
305
|
}
|
|
270
306
|
|
|
271
307
|
/** Killing the v2 client does not stop its session; the service must be told (research §2.4). */
|
|
@@ -327,6 +363,7 @@ export const opencodeAdapter: BackendAdapter = {
|
|
|
327
363
|
resume: { supported: true, sameAccessOnly: false, threadPattern: THREAD },
|
|
328
364
|
interrupt: (thread) => interrupt(thread),
|
|
329
365
|
isBusy: (thread, _cwd, sinceMs) => isBusy(thread, sinceMs),
|
|
366
|
+
providerRetry: (thread, _cwd, sinceMs) => providerRetry(thread, sinceMs),
|
|
330
367
|
failoverFor,
|
|
331
368
|
graceAfterFinalMs: null,
|
|
332
369
|
reportsCost: true,
|
package/src/cli.ts
CHANGED
|
@@ -50,7 +50,12 @@ export const main: Command = defineCommand({
|
|
|
50
50
|
doctor: () => import("./entry/doctor-command.ts").then((m) => m.doctorCommand),
|
|
51
51
|
catalog: () => import("./entry/catalog-command.ts").then((m) => m.catalogCommand),
|
|
52
52
|
knowledge: () => import("./entry/knowledge-command.ts").then((m) => m.knowledgeCommand),
|
|
53
|
+
workspace: () => import("./entry/workspace-command.ts").then((m) => m.workspaceCommand),
|
|
53
54
|
lock: () => import("./entry/lock-command.ts").then((m) => m.lockCommand),
|
|
55
|
+
"run-file": () => import("./entry/role-cli-command.ts").then((m) => m.runFileCommand),
|
|
56
|
+
gate: () => import("./entry/role-cli-command.ts").then((m) => m.gateCommand),
|
|
57
|
+
pause: () => import("./entry/pause-command.ts").then((m) => m.pauseCommand),
|
|
58
|
+
resume: () => import("./entry/pause-command.ts").then((m) => m.resumeCommand),
|
|
54
59
|
"capture-fixtures": () =>
|
|
55
60
|
import("./entry/capture-fixtures-command.ts").then((m) => m.captureFixturesCommand),
|
|
56
61
|
mcp: () => import("./entry/mcp/command.ts").then((m) => m.mcpCommand),
|
package/src/domain/agents.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseRung, tryParseRung } from "./ids.ts";
|
|
2
2
|
import { agentName, type Profile } from "./profile.ts";
|
|
3
3
|
import type { Access } from "./record.ts";
|
|
4
|
-
import { nativeDisallowedTools, rolePrompt } from "./role-prompts.ts";
|
|
4
|
+
import { NOT_THE_ORCHESTRATOR, nativeDisallowedTools, rolePrompt } from "./role-prompts.ts";
|
|
5
5
|
import { ROLES, type Role } from "./roles.ts";
|
|
6
6
|
|
|
7
7
|
export interface AgentFile {
|
|
@@ -32,6 +32,8 @@ export function renderAgent(o: {
|
|
|
32
32
|
"",
|
|
33
33
|
rolePrompt(o.role, o.version),
|
|
34
34
|
"",
|
|
35
|
+
NOT_THE_ORCHESTRATOR,
|
|
36
|
+
"",
|
|
35
37
|
].join("\n");
|
|
36
38
|
}
|
|
37
39
|
|