@tablation/crew 0.27.0 → 0.28.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/crew.example.yaml +16 -0
- package/dist/cli.js +513 -304
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,9 +7,9 @@ import { hostname as hostname2, homedir as homedir5 } from "os";
|
|
|
7
7
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
|
-
import { readFileSync as
|
|
11
|
-
import { resolve, dirname as dirname2, isAbsolute, join as
|
|
12
|
-
import { homedir, tmpdir } from "os";
|
|
10
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
|
|
11
|
+
import { resolve, dirname as dirname2, isAbsolute, join as join4, basename as basename2 } from "path";
|
|
12
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
13
13
|
import { parse as parse2 } from "yaml";
|
|
14
14
|
|
|
15
15
|
// src/events.ts
|
|
@@ -157,6 +157,261 @@ var Emitter = class _Emitter {
|
|
|
157
157
|
};
|
|
158
158
|
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
159
159
|
|
|
160
|
+
// src/stream.ts
|
|
161
|
+
function toolTarget(name, input) {
|
|
162
|
+
if (!input) return void 0;
|
|
163
|
+
for (const key of ["file_path", "path", "notebook_path", "url", "pattern"]) {
|
|
164
|
+
const v = input[key];
|
|
165
|
+
if (typeof v === "string" && v) return v;
|
|
166
|
+
}
|
|
167
|
+
if (name === "Bash" && typeof input.command === "string") {
|
|
168
|
+
return input.command.trim().split(/\s+/)[0];
|
|
169
|
+
}
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
function mapStreamLine(line) {
|
|
173
|
+
const obj = line;
|
|
174
|
+
if (!obj || obj.type !== "assistant") return [];
|
|
175
|
+
const content = obj.message?.content;
|
|
176
|
+
if (!Array.isArray(content)) return [];
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const block of content) {
|
|
179
|
+
if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
|
|
180
|
+
else if (block.type === "text") out.push({ kind: "text", text: block.text });
|
|
181
|
+
else if (block.type === "tool_use") {
|
|
182
|
+
out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
function extractTurnTokens(line) {
|
|
188
|
+
const obj = line;
|
|
189
|
+
if (!obj || obj.type !== "assistant") return void 0;
|
|
190
|
+
const usage2 = obj.message?.usage;
|
|
191
|
+
if (!usage2) return void 0;
|
|
192
|
+
return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
|
|
193
|
+
}
|
|
194
|
+
function extractResult(line) {
|
|
195
|
+
const obj = line;
|
|
196
|
+
if (!obj || obj.type !== "result") return void 0;
|
|
197
|
+
return {
|
|
198
|
+
sessionId: obj.session_id,
|
|
199
|
+
numTurns: obj.num_turns,
|
|
200
|
+
totalCostUsd: obj.total_cost_usd,
|
|
201
|
+
inputTokens: obj.usage?.input_tokens,
|
|
202
|
+
outputTokens: obj.usage?.output_tokens,
|
|
203
|
+
cacheReadTokens: obj.usage?.cache_read_input_tokens,
|
|
204
|
+
cacheCreationTokens: obj.usage?.cache_creation_input_tokens
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/claude-mcp.ts
|
|
209
|
+
import { spawnSync } from "child_process";
|
|
210
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
211
|
+
import { homedir } from "os";
|
|
212
|
+
import { join as join2 } from "path";
|
|
213
|
+
var MCP_SERVER_NAME = "tablation";
|
|
214
|
+
var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
|
|
215
|
+
function mcpUrlFor(baseUrl) {
|
|
216
|
+
return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
|
|
217
|
+
}
|
|
218
|
+
function runClaude(bin, args) {
|
|
219
|
+
const r = spawnSync(bin, args, { encoding: "utf8", timeout: 3e4 });
|
|
220
|
+
const missing = r.error?.code === "ENOENT";
|
|
221
|
+
return { status: r.status, missing, stderr: r.stderr ?? "" };
|
|
222
|
+
}
|
|
223
|
+
function userScopeEntry(home) {
|
|
224
|
+
try {
|
|
225
|
+
const j = JSON.parse(readFileSync2(join2(home, ".claude.json"), "utf8"));
|
|
226
|
+
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
227
|
+
} catch {
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function inspectClaudeMcp(o) {
|
|
232
|
+
const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
|
|
233
|
+
if (got.missing) return { state: "no-claude" };
|
|
234
|
+
if (got.status !== 0) return { state: "missing" };
|
|
235
|
+
const entry = userScopeEntry(o.home ?? homedir());
|
|
236
|
+
if (!entry) {
|
|
237
|
+
return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
|
|
238
|
+
}
|
|
239
|
+
if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
|
|
240
|
+
const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
|
|
241
|
+
if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
|
|
242
|
+
return { state: "registered", url: o.url };
|
|
243
|
+
}
|
|
244
|
+
function ensureClaudeMcp(o) {
|
|
245
|
+
const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
|
|
246
|
+
const st = inspectClaudeMcp(o);
|
|
247
|
+
if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
|
|
248
|
+
if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
|
|
249
|
+
if (st.state === "different" && !o.replace) {
|
|
250
|
+
return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
251
|
+
}
|
|
252
|
+
if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
|
|
253
|
+
if (st.state === "different") {
|
|
254
|
+
runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
255
|
+
}
|
|
256
|
+
const add = runClaude(o.bin, [
|
|
257
|
+
"mcp",
|
|
258
|
+
"add",
|
|
259
|
+
"--transport",
|
|
260
|
+
"http",
|
|
261
|
+
MCP_SERVER_NAME,
|
|
262
|
+
o.url,
|
|
263
|
+
"--header",
|
|
264
|
+
`Authorization: Bearer ${o.key}`,
|
|
265
|
+
"--scope",
|
|
266
|
+
"user"
|
|
267
|
+
]);
|
|
268
|
+
if (add.status !== 0) {
|
|
269
|
+
return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
|
|
270
|
+
}
|
|
271
|
+
return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/capability.ts
|
|
275
|
+
function profileFor(role) {
|
|
276
|
+
if (role === "triage") return { level: "read", designTools: false };
|
|
277
|
+
if (role === "qa") return { level: "write", designTools: false };
|
|
278
|
+
return { level: "edit", designTools: role === "design" };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/adapters/claude.ts
|
|
282
|
+
var BILLING_VARS_TO_UNSET = [
|
|
283
|
+
"ANTHROPIC_API_KEY",
|
|
284
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
285
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
286
|
+
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
287
|
+
"ANTHROPIC_VERTEX_REGION",
|
|
288
|
+
"CLOUD_ML_REGION",
|
|
289
|
+
"ANTHROPIC_MODEL"
|
|
290
|
+
];
|
|
291
|
+
var DISALLOWED_TOOLS = [
|
|
292
|
+
"Bash(rm -rf:*)",
|
|
293
|
+
"Bash(rm -fr:*)",
|
|
294
|
+
"Bash(git push --force*)",
|
|
295
|
+
"Bash(git push -f*)",
|
|
296
|
+
"Bash(curl*| sh)",
|
|
297
|
+
"Bash(curl*|sh)",
|
|
298
|
+
"Bash(curl*| bash)",
|
|
299
|
+
"Bash(curl*|bash)",
|
|
300
|
+
"Bash(sudo:*)"
|
|
301
|
+
];
|
|
302
|
+
function claudeToolsFor(p) {
|
|
303
|
+
const tablation = "mcp__tablation__*";
|
|
304
|
+
if (p.level === "read") return ["Bash", "Read", tablation];
|
|
305
|
+
if (p.level === "write") return ["Bash", "Read", "Write", "Grep", "Glob", tablation];
|
|
306
|
+
const base = ["Bash", "Read", "Edit", "Write", "Grep", "Glob", tablation];
|
|
307
|
+
return p.designTools ? [...base, "Skill", "Artifact"] : base;
|
|
308
|
+
}
|
|
309
|
+
function allowedTools(role) {
|
|
310
|
+
return claudeToolsFor(profileFor(role));
|
|
311
|
+
}
|
|
312
|
+
var claudeAdapter = {
|
|
313
|
+
harness: "claude",
|
|
314
|
+
displayName: "Claude Code",
|
|
315
|
+
defaultBin: "claude",
|
|
316
|
+
defaultModel: "claude-sonnet-5",
|
|
317
|
+
defaultTiers: {
|
|
318
|
+
light: { model: "claude-haiku-4-5-20251001" },
|
|
319
|
+
standard: { model: "claude-sonnet-5-5" },
|
|
320
|
+
deep: { model: "claude-opus-5-5" }
|
|
321
|
+
},
|
|
322
|
+
limitations: [
|
|
323
|
+
"Bash is not sandboxed: the deny list refuses known destructive patterns, but a one-liner reaching the same end another way is not caught",
|
|
324
|
+
"the write profile (QA) withholds the Edit tool, not file writes \u2014 Bash can still write files",
|
|
325
|
+
"git push is not blocked, only forced pushes"
|
|
326
|
+
],
|
|
327
|
+
invocation(spec, o) {
|
|
328
|
+
const args = [
|
|
329
|
+
"-p",
|
|
330
|
+
"--allowedTools",
|
|
331
|
+
...allowedTools(o.role),
|
|
332
|
+
"--disallowedTools",
|
|
333
|
+
...DISALLOWED_TOOLS,
|
|
334
|
+
"--model",
|
|
335
|
+
o.model,
|
|
336
|
+
"--output-format",
|
|
337
|
+
"stream-json",
|
|
338
|
+
"--verbose",
|
|
339
|
+
// Registers the tracker's own MCP tool surface for this run only —
|
|
340
|
+
// `${CREW_API_KEY}` is interpolated by the CLI from the environment,
|
|
341
|
+
// so the resolved key is never written to an argv string or a config
|
|
342
|
+
// file on disk. Skipped when there's no key to send: a route `crew
|
|
343
|
+
// connect` hasn't resolved yet has nothing to authenticate with.
|
|
344
|
+
...o.mcp ? ["--mcp-config", JSON.stringify({
|
|
345
|
+
mcpServers: {
|
|
346
|
+
tablation: {
|
|
347
|
+
type: "http",
|
|
348
|
+
url: o.mcp.url,
|
|
349
|
+
headers: { Authorization: `Bearer \${${o.mcp.keyVar}}` }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
})] : [],
|
|
353
|
+
...spec.args
|
|
354
|
+
];
|
|
355
|
+
return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
|
|
356
|
+
},
|
|
357
|
+
runEnv(spec) {
|
|
358
|
+
return {
|
|
359
|
+
// Extended thinking is what makes the CLI emit `thinking` stream
|
|
360
|
+
// blocks at all — without it, Agent Log Cycles reporting has nothing
|
|
361
|
+
// to report (ISSUE-558).
|
|
362
|
+
...spec.maxThinkingTokens > 0 ? { MAX_THINKING_TOKENS: String(spec.maxThinkingTokens) } : {},
|
|
363
|
+
...spec.env
|
|
364
|
+
};
|
|
365
|
+
},
|
|
366
|
+
unsetEnv: BILLING_VARS_TO_UNSET,
|
|
367
|
+
mapStreamLine,
|
|
368
|
+
extractResult,
|
|
369
|
+
extractTurnTokens,
|
|
370
|
+
inspectMcp(o) {
|
|
371
|
+
const s = inspectClaudeMcp(o);
|
|
372
|
+
return s.state === "no-claude" ? { state: "no-binary" } : s;
|
|
373
|
+
},
|
|
374
|
+
ensureMcp: ensureClaudeMcp
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// src/toolchain.ts
|
|
378
|
+
var TIERS = ["light", "standard", "deep"];
|
|
379
|
+
var ADAPTERS = {
|
|
380
|
+
[claudeAdapter.harness]: claudeAdapter
|
|
381
|
+
};
|
|
382
|
+
var HARNESS_NAMES = Object.keys(ADAPTERS);
|
|
383
|
+
function isHarness(name) {
|
|
384
|
+
return Object.hasOwn(ADAPTERS, name);
|
|
385
|
+
}
|
|
386
|
+
function adapterFor(harness) {
|
|
387
|
+
const a = Object.hasOwn(ADAPTERS, harness) ? ADAPTERS[harness] : void 0;
|
|
388
|
+
if (!a) throw new Error(`unknown harness "${harness}" (have: ${HARNESS_NAMES.join(", ")})`);
|
|
389
|
+
return a;
|
|
390
|
+
}
|
|
391
|
+
var LEGACY_TOOLCHAIN = "claude";
|
|
392
|
+
function shipToolchains(ship) {
|
|
393
|
+
const explicit = ship.toolchains ?? [];
|
|
394
|
+
if (explicit.some((t) => t.name === LEGACY_TOOLCHAIN)) return explicit;
|
|
395
|
+
const a = claudeAdapter;
|
|
396
|
+
const legacy = {
|
|
397
|
+
name: LEGACY_TOOLCHAIN,
|
|
398
|
+
harness: a.harness,
|
|
399
|
+
bin: ship.agent.bin,
|
|
400
|
+
model: ship.agent.model,
|
|
401
|
+
tiers: a.defaultTiers,
|
|
402
|
+
args: [],
|
|
403
|
+
env: {},
|
|
404
|
+
maxThinkingTokens: ship.agent.maxThinkingTokens
|
|
405
|
+
};
|
|
406
|
+
return [legacy, ...explicit];
|
|
407
|
+
}
|
|
408
|
+
function resolveToolchain(ship, route) {
|
|
409
|
+
const name = route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
|
|
410
|
+
const found = shipToolchains(ship).find((t) => t.name === name);
|
|
411
|
+
if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
|
|
412
|
+
return found;
|
|
413
|
+
}
|
|
414
|
+
|
|
160
415
|
// src/config.ts
|
|
161
416
|
import { getSessionStore, hostFromUrl } from "@tablation/client";
|
|
162
417
|
|
|
@@ -197,8 +452,8 @@ function explain(ship, required) {
|
|
|
197
452
|
}
|
|
198
453
|
|
|
199
454
|
// src/repo-config.ts
|
|
200
|
-
import { readFileSync as
|
|
201
|
-
import { join as
|
|
455
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
456
|
+
import { join as join3, basename } from "path";
|
|
202
457
|
import { parse } from "yaml";
|
|
203
458
|
var SPEC_VERSION = 1;
|
|
204
459
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -271,7 +526,7 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
271
526
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
272
527
|
function findRepoConfig(dir) {
|
|
273
528
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
274
|
-
const p =
|
|
529
|
+
const p = join3(dir, name);
|
|
275
530
|
if (existsSync2(p)) return p;
|
|
276
531
|
}
|
|
277
532
|
return null;
|
|
@@ -502,7 +757,7 @@ function parseRepoConfig(text, file) {
|
|
|
502
757
|
function loadRepoConfig(dir) {
|
|
503
758
|
const file = findRepoConfig(dir);
|
|
504
759
|
if (!file) return null;
|
|
505
|
-
return parseRepoConfig(
|
|
760
|
+
return parseRepoConfig(readFileSync3(file, "utf8"), file);
|
|
506
761
|
}
|
|
507
762
|
function hookLabel(cfg, hook2) {
|
|
508
763
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -767,6 +1022,8 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
767
1022
|
"name",
|
|
768
1023
|
"platform",
|
|
769
1024
|
"agent",
|
|
1025
|
+
"toolchains",
|
|
1026
|
+
"toolchain",
|
|
770
1027
|
"shell",
|
|
771
1028
|
"extraPath",
|
|
772
1029
|
"useNvm",
|
|
@@ -806,7 +1063,8 @@ var ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
|
806
1063
|
"contract",
|
|
807
1064
|
"resolved",
|
|
808
1065
|
"promptSet",
|
|
809
|
-
"hostPassengers"
|
|
1066
|
+
"hostPassengers",
|
|
1067
|
+
"toolchain"
|
|
810
1068
|
]);
|
|
811
1069
|
function parseWorktreeName(v, where, file) {
|
|
812
1070
|
const problem = typeof v === "string" ? worktreeNameProblem(v) : "must be a string";
|
|
@@ -910,16 +1168,16 @@ function mergeRouteRelease(route, override) {
|
|
|
910
1168
|
};
|
|
911
1169
|
}
|
|
912
1170
|
function expand(p, base) {
|
|
913
|
-
let out = p.startsWith("~") ? p.replace(/^~/,
|
|
914
|
-
out = out.replace(/\$\{?HOME\}?/g,
|
|
1171
|
+
let out = p.startsWith("~") ? p.replace(/^~/, homedir2()) : p;
|
|
1172
|
+
out = out.replace(/\$\{?HOME\}?/g, homedir2());
|
|
915
1173
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
916
1174
|
}
|
|
917
1175
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
918
|
-
if (!promptSet) return
|
|
1176
|
+
if (!promptSet) return join4(crewHome, "prompts", "default");
|
|
919
1177
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
920
1178
|
return expand(promptSet, base);
|
|
921
1179
|
}
|
|
922
|
-
return
|
|
1180
|
+
return join4(crewHome, "prompts", promptSet);
|
|
923
1181
|
}
|
|
924
1182
|
var Missing = class {
|
|
925
1183
|
paths = [];
|
|
@@ -943,7 +1201,7 @@ var Missing = class {
|
|
|
943
1201
|
};
|
|
944
1202
|
function crewVersion(crewHome) {
|
|
945
1203
|
try {
|
|
946
|
-
const pkg = JSON.parse(
|
|
1204
|
+
const pkg = JSON.parse(readFileSync4(join4(crewHome, "package.json"), "utf8"));
|
|
947
1205
|
return pkg.version ?? "0.0.0";
|
|
948
1206
|
} catch {
|
|
949
1207
|
return "0.0.0";
|
|
@@ -953,8 +1211,8 @@ function configSearchPath(crewHome) {
|
|
|
953
1211
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
954
1212
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
955
1213
|
return [
|
|
956
|
-
...xdg ? [
|
|
957
|
-
|
|
1214
|
+
...xdg ? [join4(xdg, "crew", "crew.yaml")] : [],
|
|
1215
|
+
join4(homedir2(), ".config", "crew", "crew.yaml"),
|
|
958
1216
|
resolve(crewHome, "crew.yaml")
|
|
959
1217
|
];
|
|
960
1218
|
}
|
|
@@ -962,17 +1220,17 @@ function findConfigFile(crewHome) {
|
|
|
962
1220
|
const candidates = configSearchPath(crewHome);
|
|
963
1221
|
return candidates.find((c) => existsSync3(c)) ?? candidates[candidates.length - 1];
|
|
964
1222
|
}
|
|
965
|
-
var defaultConfigPath = () =>
|
|
1223
|
+
var defaultConfigPath = () => join4(process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config"), "crew", "crew.yaml");
|
|
966
1224
|
function routeSlug(route) {
|
|
967
1225
|
return route.replace(/\//g, "-");
|
|
968
1226
|
}
|
|
969
1227
|
function resolvedPathFor(stateDir, route) {
|
|
970
1228
|
const [workspace, project] = splitRoute(route);
|
|
971
|
-
return
|
|
1229
|
+
return join4(stateDir, "resolved", workspace, `${project}.json`);
|
|
972
1230
|
}
|
|
973
1231
|
function apiKeyPathFor(stateDir, route) {
|
|
974
1232
|
const [workspace, project] = splitRoute(route);
|
|
975
|
-
return
|
|
1233
|
+
return join4(stateDir, "keys", workspace, `${project}.env`);
|
|
976
1234
|
}
|
|
977
1235
|
function splitRoute(route) {
|
|
978
1236
|
const parts = route.split("/");
|
|
@@ -985,7 +1243,7 @@ function readResolvedFile(stateDir, route) {
|
|
|
985
1243
|
const path = resolvedPathFor(stateDir, route);
|
|
986
1244
|
if (!existsSync3(path)) return void 0;
|
|
987
1245
|
try {
|
|
988
|
-
return JSON.parse(
|
|
1246
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
989
1247
|
} catch (e) {
|
|
990
1248
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
991
1249
|
}
|
|
@@ -1047,12 +1305,12 @@ function loadConfig(crewHome, configFile, opts = {}) {
|
|
|
1047
1305
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
1048
1306
|
|
|
1049
1307
|
Create the first of those:
|
|
1050
|
-
mkdir -p ${dirname2(defaultConfigPath())} && cp ${
|
|
1308
|
+
mkdir -p ${dirname2(defaultConfigPath())} && cp ${join4(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
1051
1309
|
);
|
|
1052
1310
|
}
|
|
1053
1311
|
let raw;
|
|
1054
1312
|
try {
|
|
1055
|
-
raw = parse2(opts.text ??
|
|
1313
|
+
raw = parse2(opts.text ?? readFileSync4(file, "utf8"));
|
|
1056
1314
|
} catch (e) {
|
|
1057
1315
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
1058
1316
|
}
|
|
@@ -1082,6 +1340,12 @@ Create the first of those:
|
|
|
1082
1340
|
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
1083
1341
|
missing.add(`ship.agent.maxThinkingTokens must be a non-negative integer (got "${shipRaw.agent?.maxThinkingTokens}")`);
|
|
1084
1342
|
}
|
|
1343
|
+
const toolchains = parseToolchains(shipRaw.toolchains, base, missing, file);
|
|
1344
|
+
const toolchainNames = /* @__PURE__ */ new Set([LEGACY_TOOLCHAIN, ...toolchains.map((x) => x.name)]);
|
|
1345
|
+
const shipToolchain = shipRaw.toolchain === void 0 ? void 0 : String(shipRaw.toolchain);
|
|
1346
|
+
if (shipToolchain !== void 0 && !toolchainNames.has(shipToolchain)) {
|
|
1347
|
+
missing.add(`ship.toolchain "${shipToolchain}" is not defined (have: ${[...toolchainNames].join(", ")})`);
|
|
1348
|
+
}
|
|
1085
1349
|
const stateDir = expand(shipRaw.stateDir ?? "state", base);
|
|
1086
1350
|
const routesRaw = raw.routes;
|
|
1087
1351
|
if (!opts.allowNoRoutes && (!Array.isArray(routesRaw) || routesRaw.length === 0)) {
|
|
@@ -1092,7 +1356,7 @@ Create the first of those:
|
|
|
1092
1356
|
const where = `routes[${i}]`;
|
|
1093
1357
|
const routeMissing = new Missing();
|
|
1094
1358
|
try {
|
|
1095
|
-
return [parseOneRoute(c, where, file, base, stateDir, raw, routeMissing, crewHome)];
|
|
1359
|
+
return [parseOneRoute(c, where, file, base, stateDir, raw, routeMissing, crewHome, toolchainNames)];
|
|
1096
1360
|
} catch (e) {
|
|
1097
1361
|
const label = typeof c?.route === "string" && c.route ? `"${c.route}"` : where;
|
|
1098
1362
|
routeWarnings.push(`route ${label} dropped \u2014 ${e.message}`);
|
|
@@ -1115,12 +1379,14 @@ Create the first of those:
|
|
|
1115
1379
|
model: shipRaw.agent?.model ?? "claude-sonnet-5",
|
|
1116
1380
|
maxThinkingTokens
|
|
1117
1381
|
},
|
|
1382
|
+
...toolchains.length > 0 ? { toolchains } : {},
|
|
1383
|
+
...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
|
|
1118
1384
|
shell: shipRaw.shell,
|
|
1119
1385
|
extraPath: shipRaw.extraPath,
|
|
1120
1386
|
useNvm: shipRaw.useNvm !== false,
|
|
1121
1387
|
nvmSh: shipRaw.nvmSh,
|
|
1122
1388
|
stateDir,
|
|
1123
|
-
logFile: expand(shipRaw.logFile ??
|
|
1389
|
+
logFile: expand(shipRaw.logFile ?? join4(tmpdir(), "crew.log"), base),
|
|
1124
1390
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
1125
1391
|
maxConcurrentAgents,
|
|
1126
1392
|
streamRetentionDays,
|
|
@@ -1138,8 +1404,70 @@ Create the first of those:
|
|
|
1138
1404
|
warnings: routeWarnings
|
|
1139
1405
|
};
|
|
1140
1406
|
}
|
|
1141
|
-
function
|
|
1407
|
+
function expandCommand(cmd, base) {
|
|
1408
|
+
return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
|
|
1409
|
+
}
|
|
1410
|
+
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "model", "tiers", "args", "env", "maxThinkingTokens"]);
|
|
1411
|
+
function parseToolchains(raw, base, missing, file) {
|
|
1412
|
+
if (raw === void 0 || raw === null) return [];
|
|
1413
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
1414
|
+
missing.add("ship.toolchains must be a mapping of name \u2192 toolchain");
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
const out = [];
|
|
1418
|
+
for (const [name, entryRaw] of Object.entries(raw)) {
|
|
1419
|
+
const where = `ship.toolchains.${name}`;
|
|
1420
|
+
if (!entryRaw || typeof entryRaw !== "object" || Array.isArray(entryRaw)) {
|
|
1421
|
+
missing.add(`${where} must be a mapping`);
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
rejectUnknownKeys(entryRaw, TOOLCHAIN_KEYS, where, file);
|
|
1425
|
+
const harness = String(entryRaw.harness ?? "");
|
|
1426
|
+
if (!isHarness(harness)) {
|
|
1427
|
+
missing.add(`${where}.harness must be one of ${HARNESS_NAMES.join("|")} (got "${harness}")`);
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const adapter = adapterFor(harness);
|
|
1431
|
+
const tiers = { ...adapter.defaultTiers };
|
|
1432
|
+
for (const [tier, v] of Object.entries(entryRaw.tiers ?? {})) {
|
|
1433
|
+
if (!TIERS.includes(tier)) {
|
|
1434
|
+
missing.add(`${where}.tiers.${tier} is not a tier (have: ${TIERS.join(", ")})`);
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
if (!v || typeof v.model !== "string" || !v.model) {
|
|
1438
|
+
missing.add(`${where}.tiers.${tier}.model is required`);
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
tiers[tier] = { model: v.model, ...v.reasoning !== void 0 ? { reasoning: String(v.reasoning) } : {} };
|
|
1442
|
+
}
|
|
1443
|
+
const args = entryRaw.args ?? [];
|
|
1444
|
+
if (!Array.isArray(args)) missing.add(`${where}.args must be a list of strings`);
|
|
1445
|
+
const env = entryRaw.env ?? {};
|
|
1446
|
+
if (typeof env !== "object" || Array.isArray(env)) missing.add(`${where}.env must be a mapping of name \u2192 value`);
|
|
1447
|
+
const maxThinkingTokens = entryRaw.maxThinkingTokens === void 0 ? DEFAULT_MAX_THINKING_TOKENS : Number(entryRaw.maxThinkingTokens);
|
|
1448
|
+
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
1449
|
+
missing.add(`${where}.maxThinkingTokens must be a non-negative integer (got "${entryRaw.maxThinkingTokens}")`);
|
|
1450
|
+
}
|
|
1451
|
+
out.push({
|
|
1452
|
+
name,
|
|
1453
|
+
harness,
|
|
1454
|
+
bin: expandCommand(String(entryRaw.bin ?? adapter.defaultBin), base),
|
|
1455
|
+
...entryRaw.launcher ? { launcher: expandCommand(String(entryRaw.launcher), base) } : {},
|
|
1456
|
+
model: String(entryRaw.model ?? tiers.standard?.model ?? adapter.defaultModel),
|
|
1457
|
+
tiers,
|
|
1458
|
+
args: Array.isArray(args) ? args.map(String) : [],
|
|
1459
|
+
env: Object.fromEntries(Object.entries(env).map(([k, v]) => [k, String(v)])),
|
|
1460
|
+
maxThinkingTokens
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
return out;
|
|
1464
|
+
}
|
|
1465
|
+
function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, toolchainNames) {
|
|
1142
1466
|
rejectUnknownKeys(c, ROUTE_KEYS, `${where}`, file);
|
|
1467
|
+
const toolchain = c?.toolchain === void 0 ? void 0 : String(c.toolchain);
|
|
1468
|
+
if (toolchain !== void 0 && !toolchainNames.has(toolchain)) {
|
|
1469
|
+
missing.add(`${where}.toolchain "${toolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
|
|
1470
|
+
}
|
|
1143
1471
|
const route = missing.req(c?.route, `${where}.route`);
|
|
1144
1472
|
const routeParts = route ? route.split("/") : [];
|
|
1145
1473
|
const routeWellFormed = routeParts.length === 2 && routeParts.every(Boolean);
|
|
@@ -1188,6 +1516,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome) {
|
|
|
1188
1516
|
reposBasePath: expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base),
|
|
1189
1517
|
repoOverrides,
|
|
1190
1518
|
worktreePrefix: c?.worktreePrefix ? String(c.worktreePrefix) : void 0,
|
|
1519
|
+
...toolchain !== void 0 ? { toolchain } : {},
|
|
1191
1520
|
promptsDir: resolvePromptsDir(c?.promptSet ? String(c.promptSet) : void 0, crewHome, base),
|
|
1192
1521
|
weight,
|
|
1193
1522
|
baseUrl: String(c?.baseUrl ?? raw.ship?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
@@ -1256,7 +1585,7 @@ function repoIdForName(r, name) {
|
|
|
1256
1585
|
function resolveApiKey(r) {
|
|
1257
1586
|
if (r.apiKey) return r.apiKey;
|
|
1258
1587
|
if (r.apiKeyFile && r.apiKeyVar && existsSync3(r.apiKeyFile)) {
|
|
1259
|
-
for (const line of
|
|
1588
|
+
for (const line of readFileSync4(r.apiKeyFile, "utf8").split("\n")) {
|
|
1260
1589
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
1261
1590
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
1262
1591
|
}
|
|
@@ -1319,7 +1648,7 @@ function configuredMembers(rt) {
|
|
|
1319
1648
|
return out;
|
|
1320
1649
|
}
|
|
1321
1650
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
1322
|
-
return
|
|
1651
|
+
return join4(basePath, workspace, repoName);
|
|
1323
1652
|
}
|
|
1324
1653
|
function reposOf(r) {
|
|
1325
1654
|
const named = Object.entries(r.repos);
|
|
@@ -1343,7 +1672,7 @@ function passengerRepoTargets(r) {
|
|
|
1343
1672
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
1344
1673
|
}
|
|
1345
1674
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
1346
|
-
return
|
|
1675
|
+
return join4(basePath, workspace, ".passenger-checkouts", repoName);
|
|
1347
1676
|
}
|
|
1348
1677
|
function passengerCheckoutTargets(r) {
|
|
1349
1678
|
if (!r.hostPassengers) return [];
|
|
@@ -1475,8 +1804,8 @@ function rosterMarkdown(roster, meId) {
|
|
|
1475
1804
|
}
|
|
1476
1805
|
|
|
1477
1806
|
// src/state.ts
|
|
1478
|
-
import { readFileSync as
|
|
1479
|
-
import { join as
|
|
1807
|
+
import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
1808
|
+
import { join as join5 } from "path";
|
|
1480
1809
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
1481
1810
|
var State = class _State {
|
|
1482
1811
|
dir;
|
|
@@ -1485,7 +1814,7 @@ var State = class _State {
|
|
|
1485
1814
|
mkdirSync2(dir, { recursive: true });
|
|
1486
1815
|
}
|
|
1487
1816
|
path(...parts) {
|
|
1488
|
-
return
|
|
1817
|
+
return join5(this.dir, ...parts);
|
|
1489
1818
|
}
|
|
1490
1819
|
/**
|
|
1491
1820
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -1496,7 +1825,7 @@ var State = class _State {
|
|
|
1496
1825
|
*/
|
|
1497
1826
|
watermark() {
|
|
1498
1827
|
try {
|
|
1499
|
-
const v =
|
|
1828
|
+
const v = readFileSync5(this.path(".poll-watermark"), "utf8").trim();
|
|
1500
1829
|
return v || EPOCH;
|
|
1501
1830
|
} catch {
|
|
1502
1831
|
return EPOCH;
|
|
@@ -1582,7 +1911,7 @@ var State = class _State {
|
|
|
1582
1911
|
const name = _State.safe(rawName);
|
|
1583
1912
|
const file = this.path(`.${name}.lock`);
|
|
1584
1913
|
try {
|
|
1585
|
-
const existing = Number.parseInt(
|
|
1914
|
+
const existing = Number.parseInt(readFileSync5(file, "utf8").trim(), 10);
|
|
1586
1915
|
if (Number.isFinite(existing) && existing > 0) {
|
|
1587
1916
|
try {
|
|
1588
1917
|
process.kill(existing, 0);
|
|
@@ -1676,7 +2005,7 @@ var State = class _State {
|
|
|
1676
2005
|
const heldReported = `.release-held-${suffix}`;
|
|
1677
2006
|
const read = (f) => {
|
|
1678
2007
|
try {
|
|
1679
|
-
return
|
|
2008
|
+
return readFileSync5(this.path(f), "utf8").trim() || null;
|
|
1680
2009
|
} catch {
|
|
1681
2010
|
return null;
|
|
1682
2011
|
}
|
|
@@ -1726,7 +2055,7 @@ var State = class _State {
|
|
|
1726
2055
|
/** Whether the board was already told about this head's red gate. */
|
|
1727
2056
|
heldReported: () => {
|
|
1728
2057
|
try {
|
|
1729
|
-
return
|
|
2058
|
+
return readFileSync5(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
1730
2059
|
} catch {
|
|
1731
2060
|
return null;
|
|
1732
2061
|
}
|
|
@@ -1763,7 +2092,7 @@ var State = class _State {
|
|
|
1763
2092
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
1764
2093
|
const read = (f) => {
|
|
1765
2094
|
try {
|
|
1766
|
-
return
|
|
2095
|
+
return readFileSync5(this.path(f), "utf8").trim() || null;
|
|
1767
2096
|
} catch {
|
|
1768
2097
|
return null;
|
|
1769
2098
|
}
|
|
@@ -1828,7 +2157,7 @@ var State = class _State {
|
|
|
1828
2157
|
return {
|
|
1829
2158
|
previous: () => {
|
|
1830
2159
|
try {
|
|
1831
|
-
const parsed = JSON.parse(
|
|
2160
|
+
const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
|
|
1832
2161
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1833
2162
|
} catch {
|
|
1834
2163
|
return {};
|
|
@@ -1849,7 +2178,7 @@ var State = class _State {
|
|
|
1849
2178
|
return {
|
|
1850
2179
|
previous: () => {
|
|
1851
2180
|
try {
|
|
1852
|
-
const parsed = JSON.parse(
|
|
2181
|
+
const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
|
|
1853
2182
|
return Array.isArray(parsed) ? parsed : [];
|
|
1854
2183
|
} catch {
|
|
1855
2184
|
return [];
|
|
@@ -1882,7 +2211,7 @@ var State = class _State {
|
|
|
1882
2211
|
}
|
|
1883
2212
|
readInt(file) {
|
|
1884
2213
|
try {
|
|
1885
|
-
return Number.parseInt(
|
|
2214
|
+
return Number.parseInt(readFileSync5(this.path(file), "utf8").trim(), 10) || 0;
|
|
1886
2215
|
} catch {
|
|
1887
2216
|
return 0;
|
|
1888
2217
|
}
|
|
@@ -2186,10 +2515,10 @@ function describeRelease(o, route) {
|
|
|
2186
2515
|
}
|
|
2187
2516
|
|
|
2188
2517
|
// src/install.ts
|
|
2189
|
-
import { dirname as dirname3, join as
|
|
2190
|
-
import { homedir as
|
|
2191
|
-
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as
|
|
2192
|
-
import { spawnSync } from "child_process";
|
|
2518
|
+
import { dirname as dirname3, join as join6, extname } from "path";
|
|
2519
|
+
import { homedir as homedir3 } from "os";
|
|
2520
|
+
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
|
|
2521
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2193
2522
|
import { createHash } from "crypto";
|
|
2194
2523
|
|
|
2195
2524
|
// src/runtime-info.ts
|
|
@@ -2200,7 +2529,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
2200
2529
|
// src/install.ts
|
|
2201
2530
|
function crewInvocation(crewHome) {
|
|
2202
2531
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
2203
|
-
return [process.execPath,
|
|
2532
|
+
return [process.execPath, join6(crewHome, "bin", "crew")];
|
|
2204
2533
|
}
|
|
2205
2534
|
var INTERVAL_SECONDS = 120;
|
|
2206
2535
|
function labelFor(crewHome, job) {
|
|
@@ -2238,7 +2567,7 @@ function pathFor(ship) {
|
|
|
2238
2567
|
return parts.join(":");
|
|
2239
2568
|
}
|
|
2240
2569
|
function findOnPath(bin, searchPath) {
|
|
2241
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
2570
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join6(dir, bin));
|
|
2242
2571
|
for (const candidate of candidates) {
|
|
2243
2572
|
try {
|
|
2244
2573
|
accessSync(candidate, constants.X_OK);
|
|
@@ -2248,7 +2577,7 @@ function findOnPath(bin, searchPath) {
|
|
|
2248
2577
|
}
|
|
2249
2578
|
return void 0;
|
|
2250
2579
|
}
|
|
2251
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
2580
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join6(homedir3(), ".docker", "bin")].join(":");
|
|
2252
2581
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
2253
2582
|
const schedulerPath = pathFor(ship);
|
|
2254
2583
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -2260,7 +2589,7 @@ function xmlEscape(s) {
|
|
|
2260
2589
|
}
|
|
2261
2590
|
function planLaunchd(ship, crewHome, job) {
|
|
2262
2591
|
const label = labelFor(crewHome, job);
|
|
2263
|
-
const unitPath =
|
|
2592
|
+
const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2264
2593
|
const crewLog = ship.logFile;
|
|
2265
2594
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2266
2595
|
const persistent = job === "run";
|
|
@@ -2312,8 +2641,8 @@ ${scheduleKeys}
|
|
|
2312
2641
|
}
|
|
2313
2642
|
function planSystemd(ship, crewHome, job) {
|
|
2314
2643
|
const label = labelFor(crewHome, job);
|
|
2315
|
-
const unitDir =
|
|
2316
|
-
const servicePath =
|
|
2644
|
+
const unitDir = join6(homedir3(), ".config", "systemd", "user");
|
|
2645
|
+
const servicePath = join6(unitDir, `${label}.service`);
|
|
2317
2646
|
const crewLog = ship.logFile;
|
|
2318
2647
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2319
2648
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -2350,7 +2679,7 @@ WantedBy=default.target
|
|
|
2350
2679
|
schedulerLog
|
|
2351
2680
|
};
|
|
2352
2681
|
}
|
|
2353
|
-
const timerPath =
|
|
2682
|
+
const timerPath = join6(unitDir, `${label}.timer`);
|
|
2354
2683
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
2355
2684
|
[Unit]
|
|
2356
2685
|
Description=${description}
|
|
@@ -2417,7 +2746,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2417
2746
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
2418
2747
|
const label = labelFor(crewHome, job);
|
|
2419
2748
|
if (hostShip === "macos") {
|
|
2420
|
-
const unitPath =
|
|
2749
|
+
const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2421
2750
|
return {
|
|
2422
2751
|
mechanism: "launchd",
|
|
2423
2752
|
unitPaths: [unitPath],
|
|
@@ -2427,18 +2756,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2427
2756
|
};
|
|
2428
2757
|
}
|
|
2429
2758
|
if (hostShip === "linux" && hasSystemd) {
|
|
2430
|
-
const unitDir =
|
|
2759
|
+
const unitDir = join6(homedir3(), ".config", "systemd", "user");
|
|
2431
2760
|
if (job === "run") {
|
|
2432
2761
|
return {
|
|
2433
2762
|
mechanism: "systemd",
|
|
2434
|
-
unitPaths: [
|
|
2763
|
+
unitPaths: [join6(unitDir, `${label}.service`)],
|
|
2435
2764
|
crontabLine: null,
|
|
2436
2765
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
2437
2766
|
};
|
|
2438
2767
|
}
|
|
2439
2768
|
return {
|
|
2440
2769
|
mechanism: "systemd",
|
|
2441
|
-
unitPaths: [
|
|
2770
|
+
unitPaths: [join6(unitDir, `${label}.service`), join6(unitDir, `${label}.timer`)],
|
|
2442
2771
|
crontabLine: null,
|
|
2443
2772
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
2444
2773
|
};
|
|
@@ -2456,7 +2785,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2456
2785
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
2457
2786
|
const label = labelFor(crewHome, "run");
|
|
2458
2787
|
if (hostShip === "macos") {
|
|
2459
|
-
const unitPath =
|
|
2788
|
+
const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2460
2789
|
return {
|
|
2461
2790
|
mechanism: "launchd",
|
|
2462
2791
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -2480,7 +2809,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
|
2480
2809
|
}
|
|
2481
2810
|
function runAll(commands, cwd, log) {
|
|
2482
2811
|
for (const [bin, ...args] of commands) {
|
|
2483
|
-
const res =
|
|
2812
|
+
const res = spawnSync2(bin, args, { cwd, encoding: "utf8" });
|
|
2484
2813
|
if (res.error || res.status !== 0) {
|
|
2485
2814
|
log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
|
|
2486
2815
|
output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
|
|
@@ -2545,7 +2874,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2545
2874
|
if (dryRun) {
|
|
2546
2875
|
log.emit(`would run: launchctl remove ${plan.removeLabel}`);
|
|
2547
2876
|
} else {
|
|
2548
|
-
if (
|
|
2877
|
+
if (spawnSync2("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
|
|
2549
2878
|
log.emit(`removed loaded job ${plan.removeLabel}`);
|
|
2550
2879
|
}
|
|
2551
2880
|
}
|
|
@@ -2570,7 +2899,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2570
2899
|
}
|
|
2571
2900
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
2572
2901
|
function launchAgentsDir() {
|
|
2573
|
-
return
|
|
2902
|
+
return join6(homedir3(), "Library", "LaunchAgents");
|
|
2574
2903
|
}
|
|
2575
2904
|
function xmlUnescape(s) {
|
|
2576
2905
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -2582,17 +2911,17 @@ function programFromPlist(text) {
|
|
|
2582
2911
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
2583
2912
|
}
|
|
2584
2913
|
function loadedCrewLabels() {
|
|
2585
|
-
const list =
|
|
2914
|
+
const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
|
|
2586
2915
|
if (list.status !== 0) return [];
|
|
2587
2916
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
2588
2917
|
}
|
|
2589
2918
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
2590
2919
|
const files = existsSync5(dir) ? readdirSync(dir) : [];
|
|
2591
2920
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
2592
|
-
const plistPath =
|
|
2921
|
+
const plistPath = join6(dir, f);
|
|
2593
2922
|
let program = null;
|
|
2594
2923
|
try {
|
|
2595
|
-
program = programFromPlist(
|
|
2924
|
+
program = programFromPlist(readFileSync6(plistPath, "utf8"));
|
|
2596
2925
|
} catch {
|
|
2597
2926
|
}
|
|
2598
2927
|
return {
|
|
@@ -2605,7 +2934,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
|
2605
2934
|
const seen = new Set(units.map((u) => u.label));
|
|
2606
2935
|
for (const label of [...loaded].sort()) {
|
|
2607
2936
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
2608
|
-
units.push({ label, plistPath:
|
|
2937
|
+
units.push({ label, plistPath: join6(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
2609
2938
|
}
|
|
2610
2939
|
return units;
|
|
2611
2940
|
}
|
|
@@ -2618,8 +2947,8 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
2618
2947
|
log.emit(`would unload and remove ${unit.label}`);
|
|
2619
2948
|
return;
|
|
2620
2949
|
}
|
|
2621
|
-
if (existsSync5(unit.plistPath))
|
|
2622
|
-
|
|
2950
|
+
if (existsSync5(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
2951
|
+
spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
2623
2952
|
if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
2624
2953
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
2625
2954
|
}
|
|
@@ -2652,7 +2981,7 @@ function parseLaunchctlList(text) {
|
|
|
2652
2981
|
return jobs;
|
|
2653
2982
|
}
|
|
2654
2983
|
function describeForeignLaunchdUnits(crewHome, dir) {
|
|
2655
|
-
const list =
|
|
2984
|
+
const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
|
|
2656
2985
|
const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
|
|
2657
2986
|
const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
|
|
2658
2987
|
return foreign.map((u) => {
|
|
@@ -2665,11 +2994,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
|
|
|
2665
2994
|
});
|
|
2666
2995
|
}
|
|
2667
2996
|
function readCrontab() {
|
|
2668
|
-
const res =
|
|
2997
|
+
const res = spawnSync2("crontab", ["-l"], { encoding: "utf8" });
|
|
2669
2998
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
2670
2999
|
}
|
|
2671
3000
|
function writeCrontab(text) {
|
|
2672
|
-
|
|
3001
|
+
spawnSync2("crontab", ["-"], { input: text, encoding: "utf8" });
|
|
2673
3002
|
}
|
|
2674
3003
|
function addCrontabLine(marker, line) {
|
|
2675
3004
|
const existing = readCrontab();
|
|
@@ -2693,7 +3022,7 @@ function removeCrontabLine(marker) {
|
|
|
2693
3022
|
}
|
|
2694
3023
|
function detectSystemd() {
|
|
2695
3024
|
if (process.platform !== "linux") return false;
|
|
2696
|
-
const res =
|
|
3025
|
+
const res = spawnSync2("systemctl", ["--user", "status"], { encoding: "utf8" });
|
|
2697
3026
|
return !res.error;
|
|
2698
3027
|
}
|
|
2699
3028
|
|
|
@@ -4813,7 +5142,7 @@ function materializeBranchForTicket(dir, cfg, t, role) {
|
|
|
4813
5142
|
|
|
4814
5143
|
// src/poll.ts
|
|
4815
5144
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4816
|
-
import { join as
|
|
5145
|
+
import { join as join7 } from "path";
|
|
4817
5146
|
|
|
4818
5147
|
// src/stalled.ts
|
|
4819
5148
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -5085,7 +5414,7 @@ async function decideCycle(o) {
|
|
|
5085
5414
|
}
|
|
5086
5415
|
return decision;
|
|
5087
5416
|
}
|
|
5088
|
-
var digestPath = (stateDir, route, role) =>
|
|
5417
|
+
var digestPath = (stateDir, route, role) => join7(stateDir, `digest-${route}-${role}.md`);
|
|
5089
5418
|
function writeDigest(o, d, role, stateDir) {
|
|
5090
5419
|
const me = o.route.resolved?.seats[role];
|
|
5091
5420
|
if (!me) return false;
|
|
@@ -5455,9 +5784,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
5455
5784
|
|
|
5456
5785
|
// src/agent.ts
|
|
5457
5786
|
import { spawn as spawn2 } from "child_process";
|
|
5458
|
-
import { readFileSync as
|
|
5787
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7, statSync as statSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
|
|
5459
5788
|
import { finished } from "stream/promises";
|
|
5460
|
-
import { join as
|
|
5789
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
5461
5790
|
import { createHash as createHash2 } from "crypto";
|
|
5462
5791
|
|
|
5463
5792
|
// src/environment.ts
|
|
@@ -5742,54 +6071,6 @@ function renderEnvironment(i) {
|
|
|
5742
6071
|
return lines.join("\n");
|
|
5743
6072
|
}
|
|
5744
6073
|
|
|
5745
|
-
// src/stream.ts
|
|
5746
|
-
function toolTarget(name, input) {
|
|
5747
|
-
if (!input) return void 0;
|
|
5748
|
-
for (const key of ["file_path", "path", "notebook_path", "url", "pattern"]) {
|
|
5749
|
-
const v = input[key];
|
|
5750
|
-
if (typeof v === "string" && v) return v;
|
|
5751
|
-
}
|
|
5752
|
-
if (name === "Bash" && typeof input.command === "string") {
|
|
5753
|
-
return input.command.trim().split(/\s+/)[0];
|
|
5754
|
-
}
|
|
5755
|
-
return void 0;
|
|
5756
|
-
}
|
|
5757
|
-
function mapStreamLine(line) {
|
|
5758
|
-
const obj = line;
|
|
5759
|
-
if (!obj || obj.type !== "assistant") return [];
|
|
5760
|
-
const content = obj.message?.content;
|
|
5761
|
-
if (!Array.isArray(content)) return [];
|
|
5762
|
-
const out = [];
|
|
5763
|
-
for (const block of content) {
|
|
5764
|
-
if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
|
|
5765
|
-
else if (block.type === "text") out.push({ kind: "text", text: block.text });
|
|
5766
|
-
else if (block.type === "tool_use") {
|
|
5767
|
-
out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
|
|
5768
|
-
}
|
|
5769
|
-
}
|
|
5770
|
-
return out;
|
|
5771
|
-
}
|
|
5772
|
-
function extractTurnTokens(line) {
|
|
5773
|
-
const obj = line;
|
|
5774
|
-
if (!obj || obj.type !== "assistant") return void 0;
|
|
5775
|
-
const usage2 = obj.message?.usage;
|
|
5776
|
-
if (!usage2) return void 0;
|
|
5777
|
-
return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
|
|
5778
|
-
}
|
|
5779
|
-
function extractResult(line) {
|
|
5780
|
-
const obj = line;
|
|
5781
|
-
if (!obj || obj.type !== "result") return void 0;
|
|
5782
|
-
return {
|
|
5783
|
-
sessionId: obj.session_id,
|
|
5784
|
-
numTurns: obj.num_turns,
|
|
5785
|
-
totalCostUsd: obj.total_cost_usd,
|
|
5786
|
-
inputTokens: obj.usage?.input_tokens,
|
|
5787
|
-
outputTokens: obj.usage?.output_tokens,
|
|
5788
|
-
cacheReadTokens: obj.usage?.cache_read_input_tokens,
|
|
5789
|
-
cacheCreationTokens: obj.usage?.cache_creation_input_tokens
|
|
5790
|
-
};
|
|
5791
|
-
}
|
|
5792
|
-
|
|
5793
6074
|
// src/agent-log.ts
|
|
5794
6075
|
async function post(target, path, body) {
|
|
5795
6076
|
const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
|
|
@@ -5871,36 +6152,10 @@ var TicketAttributionTracker = class {
|
|
|
5871
6152
|
|
|
5872
6153
|
// src/agent.ts
|
|
5873
6154
|
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
if (role === "triage") return ["Bash", "Read", tablation];
|
|
5877
|
-
if (role === "qa") return ["Bash", "Read", "Write", "Grep", "Glob", tablation];
|
|
5878
|
-
const base = ["Bash", "Read", "Edit", "Write", "Grep", "Glob", tablation];
|
|
5879
|
-
return role === "design" ? [...base, "Skill", "Artifact"] : base;
|
|
5880
|
-
}
|
|
5881
|
-
var DISALLOWED_TOOLS = [
|
|
5882
|
-
"Bash(rm -rf:*)",
|
|
5883
|
-
"Bash(rm -fr:*)",
|
|
5884
|
-
"Bash(git push --force*)",
|
|
5885
|
-
"Bash(git push -f*)",
|
|
5886
|
-
"Bash(curl*| sh)",
|
|
5887
|
-
"Bash(curl*|sh)",
|
|
5888
|
-
"Bash(curl*| bash)",
|
|
5889
|
-
"Bash(curl*|bash)",
|
|
5890
|
-
"Bash(sudo:*)"
|
|
5891
|
-
];
|
|
5892
|
-
var BILLING_VARS_TO_UNSET = [
|
|
5893
|
-
"ANTHROPIC_API_KEY",
|
|
5894
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
5895
|
-
"CLAUDE_CODE_USE_VERTEX",
|
|
5896
|
-
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
5897
|
-
"ANTHROPIC_VERTEX_REGION",
|
|
5898
|
-
"CLOUD_ML_REGION",
|
|
5899
|
-
"ANTHROPIC_MODEL"
|
|
5900
|
-
];
|
|
5901
|
-
function scrubbedEnv(env = process.env) {
|
|
6155
|
+
var LEGACY_TOOLCHAIN_HARNESS = "claude";
|
|
6156
|
+
function scrubbedEnv(env = process.env, harness = LEGACY_TOOLCHAIN_HARNESS) {
|
|
5902
6157
|
const out = { ...env };
|
|
5903
|
-
for (const v of
|
|
6158
|
+
for (const v of adapterFor(harness).unsetEnv) delete out[v];
|
|
5904
6159
|
return out;
|
|
5905
6160
|
}
|
|
5906
6161
|
var CREW_LANE_ROLE_VAR = "CREW_LANE_ROLE";
|
|
@@ -5914,23 +6169,23 @@ var AgentError = class extends Error {
|
|
|
5914
6169
|
var AgentSpawnError = class extends AgentError {
|
|
5915
6170
|
};
|
|
5916
6171
|
function planAgentRun(o) {
|
|
5917
|
-
const briefPath =
|
|
6172
|
+
const briefPath = join8(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
5918
6173
|
if (!existsSync7(briefPath)) {
|
|
5919
6174
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
5920
6175
|
}
|
|
5921
|
-
const commonPath =
|
|
6176
|
+
const commonPath = join8(o.route.promptsDir, "personas", "common.md");
|
|
5922
6177
|
if (!existsSync7(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
|
|
5923
6178
|
const now = o.now ?? (() => Date.now());
|
|
5924
6179
|
const routeLabel = routeSlug(o.route.route);
|
|
5925
|
-
const digestPath2 =
|
|
6180
|
+
const digestPath2 = join8(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
5926
6181
|
let digest;
|
|
5927
6182
|
let ageSeconds;
|
|
5928
6183
|
if (existsSync7(digestPath2)) {
|
|
5929
6184
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
5930
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
6185
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync7(digestPath2, "utf8");
|
|
5931
6186
|
}
|
|
5932
|
-
const common =
|
|
5933
|
-
const brief =
|
|
6187
|
+
const common = readFileSync7(commonPath, "utf8");
|
|
6188
|
+
const brief = readFileSync7(briefPath, "utf8");
|
|
5934
6189
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
5935
6190
|
const effectiveBrief = o.divergedPrompt ?? brief;
|
|
5936
6191
|
const prompt = assemblePrompt({
|
|
@@ -5941,9 +6196,18 @@ function planAgentRun(o) {
|
|
|
5941
6196
|
digest
|
|
5942
6197
|
});
|
|
5943
6198
|
const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
|
|
5944
|
-
const streamsDir =
|
|
6199
|
+
const streamsDir = join8(o.stateDir, "streams");
|
|
5945
6200
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
5946
|
-
const
|
|
6201
|
+
const toolchain = resolveToolchain(o.ship, o.route);
|
|
6202
|
+
const adapter = adapterFor(toolchain.harness);
|
|
6203
|
+
const model = o.agentModel ?? toolchain.model;
|
|
6204
|
+
const invocation = adapter.invocation(toolchain, {
|
|
6205
|
+
role: o.role,
|
|
6206
|
+
model,
|
|
6207
|
+
// Skipped when there's no key to send: a route `crew connect` hasn't
|
|
6208
|
+
// resolved yet has nothing to authenticate the MCP connection with either.
|
|
6209
|
+
mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
|
|
6210
|
+
});
|
|
5947
6211
|
const agentLog = o.route.resolved && o.apiKey ? {
|
|
5948
6212
|
baseUrl: o.route.baseUrl,
|
|
5949
6213
|
workspaceId: o.route.resolved.workspaceId,
|
|
@@ -5961,43 +6225,16 @@ function planAgentRun(o) {
|
|
|
5961
6225
|
// (config.ts), and the digest's `repo` column is what actually places
|
|
5962
6226
|
// the work. Kept deliberately rather than guessed at (ISSUE-349).
|
|
5963
6227
|
cwd: o.route.dir,
|
|
5964
|
-
bin:
|
|
5965
|
-
args:
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
...allowedTools(o.role),
|
|
5969
|
-
"--disallowedTools",
|
|
5970
|
-
...DISALLOWED_TOOLS,
|
|
5971
|
-
"--model",
|
|
5972
|
-
model,
|
|
5973
|
-
"--output-format",
|
|
5974
|
-
"stream-json",
|
|
5975
|
-
"--verbose",
|
|
5976
|
-
// Registers the tracker's own MCP tool surface for this run only —
|
|
5977
|
-
// `${CREW_API_KEY}` is interpolated by the CLI from the environment
|
|
5978
|
-
// (set below), so the resolved key is never written to an argv string
|
|
5979
|
-
// or a config file on disk. Skipped when there's no key to send: a
|
|
5980
|
-
// route `crew connect` hasn't resolved yet has nothing to authenticate
|
|
5981
|
-
// the MCP connection with either.
|
|
5982
|
-
...o.apiKey ? ["--mcp-config", JSON.stringify({
|
|
5983
|
-
mcpServers: {
|
|
5984
|
-
tablation: {
|
|
5985
|
-
type: "http",
|
|
5986
|
-
url: `${o.route.baseUrl}/api/mcp`,
|
|
5987
|
-
headers: { Authorization: `Bearer \${${API_KEY_VAR}}` }
|
|
5988
|
-
}
|
|
5989
|
-
}
|
|
5990
|
-
})] : []
|
|
5991
|
-
],
|
|
6228
|
+
bin: invocation.bin,
|
|
6229
|
+
args: invocation.args,
|
|
6230
|
+
toolchain: toolchain.name,
|
|
6231
|
+
harness: toolchain.harness,
|
|
5992
6232
|
prompt,
|
|
5993
6233
|
promptBytes: Buffer.byteLength(prompt, "utf8"),
|
|
5994
|
-
unsetEnv:
|
|
6234
|
+
unsetEnv: adapter.unsetEnv,
|
|
5995
6235
|
setEnv: {
|
|
5996
6236
|
...o.apiKey ? { [API_KEY_VAR]: o.apiKey } : {},
|
|
5997
|
-
|
|
5998
|
-
// blocks at all — without it, `run.cycles` (below) is always empty
|
|
5999
|
-
// and Agent Log Cycles reporting has nothing to report (ISSUE-558).
|
|
6000
|
-
...o.ship.agent.maxThinkingTokens > 0 ? { MAX_THINKING_TOKENS: String(o.ship.agent.maxThinkingTokens) } : {},
|
|
6237
|
+
...adapter.runEnv(toolchain),
|
|
6001
6238
|
// The operator's global SessionStart hook (pair-context-hook.sh) injects
|
|
6002
6239
|
// the interactive Pair persona's prompt for any session it can't tell
|
|
6003
6240
|
// apart from a person's own terminal. Every headless lane run this
|
|
@@ -6011,8 +6248,8 @@ function planAgentRun(o) {
|
|
|
6011
6248
|
digestAttached: digest !== void 0,
|
|
6012
6249
|
digestAgeSeconds: ageSeconds,
|
|
6013
6250
|
model,
|
|
6014
|
-
streamPath:
|
|
6015
|
-
eventsPath:
|
|
6251
|
+
streamPath: join8(streamsDir, `${base}.jsonl`),
|
|
6252
|
+
eventsPath: join8(streamsDir, `${base}.events.jsonl`),
|
|
6016
6253
|
ticket: o.ticket,
|
|
6017
6254
|
agentLog,
|
|
6018
6255
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
@@ -6022,6 +6259,8 @@ function planAgentRun(o) {
|
|
|
6022
6259
|
function describePlan(p) {
|
|
6023
6260
|
return [
|
|
6024
6261
|
`role: ${p.role}`,
|
|
6262
|
+
// Only for a non-default toolchain, so a plain Claude run's output is unchanged.
|
|
6263
|
+
...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
|
|
6025
6264
|
`cwd: ${p.cwd}`,
|
|
6026
6265
|
`command: ${p.bin} ${p.args.join(" ")}`,
|
|
6027
6266
|
`prompt: ${p.promptBytes} bytes on stdin (never argv)`,
|
|
@@ -6047,6 +6286,7 @@ function endSink(sink) {
|
|
|
6047
6286
|
return finished(sink).catch(() => void 0);
|
|
6048
6287
|
}
|
|
6049
6288
|
function spawnAgent(plan, emit) {
|
|
6289
|
+
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
6050
6290
|
const started = Date.now();
|
|
6051
6291
|
const startedAt = new Date(started).toISOString();
|
|
6052
6292
|
const rawSink = openSink(plan.streamPath);
|
|
@@ -6054,7 +6294,7 @@ function spawnAgent(plan, emit) {
|
|
|
6054
6294
|
return new Promise((resolve5, reject) => {
|
|
6055
6295
|
const child = spawn2(plan.bin, plan.args, {
|
|
6056
6296
|
cwd: plan.cwd,
|
|
6057
|
-
env: { ...scrubbedEnv(), ...plan.setEnv },
|
|
6297
|
+
env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv },
|
|
6058
6298
|
stdio: ["pipe", "pipe", "inherit"]
|
|
6059
6299
|
});
|
|
6060
6300
|
let buf = "";
|
|
@@ -6075,9 +6315,9 @@ function spawnAgent(plan, emit) {
|
|
|
6075
6315
|
return;
|
|
6076
6316
|
}
|
|
6077
6317
|
try {
|
|
6078
|
-
result = extractResult(parsed) ?? result;
|
|
6318
|
+
result = adapter.extractResult(parsed) ?? result;
|
|
6079
6319
|
let turnText = "";
|
|
6080
|
-
for (const ev of mapStreamLine(parsed)) {
|
|
6320
|
+
for (const ev of adapter.mapStreamLine(parsed)) {
|
|
6081
6321
|
const e = {
|
|
6082
6322
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6083
6323
|
cycle: emit.cycle,
|
|
@@ -6115,7 +6355,7 @@ function spawnAgent(plan, emit) {
|
|
|
6115
6355
|
`);
|
|
6116
6356
|
}
|
|
6117
6357
|
}
|
|
6118
|
-
const turnTokens = extractTurnTokens(parsed);
|
|
6358
|
+
const turnTokens = adapter.extractTurnTokens(parsed);
|
|
6119
6359
|
if (turnTokens !== void 0) attribution.add(turnText, turnTokens);
|
|
6120
6360
|
} catch {
|
|
6121
6361
|
}
|
|
@@ -6193,12 +6433,12 @@ function spawnAgent(plan, emit) {
|
|
|
6193
6433
|
|
|
6194
6434
|
// src/run-worktree.ts
|
|
6195
6435
|
import { existsSync as existsSync9 } from "fs";
|
|
6196
|
-
import { dirname as dirname6, join as
|
|
6436
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
6197
6437
|
|
|
6198
6438
|
// src/ports.ts
|
|
6199
6439
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6200
6440
|
import { existsSync as existsSync8 } from "fs";
|
|
6201
|
-
import { join as
|
|
6441
|
+
import { join as join9 } from "path";
|
|
6202
6442
|
var run = (cmd, args) => {
|
|
6203
6443
|
try {
|
|
6204
6444
|
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -6243,7 +6483,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
6243
6483
|
}
|
|
6244
6484
|
function worktreeExists(parent, prefix, n) {
|
|
6245
6485
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
6246
|
-
if (existsSync8(
|
|
6486
|
+
if (existsSync8(join9(parent, name, ".git"))) return true;
|
|
6247
6487
|
}
|
|
6248
6488
|
return false;
|
|
6249
6489
|
}
|
|
@@ -6331,7 +6571,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
6331
6571
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
6332
6572
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
6333
6573
|
}
|
|
6334
|
-
const path =
|
|
6574
|
+
const path = join10(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
|
|
6335
6575
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
6336
6576
|
return { action: "cut-remote", path, branch: located.branch };
|
|
6337
6577
|
}
|
|
@@ -6539,7 +6779,7 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
6539
6779
|
}
|
|
6540
6780
|
|
|
6541
6781
|
// src/connect-wizard.ts
|
|
6542
|
-
import { delimiter, dirname as dirname7, join as
|
|
6782
|
+
import { delimiter, dirname as dirname7, join as join11 } from "path";
|
|
6543
6783
|
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
6544
6784
|
import { stringify } from "yaml";
|
|
6545
6785
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
@@ -6562,7 +6802,7 @@ function defaultIsExecutable(p) {
|
|
|
6562
6802
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
6563
6803
|
for (const dir of pathEnv.split(delimiter)) {
|
|
6564
6804
|
if (!dir) continue;
|
|
6565
|
-
const candidate =
|
|
6805
|
+
const candidate = join11(dir, name);
|
|
6566
6806
|
if (isExecutable(candidate)) return candidate;
|
|
6567
6807
|
}
|
|
6568
6808
|
return void 0;
|
|
@@ -6657,8 +6897,8 @@ Wrote ${o.configPath}.
|
|
|
6657
6897
|
}
|
|
6658
6898
|
|
|
6659
6899
|
// src/release-run.ts
|
|
6660
|
-
import { readFileSync as
|
|
6661
|
-
import { dirname as dirname8, join as
|
|
6900
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync11, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync3 } from "fs";
|
|
6901
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
6662
6902
|
import { tmpdir as tmpdir2 } from "os";
|
|
6663
6903
|
|
|
6664
6904
|
// src/release.ts
|
|
@@ -7316,13 +7556,13 @@ async function bump(o, size) {
|
|
|
7316
7556
|
}
|
|
7317
7557
|
return printed;
|
|
7318
7558
|
}
|
|
7319
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
7559
|
+
const files = o.repo.release.versionFiles.map((f) => join12(o.cwd, f));
|
|
7320
7560
|
const first = files[0];
|
|
7321
7561
|
if (!existsSync11(first)) {
|
|
7322
7562
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
7323
7563
|
return null;
|
|
7324
7564
|
}
|
|
7325
|
-
const current = JSON.parse(
|
|
7565
|
+
const current = JSON.parse(readFileSync8(first, "utf8")).version;
|
|
7326
7566
|
if (!current) {
|
|
7327
7567
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
7328
7568
|
return null;
|
|
@@ -7335,7 +7575,7 @@ async function bump(o, size) {
|
|
|
7335
7575
|
return version;
|
|
7336
7576
|
}
|
|
7337
7577
|
for (const f of files) {
|
|
7338
|
-
const src =
|
|
7578
|
+
const src = readFileSync8(f, "utf8");
|
|
7339
7579
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
7340
7580
|
if (out === src) {
|
|
7341
7581
|
o.emit.error(`no version field in ${f}`);
|
|
@@ -7348,13 +7588,13 @@ async function bump(o, size) {
|
|
|
7348
7588
|
function writeChangelog(o, version, entries) {
|
|
7349
7589
|
const file = o.repo.release.changelog;
|
|
7350
7590
|
if (!file || entries.length === 0) return;
|
|
7351
|
-
const path =
|
|
7591
|
+
const path = join12(o.cwd, file);
|
|
7352
7592
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
7353
7593
|
if (o.dryRun) {
|
|
7354
7594
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
7355
7595
|
return;
|
|
7356
7596
|
}
|
|
7357
|
-
const existing = existsSync11(path) ?
|
|
7597
|
+
const existing = existsSync11(path) ? readFileSync8(path, "utf8") : "# Changelog\n";
|
|
7358
7598
|
writeFileSync6(path, insertChangelogSection(existing, section));
|
|
7359
7599
|
}
|
|
7360
7600
|
async function confirm(o, expected) {
|
|
@@ -7436,8 +7676,8 @@ async function confirmLastRelease(o, decision) {
|
|
|
7436
7676
|
function openReleaseCheckout(o) {
|
|
7437
7677
|
const { remote, base } = o.repo.branch;
|
|
7438
7678
|
const branch = `crew/release-${Date.now()}`;
|
|
7439
|
-
const root = mkdtempSync(
|
|
7440
|
-
const dir =
|
|
7679
|
+
const root = mkdtempSync(join12(tmpdir2(), "crew-release-"));
|
|
7680
|
+
const dir = join12(root, "checkout");
|
|
7441
7681
|
const close = () => {
|
|
7442
7682
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
7443
7683
|
rmSync3(root, { recursive: true, force: true });
|
|
@@ -7451,10 +7691,10 @@ function openReleaseCheckout(o) {
|
|
|
7451
7691
|
throw e;
|
|
7452
7692
|
}
|
|
7453
7693
|
for (const f of o.repo.worktrees.copy) {
|
|
7454
|
-
const src =
|
|
7694
|
+
const src = join12(o.cwd, f);
|
|
7455
7695
|
if (!existsSync11(src)) continue;
|
|
7456
|
-
mkdirSync7(dirname8(
|
|
7457
|
-
copyFileSync(src,
|
|
7696
|
+
mkdirSync7(dirname8(join12(dir, f)), { recursive: true });
|
|
7697
|
+
copyFileSync(src, join12(dir, f));
|
|
7458
7698
|
}
|
|
7459
7699
|
return { dir, branch, close };
|
|
7460
7700
|
}
|
|
@@ -8264,8 +8504,8 @@ function nextRoles(decision, ran) {
|
|
|
8264
8504
|
}
|
|
8265
8505
|
|
|
8266
8506
|
// src/agents.ts
|
|
8267
|
-
import { readFileSync as
|
|
8268
|
-
import { join as
|
|
8507
|
+
import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
|
|
8508
|
+
import { join as join13 } from "path";
|
|
8269
8509
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
8270
8510
|
var PERSONA_NAME = {
|
|
8271
8511
|
dev: "Developer",
|
|
@@ -8279,12 +8519,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
8279
8519
|
var AgentsSyncError = class extends Error {
|
|
8280
8520
|
};
|
|
8281
8521
|
function personaDefaultPrompt(promptsDir, role) {
|
|
8282
|
-
const briefPath =
|
|
8522
|
+
const briefPath = join13(promptsDir, "personas", `lane-${role}.md`);
|
|
8283
8523
|
if (!existsSync13(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
8284
|
-
if (role === "pair") return
|
|
8285
|
-
const commonPath =
|
|
8524
|
+
if (role === "pair") return readFileSync9(briefPath, "utf8");
|
|
8525
|
+
const commonPath = join13(promptsDir, "personas", "common.md");
|
|
8286
8526
|
if (!existsSync13(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
|
|
8287
|
-
return `${
|
|
8527
|
+
return `${readFileSync9(commonPath, "utf8")}${readFileSync9(briefPath, "utf8")}`;
|
|
8288
8528
|
}
|
|
8289
8529
|
async function resolveAgentId(route, role, opts) {
|
|
8290
8530
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -8950,14 +9190,14 @@ import {
|
|
|
8950
9190
|
} from "@tablation/client";
|
|
8951
9191
|
|
|
8952
9192
|
// src/skills.ts
|
|
8953
|
-
import { readFileSync as
|
|
8954
|
-
import { join as
|
|
9193
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync2, existsSync as existsSync14 } from "fs";
|
|
9194
|
+
import { join as join14 } from "path";
|
|
8955
9195
|
import { parse as parseYaml } from "yaml";
|
|
8956
9196
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
8957
9197
|
var SkillSyncError = class extends Error {
|
|
8958
9198
|
};
|
|
8959
9199
|
function parseSkillFile(path) {
|
|
8960
|
-
const text =
|
|
9200
|
+
const text = readFileSync10(path, "utf8");
|
|
8961
9201
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
8962
9202
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
8963
9203
|
const frontmatter = match[1];
|
|
@@ -8970,9 +9210,9 @@ function parseSkillFile(path) {
|
|
|
8970
9210
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
8971
9211
|
}
|
|
8972
9212
|
function listLocalSkills(promptsDir) {
|
|
8973
|
-
const dir =
|
|
9213
|
+
const dir = join14(promptsDir, "skills");
|
|
8974
9214
|
if (!existsSync14(dir)) return [];
|
|
8975
|
-
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
9215
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join14(dir, f)));
|
|
8976
9216
|
}
|
|
8977
9217
|
async function syncSkills(route, opts) {
|
|
8978
9218
|
if (!route.resolved) {
|
|
@@ -9151,7 +9391,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
9151
9391
|
}
|
|
9152
9392
|
|
|
9153
9393
|
// src/repos-cmd.ts
|
|
9154
|
-
import { homedir as
|
|
9394
|
+
import { homedir as homedir4 } from "os";
|
|
9155
9395
|
import { basename as basename6, resolve as resolve3 } from "path";
|
|
9156
9396
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
9157
9397
|
var ReposError = class extends Error {
|
|
@@ -9198,7 +9438,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
9198
9438
|
return { route, path: "." };
|
|
9199
9439
|
}
|
|
9200
9440
|
function expandDir(p, base) {
|
|
9201
|
-
const home = p === "~" ?
|
|
9441
|
+
const home = p === "~" ? homedir4() : p.startsWith("~/") ? `${homedir4()}/${p.slice(2)}` : p;
|
|
9202
9442
|
return resolve3(base, home);
|
|
9203
9443
|
}
|
|
9204
9444
|
function routeMapOf(doc, route) {
|
|
@@ -9295,72 +9535,6 @@ function resolveRepoProject(projects, opt, routeAreaId) {
|
|
|
9295
9535
|
);
|
|
9296
9536
|
}
|
|
9297
9537
|
|
|
9298
|
-
// src/claude-mcp.ts
|
|
9299
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
9300
|
-
import { readFileSync as readFileSync10 } from "fs";
|
|
9301
|
-
import { homedir as homedir4 } from "os";
|
|
9302
|
-
import { join as join14 } from "path";
|
|
9303
|
-
var MCP_SERVER_NAME = "tablation";
|
|
9304
|
-
var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
|
|
9305
|
-
function mcpUrlFor(baseUrl) {
|
|
9306
|
-
return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
|
|
9307
|
-
}
|
|
9308
|
-
function runClaude(bin, args) {
|
|
9309
|
-
const r = spawnSync2(bin, args, { encoding: "utf8", timeout: 3e4 });
|
|
9310
|
-
const missing = r.error?.code === "ENOENT";
|
|
9311
|
-
return { status: r.status, missing, stderr: r.stderr ?? "" };
|
|
9312
|
-
}
|
|
9313
|
-
function userScopeEntry(home) {
|
|
9314
|
-
try {
|
|
9315
|
-
const j = JSON.parse(readFileSync10(join14(home, ".claude.json"), "utf8"));
|
|
9316
|
-
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
9317
|
-
} catch {
|
|
9318
|
-
return void 0;
|
|
9319
|
-
}
|
|
9320
|
-
}
|
|
9321
|
-
function inspectClaudeMcp(o) {
|
|
9322
|
-
const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
|
|
9323
|
-
if (got.missing) return { state: "no-claude" };
|
|
9324
|
-
if (got.status !== 0) return { state: "missing" };
|
|
9325
|
-
const entry = userScopeEntry(o.home ?? homedir4());
|
|
9326
|
-
if (!entry) {
|
|
9327
|
-
return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
|
|
9328
|
-
}
|
|
9329
|
-
if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
|
|
9330
|
-
const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
|
|
9331
|
-
if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
|
|
9332
|
-
return { state: "registered", url: o.url };
|
|
9333
|
-
}
|
|
9334
|
-
function ensureClaudeMcp(o) {
|
|
9335
|
-
const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
|
|
9336
|
-
const st = inspectClaudeMcp(o);
|
|
9337
|
-
if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
|
|
9338
|
-
if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
|
|
9339
|
-
if (st.state === "different" && !o.replace) {
|
|
9340
|
-
return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
9341
|
-
}
|
|
9342
|
-
if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
|
|
9343
|
-
if (st.state === "different") {
|
|
9344
|
-
runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
9345
|
-
}
|
|
9346
|
-
const add = runClaude(o.bin, [
|
|
9347
|
-
"mcp",
|
|
9348
|
-
"add",
|
|
9349
|
-
"--transport",
|
|
9350
|
-
"http",
|
|
9351
|
-
MCP_SERVER_NAME,
|
|
9352
|
-
o.url,
|
|
9353
|
-
"--header",
|
|
9354
|
-
`Authorization: Bearer ${o.key}`,
|
|
9355
|
-
"--scope",
|
|
9356
|
-
"user"
|
|
9357
|
-
]);
|
|
9358
|
-
if (add.status !== 0) {
|
|
9359
|
-
return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
|
|
9360
|
-
}
|
|
9361
|
-
return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
|
|
9362
|
-
}
|
|
9363
|
-
|
|
9364
9538
|
// src/doctor-fix.ts
|
|
9365
9539
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
9366
9540
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
@@ -12746,8 +12920,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12746
12920
|
if (!wroteConfig) process.stdout.write(routeBlock);
|
|
12747
12921
|
if (!flag("no-mcp") && apiKey) {
|
|
12748
12922
|
const replace = value("mcp") === "replace";
|
|
12749
|
-
|
|
12923
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12924
|
+
for (const tc of shipToolchains(cfg.ship)) {
|
|
12925
|
+
const id = `${tc.harness}\0${tc.bin}`;
|
|
12926
|
+
if (seen.has(id)) continue;
|
|
12927
|
+
seen.add(id);
|
|
12928
|
+
process.stderr.write(`${adapterFor(tc.harness).ensureMcp({ bin: tc.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
|
|
12750
12929
|
`);
|
|
12930
|
+
}
|
|
12751
12931
|
}
|
|
12752
12932
|
if (found.holds.length) {
|
|
12753
12933
|
process.stderr.write(
|
|
@@ -13303,6 +13483,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13303
13483
|
name: cfg.ship.name,
|
|
13304
13484
|
platform: cfg.ship.platform,
|
|
13305
13485
|
agent: { bin: cfg.ship.agent.bin, model: cfg.ship.agent.model },
|
|
13486
|
+
// Additive (CREW-1511): the configured toolchains and the ship's
|
|
13487
|
+
// default. `agent` above stays for older crew-macos builds.
|
|
13488
|
+
toolchain: resolveToolchain(cfg.ship).name,
|
|
13489
|
+
toolchains: shipToolchains(cfg.ship).map((x) => ({
|
|
13490
|
+
name: x.name,
|
|
13491
|
+
harness: x.harness,
|
|
13492
|
+
bin: x.bin,
|
|
13493
|
+
launcher: x.launcher ?? null,
|
|
13494
|
+
model: x.model
|
|
13495
|
+
})),
|
|
13306
13496
|
stateDir: cfg.ship.stateDir,
|
|
13307
13497
|
logFile: cfg.ship.logFile,
|
|
13308
13498
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
@@ -13328,6 +13518,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13328
13518
|
dir: route.dir,
|
|
13329
13519
|
enabled: route.enabled,
|
|
13330
13520
|
area: route.area ?? null,
|
|
13521
|
+
// Additive (CREW-1511): the toolchain this route's sessions run on.
|
|
13522
|
+
toolchain: resolveToolchain(cfg.ship, route).name,
|
|
13331
13523
|
baseUrl: route.baseUrl,
|
|
13332
13524
|
repos: repos.map((r) => ({
|
|
13333
13525
|
name: r.name,
|
|
@@ -13553,14 +13745,24 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
13553
13745
|
}
|
|
13554
13746
|
}
|
|
13555
13747
|
const schedulerPath = pathFor(cfg.ship);
|
|
13556
|
-
const agentBinPath = findOnPath(cfg.ship.agent.bin, schedulerPath);
|
|
13557
13748
|
const gitPath = findOnPath("git", schedulerPath);
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13749
|
+
const defaultToolchain = resolveToolchain(cfg.ship).name;
|
|
13750
|
+
let allBinsFound = true;
|
|
13751
|
+
for (const tc of shipToolchains(cfg.ship)) {
|
|
13752
|
+
const adapter = adapterFor(tc.harness);
|
|
13753
|
+
const binPath = findOnPath(tc.launcher ?? tc.bin, schedulerPath);
|
|
13754
|
+
if (!binPath) allBinsFound = false;
|
|
13755
|
+
const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
|
|
13756
|
+
process.stdout.write(
|
|
13757
|
+
`${label} ${tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
|
|
13561
13758
|
`
|
|
13562
|
-
|
|
13563
|
-
|
|
13759
|
+
);
|
|
13760
|
+
for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
|
|
13761
|
+
`);
|
|
13762
|
+
}
|
|
13763
|
+
process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
|
|
13764
|
+
`);
|
|
13765
|
+
if (!allBinsFound || !gitPath) {
|
|
13564
13766
|
process.stdout.write(
|
|
13565
13767
|
` scheduler PATH is ${schedulerPath} \u2014 add the directory holding the missing binary to ship.extraPath in crew.yaml, then re-run \`crew doctor\`.
|
|
13566
13768
|
`
|
|
@@ -13568,14 +13770,21 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
13568
13770
|
}
|
|
13569
13771
|
if (route.apiKey) {
|
|
13570
13772
|
const mcpUrl = mcpUrlFor(route.baseUrl);
|
|
13571
|
-
const
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13773
|
+
const seenMcp = /* @__PURE__ */ new Set();
|
|
13774
|
+
for (const tc of shipToolchains(cfg.ship)) {
|
|
13775
|
+
const id = `${tc.harness}\0${tc.bin}`;
|
|
13776
|
+
if (seenMcp.has(id)) continue;
|
|
13777
|
+
seenMcp.add(id);
|
|
13778
|
+
const adapter = adapterFor(tc.harness);
|
|
13779
|
+
const m = adapter.inspectMcp({ bin: tc.bin, url: mcpUrl, key: route.apiKey });
|
|
13780
|
+
process.stdout.write(
|
|
13781
|
+
m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
|
|
13782
|
+
` : m.state === "no-binary" ? `mcp: ${tc.bin} not found \u2014 ${adapter.displayName} was not checked
|
|
13575
13783
|
` : m.state === "different" ? `mcp: tablation registered but not for this route \u2014 ${m.detail}
|
|
13576
13784
|
` : `mcp: not registered \u2014 run crew connect
|
|
13577
13785
|
`
|
|
13578
|
-
|
|
13786
|
+
);
|
|
13787
|
+
}
|
|
13579
13788
|
}
|
|
13580
13789
|
for (const c of cfg.routes.filter((r) => r.enabled)) {
|
|
13581
13790
|
let repos;
|