@tablation/crew 0.27.0 → 0.29.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 +625 -328
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { resolve as resolve4, dirname as dirname11, join as join20, basename as
|
|
4
|
+
import { resolve as resolve4, dirname as dirname11, join as join20, basename as basename9 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
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 basename3 } 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,320 @@ var Emitter = class _Emitter {
|
|
|
157
157
|
};
|
|
158
158
|
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
159
159
|
|
|
160
|
+
// src/toolchain.ts
|
|
161
|
+
import { basename } from "path";
|
|
162
|
+
|
|
163
|
+
// src/stream.ts
|
|
164
|
+
function toolTarget(name, input) {
|
|
165
|
+
if (!input) return void 0;
|
|
166
|
+
for (const key of ["file_path", "path", "notebook_path", "url", "pattern"]) {
|
|
167
|
+
const v = input[key];
|
|
168
|
+
if (typeof v === "string" && v) return v;
|
|
169
|
+
}
|
|
170
|
+
if (name === "Bash" && typeof input.command === "string") {
|
|
171
|
+
return input.command.trim().split(/\s+/)[0];
|
|
172
|
+
}
|
|
173
|
+
return void 0;
|
|
174
|
+
}
|
|
175
|
+
function mapStreamLine(line) {
|
|
176
|
+
const obj = line;
|
|
177
|
+
if (!obj || obj.type !== "assistant") return [];
|
|
178
|
+
const content = obj.message?.content;
|
|
179
|
+
if (!Array.isArray(content)) return [];
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const block of content) {
|
|
182
|
+
if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
|
|
183
|
+
else if (block.type === "text") out.push({ kind: "text", text: block.text });
|
|
184
|
+
else if (block.type === "tool_use") {
|
|
185
|
+
out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function extractTurnTokens(line) {
|
|
191
|
+
const obj = line;
|
|
192
|
+
if (!obj || obj.type !== "assistant") return void 0;
|
|
193
|
+
const usage2 = obj.message?.usage;
|
|
194
|
+
if (!usage2) return void 0;
|
|
195
|
+
return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
|
|
196
|
+
}
|
|
197
|
+
function extractResult(line) {
|
|
198
|
+
const obj = line;
|
|
199
|
+
if (!obj || obj.type !== "result") return void 0;
|
|
200
|
+
return {
|
|
201
|
+
sessionId: obj.session_id,
|
|
202
|
+
numTurns: obj.num_turns,
|
|
203
|
+
totalCostUsd: obj.total_cost_usd,
|
|
204
|
+
inputTokens: obj.usage?.input_tokens,
|
|
205
|
+
outputTokens: obj.usage?.output_tokens,
|
|
206
|
+
cacheReadTokens: obj.usage?.cache_read_input_tokens,
|
|
207
|
+
cacheCreationTokens: obj.usage?.cache_creation_input_tokens
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/claude-mcp.ts
|
|
212
|
+
import { spawnSync } from "child_process";
|
|
213
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
214
|
+
import { homedir } from "os";
|
|
215
|
+
import { join as join2 } from "path";
|
|
216
|
+
var MCP_SERVER_NAME = "tablation";
|
|
217
|
+
var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
|
|
218
|
+
function mcpUrlFor(baseUrl) {
|
|
219
|
+
return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
|
|
220
|
+
}
|
|
221
|
+
function runClaude(bin, args) {
|
|
222
|
+
const r = spawnSync(bin, args, { encoding: "utf8", timeout: 3e4 });
|
|
223
|
+
const missing = r.error?.code === "ENOENT";
|
|
224
|
+
return { status: r.status, missing, stderr: r.stderr ?? "" };
|
|
225
|
+
}
|
|
226
|
+
function userScopeEntry(home) {
|
|
227
|
+
try {
|
|
228
|
+
const j = JSON.parse(readFileSync2(join2(home, ".claude.json"), "utf8"));
|
|
229
|
+
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
230
|
+
} catch {
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function inspectClaudeMcp(o) {
|
|
235
|
+
const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
|
|
236
|
+
if (got.missing) return { state: "no-claude" };
|
|
237
|
+
if (got.status !== 0) return { state: "missing" };
|
|
238
|
+
const entry = userScopeEntry(o.home ?? homedir());
|
|
239
|
+
if (!entry) {
|
|
240
|
+
return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
|
|
241
|
+
}
|
|
242
|
+
if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
|
|
243
|
+
const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
|
|
244
|
+
if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
|
|
245
|
+
return { state: "registered", url: o.url };
|
|
246
|
+
}
|
|
247
|
+
function ensureClaudeMcp(o) {
|
|
248
|
+
const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
|
|
249
|
+
const st = inspectClaudeMcp(o);
|
|
250
|
+
if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
|
|
251
|
+
if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
|
|
252
|
+
if (st.state === "different" && !o.replace) {
|
|
253
|
+
return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
254
|
+
}
|
|
255
|
+
if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
|
|
256
|
+
if (st.state === "different") {
|
|
257
|
+
runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
258
|
+
}
|
|
259
|
+
const add = runClaude(o.bin, [
|
|
260
|
+
"mcp",
|
|
261
|
+
"add",
|
|
262
|
+
"--transport",
|
|
263
|
+
"http",
|
|
264
|
+
MCP_SERVER_NAME,
|
|
265
|
+
o.url,
|
|
266
|
+
"--header",
|
|
267
|
+
`Authorization: Bearer ${o.key}`,
|
|
268
|
+
"--scope",
|
|
269
|
+
"user"
|
|
270
|
+
]);
|
|
271
|
+
if (add.status !== 0) {
|
|
272
|
+
return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
|
|
273
|
+
}
|
|
274
|
+
return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/capability.ts
|
|
278
|
+
function profileFor(role) {
|
|
279
|
+
if (role === "triage") return { level: "read", designTools: false };
|
|
280
|
+
if (role === "qa") return { level: "write", designTools: false };
|
|
281
|
+
return { level: "edit", designTools: role === "design" };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/adapters/claude.ts
|
|
285
|
+
var BILLING_VARS_TO_UNSET = [
|
|
286
|
+
"ANTHROPIC_API_KEY",
|
|
287
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
288
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
289
|
+
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
290
|
+
"ANTHROPIC_VERTEX_REGION",
|
|
291
|
+
"CLOUD_ML_REGION",
|
|
292
|
+
"ANTHROPIC_MODEL"
|
|
293
|
+
];
|
|
294
|
+
var DISALLOWED_TOOLS = [
|
|
295
|
+
"Bash(rm -rf:*)",
|
|
296
|
+
"Bash(rm -fr:*)",
|
|
297
|
+
"Bash(git push --force*)",
|
|
298
|
+
"Bash(git push -f*)",
|
|
299
|
+
"Bash(curl*| sh)",
|
|
300
|
+
"Bash(curl*|sh)",
|
|
301
|
+
"Bash(curl*| bash)",
|
|
302
|
+
"Bash(curl*|bash)",
|
|
303
|
+
"Bash(sudo:*)"
|
|
304
|
+
];
|
|
305
|
+
function claudeToolsFor(p) {
|
|
306
|
+
const tablation = "mcp__tablation__*";
|
|
307
|
+
if (p.level === "read") return ["Bash", "Read", tablation];
|
|
308
|
+
if (p.level === "write") return ["Bash", "Read", "Write", "Grep", "Glob", tablation];
|
|
309
|
+
const base = ["Bash", "Read", "Edit", "Write", "Grep", "Glob", tablation];
|
|
310
|
+
return p.designTools ? [...base, "Skill", "Artifact"] : base;
|
|
311
|
+
}
|
|
312
|
+
function allowedTools(role) {
|
|
313
|
+
return claudeToolsFor(profileFor(role));
|
|
314
|
+
}
|
|
315
|
+
var claudeAdapter = {
|
|
316
|
+
harness: "claude",
|
|
317
|
+
displayName: "Claude Code",
|
|
318
|
+
vendor: "anthropic",
|
|
319
|
+
ownsModel: (model) => model.startsWith("claude-"),
|
|
320
|
+
// The tier's reasoning is the extended-thinking budget; it supersedes the
|
|
321
|
+
// toolchain's `maxThinkingTokens` (0 disables). A non-numeric value is ignored.
|
|
322
|
+
applyReasoning(spec, reasoning) {
|
|
323
|
+
const n = Number(reasoning);
|
|
324
|
+
return Number.isInteger(n) && n >= 0 ? { ...spec, maxThinkingTokens: n } : spec;
|
|
325
|
+
},
|
|
326
|
+
defaultBin: "claude",
|
|
327
|
+
defaultModel: "claude-sonnet-5",
|
|
328
|
+
defaultTiers: {
|
|
329
|
+
// `standard` carries no reasoning: it defers to the toolchain's own
|
|
330
|
+
// `maxThinkingTokens`, which is what a persona with no tier gets today.
|
|
331
|
+
light: { model: "claude-haiku-4-5-20251001", reasoning: "2048" },
|
|
332
|
+
standard: { model: "claude-sonnet-5-5" },
|
|
333
|
+
deep: { model: "claude-opus-5-5", reasoning: "16384" }
|
|
334
|
+
},
|
|
335
|
+
limitations: [
|
|
336
|
+
"Bash is not sandboxed: the deny list refuses known destructive patterns, but a one-liner reaching the same end another way is not caught",
|
|
337
|
+
"the write profile (QA) withholds the Edit tool, not file writes \u2014 Bash can still write files",
|
|
338
|
+
"git push is not blocked, only forced pushes"
|
|
339
|
+
],
|
|
340
|
+
invocation(spec, o) {
|
|
341
|
+
const args = [
|
|
342
|
+
"-p",
|
|
343
|
+
"--allowedTools",
|
|
344
|
+
...allowedTools(o.role),
|
|
345
|
+
"--disallowedTools",
|
|
346
|
+
...DISALLOWED_TOOLS,
|
|
347
|
+
"--model",
|
|
348
|
+
o.model,
|
|
349
|
+
"--output-format",
|
|
350
|
+
"stream-json",
|
|
351
|
+
"--verbose",
|
|
352
|
+
// Registers the tracker's own MCP tool surface for this run only —
|
|
353
|
+
// `${CREW_API_KEY}` is interpolated by the CLI from the environment,
|
|
354
|
+
// so the resolved key is never written to an argv string or a config
|
|
355
|
+
// file on disk. Skipped when there's no key to send: a route `crew
|
|
356
|
+
// connect` hasn't resolved yet has nothing to authenticate with.
|
|
357
|
+
...o.mcp ? ["--mcp-config", JSON.stringify({
|
|
358
|
+
mcpServers: {
|
|
359
|
+
tablation: {
|
|
360
|
+
type: "http",
|
|
361
|
+
url: o.mcp.url,
|
|
362
|
+
headers: { Authorization: `Bearer \${${o.mcp.keyVar}}` }
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
})] : [],
|
|
366
|
+
...spec.args
|
|
367
|
+
];
|
|
368
|
+
return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
|
|
369
|
+
},
|
|
370
|
+
runEnv(spec) {
|
|
371
|
+
return {
|
|
372
|
+
// Extended thinking is what makes the CLI emit `thinking` stream
|
|
373
|
+
// blocks at all — without it, Agent Log Cycles reporting has nothing
|
|
374
|
+
// to report (ISSUE-558).
|
|
375
|
+
...spec.maxThinkingTokens > 0 ? { MAX_THINKING_TOKENS: String(spec.maxThinkingTokens) } : {},
|
|
376
|
+
...spec.env
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
unsetEnv: BILLING_VARS_TO_UNSET,
|
|
380
|
+
mapStreamLine,
|
|
381
|
+
extractResult,
|
|
382
|
+
extractTurnTokens,
|
|
383
|
+
inspectMcp(o) {
|
|
384
|
+
const s = inspectClaudeMcp(o);
|
|
385
|
+
return s.state === "no-claude" ? { state: "no-binary" } : s;
|
|
386
|
+
},
|
|
387
|
+
ensureMcp: ensureClaudeMcp
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/toolchain.ts
|
|
391
|
+
var TIERS = ["light", "standard", "deep"];
|
|
392
|
+
var ADAPTERS = {
|
|
393
|
+
[claudeAdapter.harness]: claudeAdapter
|
|
394
|
+
};
|
|
395
|
+
var HARNESS_NAMES = Object.keys(ADAPTERS);
|
|
396
|
+
function isHarness(name) {
|
|
397
|
+
return Object.hasOwn(ADAPTERS, name);
|
|
398
|
+
}
|
|
399
|
+
function adapterFor(harness) {
|
|
400
|
+
const a = Object.hasOwn(ADAPTERS, harness) ? ADAPTERS[harness] : void 0;
|
|
401
|
+
if (!a) throw new Error(`unknown harness "${harness}" (have: ${HARNESS_NAMES.join(", ")})`);
|
|
402
|
+
return a;
|
|
403
|
+
}
|
|
404
|
+
var LEGACY_TOOLCHAIN = "claude";
|
|
405
|
+
function shipToolchains(ship) {
|
|
406
|
+
const explicit = ship.toolchains ?? [];
|
|
407
|
+
if (explicit.some((t) => t.name === LEGACY_TOOLCHAIN)) return explicit;
|
|
408
|
+
const a = claudeAdapter;
|
|
409
|
+
const legacy = {
|
|
410
|
+
name: LEGACY_TOOLCHAIN,
|
|
411
|
+
harness: a.harness,
|
|
412
|
+
bin: ship.agent.bin,
|
|
413
|
+
model: ship.agent.model,
|
|
414
|
+
tiers: a.defaultTiers,
|
|
415
|
+
args: [],
|
|
416
|
+
env: {},
|
|
417
|
+
maxThinkingTokens: ship.agent.maxThinkingTokens
|
|
418
|
+
};
|
|
419
|
+
return [legacy, ...explicit];
|
|
420
|
+
}
|
|
421
|
+
function resolveToolchain(ship, route) {
|
|
422
|
+
const name = route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
|
|
423
|
+
const found = shipToolchains(ship).find((t) => t.name === name);
|
|
424
|
+
if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
|
|
425
|
+
return found;
|
|
426
|
+
}
|
|
427
|
+
function toolchainVendor(spec) {
|
|
428
|
+
if (spec.launcher && basename(spec.launcher) === "ori") return "openrouter";
|
|
429
|
+
return adapterFor(spec.harness).vendor;
|
|
430
|
+
}
|
|
431
|
+
function resolveSeat(ship, route, role, persona = {}) {
|
|
432
|
+
const warnings = [];
|
|
433
|
+
const base = resolveToolchain(ship, route);
|
|
434
|
+
const baseVendor = toolchainVendor(base);
|
|
435
|
+
const baseWhy = route?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
|
|
436
|
+
let chosen = base;
|
|
437
|
+
let toolchainWhy = baseWhy;
|
|
438
|
+
if (persona.vendor && persona.vendor !== baseVendor) {
|
|
439
|
+
const match = shipToolchains(ship).find((t) => toolchainVendor(t) === persona.vendor);
|
|
440
|
+
if (match) {
|
|
441
|
+
chosen = match;
|
|
442
|
+
toolchainWhy = `persona vendor ${persona.vendor} \u2192 ${match.name}`;
|
|
443
|
+
} else {
|
|
444
|
+
warnings.push(`seat ${role}: persona vendor "${persona.vendor}" has no toolchain on this ship; running ${base.name} (${baseVendor}) instead`);
|
|
445
|
+
toolchainWhy = `${baseWhy} (persona vendor ${persona.vendor} has no toolchain)`;
|
|
446
|
+
}
|
|
447
|
+
} else if (persona.vendor) {
|
|
448
|
+
toolchainWhy = `${baseWhy} (serves persona vendor ${persona.vendor})`;
|
|
449
|
+
}
|
|
450
|
+
const vendor = toolchainVendor(chosen);
|
|
451
|
+
const adapter = adapterFor(chosen.harness);
|
|
452
|
+
let model = chosen.model;
|
|
453
|
+
let modelWhy = `toolchain ${chosen.name} model`;
|
|
454
|
+
let reasoning = chosen.tiers.standard?.reasoning;
|
|
455
|
+
const explicit = persona.model;
|
|
456
|
+
const explicitOk = explicit !== void 0 && (vendor === "openrouter" || adapter.ownsModel(explicit));
|
|
457
|
+
if (explicit !== void 0 && !explicitOk) {
|
|
458
|
+
warnings.push(`seat ${role}: persona model "${explicit}" does not belong to vendor "${vendor}" (toolchain ${chosen.name}); ignored`);
|
|
459
|
+
}
|
|
460
|
+
if (explicit !== void 0 && explicitOk) {
|
|
461
|
+
model = explicit;
|
|
462
|
+
modelWhy = "persona model";
|
|
463
|
+
reasoning = void 0;
|
|
464
|
+
} else if (persona.tier && persona.tier !== "standard" && chosen.tiers[persona.tier]) {
|
|
465
|
+
const t = chosen.tiers[persona.tier];
|
|
466
|
+
model = t.model;
|
|
467
|
+
reasoning = t.reasoning;
|
|
468
|
+
modelWhy = `persona tier ${persona.tier} via ${chosen.name}`;
|
|
469
|
+
}
|
|
470
|
+
const toolchain = reasoning !== void 0 && adapter.applyReasoning ? adapter.applyReasoning(chosen, reasoning) : chosen;
|
|
471
|
+
return { toolchain, model, toolchainWhy, modelWhy, warnings };
|
|
472
|
+
}
|
|
473
|
+
|
|
160
474
|
// src/config.ts
|
|
161
475
|
import { getSessionStore, hostFromUrl } from "@tablation/client";
|
|
162
476
|
|
|
@@ -197,8 +511,8 @@ function explain(ship, required) {
|
|
|
197
511
|
}
|
|
198
512
|
|
|
199
513
|
// src/repo-config.ts
|
|
200
|
-
import { readFileSync as
|
|
201
|
-
import { join as
|
|
514
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
515
|
+
import { join as join3, basename as basename2 } from "path";
|
|
202
516
|
import { parse } from "yaml";
|
|
203
517
|
var SPEC_VERSION = 1;
|
|
204
518
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -271,7 +585,7 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
271
585
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
272
586
|
function findRepoConfig(dir) {
|
|
273
587
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
274
|
-
const p =
|
|
588
|
+
const p = join3(dir, name);
|
|
275
589
|
if (existsSync2(p)) return p;
|
|
276
590
|
}
|
|
277
591
|
return null;
|
|
@@ -502,7 +816,7 @@ function parseRepoConfig(text, file) {
|
|
|
502
816
|
function loadRepoConfig(dir) {
|
|
503
817
|
const file = findRepoConfig(dir);
|
|
504
818
|
if (!file) return null;
|
|
505
|
-
return parseRepoConfig(
|
|
819
|
+
return parseRepoConfig(readFileSync3(file, "utf8"), file);
|
|
506
820
|
}
|
|
507
821
|
function hookLabel(cfg, hook2) {
|
|
508
822
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -537,7 +851,7 @@ function renderWorktreeName(template, ctx) {
|
|
|
537
851
|
return renderBranchName(template.replace(/\{dir\}/g, ctx.dir).replace(/\{issue\}/g, issue), ctx);
|
|
538
852
|
}
|
|
539
853
|
function defaultWorktreePrefix(dir) {
|
|
540
|
-
const name =
|
|
854
|
+
const name = basename2(dir.replace(/[/\\]+$/, ""));
|
|
541
855
|
return name ? `${name}-issue-` : "issue-";
|
|
542
856
|
}
|
|
543
857
|
var DEFAULTS = {
|
|
@@ -711,7 +1025,7 @@ function effectiveBranchTemplate(cfg, prefix) {
|
|
|
711
1025
|
function effectiveWorktreeDirName(cfg, dir, branchName, number, renderName) {
|
|
712
1026
|
if (cfg.worktrees.name && renderName) return renderName(cfg.worktrees.name);
|
|
713
1027
|
if (cfg.provenance["worktrees.prefix"] !== "default") return `${cfg.worktrees.prefix}${number}`;
|
|
714
|
-
return `${
|
|
1028
|
+
return `${basename2(dir.replace(/[/\\]+$/, ""))}-${branchName}`;
|
|
715
1029
|
}
|
|
716
1030
|
function validateEffective(cfg) {
|
|
717
1031
|
const problems = [];
|
|
@@ -767,6 +1081,8 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
767
1081
|
"name",
|
|
768
1082
|
"platform",
|
|
769
1083
|
"agent",
|
|
1084
|
+
"toolchains",
|
|
1085
|
+
"toolchain",
|
|
770
1086
|
"shell",
|
|
771
1087
|
"extraPath",
|
|
772
1088
|
"useNvm",
|
|
@@ -806,7 +1122,8 @@ var ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
|
806
1122
|
"contract",
|
|
807
1123
|
"resolved",
|
|
808
1124
|
"promptSet",
|
|
809
|
-
"hostPassengers"
|
|
1125
|
+
"hostPassengers",
|
|
1126
|
+
"toolchain"
|
|
810
1127
|
]);
|
|
811
1128
|
function parseWorktreeName(v, where, file) {
|
|
812
1129
|
const problem = typeof v === "string" ? worktreeNameProblem(v) : "must be a string";
|
|
@@ -910,16 +1227,16 @@ function mergeRouteRelease(route, override) {
|
|
|
910
1227
|
};
|
|
911
1228
|
}
|
|
912
1229
|
function expand(p, base) {
|
|
913
|
-
let out = p.startsWith("~") ? p.replace(/^~/,
|
|
914
|
-
out = out.replace(/\$\{?HOME\}?/g,
|
|
1230
|
+
let out = p.startsWith("~") ? p.replace(/^~/, homedir2()) : p;
|
|
1231
|
+
out = out.replace(/\$\{?HOME\}?/g, homedir2());
|
|
915
1232
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
916
1233
|
}
|
|
917
1234
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
918
|
-
if (!promptSet) return
|
|
1235
|
+
if (!promptSet) return join4(crewHome, "prompts", "default");
|
|
919
1236
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
920
1237
|
return expand(promptSet, base);
|
|
921
1238
|
}
|
|
922
|
-
return
|
|
1239
|
+
return join4(crewHome, "prompts", promptSet);
|
|
923
1240
|
}
|
|
924
1241
|
var Missing = class {
|
|
925
1242
|
paths = [];
|
|
@@ -943,7 +1260,7 @@ var Missing = class {
|
|
|
943
1260
|
};
|
|
944
1261
|
function crewVersion(crewHome) {
|
|
945
1262
|
try {
|
|
946
|
-
const pkg = JSON.parse(
|
|
1263
|
+
const pkg = JSON.parse(readFileSync4(join4(crewHome, "package.json"), "utf8"));
|
|
947
1264
|
return pkg.version ?? "0.0.0";
|
|
948
1265
|
} catch {
|
|
949
1266
|
return "0.0.0";
|
|
@@ -953,8 +1270,8 @@ function configSearchPath(crewHome) {
|
|
|
953
1270
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
954
1271
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
955
1272
|
return [
|
|
956
|
-
...xdg ? [
|
|
957
|
-
|
|
1273
|
+
...xdg ? [join4(xdg, "crew", "crew.yaml")] : [],
|
|
1274
|
+
join4(homedir2(), ".config", "crew", "crew.yaml"),
|
|
958
1275
|
resolve(crewHome, "crew.yaml")
|
|
959
1276
|
];
|
|
960
1277
|
}
|
|
@@ -962,17 +1279,17 @@ function findConfigFile(crewHome) {
|
|
|
962
1279
|
const candidates = configSearchPath(crewHome);
|
|
963
1280
|
return candidates.find((c) => existsSync3(c)) ?? candidates[candidates.length - 1];
|
|
964
1281
|
}
|
|
965
|
-
var defaultConfigPath = () =>
|
|
1282
|
+
var defaultConfigPath = () => join4(process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config"), "crew", "crew.yaml");
|
|
966
1283
|
function routeSlug(route) {
|
|
967
1284
|
return route.replace(/\//g, "-");
|
|
968
1285
|
}
|
|
969
1286
|
function resolvedPathFor(stateDir, route) {
|
|
970
1287
|
const [workspace, project] = splitRoute(route);
|
|
971
|
-
return
|
|
1288
|
+
return join4(stateDir, "resolved", workspace, `${project}.json`);
|
|
972
1289
|
}
|
|
973
1290
|
function apiKeyPathFor(stateDir, route) {
|
|
974
1291
|
const [workspace, project] = splitRoute(route);
|
|
975
|
-
return
|
|
1292
|
+
return join4(stateDir, "keys", workspace, `${project}.env`);
|
|
976
1293
|
}
|
|
977
1294
|
function splitRoute(route) {
|
|
978
1295
|
const parts = route.split("/");
|
|
@@ -985,7 +1302,7 @@ function readResolvedFile(stateDir, route) {
|
|
|
985
1302
|
const path = resolvedPathFor(stateDir, route);
|
|
986
1303
|
if (!existsSync3(path)) return void 0;
|
|
987
1304
|
try {
|
|
988
|
-
return JSON.parse(
|
|
1305
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
989
1306
|
} catch (e) {
|
|
990
1307
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
991
1308
|
}
|
|
@@ -1047,12 +1364,12 @@ function loadConfig(crewHome, configFile, opts = {}) {
|
|
|
1047
1364
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
1048
1365
|
|
|
1049
1366
|
Create the first of those:
|
|
1050
|
-
mkdir -p ${dirname2(defaultConfigPath())} && cp ${
|
|
1367
|
+
mkdir -p ${dirname2(defaultConfigPath())} && cp ${join4(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
1051
1368
|
);
|
|
1052
1369
|
}
|
|
1053
1370
|
let raw;
|
|
1054
1371
|
try {
|
|
1055
|
-
raw = parse2(opts.text ??
|
|
1372
|
+
raw = parse2(opts.text ?? readFileSync4(file, "utf8"));
|
|
1056
1373
|
} catch (e) {
|
|
1057
1374
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
1058
1375
|
}
|
|
@@ -1082,6 +1399,12 @@ Create the first of those:
|
|
|
1082
1399
|
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
1083
1400
|
missing.add(`ship.agent.maxThinkingTokens must be a non-negative integer (got "${shipRaw.agent?.maxThinkingTokens}")`);
|
|
1084
1401
|
}
|
|
1402
|
+
const toolchains = parseToolchains(shipRaw.toolchains, base, missing, file);
|
|
1403
|
+
const toolchainNames = /* @__PURE__ */ new Set([LEGACY_TOOLCHAIN, ...toolchains.map((x) => x.name)]);
|
|
1404
|
+
const shipToolchain = shipRaw.toolchain === void 0 ? void 0 : String(shipRaw.toolchain);
|
|
1405
|
+
if (shipToolchain !== void 0 && !toolchainNames.has(shipToolchain)) {
|
|
1406
|
+
missing.add(`ship.toolchain "${shipToolchain}" is not defined (have: ${[...toolchainNames].join(", ")})`);
|
|
1407
|
+
}
|
|
1085
1408
|
const stateDir = expand(shipRaw.stateDir ?? "state", base);
|
|
1086
1409
|
const routesRaw = raw.routes;
|
|
1087
1410
|
if (!opts.allowNoRoutes && (!Array.isArray(routesRaw) || routesRaw.length === 0)) {
|
|
@@ -1092,7 +1415,7 @@ Create the first of those:
|
|
|
1092
1415
|
const where = `routes[${i}]`;
|
|
1093
1416
|
const routeMissing = new Missing();
|
|
1094
1417
|
try {
|
|
1095
|
-
return [parseOneRoute(c, where, file, base, stateDir, raw, routeMissing, crewHome)];
|
|
1418
|
+
return [parseOneRoute(c, where, file, base, stateDir, raw, routeMissing, crewHome, toolchainNames)];
|
|
1096
1419
|
} catch (e) {
|
|
1097
1420
|
const label = typeof c?.route === "string" && c.route ? `"${c.route}"` : where;
|
|
1098
1421
|
routeWarnings.push(`route ${label} dropped \u2014 ${e.message}`);
|
|
@@ -1115,12 +1438,14 @@ Create the first of those:
|
|
|
1115
1438
|
model: shipRaw.agent?.model ?? "claude-sonnet-5",
|
|
1116
1439
|
maxThinkingTokens
|
|
1117
1440
|
},
|
|
1441
|
+
...toolchains.length > 0 ? { toolchains } : {},
|
|
1442
|
+
...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
|
|
1118
1443
|
shell: shipRaw.shell,
|
|
1119
1444
|
extraPath: shipRaw.extraPath,
|
|
1120
1445
|
useNvm: shipRaw.useNvm !== false,
|
|
1121
1446
|
nvmSh: shipRaw.nvmSh,
|
|
1122
1447
|
stateDir,
|
|
1123
|
-
logFile: expand(shipRaw.logFile ??
|
|
1448
|
+
logFile: expand(shipRaw.logFile ?? join4(tmpdir(), "crew.log"), base),
|
|
1124
1449
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
1125
1450
|
maxConcurrentAgents,
|
|
1126
1451
|
streamRetentionDays,
|
|
@@ -1138,8 +1463,70 @@ Create the first of those:
|
|
|
1138
1463
|
warnings: routeWarnings
|
|
1139
1464
|
};
|
|
1140
1465
|
}
|
|
1141
|
-
function
|
|
1466
|
+
function expandCommand(cmd, base) {
|
|
1467
|
+
return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
|
|
1468
|
+
}
|
|
1469
|
+
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "model", "tiers", "args", "env", "maxThinkingTokens"]);
|
|
1470
|
+
function parseToolchains(raw, base, missing, file) {
|
|
1471
|
+
if (raw === void 0 || raw === null) return [];
|
|
1472
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
1473
|
+
missing.add("ship.toolchains must be a mapping of name \u2192 toolchain");
|
|
1474
|
+
return [];
|
|
1475
|
+
}
|
|
1476
|
+
const out = [];
|
|
1477
|
+
for (const [name, entryRaw] of Object.entries(raw)) {
|
|
1478
|
+
const where = `ship.toolchains.${name}`;
|
|
1479
|
+
if (!entryRaw || typeof entryRaw !== "object" || Array.isArray(entryRaw)) {
|
|
1480
|
+
missing.add(`${where} must be a mapping`);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
rejectUnknownKeys(entryRaw, TOOLCHAIN_KEYS, where, file);
|
|
1484
|
+
const harness = String(entryRaw.harness ?? "");
|
|
1485
|
+
if (!isHarness(harness)) {
|
|
1486
|
+
missing.add(`${where}.harness must be one of ${HARNESS_NAMES.join("|")} (got "${harness}")`);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
const adapter = adapterFor(harness);
|
|
1490
|
+
const tiers = { ...adapter.defaultTiers };
|
|
1491
|
+
for (const [tier, v] of Object.entries(entryRaw.tiers ?? {})) {
|
|
1492
|
+
if (!TIERS.includes(tier)) {
|
|
1493
|
+
missing.add(`${where}.tiers.${tier} is not a tier (have: ${TIERS.join(", ")})`);
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if (!v || typeof v.model !== "string" || !v.model) {
|
|
1497
|
+
missing.add(`${where}.tiers.${tier}.model is required`);
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
tiers[tier] = { model: v.model, ...v.reasoning !== void 0 ? { reasoning: String(v.reasoning) } : {} };
|
|
1501
|
+
}
|
|
1502
|
+
const args = entryRaw.args ?? [];
|
|
1503
|
+
if (!Array.isArray(args)) missing.add(`${where}.args must be a list of strings`);
|
|
1504
|
+
const env = entryRaw.env ?? {};
|
|
1505
|
+
if (typeof env !== "object" || Array.isArray(env)) missing.add(`${where}.env must be a mapping of name \u2192 value`);
|
|
1506
|
+
const maxThinkingTokens = entryRaw.maxThinkingTokens === void 0 ? DEFAULT_MAX_THINKING_TOKENS : Number(entryRaw.maxThinkingTokens);
|
|
1507
|
+
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
1508
|
+
missing.add(`${where}.maxThinkingTokens must be a non-negative integer (got "${entryRaw.maxThinkingTokens}")`);
|
|
1509
|
+
}
|
|
1510
|
+
out.push({
|
|
1511
|
+
name,
|
|
1512
|
+
harness,
|
|
1513
|
+
bin: expandCommand(String(entryRaw.bin ?? adapter.defaultBin), base),
|
|
1514
|
+
...entryRaw.launcher ? { launcher: expandCommand(String(entryRaw.launcher), base) } : {},
|
|
1515
|
+
model: String(entryRaw.model ?? tiers.standard?.model ?? adapter.defaultModel),
|
|
1516
|
+
tiers,
|
|
1517
|
+
args: Array.isArray(args) ? args.map(String) : [],
|
|
1518
|
+
env: Object.fromEntries(Object.entries(env).map(([k, v]) => [k, String(v)])),
|
|
1519
|
+
maxThinkingTokens
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return out;
|
|
1523
|
+
}
|
|
1524
|
+
function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, toolchainNames) {
|
|
1142
1525
|
rejectUnknownKeys(c, ROUTE_KEYS, `${where}`, file);
|
|
1526
|
+
const toolchain = c?.toolchain === void 0 ? void 0 : String(c.toolchain);
|
|
1527
|
+
if (toolchain !== void 0 && !toolchainNames.has(toolchain)) {
|
|
1528
|
+
missing.add(`${where}.toolchain "${toolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
|
|
1529
|
+
}
|
|
1143
1530
|
const route = missing.req(c?.route, `${where}.route`);
|
|
1144
1531
|
const routeParts = route ? route.split("/") : [];
|
|
1145
1532
|
const routeWellFormed = routeParts.length === 2 && routeParts.every(Boolean);
|
|
@@ -1188,6 +1575,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome) {
|
|
|
1188
1575
|
reposBasePath: expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base),
|
|
1189
1576
|
repoOverrides,
|
|
1190
1577
|
worktreePrefix: c?.worktreePrefix ? String(c.worktreePrefix) : void 0,
|
|
1578
|
+
...toolchain !== void 0 ? { toolchain } : {},
|
|
1191
1579
|
promptsDir: resolvePromptsDir(c?.promptSet ? String(c.promptSet) : void 0, crewHome, base),
|
|
1192
1580
|
weight,
|
|
1193
1581
|
baseUrl: String(c?.baseUrl ?? raw.ship?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
@@ -1242,7 +1630,7 @@ function dirForRepo(r, repoId) {
|
|
|
1242
1630
|
}
|
|
1243
1631
|
function repoTargetFor(r, repoId) {
|
|
1244
1632
|
const names = r.resolved?.repoNames;
|
|
1245
|
-
if (!repoId || !names) return Object.keys(r.repos).length ? null : { name:
|
|
1633
|
+
if (!repoId || !names) return Object.keys(r.repos).length ? null : { name: basename3(r.dir), dir: r.dir };
|
|
1246
1634
|
const name = names[repoId];
|
|
1247
1635
|
if (!name) return null;
|
|
1248
1636
|
return reposOf(r).find((t) => t.name === name) ?? null;
|
|
@@ -1256,7 +1644,7 @@ function repoIdForName(r, name) {
|
|
|
1256
1644
|
function resolveApiKey(r) {
|
|
1257
1645
|
if (r.apiKey) return r.apiKey;
|
|
1258
1646
|
if (r.apiKeyFile && r.apiKeyVar && existsSync3(r.apiKeyFile)) {
|
|
1259
|
-
for (const line of
|
|
1647
|
+
for (const line of readFileSync4(r.apiKeyFile, "utf8").split("\n")) {
|
|
1260
1648
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
1261
1649
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
1262
1650
|
}
|
|
@@ -1319,13 +1707,13 @@ function configuredMembers(rt) {
|
|
|
1319
1707
|
return out;
|
|
1320
1708
|
}
|
|
1321
1709
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
1322
|
-
return
|
|
1710
|
+
return join4(basePath, workspace, repoName);
|
|
1323
1711
|
}
|
|
1324
1712
|
function reposOf(r) {
|
|
1325
1713
|
const named = Object.entries(r.repos);
|
|
1326
1714
|
if (named.length > 0) return named.map(([name, dir]) => ({ name, dir }));
|
|
1327
1715
|
const trackerNames = r.resolved?.repoNames ? Object.values(r.resolved.repoNames) : [];
|
|
1328
|
-
if (trackerNames.length <= 1) return [{ name:
|
|
1716
|
+
if (trackerNames.length <= 1) return [{ name: basename3(r.dir), dir: r.dir }];
|
|
1329
1717
|
const remotes = r.resolved?.repoRemotes ?? {};
|
|
1330
1718
|
const remoteByName = {};
|
|
1331
1719
|
for (const [id, name] of Object.entries(r.resolved.repoNames)) {
|
|
@@ -1343,7 +1731,7 @@ function passengerRepoTargets(r) {
|
|
|
1343
1731
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
1344
1732
|
}
|
|
1345
1733
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
1346
|
-
return
|
|
1734
|
+
return join4(basePath, workspace, ".passenger-checkouts", repoName);
|
|
1347
1735
|
}
|
|
1348
1736
|
function passengerCheckoutTargets(r) {
|
|
1349
1737
|
if (!r.hostPassengers) return [];
|
|
@@ -1475,8 +1863,8 @@ function rosterMarkdown(roster, meId) {
|
|
|
1475
1863
|
}
|
|
1476
1864
|
|
|
1477
1865
|
// src/state.ts
|
|
1478
|
-
import { readFileSync as
|
|
1479
|
-
import { join as
|
|
1866
|
+
import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
1867
|
+
import { join as join5 } from "path";
|
|
1480
1868
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
1481
1869
|
var State = class _State {
|
|
1482
1870
|
dir;
|
|
@@ -1485,7 +1873,7 @@ var State = class _State {
|
|
|
1485
1873
|
mkdirSync2(dir, { recursive: true });
|
|
1486
1874
|
}
|
|
1487
1875
|
path(...parts) {
|
|
1488
|
-
return
|
|
1876
|
+
return join5(this.dir, ...parts);
|
|
1489
1877
|
}
|
|
1490
1878
|
/**
|
|
1491
1879
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -1496,7 +1884,7 @@ var State = class _State {
|
|
|
1496
1884
|
*/
|
|
1497
1885
|
watermark() {
|
|
1498
1886
|
try {
|
|
1499
|
-
const v =
|
|
1887
|
+
const v = readFileSync5(this.path(".poll-watermark"), "utf8").trim();
|
|
1500
1888
|
return v || EPOCH;
|
|
1501
1889
|
} catch {
|
|
1502
1890
|
return EPOCH;
|
|
@@ -1582,7 +1970,7 @@ var State = class _State {
|
|
|
1582
1970
|
const name = _State.safe(rawName);
|
|
1583
1971
|
const file = this.path(`.${name}.lock`);
|
|
1584
1972
|
try {
|
|
1585
|
-
const existing = Number.parseInt(
|
|
1973
|
+
const existing = Number.parseInt(readFileSync5(file, "utf8").trim(), 10);
|
|
1586
1974
|
if (Number.isFinite(existing) && existing > 0) {
|
|
1587
1975
|
try {
|
|
1588
1976
|
process.kill(existing, 0);
|
|
@@ -1676,7 +2064,7 @@ var State = class _State {
|
|
|
1676
2064
|
const heldReported = `.release-held-${suffix}`;
|
|
1677
2065
|
const read = (f) => {
|
|
1678
2066
|
try {
|
|
1679
|
-
return
|
|
2067
|
+
return readFileSync5(this.path(f), "utf8").trim() || null;
|
|
1680
2068
|
} catch {
|
|
1681
2069
|
return null;
|
|
1682
2070
|
}
|
|
@@ -1726,7 +2114,7 @@ var State = class _State {
|
|
|
1726
2114
|
/** Whether the board was already told about this head's red gate. */
|
|
1727
2115
|
heldReported: () => {
|
|
1728
2116
|
try {
|
|
1729
|
-
return
|
|
2117
|
+
return readFileSync5(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
1730
2118
|
} catch {
|
|
1731
2119
|
return null;
|
|
1732
2120
|
}
|
|
@@ -1763,7 +2151,7 @@ var State = class _State {
|
|
|
1763
2151
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
1764
2152
|
const read = (f) => {
|
|
1765
2153
|
try {
|
|
1766
|
-
return
|
|
2154
|
+
return readFileSync5(this.path(f), "utf8").trim() || null;
|
|
1767
2155
|
} catch {
|
|
1768
2156
|
return null;
|
|
1769
2157
|
}
|
|
@@ -1828,7 +2216,7 @@ var State = class _State {
|
|
|
1828
2216
|
return {
|
|
1829
2217
|
previous: () => {
|
|
1830
2218
|
try {
|
|
1831
|
-
const parsed = JSON.parse(
|
|
2219
|
+
const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
|
|
1832
2220
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1833
2221
|
} catch {
|
|
1834
2222
|
return {};
|
|
@@ -1849,7 +2237,7 @@ var State = class _State {
|
|
|
1849
2237
|
return {
|
|
1850
2238
|
previous: () => {
|
|
1851
2239
|
try {
|
|
1852
|
-
const parsed = JSON.parse(
|
|
2240
|
+
const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
|
|
1853
2241
|
return Array.isArray(parsed) ? parsed : [];
|
|
1854
2242
|
} catch {
|
|
1855
2243
|
return [];
|
|
@@ -1882,7 +2270,7 @@ var State = class _State {
|
|
|
1882
2270
|
}
|
|
1883
2271
|
readInt(file) {
|
|
1884
2272
|
try {
|
|
1885
|
-
return Number.parseInt(
|
|
2273
|
+
return Number.parseInt(readFileSync5(this.path(file), "utf8").trim(), 10) || 0;
|
|
1886
2274
|
} catch {
|
|
1887
2275
|
return 0;
|
|
1888
2276
|
}
|
|
@@ -2186,10 +2574,10 @@ function describeRelease(o, route) {
|
|
|
2186
2574
|
}
|
|
2187
2575
|
|
|
2188
2576
|
// 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";
|
|
2577
|
+
import { dirname as dirname3, join as join6, extname } from "path";
|
|
2578
|
+
import { homedir as homedir3 } from "os";
|
|
2579
|
+
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
|
|
2580
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2193
2581
|
import { createHash } from "crypto";
|
|
2194
2582
|
|
|
2195
2583
|
// src/runtime-info.ts
|
|
@@ -2200,7 +2588,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
2200
2588
|
// src/install.ts
|
|
2201
2589
|
function crewInvocation(crewHome) {
|
|
2202
2590
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
2203
|
-
return [process.execPath,
|
|
2591
|
+
return [process.execPath, join6(crewHome, "bin", "crew")];
|
|
2204
2592
|
}
|
|
2205
2593
|
var INTERVAL_SECONDS = 120;
|
|
2206
2594
|
function labelFor(crewHome, job) {
|
|
@@ -2238,7 +2626,7 @@ function pathFor(ship) {
|
|
|
2238
2626
|
return parts.join(":");
|
|
2239
2627
|
}
|
|
2240
2628
|
function findOnPath(bin, searchPath) {
|
|
2241
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
2629
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join6(dir, bin));
|
|
2242
2630
|
for (const candidate of candidates) {
|
|
2243
2631
|
try {
|
|
2244
2632
|
accessSync(candidate, constants.X_OK);
|
|
@@ -2248,7 +2636,7 @@ function findOnPath(bin, searchPath) {
|
|
|
2248
2636
|
}
|
|
2249
2637
|
return void 0;
|
|
2250
2638
|
}
|
|
2251
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
2639
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join6(homedir3(), ".docker", "bin")].join(":");
|
|
2252
2640
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
2253
2641
|
const schedulerPath = pathFor(ship);
|
|
2254
2642
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -2260,7 +2648,7 @@ function xmlEscape(s) {
|
|
|
2260
2648
|
}
|
|
2261
2649
|
function planLaunchd(ship, crewHome, job) {
|
|
2262
2650
|
const label = labelFor(crewHome, job);
|
|
2263
|
-
const unitPath =
|
|
2651
|
+
const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2264
2652
|
const crewLog = ship.logFile;
|
|
2265
2653
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2266
2654
|
const persistent = job === "run";
|
|
@@ -2312,8 +2700,8 @@ ${scheduleKeys}
|
|
|
2312
2700
|
}
|
|
2313
2701
|
function planSystemd(ship, crewHome, job) {
|
|
2314
2702
|
const label = labelFor(crewHome, job);
|
|
2315
|
-
const unitDir =
|
|
2316
|
-
const servicePath =
|
|
2703
|
+
const unitDir = join6(homedir3(), ".config", "systemd", "user");
|
|
2704
|
+
const servicePath = join6(unitDir, `${label}.service`);
|
|
2317
2705
|
const crewLog = ship.logFile;
|
|
2318
2706
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2319
2707
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -2350,7 +2738,7 @@ WantedBy=default.target
|
|
|
2350
2738
|
schedulerLog
|
|
2351
2739
|
};
|
|
2352
2740
|
}
|
|
2353
|
-
const timerPath =
|
|
2741
|
+
const timerPath = join6(unitDir, `${label}.timer`);
|
|
2354
2742
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
2355
2743
|
[Unit]
|
|
2356
2744
|
Description=${description}
|
|
@@ -2417,7 +2805,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2417
2805
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
2418
2806
|
const label = labelFor(crewHome, job);
|
|
2419
2807
|
if (hostShip === "macos") {
|
|
2420
|
-
const unitPath =
|
|
2808
|
+
const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2421
2809
|
return {
|
|
2422
2810
|
mechanism: "launchd",
|
|
2423
2811
|
unitPaths: [unitPath],
|
|
@@ -2427,18 +2815,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2427
2815
|
};
|
|
2428
2816
|
}
|
|
2429
2817
|
if (hostShip === "linux" && hasSystemd) {
|
|
2430
|
-
const unitDir =
|
|
2818
|
+
const unitDir = join6(homedir3(), ".config", "systemd", "user");
|
|
2431
2819
|
if (job === "run") {
|
|
2432
2820
|
return {
|
|
2433
2821
|
mechanism: "systemd",
|
|
2434
|
-
unitPaths: [
|
|
2822
|
+
unitPaths: [join6(unitDir, `${label}.service`)],
|
|
2435
2823
|
crontabLine: null,
|
|
2436
2824
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
2437
2825
|
};
|
|
2438
2826
|
}
|
|
2439
2827
|
return {
|
|
2440
2828
|
mechanism: "systemd",
|
|
2441
|
-
unitPaths: [
|
|
2829
|
+
unitPaths: [join6(unitDir, `${label}.service`), join6(unitDir, `${label}.timer`)],
|
|
2442
2830
|
crontabLine: null,
|
|
2443
2831
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
2444
2832
|
};
|
|
@@ -2456,7 +2844,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2456
2844
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
2457
2845
|
const label = labelFor(crewHome, "run");
|
|
2458
2846
|
if (hostShip === "macos") {
|
|
2459
|
-
const unitPath =
|
|
2847
|
+
const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2460
2848
|
return {
|
|
2461
2849
|
mechanism: "launchd",
|
|
2462
2850
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -2480,7 +2868,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
|
2480
2868
|
}
|
|
2481
2869
|
function runAll(commands, cwd, log) {
|
|
2482
2870
|
for (const [bin, ...args] of commands) {
|
|
2483
|
-
const res =
|
|
2871
|
+
const res = spawnSync2(bin, args, { cwd, encoding: "utf8" });
|
|
2484
2872
|
if (res.error || res.status !== 0) {
|
|
2485
2873
|
log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
|
|
2486
2874
|
output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
|
|
@@ -2545,7 +2933,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2545
2933
|
if (dryRun) {
|
|
2546
2934
|
log.emit(`would run: launchctl remove ${plan.removeLabel}`);
|
|
2547
2935
|
} else {
|
|
2548
|
-
if (
|
|
2936
|
+
if (spawnSync2("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
|
|
2549
2937
|
log.emit(`removed loaded job ${plan.removeLabel}`);
|
|
2550
2938
|
}
|
|
2551
2939
|
}
|
|
@@ -2570,7 +2958,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2570
2958
|
}
|
|
2571
2959
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
2572
2960
|
function launchAgentsDir() {
|
|
2573
|
-
return
|
|
2961
|
+
return join6(homedir3(), "Library", "LaunchAgents");
|
|
2574
2962
|
}
|
|
2575
2963
|
function xmlUnescape(s) {
|
|
2576
2964
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -2582,17 +2970,17 @@ function programFromPlist(text) {
|
|
|
2582
2970
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
2583
2971
|
}
|
|
2584
2972
|
function loadedCrewLabels() {
|
|
2585
|
-
const list =
|
|
2973
|
+
const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
|
|
2586
2974
|
if (list.status !== 0) return [];
|
|
2587
2975
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
2588
2976
|
}
|
|
2589
2977
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
2590
2978
|
const files = existsSync5(dir) ? readdirSync(dir) : [];
|
|
2591
2979
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
2592
|
-
const plistPath =
|
|
2980
|
+
const plistPath = join6(dir, f);
|
|
2593
2981
|
let program = null;
|
|
2594
2982
|
try {
|
|
2595
|
-
program = programFromPlist(
|
|
2983
|
+
program = programFromPlist(readFileSync6(plistPath, "utf8"));
|
|
2596
2984
|
} catch {
|
|
2597
2985
|
}
|
|
2598
2986
|
return {
|
|
@@ -2605,7 +2993,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
|
2605
2993
|
const seen = new Set(units.map((u) => u.label));
|
|
2606
2994
|
for (const label of [...loaded].sort()) {
|
|
2607
2995
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
2608
|
-
units.push({ label, plistPath:
|
|
2996
|
+
units.push({ label, plistPath: join6(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
2609
2997
|
}
|
|
2610
2998
|
return units;
|
|
2611
2999
|
}
|
|
@@ -2618,8 +3006,8 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
2618
3006
|
log.emit(`would unload and remove ${unit.label}`);
|
|
2619
3007
|
return;
|
|
2620
3008
|
}
|
|
2621
|
-
if (existsSync5(unit.plistPath))
|
|
2622
|
-
|
|
3009
|
+
if (existsSync5(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3010
|
+
spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
2623
3011
|
if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
2624
3012
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
2625
3013
|
}
|
|
@@ -2652,7 +3040,7 @@ function parseLaunchctlList(text) {
|
|
|
2652
3040
|
return jobs;
|
|
2653
3041
|
}
|
|
2654
3042
|
function describeForeignLaunchdUnits(crewHome, dir) {
|
|
2655
|
-
const list =
|
|
3043
|
+
const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
|
|
2656
3044
|
const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
|
|
2657
3045
|
const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
|
|
2658
3046
|
return foreign.map((u) => {
|
|
@@ -2665,11 +3053,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
|
|
|
2665
3053
|
});
|
|
2666
3054
|
}
|
|
2667
3055
|
function readCrontab() {
|
|
2668
|
-
const res =
|
|
3056
|
+
const res = spawnSync2("crontab", ["-l"], { encoding: "utf8" });
|
|
2669
3057
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
2670
3058
|
}
|
|
2671
3059
|
function writeCrontab(text) {
|
|
2672
|
-
|
|
3060
|
+
spawnSync2("crontab", ["-"], { input: text, encoding: "utf8" });
|
|
2673
3061
|
}
|
|
2674
3062
|
function addCrontabLine(marker, line) {
|
|
2675
3063
|
const existing = readCrontab();
|
|
@@ -2693,7 +3081,7 @@ function removeCrontabLine(marker) {
|
|
|
2693
3081
|
}
|
|
2694
3082
|
function detectSystemd() {
|
|
2695
3083
|
if (process.platform !== "linux") return false;
|
|
2696
|
-
const res =
|
|
3084
|
+
const res = spawnSync2("systemctl", ["--user", "status"], { encoding: "utf8" });
|
|
2697
3085
|
return !res.error;
|
|
2698
3086
|
}
|
|
2699
3087
|
|
|
@@ -2711,7 +3099,7 @@ import {
|
|
|
2711
3099
|
statSync as statSync4,
|
|
2712
3100
|
unlinkSync as unlinkSync2
|
|
2713
3101
|
} from "fs";
|
|
2714
|
-
import { dirname as dirname4, basename as
|
|
3102
|
+
import { dirname as dirname4, basename as basename4 } from "path";
|
|
2715
3103
|
var GitError = class extends Error {
|
|
2716
3104
|
};
|
|
2717
3105
|
function gitRaw(cwd, args) {
|
|
@@ -2807,7 +3195,7 @@ function deleteRemoteBranch(cwd, remote, branch) {
|
|
|
2807
3195
|
}
|
|
2808
3196
|
function worktreeForNumber(cwd, num) {
|
|
2809
3197
|
const re = new RegExp(`(?:^|-)${num}$`);
|
|
2810
|
-
return worktrees(cwd).find((w) => re.test(
|
|
3198
|
+
return worktrees(cwd).find((w) => re.test(basename4(w.path))) ?? null;
|
|
2811
3199
|
}
|
|
2812
3200
|
var commitBodies = (cwd, range) => gitOk(cwd, ["log", range, "--format=%B"]) ?? "";
|
|
2813
3201
|
var resolve2 = (cwd, ref) => gitOk(cwd, ["rev-parse", "-q", "--verify", `${ref}^{commit}`]);
|
|
@@ -4761,7 +5149,7 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
|
|
|
4761
5149
|
}
|
|
4762
5150
|
|
|
4763
5151
|
// src/ticket-branch.ts
|
|
4764
|
-
import { basename as
|
|
5152
|
+
import { basename as basename5 } from "path";
|
|
4765
5153
|
function branchRenderer(t, role) {
|
|
4766
5154
|
const { tag, prefix } = ticketBranchContext(t);
|
|
4767
5155
|
return (template) => renderBranchName(template, { key: t.issue_id, title: t.title ?? void 0, role, tag, prefix });
|
|
@@ -4772,7 +5160,7 @@ function worktreeForTicket(dir, cfg, t, role) {
|
|
|
4772
5160
|
const number = t.issue_id.replace(/^\D+/, "");
|
|
4773
5161
|
const { tag } = ticketBranchContext(t);
|
|
4774
5162
|
return effectiveWorktreeDirName(cfg, dir, branchName, number, (template) => renderWorktreeName(template, {
|
|
4775
|
-
dir:
|
|
5163
|
+
dir: basename5(dir.replace(/[/\\]+$/, "")),
|
|
4776
5164
|
key: t.issue_id,
|
|
4777
5165
|
title: t.title ?? void 0,
|
|
4778
5166
|
role,
|
|
@@ -4813,7 +5201,7 @@ function materializeBranchForTicket(dir, cfg, t, role) {
|
|
|
4813
5201
|
|
|
4814
5202
|
// src/poll.ts
|
|
4815
5203
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4816
|
-
import { join as
|
|
5204
|
+
import { join as join7 } from "path";
|
|
4817
5205
|
|
|
4818
5206
|
// src/stalled.ts
|
|
4819
5207
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -5085,7 +5473,7 @@ async function decideCycle(o) {
|
|
|
5085
5473
|
}
|
|
5086
5474
|
return decision;
|
|
5087
5475
|
}
|
|
5088
|
-
var digestPath = (stateDir, route, role) =>
|
|
5476
|
+
var digestPath = (stateDir, route, role) => join7(stateDir, `digest-${route}-${role}.md`);
|
|
5089
5477
|
function writeDigest(o, d, role, stateDir) {
|
|
5090
5478
|
const me = o.route.resolved?.seats[role];
|
|
5091
5479
|
if (!me) return false;
|
|
@@ -5455,13 +5843,13 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
5455
5843
|
|
|
5456
5844
|
// src/agent.ts
|
|
5457
5845
|
import { spawn as spawn2 } from "child_process";
|
|
5458
|
-
import { readFileSync as
|
|
5846
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7, statSync as statSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
|
|
5459
5847
|
import { finished } from "stream/promises";
|
|
5460
|
-
import { join as
|
|
5848
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
5461
5849
|
import { createHash as createHash2 } from "crypto";
|
|
5462
5850
|
|
|
5463
5851
|
// src/environment.ts
|
|
5464
|
-
import { basename as
|
|
5852
|
+
import { basename as basename6 } from "path";
|
|
5465
5853
|
var API_KEY_VAR = "CREW_API_KEY";
|
|
5466
5854
|
function authorshipBullets(a, contract) {
|
|
5467
5855
|
if (!a) return [];
|
|
@@ -5512,7 +5900,7 @@ function repoSection(r, key, heading) {
|
|
|
5512
5900
|
const repo = r.config;
|
|
5513
5901
|
const branchTemplate = effectiveBranchTemplate(repo, repoPrefix(repo));
|
|
5514
5902
|
const branchName = branchExample(repo, key);
|
|
5515
|
-
const checkoutName =
|
|
5903
|
+
const checkoutName = basename6(r.dir.replace(/[/\\]+$/, ""));
|
|
5516
5904
|
const worktreeName = effectiveWorktreeDirName(
|
|
5517
5905
|
repo,
|
|
5518
5906
|
r.dir,
|
|
@@ -5742,54 +6130,6 @@ function renderEnvironment(i) {
|
|
|
5742
6130
|
return lines.join("\n");
|
|
5743
6131
|
}
|
|
5744
6132
|
|
|
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
6133
|
// src/agent-log.ts
|
|
5794
6134
|
async function post(target, path, body) {
|
|
5795
6135
|
const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
|
|
@@ -5871,36 +6211,10 @@ var TicketAttributionTracker = class {
|
|
|
5871
6211
|
|
|
5872
6212
|
// src/agent.ts
|
|
5873
6213
|
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) {
|
|
6214
|
+
var LEGACY_TOOLCHAIN_HARNESS = "claude";
|
|
6215
|
+
function scrubbedEnv(env = process.env, harness = LEGACY_TOOLCHAIN_HARNESS) {
|
|
5902
6216
|
const out = { ...env };
|
|
5903
|
-
for (const v of
|
|
6217
|
+
for (const v of adapterFor(harness).unsetEnv) delete out[v];
|
|
5904
6218
|
return out;
|
|
5905
6219
|
}
|
|
5906
6220
|
var CREW_LANE_ROLE_VAR = "CREW_LANE_ROLE";
|
|
@@ -5914,23 +6228,23 @@ var AgentError = class extends Error {
|
|
|
5914
6228
|
var AgentSpawnError = class extends AgentError {
|
|
5915
6229
|
};
|
|
5916
6230
|
function planAgentRun(o) {
|
|
5917
|
-
const briefPath =
|
|
6231
|
+
const briefPath = join8(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
5918
6232
|
if (!existsSync7(briefPath)) {
|
|
5919
6233
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
5920
6234
|
}
|
|
5921
|
-
const commonPath =
|
|
6235
|
+
const commonPath = join8(o.route.promptsDir, "personas", "common.md");
|
|
5922
6236
|
if (!existsSync7(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
|
|
5923
6237
|
const now = o.now ?? (() => Date.now());
|
|
5924
6238
|
const routeLabel = routeSlug(o.route.route);
|
|
5925
|
-
const digestPath2 =
|
|
6239
|
+
const digestPath2 = join8(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
5926
6240
|
let digest;
|
|
5927
6241
|
let ageSeconds;
|
|
5928
6242
|
if (existsSync7(digestPath2)) {
|
|
5929
6243
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
5930
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
6244
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync7(digestPath2, "utf8");
|
|
5931
6245
|
}
|
|
5932
|
-
const common =
|
|
5933
|
-
const brief =
|
|
6246
|
+
const common = readFileSync7(commonPath, "utf8");
|
|
6247
|
+
const brief = readFileSync7(briefPath, "utf8");
|
|
5934
6248
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
5935
6249
|
const effectiveBrief = o.divergedPrompt ?? brief;
|
|
5936
6250
|
const prompt = assemblePrompt({
|
|
@@ -5941,9 +6255,18 @@ function planAgentRun(o) {
|
|
|
5941
6255
|
digest
|
|
5942
6256
|
});
|
|
5943
6257
|
const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
|
|
5944
|
-
const streamsDir =
|
|
6258
|
+
const streamsDir = join8(o.stateDir, "streams");
|
|
5945
6259
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
5946
|
-
const
|
|
6260
|
+
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
|
|
6261
|
+
const { toolchain, model } = seat;
|
|
6262
|
+
const adapter = adapterFor(toolchain.harness);
|
|
6263
|
+
const invocation = adapter.invocation(toolchain, {
|
|
6264
|
+
role: o.role,
|
|
6265
|
+
model,
|
|
6266
|
+
// Skipped when there's no key to send: a route `crew connect` hasn't
|
|
6267
|
+
// resolved yet has nothing to authenticate the MCP connection with either.
|
|
6268
|
+
mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
|
|
6269
|
+
});
|
|
5947
6270
|
const agentLog = o.route.resolved && o.apiKey ? {
|
|
5948
6271
|
baseUrl: o.route.baseUrl,
|
|
5949
6272
|
workspaceId: o.route.resolved.workspaceId,
|
|
@@ -5961,43 +6284,18 @@ function planAgentRun(o) {
|
|
|
5961
6284
|
// (config.ts), and the digest's `repo` column is what actually places
|
|
5962
6285
|
// the work. Kept deliberately rather than guessed at (ISSUE-349).
|
|
5963
6286
|
cwd: o.route.dir,
|
|
5964
|
-
bin:
|
|
5965
|
-
args:
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
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
|
-
],
|
|
6287
|
+
bin: invocation.bin,
|
|
6288
|
+
args: invocation.args,
|
|
6289
|
+
toolchain: toolchain.name,
|
|
6290
|
+
harness: toolchain.harness,
|
|
6291
|
+
resolution: { toolchain: seat.toolchainWhy, model: seat.modelWhy },
|
|
6292
|
+
warnings: seat.warnings,
|
|
5992
6293
|
prompt,
|
|
5993
6294
|
promptBytes: Buffer.byteLength(prompt, "utf8"),
|
|
5994
|
-
unsetEnv:
|
|
6295
|
+
unsetEnv: adapter.unsetEnv,
|
|
5995
6296
|
setEnv: {
|
|
5996
6297
|
...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) } : {},
|
|
6298
|
+
...adapter.runEnv(toolchain),
|
|
6001
6299
|
// The operator's global SessionStart hook (pair-context-hook.sh) injects
|
|
6002
6300
|
// the interactive Pair persona's prompt for any session it can't tell
|
|
6003
6301
|
// apart from a person's own terminal. Every headless lane run this
|
|
@@ -6011,8 +6309,8 @@ function planAgentRun(o) {
|
|
|
6011
6309
|
digestAttached: digest !== void 0,
|
|
6012
6310
|
digestAgeSeconds: ageSeconds,
|
|
6013
6311
|
model,
|
|
6014
|
-
streamPath:
|
|
6015
|
-
eventsPath:
|
|
6312
|
+
streamPath: join8(streamsDir, `${base}.jsonl`),
|
|
6313
|
+
eventsPath: join8(streamsDir, `${base}.events.jsonl`),
|
|
6016
6314
|
ticket: o.ticket,
|
|
6017
6315
|
agentLog,
|
|
6018
6316
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
@@ -6022,6 +6320,10 @@ function planAgentRun(o) {
|
|
|
6022
6320
|
function describePlan(p) {
|
|
6023
6321
|
return [
|
|
6024
6322
|
`role: ${p.role}`,
|
|
6323
|
+
...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}`] : [],
|
|
6324
|
+
...(p.warnings ?? []).map((w) => `warning: ${w}`),
|
|
6325
|
+
// Only for a non-default toolchain, so a plain Claude run's output is unchanged.
|
|
6326
|
+
...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
|
|
6025
6327
|
`cwd: ${p.cwd}`,
|
|
6026
6328
|
`command: ${p.bin} ${p.args.join(" ")}`,
|
|
6027
6329
|
`prompt: ${p.promptBytes} bytes on stdin (never argv)`,
|
|
@@ -6047,14 +6349,16 @@ function endSink(sink) {
|
|
|
6047
6349
|
return finished(sink).catch(() => void 0);
|
|
6048
6350
|
}
|
|
6049
6351
|
function spawnAgent(plan, emit) {
|
|
6352
|
+
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
6050
6353
|
const started = Date.now();
|
|
6051
6354
|
const startedAt = new Date(started).toISOString();
|
|
6355
|
+
for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
|
|
6052
6356
|
const rawSink = openSink(plan.streamPath);
|
|
6053
6357
|
const eventsSink = openSink(plan.eventsPath);
|
|
6054
6358
|
return new Promise((resolve5, reject) => {
|
|
6055
6359
|
const child = spawn2(plan.bin, plan.args, {
|
|
6056
6360
|
cwd: plan.cwd,
|
|
6057
|
-
env: { ...scrubbedEnv(), ...plan.setEnv },
|
|
6361
|
+
env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv },
|
|
6058
6362
|
stdio: ["pipe", "pipe", "inherit"]
|
|
6059
6363
|
});
|
|
6060
6364
|
let buf = "";
|
|
@@ -6075,9 +6379,9 @@ function spawnAgent(plan, emit) {
|
|
|
6075
6379
|
return;
|
|
6076
6380
|
}
|
|
6077
6381
|
try {
|
|
6078
|
-
result = extractResult(parsed) ?? result;
|
|
6382
|
+
result = adapter.extractResult(parsed) ?? result;
|
|
6079
6383
|
let turnText = "";
|
|
6080
|
-
for (const ev of mapStreamLine(parsed)) {
|
|
6384
|
+
for (const ev of adapter.mapStreamLine(parsed)) {
|
|
6081
6385
|
const e = {
|
|
6082
6386
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6083
6387
|
cycle: emit.cycle,
|
|
@@ -6115,7 +6419,7 @@ function spawnAgent(plan, emit) {
|
|
|
6115
6419
|
`);
|
|
6116
6420
|
}
|
|
6117
6421
|
}
|
|
6118
|
-
const turnTokens = extractTurnTokens(parsed);
|
|
6422
|
+
const turnTokens = adapter.extractTurnTokens(parsed);
|
|
6119
6423
|
if (turnTokens !== void 0) attribution.add(turnText, turnTokens);
|
|
6120
6424
|
} catch {
|
|
6121
6425
|
}
|
|
@@ -6193,12 +6497,12 @@ function spawnAgent(plan, emit) {
|
|
|
6193
6497
|
|
|
6194
6498
|
// src/run-worktree.ts
|
|
6195
6499
|
import { existsSync as existsSync9 } from "fs";
|
|
6196
|
-
import { dirname as dirname6, join as
|
|
6500
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
6197
6501
|
|
|
6198
6502
|
// src/ports.ts
|
|
6199
6503
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6200
6504
|
import { existsSync as existsSync8 } from "fs";
|
|
6201
|
-
import { join as
|
|
6505
|
+
import { join as join9 } from "path";
|
|
6202
6506
|
var run = (cmd, args) => {
|
|
6203
6507
|
try {
|
|
6204
6508
|
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -6243,7 +6547,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
6243
6547
|
}
|
|
6244
6548
|
function worktreeExists(parent, prefix, n) {
|
|
6245
6549
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
6246
|
-
if (existsSync8(
|
|
6550
|
+
if (existsSync8(join9(parent, name, ".git"))) return true;
|
|
6247
6551
|
}
|
|
6248
6552
|
return false;
|
|
6249
6553
|
}
|
|
@@ -6331,7 +6635,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
6331
6635
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
6332
6636
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
6333
6637
|
}
|
|
6334
|
-
const path =
|
|
6638
|
+
const path = join10(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
|
|
6335
6639
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
6336
6640
|
return { action: "cut-remote", path, branch: located.branch };
|
|
6337
6641
|
}
|
|
@@ -6539,7 +6843,7 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
6539
6843
|
}
|
|
6540
6844
|
|
|
6541
6845
|
// src/connect-wizard.ts
|
|
6542
|
-
import { delimiter, dirname as dirname7, join as
|
|
6846
|
+
import { delimiter, dirname as dirname7, join as join11 } from "path";
|
|
6543
6847
|
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
6544
6848
|
import { stringify } from "yaml";
|
|
6545
6849
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
@@ -6562,7 +6866,7 @@ function defaultIsExecutable(p) {
|
|
|
6562
6866
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
6563
6867
|
for (const dir of pathEnv.split(delimiter)) {
|
|
6564
6868
|
if (!dir) continue;
|
|
6565
|
-
const candidate =
|
|
6869
|
+
const candidate = join11(dir, name);
|
|
6566
6870
|
if (isExecutable(candidate)) return candidate;
|
|
6567
6871
|
}
|
|
6568
6872
|
return void 0;
|
|
@@ -6657,8 +6961,8 @@ Wrote ${o.configPath}.
|
|
|
6657
6961
|
}
|
|
6658
6962
|
|
|
6659
6963
|
// src/release-run.ts
|
|
6660
|
-
import { readFileSync as
|
|
6661
|
-
import { dirname as dirname8, join as
|
|
6964
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync11, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync3 } from "fs";
|
|
6965
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
6662
6966
|
import { tmpdir as tmpdir2 } from "os";
|
|
6663
6967
|
|
|
6664
6968
|
// src/release.ts
|
|
@@ -7316,13 +7620,13 @@ async function bump(o, size) {
|
|
|
7316
7620
|
}
|
|
7317
7621
|
return printed;
|
|
7318
7622
|
}
|
|
7319
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
7623
|
+
const files = o.repo.release.versionFiles.map((f) => join12(o.cwd, f));
|
|
7320
7624
|
const first = files[0];
|
|
7321
7625
|
if (!existsSync11(first)) {
|
|
7322
7626
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
7323
7627
|
return null;
|
|
7324
7628
|
}
|
|
7325
|
-
const current = JSON.parse(
|
|
7629
|
+
const current = JSON.parse(readFileSync8(first, "utf8")).version;
|
|
7326
7630
|
if (!current) {
|
|
7327
7631
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
7328
7632
|
return null;
|
|
@@ -7335,7 +7639,7 @@ async function bump(o, size) {
|
|
|
7335
7639
|
return version;
|
|
7336
7640
|
}
|
|
7337
7641
|
for (const f of files) {
|
|
7338
|
-
const src =
|
|
7642
|
+
const src = readFileSync8(f, "utf8");
|
|
7339
7643
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
7340
7644
|
if (out === src) {
|
|
7341
7645
|
o.emit.error(`no version field in ${f}`);
|
|
@@ -7348,13 +7652,13 @@ async function bump(o, size) {
|
|
|
7348
7652
|
function writeChangelog(o, version, entries) {
|
|
7349
7653
|
const file = o.repo.release.changelog;
|
|
7350
7654
|
if (!file || entries.length === 0) return;
|
|
7351
|
-
const path =
|
|
7655
|
+
const path = join12(o.cwd, file);
|
|
7352
7656
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
7353
7657
|
if (o.dryRun) {
|
|
7354
7658
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
7355
7659
|
return;
|
|
7356
7660
|
}
|
|
7357
|
-
const existing = existsSync11(path) ?
|
|
7661
|
+
const existing = existsSync11(path) ? readFileSync8(path, "utf8") : "# Changelog\n";
|
|
7358
7662
|
writeFileSync6(path, insertChangelogSection(existing, section));
|
|
7359
7663
|
}
|
|
7360
7664
|
async function confirm(o, expected) {
|
|
@@ -7436,8 +7740,8 @@ async function confirmLastRelease(o, decision) {
|
|
|
7436
7740
|
function openReleaseCheckout(o) {
|
|
7437
7741
|
const { remote, base } = o.repo.branch;
|
|
7438
7742
|
const branch = `crew/release-${Date.now()}`;
|
|
7439
|
-
const root = mkdtempSync(
|
|
7440
|
-
const dir =
|
|
7743
|
+
const root = mkdtempSync(join12(tmpdir2(), "crew-release-"));
|
|
7744
|
+
const dir = join12(root, "checkout");
|
|
7441
7745
|
const close = () => {
|
|
7442
7746
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
7443
7747
|
rmSync3(root, { recursive: true, force: true });
|
|
@@ -7451,10 +7755,10 @@ function openReleaseCheckout(o) {
|
|
|
7451
7755
|
throw e;
|
|
7452
7756
|
}
|
|
7453
7757
|
for (const f of o.repo.worktrees.copy) {
|
|
7454
|
-
const src =
|
|
7758
|
+
const src = join12(o.cwd, f);
|
|
7455
7759
|
if (!existsSync11(src)) continue;
|
|
7456
|
-
mkdirSync7(dirname8(
|
|
7457
|
-
copyFileSync(src,
|
|
7760
|
+
mkdirSync7(dirname8(join12(dir, f)), { recursive: true });
|
|
7761
|
+
copyFileSync(src, join12(dir, f));
|
|
7458
7762
|
}
|
|
7459
7763
|
return { dir, branch, close };
|
|
7460
7764
|
}
|
|
@@ -8264,8 +8568,8 @@ function nextRoles(decision, ran) {
|
|
|
8264
8568
|
}
|
|
8265
8569
|
|
|
8266
8570
|
// src/agents.ts
|
|
8267
|
-
import { readFileSync as
|
|
8268
|
-
import { join as
|
|
8571
|
+
import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
|
|
8572
|
+
import { join as join13 } from "path";
|
|
8269
8573
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
8270
8574
|
var PERSONA_NAME = {
|
|
8271
8575
|
dev: "Developer",
|
|
@@ -8279,12 +8583,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
8279
8583
|
var AgentsSyncError = class extends Error {
|
|
8280
8584
|
};
|
|
8281
8585
|
function personaDefaultPrompt(promptsDir, role) {
|
|
8282
|
-
const briefPath =
|
|
8586
|
+
const briefPath = join13(promptsDir, "personas", `lane-${role}.md`);
|
|
8283
8587
|
if (!existsSync13(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
8284
|
-
if (role === "pair") return
|
|
8285
|
-
const commonPath =
|
|
8588
|
+
if (role === "pair") return readFileSync9(briefPath, "utf8");
|
|
8589
|
+
const commonPath = join13(promptsDir, "personas", "common.md");
|
|
8286
8590
|
if (!existsSync13(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
|
|
8287
|
-
return `${
|
|
8591
|
+
return `${readFileSync9(commonPath, "utf8")}${readFileSync9(briefPath, "utf8")}`;
|
|
8288
8592
|
}
|
|
8289
8593
|
async function resolveAgentId(route, role, opts) {
|
|
8290
8594
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -8337,7 +8641,11 @@ async function fetchLastSyncedPrompt(route, agentsModelId, recordId, historyId,
|
|
|
8337
8641
|
return void 0;
|
|
8338
8642
|
}
|
|
8339
8643
|
}
|
|
8340
|
-
|
|
8644
|
+
function choiceValue(v) {
|
|
8645
|
+
const raw = typeof v === "string" ? v : v && typeof v === "object" ? v.value ?? v.key : void 0;
|
|
8646
|
+
return typeof raw === "string" && raw.trim() ? raw.trim().toLowerCase() : void 0;
|
|
8647
|
+
}
|
|
8648
|
+
async function fetchSeatPersona(route, role, opts) {
|
|
8341
8649
|
const agentId = await resolveAgentId(route, role, opts);
|
|
8342
8650
|
if (!agentId || !route.resolved) return void 0;
|
|
8343
8651
|
const client2 = new TablationClient2({
|
|
@@ -8348,7 +8656,14 @@ async function fetchSeatAgentModel(route, role, opts) {
|
|
|
8348
8656
|
try {
|
|
8349
8657
|
const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
|
|
8350
8658
|
const row = await client2.records.get(agentsModel.id, agentId);
|
|
8351
|
-
|
|
8659
|
+
const tier = choiceValue(row.tier);
|
|
8660
|
+
const vendor = choiceValue(row.vendor);
|
|
8661
|
+
const persona = {
|
|
8662
|
+
...row.model ? { model: row.model } : {},
|
|
8663
|
+
...tier && TIERS.includes(tier) ? { tier } : {},
|
|
8664
|
+
...vendor ? { vendor } : {}
|
|
8665
|
+
};
|
|
8666
|
+
return Object.keys(persona).length > 0 ? persona : void 0;
|
|
8352
8667
|
} catch {
|
|
8353
8668
|
return void 0;
|
|
8354
8669
|
}
|
|
@@ -8950,14 +9265,14 @@ import {
|
|
|
8950
9265
|
} from "@tablation/client";
|
|
8951
9266
|
|
|
8952
9267
|
// src/skills.ts
|
|
8953
|
-
import { readFileSync as
|
|
8954
|
-
import { join as
|
|
9268
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync2, existsSync as existsSync14 } from "fs";
|
|
9269
|
+
import { join as join14 } from "path";
|
|
8955
9270
|
import { parse as parseYaml } from "yaml";
|
|
8956
9271
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
8957
9272
|
var SkillSyncError = class extends Error {
|
|
8958
9273
|
};
|
|
8959
9274
|
function parseSkillFile(path) {
|
|
8960
|
-
const text =
|
|
9275
|
+
const text = readFileSync10(path, "utf8");
|
|
8961
9276
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
8962
9277
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
8963
9278
|
const frontmatter = match[1];
|
|
@@ -8970,9 +9285,9 @@ function parseSkillFile(path) {
|
|
|
8970
9285
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
8971
9286
|
}
|
|
8972
9287
|
function listLocalSkills(promptsDir) {
|
|
8973
|
-
const dir =
|
|
9288
|
+
const dir = join14(promptsDir, "skills");
|
|
8974
9289
|
if (!existsSync14(dir)) return [];
|
|
8975
|
-
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
9290
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join14(dir, f)));
|
|
8976
9291
|
}
|
|
8977
9292
|
async function syncSkills(route, opts) {
|
|
8978
9293
|
if (!route.resolved) {
|
|
@@ -9151,8 +9466,8 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
9151
9466
|
}
|
|
9152
9467
|
|
|
9153
9468
|
// src/repos-cmd.ts
|
|
9154
|
-
import { homedir as
|
|
9155
|
-
import { basename as
|
|
9469
|
+
import { homedir as homedir4 } from "os";
|
|
9470
|
+
import { basename as basename7, resolve as resolve3 } from "path";
|
|
9156
9471
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
9157
9472
|
var ReposError = class extends Error {
|
|
9158
9473
|
};
|
|
@@ -9198,7 +9513,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
9198
9513
|
return { route, path: "." };
|
|
9199
9514
|
}
|
|
9200
9515
|
function expandDir(p, base) {
|
|
9201
|
-
const home = p === "~" ?
|
|
9516
|
+
const home = p === "~" ? homedir4() : p.startsWith("~/") ? `${homedir4()}/${p.slice(2)}` : p;
|
|
9202
9517
|
return resolve3(base, home);
|
|
9203
9518
|
}
|
|
9204
9519
|
function routeMapOf(doc, route) {
|
|
@@ -9224,7 +9539,7 @@ function configuredRepos(text, route, base) {
|
|
|
9224
9539
|
});
|
|
9225
9540
|
}
|
|
9226
9541
|
const dir = m.get("dir");
|
|
9227
|
-
return dir ? [{ name:
|
|
9542
|
+
return dir ? [{ name: basename7(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
9228
9543
|
}
|
|
9229
9544
|
function planRepoAdd(i) {
|
|
9230
9545
|
const doc = parseDocument(i.text);
|
|
@@ -9236,7 +9551,7 @@ function planRepoAdd(i) {
|
|
|
9236
9551
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
9237
9552
|
}
|
|
9238
9553
|
const matched = matchRepoName(i.origin, i.resolved);
|
|
9239
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ??
|
|
9554
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename7(target);
|
|
9240
9555
|
let warning;
|
|
9241
9556
|
if (!matched) {
|
|
9242
9557
|
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
@@ -9295,72 +9610,6 @@ function resolveRepoProject(projects, opt, routeAreaId) {
|
|
|
9295
9610
|
);
|
|
9296
9611
|
}
|
|
9297
9612
|
|
|
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
9613
|
// src/doctor-fix.ts
|
|
9365
9614
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
9366
9615
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
@@ -9400,14 +9649,14 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
9400
9649
|
|
|
9401
9650
|
// src/worktree-sweep.ts
|
|
9402
9651
|
import { existsSync as existsSync15 } from "fs";
|
|
9403
|
-
import { basename as
|
|
9652
|
+
import { basename as basename8 } from "path";
|
|
9404
9653
|
function planWorktreeSweep(target, tickets, contract) {
|
|
9405
9654
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
9406
9655
|
const verified = contract.statuses.verified;
|
|
9407
9656
|
const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
|
|
9408
9657
|
const actions = [];
|
|
9409
9658
|
for (const w of worktrees(target.dir)) {
|
|
9410
|
-
const m = /(?:^|-)(\d+)$/.exec(
|
|
9659
|
+
const m = /(?:^|-)(\d+)$/.exec(basename8(w.path));
|
|
9411
9660
|
if (!m) continue;
|
|
9412
9661
|
const ticket = byNumber.get(m[1]);
|
|
9413
9662
|
if (!ticket) continue;
|
|
@@ -10572,7 +10821,7 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
10572
10821
|
cycle: emit.cycle,
|
|
10573
10822
|
ticket: ticketHint,
|
|
10574
10823
|
divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
|
|
10575
|
-
|
|
10824
|
+
agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
|
|
10576
10825
|
resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
|
|
10577
10826
|
});
|
|
10578
10827
|
emit.enter("agent", role);
|
|
@@ -11658,7 +11907,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
11658
11907
|
cycle: wemit.cycle,
|
|
11659
11908
|
ticket: w.decision.actionable.top?.issue_id,
|
|
11660
11909
|
divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
|
|
11661
|
-
|
|
11910
|
+
agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
|
|
11662
11911
|
resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
|
|
11663
11912
|
});
|
|
11664
11913
|
process.stdout.write(`${describePlan(fleetPlan2)}
|
|
@@ -11751,7 +12000,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
11751
12000
|
cycle: wemit.cycle,
|
|
11752
12001
|
ticket: fleetTicketHint,
|
|
11753
12002
|
divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
|
|
11754
|
-
|
|
12003
|
+
agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
|
|
11755
12004
|
resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
|
|
11756
12005
|
});
|
|
11757
12006
|
wemit.enter("agent", w.role);
|
|
@@ -11956,7 +12205,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
11956
12205
|
cycle: emit.cycle,
|
|
11957
12206
|
ticket: ticketHint,
|
|
11958
12207
|
divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
|
|
11959
|
-
|
|
12208
|
+
agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
|
|
11960
12209
|
resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
|
|
11961
12210
|
});
|
|
11962
12211
|
if (dryRun) {
|
|
@@ -12746,8 +12995,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12746
12995
|
if (!wroteConfig) process.stdout.write(routeBlock);
|
|
12747
12996
|
if (!flag("no-mcp") && apiKey) {
|
|
12748
12997
|
const replace = value("mcp") === "replace";
|
|
12749
|
-
|
|
12998
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12999
|
+
for (const tc of shipToolchains(cfg.ship)) {
|
|
13000
|
+
const id = `${tc.harness}\0${tc.bin}`;
|
|
13001
|
+
if (seen.has(id)) continue;
|
|
13002
|
+
seen.add(id);
|
|
13003
|
+
process.stderr.write(`${adapterFor(tc.harness).ensureMcp({ bin: tc.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
|
|
12750
13004
|
`);
|
|
13005
|
+
}
|
|
12751
13006
|
}
|
|
12752
13007
|
if (found.holds.length) {
|
|
12753
13008
|
process.stderr.write(
|
|
@@ -13303,6 +13558,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13303
13558
|
name: cfg.ship.name,
|
|
13304
13559
|
platform: cfg.ship.platform,
|
|
13305
13560
|
agent: { bin: cfg.ship.agent.bin, model: cfg.ship.agent.model },
|
|
13561
|
+
// Additive (CREW-1511): the configured toolchains and the ship's
|
|
13562
|
+
// default. `agent` above stays for older crew-macos builds.
|
|
13563
|
+
toolchain: resolveToolchain(cfg.ship).name,
|
|
13564
|
+
toolchains: shipToolchains(cfg.ship).map((x) => ({
|
|
13565
|
+
name: x.name,
|
|
13566
|
+
harness: x.harness,
|
|
13567
|
+
bin: x.bin,
|
|
13568
|
+
launcher: x.launcher ?? null,
|
|
13569
|
+
model: x.model
|
|
13570
|
+
})),
|
|
13306
13571
|
stateDir: cfg.ship.stateDir,
|
|
13307
13572
|
logFile: cfg.ship.logFile,
|
|
13308
13573
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
@@ -13328,6 +13593,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13328
13593
|
dir: route.dir,
|
|
13329
13594
|
enabled: route.enabled,
|
|
13330
13595
|
area: route.area ?? null,
|
|
13596
|
+
// Additive (CREW-1511): the toolchain this route's sessions run on.
|
|
13597
|
+
toolchain: resolveToolchain(cfg.ship, route).name,
|
|
13331
13598
|
baseUrl: route.baseUrl,
|
|
13332
13599
|
repos: repos.map((r) => ({
|
|
13333
13600
|
name: r.name,
|
|
@@ -13342,7 +13609,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13342
13609
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
13343
13610
|
// config-provenance display; this is the field that actually
|
|
13344
13611
|
// describes where a worktree lands by default.
|
|
13345
|
-
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${
|
|
13612
|
+
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename9(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
|
|
13346
13613
|
worktreeParent: resolvePath(r.dir, "..")
|
|
13347
13614
|
})),
|
|
13348
13615
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -13450,7 +13717,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
13450
13717
|
);
|
|
13451
13718
|
for (const r of await resolvedRepos(route)) {
|
|
13452
13719
|
const { config } = r;
|
|
13453
|
-
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${
|
|
13720
|
+
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename9(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
|
|
13454
13721
|
process.stdout.write(
|
|
13455
13722
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
13456
13723
|
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
|
@@ -13553,14 +13820,24 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
13553
13820
|
}
|
|
13554
13821
|
}
|
|
13555
13822
|
const schedulerPath = pathFor(cfg.ship);
|
|
13556
|
-
const agentBinPath = findOnPath(cfg.ship.agent.bin, schedulerPath);
|
|
13557
13823
|
const gitPath = findOnPath("git", schedulerPath);
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13824
|
+
const defaultToolchain = resolveToolchain(cfg.ship).name;
|
|
13825
|
+
let allBinsFound = true;
|
|
13826
|
+
for (const tc of shipToolchains(cfg.ship)) {
|
|
13827
|
+
const adapter = adapterFor(tc.harness);
|
|
13828
|
+
const binPath = findOnPath(tc.launcher ?? tc.bin, schedulerPath);
|
|
13829
|
+
if (!binPath) allBinsFound = false;
|
|
13830
|
+
const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
|
|
13831
|
+
process.stdout.write(
|
|
13832
|
+
`${label} ${tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
|
|
13561
13833
|
`
|
|
13562
|
-
|
|
13563
|
-
|
|
13834
|
+
);
|
|
13835
|
+
for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
|
|
13836
|
+
`);
|
|
13837
|
+
}
|
|
13838
|
+
process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
|
|
13839
|
+
`);
|
|
13840
|
+
if (!allBinsFound || !gitPath) {
|
|
13564
13841
|
process.stdout.write(
|
|
13565
13842
|
` scheduler PATH is ${schedulerPath} \u2014 add the directory holding the missing binary to ship.extraPath in crew.yaml, then re-run \`crew doctor\`.
|
|
13566
13843
|
`
|
|
@@ -13568,14 +13845,34 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
13568
13845
|
}
|
|
13569
13846
|
if (route.apiKey) {
|
|
13570
13847
|
const mcpUrl = mcpUrlFor(route.baseUrl);
|
|
13571
|
-
const
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13848
|
+
const seenMcp = /* @__PURE__ */ new Set();
|
|
13849
|
+
for (const tc of shipToolchains(cfg.ship)) {
|
|
13850
|
+
const id = `${tc.harness}\0${tc.bin}`;
|
|
13851
|
+
if (seenMcp.has(id)) continue;
|
|
13852
|
+
seenMcp.add(id);
|
|
13853
|
+
const adapter = adapterFor(tc.harness);
|
|
13854
|
+
const m = adapter.inspectMcp({ bin: tc.bin, url: mcpUrl, key: route.apiKey });
|
|
13855
|
+
process.stdout.write(
|
|
13856
|
+
m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
|
|
13857
|
+
` : m.state === "no-binary" ? `mcp: ${tc.bin} not found \u2014 ${adapter.displayName} was not checked
|
|
13575
13858
|
` : m.state === "different" ? `mcp: tablation registered but not for this route \u2014 ${m.detail}
|
|
13576
13859
|
` : `mcp: not registered \u2014 run crew connect
|
|
13577
13860
|
`
|
|
13578
|
-
|
|
13861
|
+
);
|
|
13862
|
+
}
|
|
13863
|
+
}
|
|
13864
|
+
const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
|
|
13865
|
+
if (route.apiKey && route.resolved) {
|
|
13866
|
+
let flagged = 0;
|
|
13867
|
+
for (const role of ROLE_NAMES) {
|
|
13868
|
+
const persona = await fetchSeatPersona(route, role, { userAgent: cfg.ship.userAgent });
|
|
13869
|
+
if (persona?.vendor && !shipVendors.has(persona.vendor)) {
|
|
13870
|
+
flagged++;
|
|
13871
|
+
process.stdout.write(`seat vendor: ${role} persona wants "${persona.vendor}" \u2014 no toolchain on this ship serves it (have: ${[...shipVendors].join(", ")}); it will run ${resolveSeat(cfg.ship, route, role).toolchain.name} with a warning
|
|
13872
|
+
`);
|
|
13873
|
+
}
|
|
13874
|
+
}
|
|
13875
|
+
if (flagged === 0) process.stdout.write("seat vendor: OK (every persona vendor has a toolchain on this ship)\n");
|
|
13579
13876
|
}
|
|
13580
13877
|
for (const c of cfg.routes.filter((r) => r.enabled)) {
|
|
13581
13878
|
let repos;
|