@tablation/crew 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/crew.example.yaml +16 -0
- package/dist/cli.js +781 -480
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,10 +7,412 @@ 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, isAbsolute, join as
|
|
12
|
-
import { homedir, tmpdir } from "os";
|
|
10
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
|
|
11
|
+
import { resolve, dirname as dirname2, isAbsolute, join as join4, basename as basename2 } from "path";
|
|
12
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
13
13
|
import { parse as parse2 } from "yaml";
|
|
14
|
+
|
|
15
|
+
// src/events.ts
|
|
16
|
+
import {
|
|
17
|
+
appendFileSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
statSync,
|
|
21
|
+
renameSync,
|
|
22
|
+
openSync,
|
|
23
|
+
closeSync,
|
|
24
|
+
rmSync
|
|
25
|
+
} from "fs";
|
|
26
|
+
import { join, dirname } from "path";
|
|
27
|
+
function render(e) {
|
|
28
|
+
const scope = e.role ? `${e.step}[${e.role}]` : e.step;
|
|
29
|
+
const ticket = e.ticket ? ` ${e.ticket}` : "";
|
|
30
|
+
const level = e.level === "info" ? "" : `${e.level.toUpperCase()}: `;
|
|
31
|
+
return `${e.at} ${scope}:${ticket} ${level}${e.message}`;
|
|
32
|
+
}
|
|
33
|
+
var DEFAULT_EVENTS_ROTATE_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
var ROTATED_EVENTS_RE = /^events\.\d{8}T\d{6}Z(?:-\d+)?\.jsonl$/;
|
|
35
|
+
var ROTATE_LOCK_STALE_MS = 6e4;
|
|
36
|
+
function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(file) || statSync(file).size <= maxBytes) return void 0;
|
|
39
|
+
} catch {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
const lock = `${file}.rotate.lock`;
|
|
43
|
+
try {
|
|
44
|
+
try {
|
|
45
|
+
closeSync(openSync(lock, "wx"));
|
|
46
|
+
} catch {
|
|
47
|
+
try {
|
|
48
|
+
if (Date.now() - statSync(lock).mtimeMs < ROTATE_LOCK_STALE_MS) return void 0;
|
|
49
|
+
rmSync(lock, { force: true });
|
|
50
|
+
closeSync(openSync(lock, "wx"));
|
|
51
|
+
} catch {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
if (statSync(file).size <= maxBytes) return void 0;
|
|
57
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
58
|
+
let dest = join(dirname(file), `events.${stamp}.jsonl`);
|
|
59
|
+
for (let n = 1; existsSync(dest); n++) dest = join(dirname(file), `events.${stamp}-${n}.jsonl`);
|
|
60
|
+
renameSync(file, dest);
|
|
61
|
+
return dest;
|
|
62
|
+
} finally {
|
|
63
|
+
rmSync(lock, { force: true });
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
var Emitter = class _Emitter {
|
|
70
|
+
cycle;
|
|
71
|
+
opts;
|
|
72
|
+
now;
|
|
73
|
+
step = "poll";
|
|
74
|
+
role;
|
|
75
|
+
constructor(opts) {
|
|
76
|
+
this.opts = opts;
|
|
77
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
78
|
+
this.cycle = opts.cycleId ?? this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
79
|
+
for (const f of [opts.eventFile, opts.logFile]) {
|
|
80
|
+
if (!f) continue;
|
|
81
|
+
try {
|
|
82
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Everything emitted after this belongs to `step`, until the next one. */
|
|
88
|
+
enter(step, role) {
|
|
89
|
+
this.step = step;
|
|
90
|
+
this.role = role;
|
|
91
|
+
}
|
|
92
|
+
emit(message, extra = {}) {
|
|
93
|
+
const e = {
|
|
94
|
+
at: this.now().toISOString(),
|
|
95
|
+
cycle: this.cycle,
|
|
96
|
+
route: this.opts.route,
|
|
97
|
+
step: extra.step ?? this.step,
|
|
98
|
+
level: extra.level ?? "info",
|
|
99
|
+
message,
|
|
100
|
+
...extra.role ?? this.role ? { role: extra.role ?? this.role } : {},
|
|
101
|
+
...extra.ticket ? { ticket: extra.ticket } : {},
|
|
102
|
+
...extra.data ? { data: extra.data } : {}
|
|
103
|
+
};
|
|
104
|
+
const line = render(e);
|
|
105
|
+
try {
|
|
106
|
+
if (this.opts.eventFile && this.opts.rotateBytes) rotateEventsFile(this.opts.eventFile, this.opts.rotateBytes, this.now());
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
if (this.opts.eventFile) appendFileSync(this.opts.eventFile, `${JSON.stringify(e)}
|
|
111
|
+
`);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
if (this.opts.logFile) appendFileSync(this.opts.logFile, `${line}
|
|
116
|
+
`);
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
(this.opts.console ?? ((l) => process.stderr.write(`${l}
|
|
121
|
+
`)))(line);
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
return e;
|
|
125
|
+
}
|
|
126
|
+
warn(message, extra = {}) {
|
|
127
|
+
return this.emit(message, { ...extra, level: "warn" });
|
|
128
|
+
}
|
|
129
|
+
error(message, extra = {}) {
|
|
130
|
+
return this.emit(message, { ...extra, level: "error" });
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A view of this emitter labelling every event with a different route,
|
|
134
|
+
* sharing the same sinks and cycle id. For a fleet-wide run, the top-level
|
|
135
|
+
* Emitter is stamped with just one route (see cli.ts) — a phase that
|
|
136
|
+
* actually knows which route it's working (release, per-repo poll) uses
|
|
137
|
+
* this instead, so its events aren't all attributed to `routes[0]`.
|
|
138
|
+
*/
|
|
139
|
+
forRoute(route) {
|
|
140
|
+
return new _Emitter({ ...this.opts, route, cycleId: this.cycle });
|
|
141
|
+
}
|
|
142
|
+
/** Times a step and emits its outcome, so a view can show duration. */
|
|
143
|
+
async timed(step, message, fn, role) {
|
|
144
|
+
this.enter(step, role);
|
|
145
|
+
const started = this.now().getTime();
|
|
146
|
+
try {
|
|
147
|
+
const result = await fn();
|
|
148
|
+
this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
|
|
149
|
+
return result;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this.error(`${message} \u2014 ${err.message}`, {
|
|
152
|
+
data: { ms: this.now().getTime() - started, ok: false }
|
|
153
|
+
});
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
159
|
+
|
|
160
|
+
// src/stream.ts
|
|
161
|
+
function toolTarget(name, input) {
|
|
162
|
+
if (!input) return void 0;
|
|
163
|
+
for (const key of ["file_path", "path", "notebook_path", "url", "pattern"]) {
|
|
164
|
+
const v = input[key];
|
|
165
|
+
if (typeof v === "string" && v) return v;
|
|
166
|
+
}
|
|
167
|
+
if (name === "Bash" && typeof input.command === "string") {
|
|
168
|
+
return input.command.trim().split(/\s+/)[0];
|
|
169
|
+
}
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
function mapStreamLine(line) {
|
|
173
|
+
const obj = line;
|
|
174
|
+
if (!obj || obj.type !== "assistant") return [];
|
|
175
|
+
const content = obj.message?.content;
|
|
176
|
+
if (!Array.isArray(content)) return [];
|
|
177
|
+
const out = [];
|
|
178
|
+
for (const block of content) {
|
|
179
|
+
if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
|
|
180
|
+
else if (block.type === "text") out.push({ kind: "text", text: block.text });
|
|
181
|
+
else if (block.type === "tool_use") {
|
|
182
|
+
out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
function extractTurnTokens(line) {
|
|
188
|
+
const obj = line;
|
|
189
|
+
if (!obj || obj.type !== "assistant") return void 0;
|
|
190
|
+
const usage2 = obj.message?.usage;
|
|
191
|
+
if (!usage2) return void 0;
|
|
192
|
+
return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
|
|
193
|
+
}
|
|
194
|
+
function extractResult(line) {
|
|
195
|
+
const obj = line;
|
|
196
|
+
if (!obj || obj.type !== "result") return void 0;
|
|
197
|
+
return {
|
|
198
|
+
sessionId: obj.session_id,
|
|
199
|
+
numTurns: obj.num_turns,
|
|
200
|
+
totalCostUsd: obj.total_cost_usd,
|
|
201
|
+
inputTokens: obj.usage?.input_tokens,
|
|
202
|
+
outputTokens: obj.usage?.output_tokens,
|
|
203
|
+
cacheReadTokens: obj.usage?.cache_read_input_tokens,
|
|
204
|
+
cacheCreationTokens: obj.usage?.cache_creation_input_tokens
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/claude-mcp.ts
|
|
209
|
+
import { spawnSync } from "child_process";
|
|
210
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
211
|
+
import { homedir } from "os";
|
|
212
|
+
import { join as join2 } from "path";
|
|
213
|
+
var MCP_SERVER_NAME = "tablation";
|
|
214
|
+
var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
|
|
215
|
+
function mcpUrlFor(baseUrl) {
|
|
216
|
+
return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
|
|
217
|
+
}
|
|
218
|
+
function runClaude(bin, args) {
|
|
219
|
+
const r = spawnSync(bin, args, { encoding: "utf8", timeout: 3e4 });
|
|
220
|
+
const missing = r.error?.code === "ENOENT";
|
|
221
|
+
return { status: r.status, missing, stderr: r.stderr ?? "" };
|
|
222
|
+
}
|
|
223
|
+
function userScopeEntry(home) {
|
|
224
|
+
try {
|
|
225
|
+
const j = JSON.parse(readFileSync2(join2(home, ".claude.json"), "utf8"));
|
|
226
|
+
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
227
|
+
} catch {
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function inspectClaudeMcp(o) {
|
|
232
|
+
const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
|
|
233
|
+
if (got.missing) return { state: "no-claude" };
|
|
234
|
+
if (got.status !== 0) return { state: "missing" };
|
|
235
|
+
const entry = userScopeEntry(o.home ?? homedir());
|
|
236
|
+
if (!entry) {
|
|
237
|
+
return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
|
|
238
|
+
}
|
|
239
|
+
if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
|
|
240
|
+
const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
|
|
241
|
+
if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
|
|
242
|
+
return { state: "registered", url: o.url };
|
|
243
|
+
}
|
|
244
|
+
function ensureClaudeMcp(o) {
|
|
245
|
+
const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
|
|
246
|
+
const st = inspectClaudeMcp(o);
|
|
247
|
+
if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
|
|
248
|
+
if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
|
|
249
|
+
if (st.state === "different" && !o.replace) {
|
|
250
|
+
return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
251
|
+
}
|
|
252
|
+
if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
|
|
253
|
+
if (st.state === "different") {
|
|
254
|
+
runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
255
|
+
}
|
|
256
|
+
const add = runClaude(o.bin, [
|
|
257
|
+
"mcp",
|
|
258
|
+
"add",
|
|
259
|
+
"--transport",
|
|
260
|
+
"http",
|
|
261
|
+
MCP_SERVER_NAME,
|
|
262
|
+
o.url,
|
|
263
|
+
"--header",
|
|
264
|
+
`Authorization: Bearer ${o.key}`,
|
|
265
|
+
"--scope",
|
|
266
|
+
"user"
|
|
267
|
+
]);
|
|
268
|
+
if (add.status !== 0) {
|
|
269
|
+
return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
|
|
270
|
+
}
|
|
271
|
+
return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/capability.ts
|
|
275
|
+
function profileFor(role) {
|
|
276
|
+
if (role === "triage") return { level: "read", designTools: false };
|
|
277
|
+
if (role === "qa") return { level: "write", designTools: false };
|
|
278
|
+
return { level: "edit", designTools: role === "design" };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/adapters/claude.ts
|
|
282
|
+
var BILLING_VARS_TO_UNSET = [
|
|
283
|
+
"ANTHROPIC_API_KEY",
|
|
284
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
285
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
286
|
+
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
287
|
+
"ANTHROPIC_VERTEX_REGION",
|
|
288
|
+
"CLOUD_ML_REGION",
|
|
289
|
+
"ANTHROPIC_MODEL"
|
|
290
|
+
];
|
|
291
|
+
var DISALLOWED_TOOLS = [
|
|
292
|
+
"Bash(rm -rf:*)",
|
|
293
|
+
"Bash(rm -fr:*)",
|
|
294
|
+
"Bash(git push --force*)",
|
|
295
|
+
"Bash(git push -f*)",
|
|
296
|
+
"Bash(curl*| sh)",
|
|
297
|
+
"Bash(curl*|sh)",
|
|
298
|
+
"Bash(curl*| bash)",
|
|
299
|
+
"Bash(curl*|bash)",
|
|
300
|
+
"Bash(sudo:*)"
|
|
301
|
+
];
|
|
302
|
+
function claudeToolsFor(p) {
|
|
303
|
+
const tablation = "mcp__tablation__*";
|
|
304
|
+
if (p.level === "read") return ["Bash", "Read", tablation];
|
|
305
|
+
if (p.level === "write") return ["Bash", "Read", "Write", "Grep", "Glob", tablation];
|
|
306
|
+
const base = ["Bash", "Read", "Edit", "Write", "Grep", "Glob", tablation];
|
|
307
|
+
return p.designTools ? [...base, "Skill", "Artifact"] : base;
|
|
308
|
+
}
|
|
309
|
+
function allowedTools(role) {
|
|
310
|
+
return claudeToolsFor(profileFor(role));
|
|
311
|
+
}
|
|
312
|
+
var claudeAdapter = {
|
|
313
|
+
harness: "claude",
|
|
314
|
+
displayName: "Claude Code",
|
|
315
|
+
defaultBin: "claude",
|
|
316
|
+
defaultModel: "claude-sonnet-5",
|
|
317
|
+
defaultTiers: {
|
|
318
|
+
light: { model: "claude-haiku-4-5-20251001" },
|
|
319
|
+
standard: { model: "claude-sonnet-5-5" },
|
|
320
|
+
deep: { model: "claude-opus-5-5" }
|
|
321
|
+
},
|
|
322
|
+
limitations: [
|
|
323
|
+
"Bash is not sandboxed: the deny list refuses known destructive patterns, but a one-liner reaching the same end another way is not caught",
|
|
324
|
+
"the write profile (QA) withholds the Edit tool, not file writes \u2014 Bash can still write files",
|
|
325
|
+
"git push is not blocked, only forced pushes"
|
|
326
|
+
],
|
|
327
|
+
invocation(spec, o) {
|
|
328
|
+
const args = [
|
|
329
|
+
"-p",
|
|
330
|
+
"--allowedTools",
|
|
331
|
+
...allowedTools(o.role),
|
|
332
|
+
"--disallowedTools",
|
|
333
|
+
...DISALLOWED_TOOLS,
|
|
334
|
+
"--model",
|
|
335
|
+
o.model,
|
|
336
|
+
"--output-format",
|
|
337
|
+
"stream-json",
|
|
338
|
+
"--verbose",
|
|
339
|
+
// Registers the tracker's own MCP tool surface for this run only —
|
|
340
|
+
// `${CREW_API_KEY}` is interpolated by the CLI from the environment,
|
|
341
|
+
// so the resolved key is never written to an argv string or a config
|
|
342
|
+
// file on disk. Skipped when there's no key to send: a route `crew
|
|
343
|
+
// connect` hasn't resolved yet has nothing to authenticate with.
|
|
344
|
+
...o.mcp ? ["--mcp-config", JSON.stringify({
|
|
345
|
+
mcpServers: {
|
|
346
|
+
tablation: {
|
|
347
|
+
type: "http",
|
|
348
|
+
url: o.mcp.url,
|
|
349
|
+
headers: { Authorization: `Bearer \${${o.mcp.keyVar}}` }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
})] : [],
|
|
353
|
+
...spec.args
|
|
354
|
+
];
|
|
355
|
+
return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
|
|
356
|
+
},
|
|
357
|
+
runEnv(spec) {
|
|
358
|
+
return {
|
|
359
|
+
// Extended thinking is what makes the CLI emit `thinking` stream
|
|
360
|
+
// blocks at all — without it, Agent Log Cycles reporting has nothing
|
|
361
|
+
// to report (ISSUE-558).
|
|
362
|
+
...spec.maxThinkingTokens > 0 ? { MAX_THINKING_TOKENS: String(spec.maxThinkingTokens) } : {},
|
|
363
|
+
...spec.env
|
|
364
|
+
};
|
|
365
|
+
},
|
|
366
|
+
unsetEnv: BILLING_VARS_TO_UNSET,
|
|
367
|
+
mapStreamLine,
|
|
368
|
+
extractResult,
|
|
369
|
+
extractTurnTokens,
|
|
370
|
+
inspectMcp(o) {
|
|
371
|
+
const s = inspectClaudeMcp(o);
|
|
372
|
+
return s.state === "no-claude" ? { state: "no-binary" } : s;
|
|
373
|
+
},
|
|
374
|
+
ensureMcp: ensureClaudeMcp
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// src/toolchain.ts
|
|
378
|
+
var TIERS = ["light", "standard", "deep"];
|
|
379
|
+
var ADAPTERS = {
|
|
380
|
+
[claudeAdapter.harness]: claudeAdapter
|
|
381
|
+
};
|
|
382
|
+
var HARNESS_NAMES = Object.keys(ADAPTERS);
|
|
383
|
+
function isHarness(name) {
|
|
384
|
+
return Object.hasOwn(ADAPTERS, name);
|
|
385
|
+
}
|
|
386
|
+
function adapterFor(harness) {
|
|
387
|
+
const a = Object.hasOwn(ADAPTERS, harness) ? ADAPTERS[harness] : void 0;
|
|
388
|
+
if (!a) throw new Error(`unknown harness "${harness}" (have: ${HARNESS_NAMES.join(", ")})`);
|
|
389
|
+
return a;
|
|
390
|
+
}
|
|
391
|
+
var LEGACY_TOOLCHAIN = "claude";
|
|
392
|
+
function shipToolchains(ship) {
|
|
393
|
+
const explicit = ship.toolchains ?? [];
|
|
394
|
+
if (explicit.some((t) => t.name === LEGACY_TOOLCHAIN)) return explicit;
|
|
395
|
+
const a = claudeAdapter;
|
|
396
|
+
const legacy = {
|
|
397
|
+
name: LEGACY_TOOLCHAIN,
|
|
398
|
+
harness: a.harness,
|
|
399
|
+
bin: ship.agent.bin,
|
|
400
|
+
model: ship.agent.model,
|
|
401
|
+
tiers: a.defaultTiers,
|
|
402
|
+
args: [],
|
|
403
|
+
env: {},
|
|
404
|
+
maxThinkingTokens: ship.agent.maxThinkingTokens
|
|
405
|
+
};
|
|
406
|
+
return [legacy, ...explicit];
|
|
407
|
+
}
|
|
408
|
+
function resolveToolchain(ship, route) {
|
|
409
|
+
const name = route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
|
|
410
|
+
const found = shipToolchains(ship).find((t) => t.name === name);
|
|
411
|
+
if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
|
|
412
|
+
return found;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/config.ts
|
|
14
416
|
import { getSessionStore, hostFromUrl } from "@tablation/client";
|
|
15
417
|
|
|
16
418
|
// src/platform.ts
|
|
@@ -50,8 +452,8 @@ function explain(ship, required) {
|
|
|
50
452
|
}
|
|
51
453
|
|
|
52
454
|
// src/repo-config.ts
|
|
53
|
-
import { readFileSync, existsSync } from "fs";
|
|
54
|
-
import { join, basename } from "path";
|
|
455
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
456
|
+
import { join as join3, basename } from "path";
|
|
55
457
|
import { parse } from "yaml";
|
|
56
458
|
var SPEC_VERSION = 1;
|
|
57
459
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -124,8 +526,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
124
526
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
125
527
|
function findRepoConfig(dir) {
|
|
126
528
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
127
|
-
const p =
|
|
128
|
-
if (
|
|
529
|
+
const p = join3(dir, name);
|
|
530
|
+
if (existsSync2(p)) return p;
|
|
129
531
|
}
|
|
130
532
|
return null;
|
|
131
533
|
}
|
|
@@ -355,7 +757,7 @@ function parseRepoConfig(text, file) {
|
|
|
355
757
|
function loadRepoConfig(dir) {
|
|
356
758
|
const file = findRepoConfig(dir);
|
|
357
759
|
if (!file) return null;
|
|
358
|
-
return parseRepoConfig(
|
|
760
|
+
return parseRepoConfig(readFileSync3(file, "utf8"), file);
|
|
359
761
|
}
|
|
360
762
|
function hookLabel(cfg, hook2) {
|
|
361
763
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -620,6 +1022,8 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
620
1022
|
"name",
|
|
621
1023
|
"platform",
|
|
622
1024
|
"agent",
|
|
1025
|
+
"toolchains",
|
|
1026
|
+
"toolchain",
|
|
623
1027
|
"shell",
|
|
624
1028
|
"extraPath",
|
|
625
1029
|
"useNvm",
|
|
@@ -631,6 +1035,7 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
631
1035
|
"apiKey",
|
|
632
1036
|
"maxConcurrentAgents",
|
|
633
1037
|
"streamRetentionDays",
|
|
1038
|
+
"eventsRotateBytes",
|
|
634
1039
|
"reposBasePath",
|
|
635
1040
|
"relayHost",
|
|
636
1041
|
"relayPort",
|
|
@@ -658,7 +1063,8 @@ var ROUTE_KEYS = /* @__PURE__ */ new Set([
|
|
|
658
1063
|
"contract",
|
|
659
1064
|
"resolved",
|
|
660
1065
|
"promptSet",
|
|
661
|
-
"hostPassengers"
|
|
1066
|
+
"hostPassengers",
|
|
1067
|
+
"toolchain"
|
|
662
1068
|
]);
|
|
663
1069
|
function parseWorktreeName(v, where, file) {
|
|
664
1070
|
const problem = typeof v === "string" ? worktreeNameProblem(v) : "must be a string";
|
|
@@ -762,16 +1168,16 @@ function mergeRouteRelease(route, override) {
|
|
|
762
1168
|
};
|
|
763
1169
|
}
|
|
764
1170
|
function expand(p, base) {
|
|
765
|
-
let out = p.startsWith("~") ? p.replace(/^~/,
|
|
766
|
-
out = out.replace(/\$\{?HOME\}?/g,
|
|
1171
|
+
let out = p.startsWith("~") ? p.replace(/^~/, homedir2()) : p;
|
|
1172
|
+
out = out.replace(/\$\{?HOME\}?/g, homedir2());
|
|
767
1173
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
768
1174
|
}
|
|
769
1175
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
770
|
-
if (!promptSet) return
|
|
1176
|
+
if (!promptSet) return join4(crewHome, "prompts", "default");
|
|
771
1177
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
772
1178
|
return expand(promptSet, base);
|
|
773
1179
|
}
|
|
774
|
-
return
|
|
1180
|
+
return join4(crewHome, "prompts", promptSet);
|
|
775
1181
|
}
|
|
776
1182
|
var Missing = class {
|
|
777
1183
|
paths = [];
|
|
@@ -795,7 +1201,7 @@ var Missing = class {
|
|
|
795
1201
|
};
|
|
796
1202
|
function crewVersion(crewHome) {
|
|
797
1203
|
try {
|
|
798
|
-
const pkg = JSON.parse(
|
|
1204
|
+
const pkg = JSON.parse(readFileSync4(join4(crewHome, "package.json"), "utf8"));
|
|
799
1205
|
return pkg.version ?? "0.0.0";
|
|
800
1206
|
} catch {
|
|
801
1207
|
return "0.0.0";
|
|
@@ -805,26 +1211,26 @@ function configSearchPath(crewHome) {
|
|
|
805
1211
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
806
1212
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
807
1213
|
return [
|
|
808
|
-
...xdg ? [
|
|
809
|
-
|
|
1214
|
+
...xdg ? [join4(xdg, "crew", "crew.yaml")] : [],
|
|
1215
|
+
join4(homedir2(), ".config", "crew", "crew.yaml"),
|
|
810
1216
|
resolve(crewHome, "crew.yaml")
|
|
811
1217
|
];
|
|
812
1218
|
}
|
|
813
1219
|
function findConfigFile(crewHome) {
|
|
814
1220
|
const candidates = configSearchPath(crewHome);
|
|
815
|
-
return candidates.find((c) =>
|
|
1221
|
+
return candidates.find((c) => existsSync3(c)) ?? candidates[candidates.length - 1];
|
|
816
1222
|
}
|
|
817
|
-
var defaultConfigPath = () =>
|
|
1223
|
+
var defaultConfigPath = () => join4(process.env.XDG_CONFIG_HOME ?? join4(homedir2(), ".config"), "crew", "crew.yaml");
|
|
818
1224
|
function routeSlug(route) {
|
|
819
1225
|
return route.replace(/\//g, "-");
|
|
820
1226
|
}
|
|
821
1227
|
function resolvedPathFor(stateDir, route) {
|
|
822
1228
|
const [workspace, project] = splitRoute(route);
|
|
823
|
-
return
|
|
1229
|
+
return join4(stateDir, "resolved", workspace, `${project}.json`);
|
|
824
1230
|
}
|
|
825
1231
|
function apiKeyPathFor(stateDir, route) {
|
|
826
1232
|
const [workspace, project] = splitRoute(route);
|
|
827
|
-
return
|
|
1233
|
+
return join4(stateDir, "keys", workspace, `${project}.env`);
|
|
828
1234
|
}
|
|
829
1235
|
function splitRoute(route) {
|
|
830
1236
|
const parts = route.split("/");
|
|
@@ -835,9 +1241,9 @@ function splitRoute(route) {
|
|
|
835
1241
|
}
|
|
836
1242
|
function readResolvedFile(stateDir, route) {
|
|
837
1243
|
const path = resolvedPathFor(stateDir, route);
|
|
838
|
-
if (!
|
|
1244
|
+
if (!existsSync3(path)) return void 0;
|
|
839
1245
|
try {
|
|
840
|
-
return JSON.parse(
|
|
1246
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
841
1247
|
} catch (e) {
|
|
842
1248
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
843
1249
|
}
|
|
@@ -893,23 +1299,23 @@ function parseResolved(raw, m, where) {
|
|
|
893
1299
|
}
|
|
894
1300
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
895
1301
|
const file = configFile ?? findConfigFile(crewHome);
|
|
896
|
-
if (opts.text === void 0 && !
|
|
1302
|
+
if (opts.text === void 0 && !existsSync3(file)) {
|
|
897
1303
|
throw new ConfigError(
|
|
898
1304
|
`no config found. Looked in:
|
|
899
1305
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
900
1306
|
|
|
901
1307
|
Create the first of those:
|
|
902
|
-
mkdir -p ${
|
|
1308
|
+
mkdir -p ${dirname2(defaultConfigPath())} && cp ${join4(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
903
1309
|
);
|
|
904
1310
|
}
|
|
905
1311
|
let raw;
|
|
906
1312
|
try {
|
|
907
|
-
raw = parse2(opts.text ??
|
|
1313
|
+
raw = parse2(opts.text ?? readFileSync4(file, "utf8"));
|
|
908
1314
|
} catch (e) {
|
|
909
1315
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
910
1316
|
}
|
|
911
1317
|
if (!raw || typeof raw !== "object") throw new ConfigError(`${file} is empty or not a mapping`);
|
|
912
|
-
const base =
|
|
1318
|
+
const base = dirname2(file);
|
|
913
1319
|
const missing = new Missing();
|
|
914
1320
|
rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["ship", "routes"]), "top-level", file);
|
|
915
1321
|
const shipRaw = raw.ship ?? {};
|
|
@@ -926,10 +1332,20 @@ Create the first of those:
|
|
|
926
1332
|
if (!Number.isInteger(streamRetentionDays) || streamRetentionDays < 1) {
|
|
927
1333
|
missing.add(`ship.streamRetentionDays must be a positive integer (got "${shipRaw.streamRetentionDays}")`);
|
|
928
1334
|
}
|
|
1335
|
+
const eventsRotateBytes = shipRaw.eventsRotateBytes === void 0 ? DEFAULT_EVENTS_ROTATE_BYTES : Number(shipRaw.eventsRotateBytes);
|
|
1336
|
+
if (!Number.isInteger(eventsRotateBytes) || eventsRotateBytes < 1) {
|
|
1337
|
+
missing.add(`ship.eventsRotateBytes must be a positive integer (got "${shipRaw.eventsRotateBytes}")`);
|
|
1338
|
+
}
|
|
929
1339
|
const maxThinkingTokens = shipRaw.agent?.maxThinkingTokens === void 0 ? DEFAULT_MAX_THINKING_TOKENS : Number(shipRaw.agent.maxThinkingTokens);
|
|
930
1340
|
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
931
1341
|
missing.add(`ship.agent.maxThinkingTokens must be a non-negative integer (got "${shipRaw.agent?.maxThinkingTokens}")`);
|
|
932
1342
|
}
|
|
1343
|
+
const toolchains = parseToolchains(shipRaw.toolchains, base, missing, file);
|
|
1344
|
+
const toolchainNames = /* @__PURE__ */ new Set([LEGACY_TOOLCHAIN, ...toolchains.map((x) => x.name)]);
|
|
1345
|
+
const shipToolchain = shipRaw.toolchain === void 0 ? void 0 : String(shipRaw.toolchain);
|
|
1346
|
+
if (shipToolchain !== void 0 && !toolchainNames.has(shipToolchain)) {
|
|
1347
|
+
missing.add(`ship.toolchain "${shipToolchain}" is not defined (have: ${[...toolchainNames].join(", ")})`);
|
|
1348
|
+
}
|
|
933
1349
|
const stateDir = expand(shipRaw.stateDir ?? "state", base);
|
|
934
1350
|
const routesRaw = raw.routes;
|
|
935
1351
|
if (!opts.allowNoRoutes && (!Array.isArray(routesRaw) || routesRaw.length === 0)) {
|
|
@@ -940,7 +1356,7 @@ Create the first of those:
|
|
|
940
1356
|
const where = `routes[${i}]`;
|
|
941
1357
|
const routeMissing = new Missing();
|
|
942
1358
|
try {
|
|
943
|
-
return [parseOneRoute(c, where, file, base, stateDir, raw, routeMissing, crewHome)];
|
|
1359
|
+
return [parseOneRoute(c, where, file, base, stateDir, raw, routeMissing, crewHome, toolchainNames)];
|
|
944
1360
|
} catch (e) {
|
|
945
1361
|
const label = typeof c?.route === "string" && c.route ? `"${c.route}"` : where;
|
|
946
1362
|
routeWarnings.push(`route ${label} dropped \u2014 ${e.message}`);
|
|
@@ -963,15 +1379,18 @@ Create the first of those:
|
|
|
963
1379
|
model: shipRaw.agent?.model ?? "claude-sonnet-5",
|
|
964
1380
|
maxThinkingTokens
|
|
965
1381
|
},
|
|
1382
|
+
...toolchains.length > 0 ? { toolchains } : {},
|
|
1383
|
+
...shipToolchain !== void 0 ? { toolchain: shipToolchain } : {},
|
|
966
1384
|
shell: shipRaw.shell,
|
|
967
1385
|
extraPath: shipRaw.extraPath,
|
|
968
1386
|
useNvm: shipRaw.useNvm !== false,
|
|
969
1387
|
nvmSh: shipRaw.nvmSh,
|
|
970
1388
|
stateDir,
|
|
971
|
-
logFile: expand(shipRaw.logFile ??
|
|
1389
|
+
logFile: expand(shipRaw.logFile ?? join4(tmpdir(), "crew.log"), base),
|
|
972
1390
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
973
1391
|
maxConcurrentAgents,
|
|
974
1392
|
streamRetentionDays,
|
|
1393
|
+
eventsRotateBytes,
|
|
975
1394
|
relayHost: shipRaw.relayHost ? String(shipRaw.relayHost) : void 0,
|
|
976
1395
|
// 2222 matches the relay's own RELAY_SSH_PORT default (crew-relay README).
|
|
977
1396
|
relayPort: shipRaw.relayPort !== void 0 ? Number(shipRaw.relayPort) : 2222,
|
|
@@ -985,8 +1404,70 @@ Create the first of those:
|
|
|
985
1404
|
warnings: routeWarnings
|
|
986
1405
|
};
|
|
987
1406
|
}
|
|
988
|
-
function
|
|
1407
|
+
function expandCommand(cmd, base) {
|
|
1408
|
+
return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
|
|
1409
|
+
}
|
|
1410
|
+
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "model", "tiers", "args", "env", "maxThinkingTokens"]);
|
|
1411
|
+
function parseToolchains(raw, base, missing, file) {
|
|
1412
|
+
if (raw === void 0 || raw === null) return [];
|
|
1413
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
1414
|
+
missing.add("ship.toolchains must be a mapping of name \u2192 toolchain");
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
const out = [];
|
|
1418
|
+
for (const [name, entryRaw] of Object.entries(raw)) {
|
|
1419
|
+
const where = `ship.toolchains.${name}`;
|
|
1420
|
+
if (!entryRaw || typeof entryRaw !== "object" || Array.isArray(entryRaw)) {
|
|
1421
|
+
missing.add(`${where} must be a mapping`);
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
rejectUnknownKeys(entryRaw, TOOLCHAIN_KEYS, where, file);
|
|
1425
|
+
const harness = String(entryRaw.harness ?? "");
|
|
1426
|
+
if (!isHarness(harness)) {
|
|
1427
|
+
missing.add(`${where}.harness must be one of ${HARNESS_NAMES.join("|")} (got "${harness}")`);
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const adapter = adapterFor(harness);
|
|
1431
|
+
const tiers = { ...adapter.defaultTiers };
|
|
1432
|
+
for (const [tier, v] of Object.entries(entryRaw.tiers ?? {})) {
|
|
1433
|
+
if (!TIERS.includes(tier)) {
|
|
1434
|
+
missing.add(`${where}.tiers.${tier} is not a tier (have: ${TIERS.join(", ")})`);
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
if (!v || typeof v.model !== "string" || !v.model) {
|
|
1438
|
+
missing.add(`${where}.tiers.${tier}.model is required`);
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
tiers[tier] = { model: v.model, ...v.reasoning !== void 0 ? { reasoning: String(v.reasoning) } : {} };
|
|
1442
|
+
}
|
|
1443
|
+
const args = entryRaw.args ?? [];
|
|
1444
|
+
if (!Array.isArray(args)) missing.add(`${where}.args must be a list of strings`);
|
|
1445
|
+
const env = entryRaw.env ?? {};
|
|
1446
|
+
if (typeof env !== "object" || Array.isArray(env)) missing.add(`${where}.env must be a mapping of name \u2192 value`);
|
|
1447
|
+
const maxThinkingTokens = entryRaw.maxThinkingTokens === void 0 ? DEFAULT_MAX_THINKING_TOKENS : Number(entryRaw.maxThinkingTokens);
|
|
1448
|
+
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
1449
|
+
missing.add(`${where}.maxThinkingTokens must be a non-negative integer (got "${entryRaw.maxThinkingTokens}")`);
|
|
1450
|
+
}
|
|
1451
|
+
out.push({
|
|
1452
|
+
name,
|
|
1453
|
+
harness,
|
|
1454
|
+
bin: expandCommand(String(entryRaw.bin ?? adapter.defaultBin), base),
|
|
1455
|
+
...entryRaw.launcher ? { launcher: expandCommand(String(entryRaw.launcher), base) } : {},
|
|
1456
|
+
model: String(entryRaw.model ?? tiers.standard?.model ?? adapter.defaultModel),
|
|
1457
|
+
tiers,
|
|
1458
|
+
args: Array.isArray(args) ? args.map(String) : [],
|
|
1459
|
+
env: Object.fromEntries(Object.entries(env).map(([k, v]) => [k, String(v)])),
|
|
1460
|
+
maxThinkingTokens
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
return out;
|
|
1464
|
+
}
|
|
1465
|
+
function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, toolchainNames) {
|
|
989
1466
|
rejectUnknownKeys(c, ROUTE_KEYS, `${where}`, file);
|
|
1467
|
+
const toolchain = c?.toolchain === void 0 ? void 0 : String(c.toolchain);
|
|
1468
|
+
if (toolchain !== void 0 && !toolchainNames.has(toolchain)) {
|
|
1469
|
+
missing.add(`${where}.toolchain "${toolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
|
|
1470
|
+
}
|
|
990
1471
|
const route = missing.req(c?.route, `${where}.route`);
|
|
991
1472
|
const routeParts = route ? route.split("/") : [];
|
|
992
1473
|
const routeWellFormed = routeParts.length === 2 && routeParts.every(Boolean);
|
|
@@ -1035,6 +1516,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome) {
|
|
|
1035
1516
|
reposBasePath: expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base),
|
|
1036
1517
|
repoOverrides,
|
|
1037
1518
|
worktreePrefix: c?.worktreePrefix ? String(c.worktreePrefix) : void 0,
|
|
1519
|
+
...toolchain !== void 0 ? { toolchain } : {},
|
|
1038
1520
|
promptsDir: resolvePromptsDir(c?.promptSet ? String(c.promptSet) : void 0, crewHome, base),
|
|
1039
1521
|
weight,
|
|
1040
1522
|
baseUrl: String(c?.baseUrl ?? raw.ship?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
@@ -1102,8 +1584,8 @@ function repoIdForName(r, name) {
|
|
|
1102
1584
|
}
|
|
1103
1585
|
function resolveApiKey(r) {
|
|
1104
1586
|
if (r.apiKey) return r.apiKey;
|
|
1105
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
1106
|
-
for (const line of
|
|
1587
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync3(r.apiKeyFile)) {
|
|
1588
|
+
for (const line of readFileSync4(r.apiKeyFile, "utf8").split("\n")) {
|
|
1107
1589
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
1108
1590
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
1109
1591
|
}
|
|
@@ -1166,7 +1648,7 @@ function configuredMembers(rt) {
|
|
|
1166
1648
|
return out;
|
|
1167
1649
|
}
|
|
1168
1650
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
1169
|
-
return
|
|
1651
|
+
return join4(basePath, workspace, repoName);
|
|
1170
1652
|
}
|
|
1171
1653
|
function reposOf(r) {
|
|
1172
1654
|
const named = Object.entries(r.repos);
|
|
@@ -1190,7 +1672,7 @@ function passengerRepoTargets(r) {
|
|
|
1190
1672
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
1191
1673
|
}
|
|
1192
1674
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
1193
|
-
return
|
|
1675
|
+
return join4(basePath, workspace, ".passenger-checkouts", repoName);
|
|
1194
1676
|
}
|
|
1195
1677
|
function passengerCheckoutTargets(r) {
|
|
1196
1678
|
if (!r.hostPassengers) return [];
|
|
@@ -1322,17 +1804,17 @@ function rosterMarkdown(roster, meId) {
|
|
|
1322
1804
|
}
|
|
1323
1805
|
|
|
1324
1806
|
// src/state.ts
|
|
1325
|
-
import { readFileSync as
|
|
1326
|
-
import { join as
|
|
1807
|
+
import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
1808
|
+
import { join as join5 } from "path";
|
|
1327
1809
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
1328
1810
|
var State = class _State {
|
|
1329
1811
|
dir;
|
|
1330
1812
|
constructor(dir) {
|
|
1331
1813
|
this.dir = dir;
|
|
1332
|
-
|
|
1814
|
+
mkdirSync2(dir, { recursive: true });
|
|
1333
1815
|
}
|
|
1334
1816
|
path(...parts) {
|
|
1335
|
-
return
|
|
1817
|
+
return join5(this.dir, ...parts);
|
|
1336
1818
|
}
|
|
1337
1819
|
/**
|
|
1338
1820
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -1343,7 +1825,7 @@ var State = class _State {
|
|
|
1343
1825
|
*/
|
|
1344
1826
|
watermark() {
|
|
1345
1827
|
try {
|
|
1346
|
-
const v =
|
|
1828
|
+
const v = readFileSync5(this.path(".poll-watermark"), "utf8").trim();
|
|
1347
1829
|
return v || EPOCH;
|
|
1348
1830
|
} catch {
|
|
1349
1831
|
return EPOCH;
|
|
@@ -1368,10 +1850,10 @@ var State = class _State {
|
|
|
1368
1850
|
}
|
|
1369
1851
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
1370
1852
|
isPaused() {
|
|
1371
|
-
return
|
|
1853
|
+
return existsSync4(this.path(".crew-paused"));
|
|
1372
1854
|
}
|
|
1373
1855
|
isRolePaused(role) {
|
|
1374
|
-
return
|
|
1856
|
+
return existsSync4(this.path(`.role-paused-${role}`));
|
|
1375
1857
|
}
|
|
1376
1858
|
pausedRoles(roles) {
|
|
1377
1859
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
@@ -1380,7 +1862,7 @@ var State = class _State {
|
|
|
1380
1862
|
writeFileSync(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
1381
1863
|
}
|
|
1382
1864
|
resume(role) {
|
|
1383
|
-
|
|
1865
|
+
rmSync2(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
1384
1866
|
}
|
|
1385
1867
|
/**
|
|
1386
1868
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -1388,11 +1870,11 @@ var State = class _State {
|
|
|
1388
1870
|
* while agents keep working.
|
|
1389
1871
|
*/
|
|
1390
1872
|
isReleasePaused() {
|
|
1391
|
-
return
|
|
1873
|
+
return existsSync4(this.path(".release-paused"));
|
|
1392
1874
|
}
|
|
1393
1875
|
setReleasePaused(paused) {
|
|
1394
1876
|
if (paused) writeFileSync(this.path(".release-paused"), "");
|
|
1395
|
-
else
|
|
1877
|
+
else rmSync2(this.path(".release-paused"), { force: true });
|
|
1396
1878
|
}
|
|
1397
1879
|
/**
|
|
1398
1880
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -1429,7 +1911,7 @@ var State = class _State {
|
|
|
1429
1911
|
const name = _State.safe(rawName);
|
|
1430
1912
|
const file = this.path(`.${name}.lock`);
|
|
1431
1913
|
try {
|
|
1432
|
-
const existing = Number.parseInt(
|
|
1914
|
+
const existing = Number.parseInt(readFileSync5(file, "utf8").trim(), 10);
|
|
1433
1915
|
if (Number.isFinite(existing) && existing > 0) {
|
|
1434
1916
|
try {
|
|
1435
1917
|
process.kill(existing, 0);
|
|
@@ -1445,7 +1927,7 @@ var State = class _State {
|
|
|
1445
1927
|
ok: true,
|
|
1446
1928
|
release: () => {
|
|
1447
1929
|
try {
|
|
1448
|
-
|
|
1930
|
+
rmSync2(file, { force: true });
|
|
1449
1931
|
} catch {
|
|
1450
1932
|
}
|
|
1451
1933
|
}
|
|
@@ -1523,7 +2005,7 @@ var State = class _State {
|
|
|
1523
2005
|
const heldReported = `.release-held-${suffix}`;
|
|
1524
2006
|
const read = (f) => {
|
|
1525
2007
|
try {
|
|
1526
|
-
return
|
|
2008
|
+
return readFileSync5(this.path(f), "utf8").trim() || null;
|
|
1527
2009
|
} catch {
|
|
1528
2010
|
return null;
|
|
1529
2011
|
}
|
|
@@ -1537,7 +2019,7 @@ var State = class _State {
|
|
|
1537
2019
|
return n;
|
|
1538
2020
|
},
|
|
1539
2021
|
clearBlocked: () => {
|
|
1540
|
-
|
|
2022
|
+
rmSync2(this.path(blocked), { force: true });
|
|
1541
2023
|
},
|
|
1542
2024
|
/**
|
|
1543
2025
|
* The commit a deploy last failed on.
|
|
@@ -1553,7 +2035,7 @@ var State = class _State {
|
|
|
1553
2035
|
`);
|
|
1554
2036
|
},
|
|
1555
2037
|
clearDeployFailed: () => {
|
|
1556
|
-
|
|
2038
|
+
rmSync2(this.path(failed), { force: true });
|
|
1557
2039
|
},
|
|
1558
2040
|
/**
|
|
1559
2041
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -1568,12 +2050,12 @@ var State = class _State {
|
|
|
1568
2050
|
return next;
|
|
1569
2051
|
},
|
|
1570
2052
|
clearTestGateFailed: () => {
|
|
1571
|
-
|
|
2053
|
+
rmSync2(this.path(gateFailed), { force: true });
|
|
1572
2054
|
},
|
|
1573
2055
|
/** Whether the board was already told about this head's red gate. */
|
|
1574
2056
|
heldReported: () => {
|
|
1575
2057
|
try {
|
|
1576
|
-
return
|
|
2058
|
+
return readFileSync5(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
1577
2059
|
} catch {
|
|
1578
2060
|
return null;
|
|
1579
2061
|
}
|
|
@@ -1583,7 +2065,7 @@ var State = class _State {
|
|
|
1583
2065
|
`);
|
|
1584
2066
|
},
|
|
1585
2067
|
clearHeldReported: () => {
|
|
1586
|
-
|
|
2068
|
+
rmSync2(this.path(heldReported), { force: true });
|
|
1587
2069
|
},
|
|
1588
2070
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
1589
2071
|
noteTestGateReported: (sha) => {
|
|
@@ -1610,13 +2092,13 @@ var State = class _State {
|
|
|
1610
2092
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
1611
2093
|
const read = (f) => {
|
|
1612
2094
|
try {
|
|
1613
|
-
return
|
|
2095
|
+
return readFileSync5(this.path(f), "utf8").trim() || null;
|
|
1614
2096
|
} catch {
|
|
1615
2097
|
return null;
|
|
1616
2098
|
}
|
|
1617
2099
|
};
|
|
1618
2100
|
const clear2 = () => {
|
|
1619
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
2101
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync2(this.path(f), { force: true });
|
|
1620
2102
|
};
|
|
1621
2103
|
return {
|
|
1622
2104
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -1675,7 +2157,7 @@ var State = class _State {
|
|
|
1675
2157
|
return {
|
|
1676
2158
|
previous: () => {
|
|
1677
2159
|
try {
|
|
1678
|
-
const parsed = JSON.parse(
|
|
2160
|
+
const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
|
|
1679
2161
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1680
2162
|
} catch {
|
|
1681
2163
|
return {};
|
|
@@ -1684,153 +2166,57 @@ var State = class _State {
|
|
|
1684
2166
|
persist: (next) => {
|
|
1685
2167
|
writeFileSync(this.path(file), JSON.stringify(next));
|
|
1686
2168
|
}
|
|
1687
|
-
};
|
|
1688
|
-
}
|
|
1689
|
-
/**
|
|
1690
|
-
* Open ship-level attention items (CREW-1373), ship-wide rather than
|
|
1691
|
-
* per-route: a parked role or a missing hook command is about this
|
|
1692
|
-
* machine. A corrupt or missing file reads as empty.
|
|
1693
|
-
*/
|
|
1694
|
-
shipAttention() {
|
|
1695
|
-
const file = ".ship-attention.json";
|
|
1696
|
-
return {
|
|
1697
|
-
previous: () => {
|
|
1698
|
-
try {
|
|
1699
|
-
const parsed = JSON.parse(readFileSync3(this.path(file), "utf8"));
|
|
1700
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
1701
|
-
} catch {
|
|
1702
|
-
return [];
|
|
1703
|
-
}
|
|
1704
|
-
},
|
|
1705
|
-
persist: (next) => {
|
|
1706
|
-
writeFileSync(this.path(file), JSON.stringify(next));
|
|
1707
|
-
}
|
|
1708
|
-
};
|
|
1709
|
-
}
|
|
1710
|
-
/**
|
|
1711
|
-
* When the release phase last ran a cycle (CREW-1373), as the mtime of a
|
|
1712
|
-
* marker file. `touch()` is called at the start and the end of every cycle
|
|
1713
|
-
* so a long deploy does not read as a stopped timer.
|
|
1714
|
-
*/
|
|
1715
|
-
releaseHeartbeat() {
|
|
1716
|
-
const file = ".release-last-run";
|
|
1717
|
-
return {
|
|
1718
|
-
touch: () => {
|
|
1719
|
-
writeFileSync(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
1720
|
-
},
|
|
1721
|
-
at: () => {
|
|
1722
|
-
try {
|
|
1723
|
-
return statSync(this.path(file)).mtimeMs;
|
|
1724
|
-
} catch {
|
|
1725
|
-
return null;
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
readInt(file) {
|
|
1731
|
-
try {
|
|
1732
|
-
return Number.parseInt(readFileSync3(this.path(file), "utf8").trim(), 10) || 0;
|
|
1733
|
-
} catch {
|
|
1734
|
-
return 0;
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
};
|
|
1738
|
-
|
|
1739
|
-
// src/events.ts
|
|
1740
|
-
import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1741
|
-
import { join as join4, dirname as dirname2 } from "path";
|
|
1742
|
-
function render(e) {
|
|
1743
|
-
const scope = e.role ? `${e.step}[${e.role}]` : e.step;
|
|
1744
|
-
const ticket = e.ticket ? ` ${e.ticket}` : "";
|
|
1745
|
-
const level = e.level === "info" ? "" : `${e.level.toUpperCase()}: `;
|
|
1746
|
-
return `${e.at} ${scope}:${ticket} ${level}${e.message}`;
|
|
1747
|
-
}
|
|
1748
|
-
var Emitter = class _Emitter {
|
|
1749
|
-
cycle;
|
|
1750
|
-
opts;
|
|
1751
|
-
now;
|
|
1752
|
-
step = "poll";
|
|
1753
|
-
role;
|
|
1754
|
-
constructor(opts) {
|
|
1755
|
-
this.opts = opts;
|
|
1756
|
-
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
1757
|
-
this.cycle = opts.cycleId ?? this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
1758
|
-
for (const f of [opts.eventFile, opts.logFile]) {
|
|
1759
|
-
if (!f) continue;
|
|
1760
|
-
try {
|
|
1761
|
-
mkdirSync2(dirname2(f), { recursive: true });
|
|
1762
|
-
} catch {
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
/** Everything emitted after this belongs to `step`, until the next one. */
|
|
1767
|
-
enter(step, role) {
|
|
1768
|
-
this.step = step;
|
|
1769
|
-
this.role = role;
|
|
1770
|
-
}
|
|
1771
|
-
emit(message, extra = {}) {
|
|
1772
|
-
const e = {
|
|
1773
|
-
at: this.now().toISOString(),
|
|
1774
|
-
cycle: this.cycle,
|
|
1775
|
-
route: this.opts.route,
|
|
1776
|
-
step: extra.step ?? this.step,
|
|
1777
|
-
level: extra.level ?? "info",
|
|
1778
|
-
message,
|
|
1779
|
-
...extra.role ?? this.role ? { role: extra.role ?? this.role } : {},
|
|
1780
|
-
...extra.ticket ? { ticket: extra.ticket } : {},
|
|
1781
|
-
...extra.data ? { data: extra.data } : {}
|
|
1782
|
-
};
|
|
1783
|
-
const line = render(e);
|
|
1784
|
-
try {
|
|
1785
|
-
if (this.opts.eventFile) appendFileSync(this.opts.eventFile, `${JSON.stringify(e)}
|
|
1786
|
-
`);
|
|
1787
|
-
} catch {
|
|
1788
|
-
}
|
|
1789
|
-
try {
|
|
1790
|
-
if (this.opts.logFile) appendFileSync(this.opts.logFile, `${line}
|
|
1791
|
-
`);
|
|
1792
|
-
} catch {
|
|
1793
|
-
}
|
|
1794
|
-
try {
|
|
1795
|
-
(this.opts.console ?? ((l) => process.stderr.write(`${l}
|
|
1796
|
-
`)))(line);
|
|
1797
|
-
} catch {
|
|
1798
|
-
}
|
|
1799
|
-
return e;
|
|
1800
|
-
}
|
|
1801
|
-
warn(message, extra = {}) {
|
|
1802
|
-
return this.emit(message, { ...extra, level: "warn" });
|
|
2169
|
+
};
|
|
1803
2170
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
2171
|
+
/**
|
|
2172
|
+
* Open ship-level attention items (CREW-1373), ship-wide rather than
|
|
2173
|
+
* per-route: a parked role or a missing hook command is about this
|
|
2174
|
+
* machine. A corrupt or missing file reads as empty.
|
|
2175
|
+
*/
|
|
2176
|
+
shipAttention() {
|
|
2177
|
+
const file = ".ship-attention.json";
|
|
2178
|
+
return {
|
|
2179
|
+
previous: () => {
|
|
2180
|
+
try {
|
|
2181
|
+
const parsed = JSON.parse(readFileSync5(this.path(file), "utf8"));
|
|
2182
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2183
|
+
} catch {
|
|
2184
|
+
return [];
|
|
2185
|
+
}
|
|
2186
|
+
},
|
|
2187
|
+
persist: (next) => {
|
|
2188
|
+
writeFileSync(this.path(file), JSON.stringify(next));
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
1806
2191
|
}
|
|
1807
2192
|
/**
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1811
|
-
* actually knows which route it's working (release, per-repo poll) uses
|
|
1812
|
-
* this instead, so its events aren't all attributed to `routes[0]`.
|
|
2193
|
+
* When the release phase last ran a cycle (CREW-1373), as the mtime of a
|
|
2194
|
+
* marker file. `touch()` is called at the start and the end of every cycle
|
|
2195
|
+
* so a long deploy does not read as a stopped timer.
|
|
1813
2196
|
*/
|
|
1814
|
-
|
|
1815
|
-
|
|
2197
|
+
releaseHeartbeat() {
|
|
2198
|
+
const file = ".release-last-run";
|
|
2199
|
+
return {
|
|
2200
|
+
touch: () => {
|
|
2201
|
+
writeFileSync(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
2202
|
+
},
|
|
2203
|
+
at: () => {
|
|
2204
|
+
try {
|
|
2205
|
+
return statSync2(this.path(file)).mtimeMs;
|
|
2206
|
+
} catch {
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
1816
2211
|
}
|
|
1817
|
-
|
|
1818
|
-
async timed(step, message, fn, role) {
|
|
1819
|
-
this.enter(step, role);
|
|
1820
|
-
const started = this.now().getTime();
|
|
2212
|
+
readInt(file) {
|
|
1821
2213
|
try {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
return
|
|
1825
|
-
} catch (err) {
|
|
1826
|
-
this.error(`${message} \u2014 ${err.message}`, {
|
|
1827
|
-
data: { ms: this.now().getTime() - started, ok: false }
|
|
1828
|
-
});
|
|
1829
|
-
throw err;
|
|
2214
|
+
return Number.parseInt(readFileSync5(this.path(file), "utf8").trim(), 10) || 0;
|
|
2215
|
+
} catch {
|
|
2216
|
+
return 0;
|
|
1830
2217
|
}
|
|
1831
2218
|
}
|
|
1832
2219
|
};
|
|
1833
|
-
var eventFileFor = (stateDir) => join4(stateDir, "events.jsonl");
|
|
1834
2220
|
|
|
1835
2221
|
// src/tracker.ts
|
|
1836
2222
|
import { hostname } from "os";
|
|
@@ -2129,10 +2515,10 @@ function describeRelease(o, route) {
|
|
|
2129
2515
|
}
|
|
2130
2516
|
|
|
2131
2517
|
// src/install.ts
|
|
2132
|
-
import { dirname as dirname3, join as
|
|
2133
|
-
import { homedir as
|
|
2134
|
-
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as
|
|
2135
|
-
import { spawnSync } from "child_process";
|
|
2518
|
+
import { dirname as dirname3, join as join6, extname } from "path";
|
|
2519
|
+
import { homedir as homedir3 } from "os";
|
|
2520
|
+
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
|
|
2521
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2136
2522
|
import { createHash } from "crypto";
|
|
2137
2523
|
|
|
2138
2524
|
// src/runtime-info.ts
|
|
@@ -2143,7 +2529,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
2143
2529
|
// src/install.ts
|
|
2144
2530
|
function crewInvocation(crewHome) {
|
|
2145
2531
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
2146
|
-
return [process.execPath,
|
|
2532
|
+
return [process.execPath, join6(crewHome, "bin", "crew")];
|
|
2147
2533
|
}
|
|
2148
2534
|
var INTERVAL_SECONDS = 120;
|
|
2149
2535
|
function labelFor(crewHome, job) {
|
|
@@ -2181,17 +2567,17 @@ function pathFor(ship) {
|
|
|
2181
2567
|
return parts.join(":");
|
|
2182
2568
|
}
|
|
2183
2569
|
function findOnPath(bin, searchPath) {
|
|
2184
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
2570
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join6(dir, bin));
|
|
2185
2571
|
for (const candidate of candidates) {
|
|
2186
2572
|
try {
|
|
2187
2573
|
accessSync(candidate, constants.X_OK);
|
|
2188
|
-
if (
|
|
2574
|
+
if (statSync3(candidate).isFile()) return candidate;
|
|
2189
2575
|
} catch {
|
|
2190
2576
|
}
|
|
2191
2577
|
}
|
|
2192
2578
|
return void 0;
|
|
2193
2579
|
}
|
|
2194
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
2580
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join6(homedir3(), ".docker", "bin")].join(":");
|
|
2195
2581
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
2196
2582
|
const schedulerPath = pathFor(ship);
|
|
2197
2583
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -2203,7 +2589,7 @@ function xmlEscape(s) {
|
|
|
2203
2589
|
}
|
|
2204
2590
|
function planLaunchd(ship, crewHome, job) {
|
|
2205
2591
|
const label = labelFor(crewHome, job);
|
|
2206
|
-
const unitPath =
|
|
2592
|
+
const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2207
2593
|
const crewLog = ship.logFile;
|
|
2208
2594
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2209
2595
|
const persistent = job === "run";
|
|
@@ -2255,8 +2641,8 @@ ${scheduleKeys}
|
|
|
2255
2641
|
}
|
|
2256
2642
|
function planSystemd(ship, crewHome, job) {
|
|
2257
2643
|
const label = labelFor(crewHome, job);
|
|
2258
|
-
const unitDir =
|
|
2259
|
-
const servicePath =
|
|
2644
|
+
const unitDir = join6(homedir3(), ".config", "systemd", "user");
|
|
2645
|
+
const servicePath = join6(unitDir, `${label}.service`);
|
|
2260
2646
|
const crewLog = ship.logFile;
|
|
2261
2647
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
2262
2648
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -2293,7 +2679,7 @@ WantedBy=default.target
|
|
|
2293
2679
|
schedulerLog
|
|
2294
2680
|
};
|
|
2295
2681
|
}
|
|
2296
|
-
const timerPath =
|
|
2682
|
+
const timerPath = join6(unitDir, `${label}.timer`);
|
|
2297
2683
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
2298
2684
|
[Unit]
|
|
2299
2685
|
Description=${description}
|
|
@@ -2360,7 +2746,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2360
2746
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
2361
2747
|
const label = labelFor(crewHome, job);
|
|
2362
2748
|
if (hostShip === "macos") {
|
|
2363
|
-
const unitPath =
|
|
2749
|
+
const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2364
2750
|
return {
|
|
2365
2751
|
mechanism: "launchd",
|
|
2366
2752
|
unitPaths: [unitPath],
|
|
@@ -2370,18 +2756,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2370
2756
|
};
|
|
2371
2757
|
}
|
|
2372
2758
|
if (hostShip === "linux" && hasSystemd) {
|
|
2373
|
-
const unitDir =
|
|
2759
|
+
const unitDir = join6(homedir3(), ".config", "systemd", "user");
|
|
2374
2760
|
if (job === "run") {
|
|
2375
2761
|
return {
|
|
2376
2762
|
mechanism: "systemd",
|
|
2377
|
-
unitPaths: [
|
|
2763
|
+
unitPaths: [join6(unitDir, `${label}.service`)],
|
|
2378
2764
|
crontabLine: null,
|
|
2379
2765
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
2380
2766
|
};
|
|
2381
2767
|
}
|
|
2382
2768
|
return {
|
|
2383
2769
|
mechanism: "systemd",
|
|
2384
|
-
unitPaths: [
|
|
2770
|
+
unitPaths: [join6(unitDir, `${label}.service`), join6(unitDir, `${label}.timer`)],
|
|
2385
2771
|
crontabLine: null,
|
|
2386
2772
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
2387
2773
|
};
|
|
@@ -2399,7 +2785,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2399
2785
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
2400
2786
|
const label = labelFor(crewHome, "run");
|
|
2401
2787
|
if (hostShip === "macos") {
|
|
2402
|
-
const unitPath =
|
|
2788
|
+
const unitPath = join6(homedir3(), "Library", "LaunchAgents", `${label}.plist`);
|
|
2403
2789
|
return {
|
|
2404
2790
|
mechanism: "launchd",
|
|
2405
2791
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -2423,7 +2809,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
|
2423
2809
|
}
|
|
2424
2810
|
function runAll(commands, cwd, log) {
|
|
2425
2811
|
for (const [bin, ...args] of commands) {
|
|
2426
|
-
const res =
|
|
2812
|
+
const res = spawnSync2(bin, args, { cwd, encoding: "utf8" });
|
|
2427
2813
|
if (res.error || res.status !== 0) {
|
|
2428
2814
|
log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
|
|
2429
2815
|
output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
|
|
@@ -2488,7 +2874,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2488
2874
|
if (dryRun) {
|
|
2489
2875
|
log.emit(`would run: launchctl remove ${plan.removeLabel}`);
|
|
2490
2876
|
} else {
|
|
2491
|
-
if (
|
|
2877
|
+
if (spawnSync2("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
|
|
2492
2878
|
log.emit(`removed loaded job ${plan.removeLabel}`);
|
|
2493
2879
|
}
|
|
2494
2880
|
}
|
|
@@ -2513,7 +2899,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2513
2899
|
}
|
|
2514
2900
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
2515
2901
|
function launchAgentsDir() {
|
|
2516
|
-
return
|
|
2902
|
+
return join6(homedir3(), "Library", "LaunchAgents");
|
|
2517
2903
|
}
|
|
2518
2904
|
function xmlUnescape(s) {
|
|
2519
2905
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -2525,17 +2911,17 @@ function programFromPlist(text) {
|
|
|
2525
2911
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
2526
2912
|
}
|
|
2527
2913
|
function loadedCrewLabels() {
|
|
2528
|
-
const list =
|
|
2914
|
+
const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
|
|
2529
2915
|
if (list.status !== 0) return [];
|
|
2530
2916
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
2531
2917
|
}
|
|
2532
2918
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
2533
2919
|
const files = existsSync5(dir) ? readdirSync(dir) : [];
|
|
2534
2920
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
2535
|
-
const plistPath =
|
|
2921
|
+
const plistPath = join6(dir, f);
|
|
2536
2922
|
let program = null;
|
|
2537
2923
|
try {
|
|
2538
|
-
program = programFromPlist(
|
|
2924
|
+
program = programFromPlist(readFileSync6(plistPath, "utf8"));
|
|
2539
2925
|
} catch {
|
|
2540
2926
|
}
|
|
2541
2927
|
return {
|
|
@@ -2548,7 +2934,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
|
2548
2934
|
const seen = new Set(units.map((u) => u.label));
|
|
2549
2935
|
for (const label of [...loaded].sort()) {
|
|
2550
2936
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
2551
|
-
units.push({ label, plistPath:
|
|
2937
|
+
units.push({ label, plistPath: join6(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
2552
2938
|
}
|
|
2553
2939
|
return units;
|
|
2554
2940
|
}
|
|
@@ -2561,8 +2947,8 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
2561
2947
|
log.emit(`would unload and remove ${unit.label}`);
|
|
2562
2948
|
return;
|
|
2563
2949
|
}
|
|
2564
|
-
if (existsSync5(unit.plistPath))
|
|
2565
|
-
|
|
2950
|
+
if (existsSync5(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
2951
|
+
spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
2566
2952
|
if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
2567
2953
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
2568
2954
|
}
|
|
@@ -2595,7 +2981,7 @@ function parseLaunchctlList(text) {
|
|
|
2595
2981
|
return jobs;
|
|
2596
2982
|
}
|
|
2597
2983
|
function describeForeignLaunchdUnits(crewHome, dir) {
|
|
2598
|
-
const list =
|
|
2984
|
+
const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
|
|
2599
2985
|
const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
|
|
2600
2986
|
const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
|
|
2601
2987
|
return foreign.map((u) => {
|
|
@@ -2608,11 +2994,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
|
|
|
2608
2994
|
});
|
|
2609
2995
|
}
|
|
2610
2996
|
function readCrontab() {
|
|
2611
|
-
const res =
|
|
2997
|
+
const res = spawnSync2("crontab", ["-l"], { encoding: "utf8" });
|
|
2612
2998
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
2613
2999
|
}
|
|
2614
3000
|
function writeCrontab(text) {
|
|
2615
|
-
|
|
3001
|
+
spawnSync2("crontab", ["-"], { input: text, encoding: "utf8" });
|
|
2616
3002
|
}
|
|
2617
3003
|
function addCrontabLine(marker, line) {
|
|
2618
3004
|
const existing = readCrontab();
|
|
@@ -2636,7 +3022,7 @@ function removeCrontabLine(marker) {
|
|
|
2636
3022
|
}
|
|
2637
3023
|
function detectSystemd() {
|
|
2638
3024
|
if (process.platform !== "linux") return false;
|
|
2639
|
-
const res =
|
|
3025
|
+
const res = spawnSync2("systemctl", ["--user", "status"], { encoding: "utf8" });
|
|
2640
3026
|
return !res.error;
|
|
2641
3027
|
}
|
|
2642
3028
|
|
|
@@ -2648,10 +3034,10 @@ import { execFileSync } from "child_process";
|
|
|
2648
3034
|
import {
|
|
2649
3035
|
existsSync as existsSync6,
|
|
2650
3036
|
mkdirSync as mkdirSync4,
|
|
2651
|
-
openSync,
|
|
2652
|
-
closeSync,
|
|
3037
|
+
openSync as openSync2,
|
|
3038
|
+
closeSync as closeSync2,
|
|
2653
3039
|
writeSync,
|
|
2654
|
-
statSync as
|
|
3040
|
+
statSync as statSync4,
|
|
2655
3041
|
unlinkSync as unlinkSync2
|
|
2656
3042
|
} from "fs";
|
|
2657
3043
|
import { dirname as dirname4, basename as basename3 } from "path";
|
|
@@ -3047,10 +3433,10 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
3047
3433
|
const deadline = Date.now() + timeoutMs;
|
|
3048
3434
|
for (; ; ) {
|
|
3049
3435
|
try {
|
|
3050
|
-
const fd =
|
|
3436
|
+
const fd = openSync2(lockPath, "wx");
|
|
3051
3437
|
writeSync(fd, `${process.pid}
|
|
3052
3438
|
`);
|
|
3053
|
-
|
|
3439
|
+
closeSync2(fd);
|
|
3054
3440
|
return () => {
|
|
3055
3441
|
try {
|
|
3056
3442
|
unlinkSync2(lockPath);
|
|
@@ -3061,7 +3447,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
3061
3447
|
const err = e;
|
|
3062
3448
|
if (err.code !== "EEXIST") throw err;
|
|
3063
3449
|
try {
|
|
3064
|
-
if (Date.now() -
|
|
3450
|
+
if (Date.now() - statSync4(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
|
|
3065
3451
|
try {
|
|
3066
3452
|
unlinkSync2(lockPath);
|
|
3067
3453
|
} catch {
|
|
@@ -4756,7 +5142,7 @@ function materializeBranchForTicket(dir, cfg, t, role) {
|
|
|
4756
5142
|
|
|
4757
5143
|
// src/poll.ts
|
|
4758
5144
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4759
|
-
import { join as
|
|
5145
|
+
import { join as join7 } from "path";
|
|
4760
5146
|
|
|
4761
5147
|
// src/stalled.ts
|
|
4762
5148
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -5028,7 +5414,7 @@ async function decideCycle(o) {
|
|
|
5028
5414
|
}
|
|
5029
5415
|
return decision;
|
|
5030
5416
|
}
|
|
5031
|
-
var digestPath = (stateDir, route, role) =>
|
|
5417
|
+
var digestPath = (stateDir, route, role) => join7(stateDir, `digest-${route}-${role}.md`);
|
|
5032
5418
|
function writeDigest(o, d, role, stateDir) {
|
|
5033
5419
|
const me = o.route.resolved?.seats[role];
|
|
5034
5420
|
if (!me) return false;
|
|
@@ -5398,9 +5784,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
5398
5784
|
|
|
5399
5785
|
// src/agent.ts
|
|
5400
5786
|
import { spawn as spawn2 } from "child_process";
|
|
5401
|
-
import { readFileSync as
|
|
5787
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7, statSync as statSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
|
|
5402
5788
|
import { finished } from "stream/promises";
|
|
5403
|
-
import { join as
|
|
5789
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
5404
5790
|
import { createHash as createHash2 } from "crypto";
|
|
5405
5791
|
|
|
5406
5792
|
// src/environment.ts
|
|
@@ -5685,54 +6071,6 @@ function renderEnvironment(i) {
|
|
|
5685
6071
|
return lines.join("\n");
|
|
5686
6072
|
}
|
|
5687
6073
|
|
|
5688
|
-
// src/stream.ts
|
|
5689
|
-
function toolTarget(name, input) {
|
|
5690
|
-
if (!input) return void 0;
|
|
5691
|
-
for (const key of ["file_path", "path", "notebook_path", "url", "pattern"]) {
|
|
5692
|
-
const v = input[key];
|
|
5693
|
-
if (typeof v === "string" && v) return v;
|
|
5694
|
-
}
|
|
5695
|
-
if (name === "Bash" && typeof input.command === "string") {
|
|
5696
|
-
return input.command.trim().split(/\s+/)[0];
|
|
5697
|
-
}
|
|
5698
|
-
return void 0;
|
|
5699
|
-
}
|
|
5700
|
-
function mapStreamLine(line) {
|
|
5701
|
-
const obj = line;
|
|
5702
|
-
if (!obj || obj.type !== "assistant") return [];
|
|
5703
|
-
const content = obj.message?.content;
|
|
5704
|
-
if (!Array.isArray(content)) return [];
|
|
5705
|
-
const out = [];
|
|
5706
|
-
for (const block of content) {
|
|
5707
|
-
if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
|
|
5708
|
-
else if (block.type === "text") out.push({ kind: "text", text: block.text });
|
|
5709
|
-
else if (block.type === "tool_use") {
|
|
5710
|
-
out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
|
|
5711
|
-
}
|
|
5712
|
-
}
|
|
5713
|
-
return out;
|
|
5714
|
-
}
|
|
5715
|
-
function extractTurnTokens(line) {
|
|
5716
|
-
const obj = line;
|
|
5717
|
-
if (!obj || obj.type !== "assistant") return void 0;
|
|
5718
|
-
const usage2 = obj.message?.usage;
|
|
5719
|
-
if (!usage2) return void 0;
|
|
5720
|
-
return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
|
|
5721
|
-
}
|
|
5722
|
-
function extractResult(line) {
|
|
5723
|
-
const obj = line;
|
|
5724
|
-
if (!obj || obj.type !== "result") return void 0;
|
|
5725
|
-
return {
|
|
5726
|
-
sessionId: obj.session_id,
|
|
5727
|
-
numTurns: obj.num_turns,
|
|
5728
|
-
totalCostUsd: obj.total_cost_usd,
|
|
5729
|
-
inputTokens: obj.usage?.input_tokens,
|
|
5730
|
-
outputTokens: obj.usage?.output_tokens,
|
|
5731
|
-
cacheReadTokens: obj.usage?.cache_read_input_tokens,
|
|
5732
|
-
cacheCreationTokens: obj.usage?.cache_creation_input_tokens
|
|
5733
|
-
};
|
|
5734
|
-
}
|
|
5735
|
-
|
|
5736
6074
|
// src/agent-log.ts
|
|
5737
6075
|
async function post(target, path, body) {
|
|
5738
6076
|
const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
|
|
@@ -5814,36 +6152,10 @@ var TicketAttributionTracker = class {
|
|
|
5814
6152
|
|
|
5815
6153
|
// src/agent.ts
|
|
5816
6154
|
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
if (role === "triage") return ["Bash", "Read", tablation];
|
|
5820
|
-
if (role === "qa") return ["Bash", "Read", "Write", "Grep", "Glob", tablation];
|
|
5821
|
-
const base = ["Bash", "Read", "Edit", "Write", "Grep", "Glob", tablation];
|
|
5822
|
-
return role === "design" ? [...base, "Skill", "Artifact"] : base;
|
|
5823
|
-
}
|
|
5824
|
-
var DISALLOWED_TOOLS = [
|
|
5825
|
-
"Bash(rm -rf:*)",
|
|
5826
|
-
"Bash(rm -fr:*)",
|
|
5827
|
-
"Bash(git push --force*)",
|
|
5828
|
-
"Bash(git push -f*)",
|
|
5829
|
-
"Bash(curl*| sh)",
|
|
5830
|
-
"Bash(curl*|sh)",
|
|
5831
|
-
"Bash(curl*| bash)",
|
|
5832
|
-
"Bash(curl*|bash)",
|
|
5833
|
-
"Bash(sudo:*)"
|
|
5834
|
-
];
|
|
5835
|
-
var BILLING_VARS_TO_UNSET = [
|
|
5836
|
-
"ANTHROPIC_API_KEY",
|
|
5837
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
5838
|
-
"CLAUDE_CODE_USE_VERTEX",
|
|
5839
|
-
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
5840
|
-
"ANTHROPIC_VERTEX_REGION",
|
|
5841
|
-
"CLOUD_ML_REGION",
|
|
5842
|
-
"ANTHROPIC_MODEL"
|
|
5843
|
-
];
|
|
5844
|
-
function scrubbedEnv(env = process.env) {
|
|
6155
|
+
var LEGACY_TOOLCHAIN_HARNESS = "claude";
|
|
6156
|
+
function scrubbedEnv(env = process.env, harness = LEGACY_TOOLCHAIN_HARNESS) {
|
|
5845
6157
|
const out = { ...env };
|
|
5846
|
-
for (const v of
|
|
6158
|
+
for (const v of adapterFor(harness).unsetEnv) delete out[v];
|
|
5847
6159
|
return out;
|
|
5848
6160
|
}
|
|
5849
6161
|
var CREW_LANE_ROLE_VAR = "CREW_LANE_ROLE";
|
|
@@ -5857,23 +6169,23 @@ var AgentError = class extends Error {
|
|
|
5857
6169
|
var AgentSpawnError = class extends AgentError {
|
|
5858
6170
|
};
|
|
5859
6171
|
function planAgentRun(o) {
|
|
5860
|
-
const briefPath =
|
|
6172
|
+
const briefPath = join8(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
5861
6173
|
if (!existsSync7(briefPath)) {
|
|
5862
6174
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
5863
6175
|
}
|
|
5864
|
-
const commonPath =
|
|
6176
|
+
const commonPath = join8(o.route.promptsDir, "personas", "common.md");
|
|
5865
6177
|
if (!existsSync7(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
|
|
5866
6178
|
const now = o.now ?? (() => Date.now());
|
|
5867
6179
|
const routeLabel = routeSlug(o.route.route);
|
|
5868
|
-
const digestPath2 =
|
|
6180
|
+
const digestPath2 = join8(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
5869
6181
|
let digest;
|
|
5870
6182
|
let ageSeconds;
|
|
5871
6183
|
if (existsSync7(digestPath2)) {
|
|
5872
|
-
ageSeconds = Math.floor((now() -
|
|
5873
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
6184
|
+
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
6185
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync7(digestPath2, "utf8");
|
|
5874
6186
|
}
|
|
5875
|
-
const common =
|
|
5876
|
-
const brief =
|
|
6187
|
+
const common = readFileSync7(commonPath, "utf8");
|
|
6188
|
+
const brief = readFileSync7(briefPath, "utf8");
|
|
5877
6189
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
5878
6190
|
const effectiveBrief = o.divergedPrompt ?? brief;
|
|
5879
6191
|
const prompt = assemblePrompt({
|
|
@@ -5884,9 +6196,18 @@ function planAgentRun(o) {
|
|
|
5884
6196
|
digest
|
|
5885
6197
|
});
|
|
5886
6198
|
const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
|
|
5887
|
-
const streamsDir =
|
|
6199
|
+
const streamsDir = join8(o.stateDir, "streams");
|
|
5888
6200
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
5889
|
-
const
|
|
6201
|
+
const toolchain = resolveToolchain(o.ship, o.route);
|
|
6202
|
+
const adapter = adapterFor(toolchain.harness);
|
|
6203
|
+
const model = o.agentModel ?? toolchain.model;
|
|
6204
|
+
const invocation = adapter.invocation(toolchain, {
|
|
6205
|
+
role: o.role,
|
|
6206
|
+
model,
|
|
6207
|
+
// Skipped when there's no key to send: a route `crew connect` hasn't
|
|
6208
|
+
// resolved yet has nothing to authenticate the MCP connection with either.
|
|
6209
|
+
mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
|
|
6210
|
+
});
|
|
5890
6211
|
const agentLog = o.route.resolved && o.apiKey ? {
|
|
5891
6212
|
baseUrl: o.route.baseUrl,
|
|
5892
6213
|
workspaceId: o.route.resolved.workspaceId,
|
|
@@ -5904,43 +6225,16 @@ function planAgentRun(o) {
|
|
|
5904
6225
|
// (config.ts), and the digest's `repo` column is what actually places
|
|
5905
6226
|
// the work. Kept deliberately rather than guessed at (ISSUE-349).
|
|
5906
6227
|
cwd: o.route.dir,
|
|
5907
|
-
bin:
|
|
5908
|
-
args:
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
...allowedTools(o.role),
|
|
5912
|
-
"--disallowedTools",
|
|
5913
|
-
...DISALLOWED_TOOLS,
|
|
5914
|
-
"--model",
|
|
5915
|
-
model,
|
|
5916
|
-
"--output-format",
|
|
5917
|
-
"stream-json",
|
|
5918
|
-
"--verbose",
|
|
5919
|
-
// Registers the tracker's own MCP tool surface for this run only —
|
|
5920
|
-
// `${CREW_API_KEY}` is interpolated by the CLI from the environment
|
|
5921
|
-
// (set below), so the resolved key is never written to an argv string
|
|
5922
|
-
// or a config file on disk. Skipped when there's no key to send: a
|
|
5923
|
-
// route `crew connect` hasn't resolved yet has nothing to authenticate
|
|
5924
|
-
// the MCP connection with either.
|
|
5925
|
-
...o.apiKey ? ["--mcp-config", JSON.stringify({
|
|
5926
|
-
mcpServers: {
|
|
5927
|
-
tablation: {
|
|
5928
|
-
type: "http",
|
|
5929
|
-
url: `${o.route.baseUrl}/api/mcp`,
|
|
5930
|
-
headers: { Authorization: `Bearer \${${API_KEY_VAR}}` }
|
|
5931
|
-
}
|
|
5932
|
-
}
|
|
5933
|
-
})] : []
|
|
5934
|
-
],
|
|
6228
|
+
bin: invocation.bin,
|
|
6229
|
+
args: invocation.args,
|
|
6230
|
+
toolchain: toolchain.name,
|
|
6231
|
+
harness: toolchain.harness,
|
|
5935
6232
|
prompt,
|
|
5936
6233
|
promptBytes: Buffer.byteLength(prompt, "utf8"),
|
|
5937
|
-
unsetEnv:
|
|
6234
|
+
unsetEnv: adapter.unsetEnv,
|
|
5938
6235
|
setEnv: {
|
|
5939
6236
|
...o.apiKey ? { [API_KEY_VAR]: o.apiKey } : {},
|
|
5940
|
-
|
|
5941
|
-
// blocks at all — without it, `run.cycles` (below) is always empty
|
|
5942
|
-
// and Agent Log Cycles reporting has nothing to report (ISSUE-558).
|
|
5943
|
-
...o.ship.agent.maxThinkingTokens > 0 ? { MAX_THINKING_TOKENS: String(o.ship.agent.maxThinkingTokens) } : {},
|
|
6237
|
+
...adapter.runEnv(toolchain),
|
|
5944
6238
|
// The operator's global SessionStart hook (pair-context-hook.sh) injects
|
|
5945
6239
|
// the interactive Pair persona's prompt for any session it can't tell
|
|
5946
6240
|
// apart from a person's own terminal. Every headless lane run this
|
|
@@ -5954,8 +6248,8 @@ function planAgentRun(o) {
|
|
|
5954
6248
|
digestAttached: digest !== void 0,
|
|
5955
6249
|
digestAgeSeconds: ageSeconds,
|
|
5956
6250
|
model,
|
|
5957
|
-
streamPath:
|
|
5958
|
-
eventsPath:
|
|
6251
|
+
streamPath: join8(streamsDir, `${base}.jsonl`),
|
|
6252
|
+
eventsPath: join8(streamsDir, `${base}.events.jsonl`),
|
|
5959
6253
|
ticket: o.ticket,
|
|
5960
6254
|
agentLog,
|
|
5961
6255
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
@@ -5965,6 +6259,8 @@ function planAgentRun(o) {
|
|
|
5965
6259
|
function describePlan(p) {
|
|
5966
6260
|
return [
|
|
5967
6261
|
`role: ${p.role}`,
|
|
6262
|
+
// Only for a non-default toolchain, so a plain Claude run's output is unchanged.
|
|
6263
|
+
...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
|
|
5968
6264
|
`cwd: ${p.cwd}`,
|
|
5969
6265
|
`command: ${p.bin} ${p.args.join(" ")}`,
|
|
5970
6266
|
`prompt: ${p.promptBytes} bytes on stdin (never argv)`,
|
|
@@ -5990,6 +6286,7 @@ function endSink(sink) {
|
|
|
5990
6286
|
return finished(sink).catch(() => void 0);
|
|
5991
6287
|
}
|
|
5992
6288
|
function spawnAgent(plan, emit) {
|
|
6289
|
+
const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
|
|
5993
6290
|
const started = Date.now();
|
|
5994
6291
|
const startedAt = new Date(started).toISOString();
|
|
5995
6292
|
const rawSink = openSink(plan.streamPath);
|
|
@@ -5997,7 +6294,7 @@ function spawnAgent(plan, emit) {
|
|
|
5997
6294
|
return new Promise((resolve5, reject) => {
|
|
5998
6295
|
const child = spawn2(plan.bin, plan.args, {
|
|
5999
6296
|
cwd: plan.cwd,
|
|
6000
|
-
env: { ...scrubbedEnv(), ...plan.setEnv },
|
|
6297
|
+
env: { ...scrubbedEnv(process.env, adapter.harness), ...plan.setEnv },
|
|
6001
6298
|
stdio: ["pipe", "pipe", "inherit"]
|
|
6002
6299
|
});
|
|
6003
6300
|
let buf = "";
|
|
@@ -6018,9 +6315,9 @@ function spawnAgent(plan, emit) {
|
|
|
6018
6315
|
return;
|
|
6019
6316
|
}
|
|
6020
6317
|
try {
|
|
6021
|
-
result = extractResult(parsed) ?? result;
|
|
6318
|
+
result = adapter.extractResult(parsed) ?? result;
|
|
6022
6319
|
let turnText = "";
|
|
6023
|
-
for (const ev of mapStreamLine(parsed)) {
|
|
6320
|
+
for (const ev of adapter.mapStreamLine(parsed)) {
|
|
6024
6321
|
const e = {
|
|
6025
6322
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6026
6323
|
cycle: emit.cycle,
|
|
@@ -6058,7 +6355,7 @@ function spawnAgent(plan, emit) {
|
|
|
6058
6355
|
`);
|
|
6059
6356
|
}
|
|
6060
6357
|
}
|
|
6061
|
-
const turnTokens = extractTurnTokens(parsed);
|
|
6358
|
+
const turnTokens = adapter.extractTurnTokens(parsed);
|
|
6062
6359
|
if (turnTokens !== void 0) attribution.add(turnText, turnTokens);
|
|
6063
6360
|
} catch {
|
|
6064
6361
|
}
|
|
@@ -6136,12 +6433,12 @@ function spawnAgent(plan, emit) {
|
|
|
6136
6433
|
|
|
6137
6434
|
// src/run-worktree.ts
|
|
6138
6435
|
import { existsSync as existsSync9 } from "fs";
|
|
6139
|
-
import { dirname as dirname6, join as
|
|
6436
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
6140
6437
|
|
|
6141
6438
|
// src/ports.ts
|
|
6142
6439
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6143
6440
|
import { existsSync as existsSync8 } from "fs";
|
|
6144
|
-
import { join as
|
|
6441
|
+
import { join as join9 } from "path";
|
|
6145
6442
|
var run = (cmd, args) => {
|
|
6146
6443
|
try {
|
|
6147
6444
|
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -6186,7 +6483,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
6186
6483
|
}
|
|
6187
6484
|
function worktreeExists(parent, prefix, n) {
|
|
6188
6485
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
6189
|
-
if (existsSync8(
|
|
6486
|
+
if (existsSync8(join9(parent, name, ".git"))) return true;
|
|
6190
6487
|
}
|
|
6191
6488
|
return false;
|
|
6192
6489
|
}
|
|
@@ -6274,7 +6571,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
6274
6571
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
6275
6572
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
6276
6573
|
}
|
|
6277
|
-
const path =
|
|
6574
|
+
const path = join10(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
|
|
6278
6575
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
6279
6576
|
return { action: "cut-remote", path, branch: located.branch };
|
|
6280
6577
|
}
|
|
@@ -6482,7 +6779,7 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
6482
6779
|
}
|
|
6483
6780
|
|
|
6484
6781
|
// src/connect-wizard.ts
|
|
6485
|
-
import { delimiter, dirname as dirname7, join as
|
|
6782
|
+
import { delimiter, dirname as dirname7, join as join11 } from "path";
|
|
6486
6783
|
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
6487
6784
|
import { stringify } from "yaml";
|
|
6488
6785
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
@@ -6505,7 +6802,7 @@ function defaultIsExecutable(p) {
|
|
|
6505
6802
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
6506
6803
|
for (const dir of pathEnv.split(delimiter)) {
|
|
6507
6804
|
if (!dir) continue;
|
|
6508
|
-
const candidate =
|
|
6805
|
+
const candidate = join11(dir, name);
|
|
6509
6806
|
if (isExecutable(candidate)) return candidate;
|
|
6510
6807
|
}
|
|
6511
6808
|
return void 0;
|
|
@@ -6600,8 +6897,8 @@ Wrote ${o.configPath}.
|
|
|
6600
6897
|
}
|
|
6601
6898
|
|
|
6602
6899
|
// src/release-run.ts
|
|
6603
|
-
import { readFileSync as
|
|
6604
|
-
import { dirname as dirname8, join as
|
|
6900
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync11, copyFileSync, mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync3 } from "fs";
|
|
6901
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
6605
6902
|
import { tmpdir as tmpdir2 } from "os";
|
|
6606
6903
|
|
|
6607
6904
|
// src/release.ts
|
|
@@ -7259,13 +7556,13 @@ async function bump(o, size) {
|
|
|
7259
7556
|
}
|
|
7260
7557
|
return printed;
|
|
7261
7558
|
}
|
|
7262
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
7559
|
+
const files = o.repo.release.versionFiles.map((f) => join12(o.cwd, f));
|
|
7263
7560
|
const first = files[0];
|
|
7264
7561
|
if (!existsSync11(first)) {
|
|
7265
7562
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
7266
7563
|
return null;
|
|
7267
7564
|
}
|
|
7268
|
-
const current = JSON.parse(
|
|
7565
|
+
const current = JSON.parse(readFileSync8(first, "utf8")).version;
|
|
7269
7566
|
if (!current) {
|
|
7270
7567
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
7271
7568
|
return null;
|
|
@@ -7278,7 +7575,7 @@ async function bump(o, size) {
|
|
|
7278
7575
|
return version;
|
|
7279
7576
|
}
|
|
7280
7577
|
for (const f of files) {
|
|
7281
|
-
const src =
|
|
7578
|
+
const src = readFileSync8(f, "utf8");
|
|
7282
7579
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
7283
7580
|
if (out === src) {
|
|
7284
7581
|
o.emit.error(`no version field in ${f}`);
|
|
@@ -7291,13 +7588,13 @@ async function bump(o, size) {
|
|
|
7291
7588
|
function writeChangelog(o, version, entries) {
|
|
7292
7589
|
const file = o.repo.release.changelog;
|
|
7293
7590
|
if (!file || entries.length === 0) return;
|
|
7294
|
-
const path =
|
|
7591
|
+
const path = join12(o.cwd, file);
|
|
7295
7592
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
7296
7593
|
if (o.dryRun) {
|
|
7297
7594
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
7298
7595
|
return;
|
|
7299
7596
|
}
|
|
7300
|
-
const existing = existsSync11(path) ?
|
|
7597
|
+
const existing = existsSync11(path) ? readFileSync8(path, "utf8") : "# Changelog\n";
|
|
7301
7598
|
writeFileSync6(path, insertChangelogSection(existing, section));
|
|
7302
7599
|
}
|
|
7303
7600
|
async function confirm(o, expected) {
|
|
@@ -7379,11 +7676,11 @@ async function confirmLastRelease(o, decision) {
|
|
|
7379
7676
|
function openReleaseCheckout(o) {
|
|
7380
7677
|
const { remote, base } = o.repo.branch;
|
|
7381
7678
|
const branch = `crew/release-${Date.now()}`;
|
|
7382
|
-
const root = mkdtempSync(
|
|
7383
|
-
const dir =
|
|
7679
|
+
const root = mkdtempSync(join12(tmpdir2(), "crew-release-"));
|
|
7680
|
+
const dir = join12(root, "checkout");
|
|
7384
7681
|
const close = () => {
|
|
7385
7682
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
7386
|
-
|
|
7683
|
+
rmSync3(root, { recursive: true, force: true });
|
|
7387
7684
|
gitOk(o.cwd, ["worktree", "prune"]);
|
|
7388
7685
|
gitOk(o.cwd, ["branch", "-D", branch]);
|
|
7389
7686
|
};
|
|
@@ -7394,10 +7691,10 @@ function openReleaseCheckout(o) {
|
|
|
7394
7691
|
throw e;
|
|
7395
7692
|
}
|
|
7396
7693
|
for (const f of o.repo.worktrees.copy) {
|
|
7397
|
-
const src =
|
|
7694
|
+
const src = join12(o.cwd, f);
|
|
7398
7695
|
if (!existsSync11(src)) continue;
|
|
7399
|
-
mkdirSync7(dirname8(
|
|
7400
|
-
copyFileSync(src,
|
|
7696
|
+
mkdirSync7(dirname8(join12(dir, f)), { recursive: true });
|
|
7697
|
+
copyFileSync(src, join12(dir, f));
|
|
7401
7698
|
}
|
|
7402
7699
|
return { dir, branch, close };
|
|
7403
7700
|
}
|
|
@@ -7866,7 +8163,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
7866
8163
|
}
|
|
7867
8164
|
|
|
7868
8165
|
// src/watch.ts
|
|
7869
|
-
import { watch as watchFile, existsSync as existsSync12, statSync as
|
|
8166
|
+
import { watch as watchFile, existsSync as existsSync12, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
|
|
7870
8167
|
var E = "\x1B[";
|
|
7871
8168
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
7872
8169
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -7968,13 +8265,13 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
7968
8265
|
}
|
|
7969
8266
|
function readFrom(file, from) {
|
|
7970
8267
|
if (!existsSync12(file)) return { events: [], offset: from };
|
|
7971
|
-
const size =
|
|
8268
|
+
const size = statSync6(file).size;
|
|
7972
8269
|
if (size < from) return readFrom(file, 0);
|
|
7973
8270
|
if (size === from) return { events: [], offset: from };
|
|
7974
|
-
const fd =
|
|
8271
|
+
const fd = openSync3(file, "r");
|
|
7975
8272
|
const buf = Buffer.alloc(size - from);
|
|
7976
8273
|
readSync(fd, buf, 0, buf.length, from);
|
|
7977
|
-
|
|
8274
|
+
closeSync3(fd);
|
|
7978
8275
|
const text = buf.toString("utf8");
|
|
7979
8276
|
const lastNewline = text.lastIndexOf("\n");
|
|
7980
8277
|
if (lastNewline === -1) return { events: [], offset: from };
|
|
@@ -8207,8 +8504,8 @@ function nextRoles(decision, ran) {
|
|
|
8207
8504
|
}
|
|
8208
8505
|
|
|
8209
8506
|
// src/agents.ts
|
|
8210
|
-
import { readFileSync as
|
|
8211
|
-
import { join as
|
|
8507
|
+
import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
|
|
8508
|
+
import { join as join13 } from "path";
|
|
8212
8509
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
8213
8510
|
var PERSONA_NAME = {
|
|
8214
8511
|
dev: "Developer",
|
|
@@ -8222,12 +8519,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
8222
8519
|
var AgentsSyncError = class extends Error {
|
|
8223
8520
|
};
|
|
8224
8521
|
function personaDefaultPrompt(promptsDir, role) {
|
|
8225
|
-
const briefPath =
|
|
8522
|
+
const briefPath = join13(promptsDir, "personas", `lane-${role}.md`);
|
|
8226
8523
|
if (!existsSync13(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
8227
|
-
if (role === "pair") return
|
|
8228
|
-
const commonPath =
|
|
8524
|
+
if (role === "pair") return readFileSync9(briefPath, "utf8");
|
|
8525
|
+
const commonPath = join13(promptsDir, "personas", "common.md");
|
|
8229
8526
|
if (!existsSync13(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
|
|
8230
|
-
return `${
|
|
8527
|
+
return `${readFileSync9(commonPath, "utf8")}${readFileSync9(briefPath, "utf8")}`;
|
|
8231
8528
|
}
|
|
8232
8529
|
async function resolveAgentId(route, role, opts) {
|
|
8233
8530
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -8893,14 +9190,14 @@ import {
|
|
|
8893
9190
|
} from "@tablation/client";
|
|
8894
9191
|
|
|
8895
9192
|
// src/skills.ts
|
|
8896
|
-
import { readFileSync as
|
|
8897
|
-
import { join as
|
|
9193
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync2, existsSync as existsSync14 } from "fs";
|
|
9194
|
+
import { join as join14 } from "path";
|
|
8898
9195
|
import { parse as parseYaml } from "yaml";
|
|
8899
9196
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
8900
9197
|
var SkillSyncError = class extends Error {
|
|
8901
9198
|
};
|
|
8902
9199
|
function parseSkillFile(path) {
|
|
8903
|
-
const text =
|
|
9200
|
+
const text = readFileSync10(path, "utf8");
|
|
8904
9201
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
8905
9202
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
8906
9203
|
const frontmatter = match[1];
|
|
@@ -8913,9 +9210,9 @@ function parseSkillFile(path) {
|
|
|
8913
9210
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
8914
9211
|
}
|
|
8915
9212
|
function listLocalSkills(promptsDir) {
|
|
8916
|
-
const dir =
|
|
9213
|
+
const dir = join14(promptsDir, "skills");
|
|
8917
9214
|
if (!existsSync14(dir)) return [];
|
|
8918
|
-
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
9215
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join14(dir, f)));
|
|
8919
9216
|
}
|
|
8920
9217
|
async function syncSkills(route, opts) {
|
|
8921
9218
|
if (!route.resolved) {
|
|
@@ -9094,7 +9391,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
9094
9391
|
}
|
|
9095
9392
|
|
|
9096
9393
|
// src/repos-cmd.ts
|
|
9097
|
-
import { homedir as
|
|
9394
|
+
import { homedir as homedir4 } from "os";
|
|
9098
9395
|
import { basename as basename6, resolve as resolve3 } from "path";
|
|
9099
9396
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
9100
9397
|
var ReposError = class extends Error {
|
|
@@ -9141,7 +9438,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
9141
9438
|
return { route, path: "." };
|
|
9142
9439
|
}
|
|
9143
9440
|
function expandDir(p, base) {
|
|
9144
|
-
const home = p === "~" ?
|
|
9441
|
+
const home = p === "~" ? homedir4() : p.startsWith("~/") ? `${homedir4()}/${p.slice(2)}` : p;
|
|
9145
9442
|
return resolve3(base, home);
|
|
9146
9443
|
}
|
|
9147
9444
|
function routeMapOf(doc, route) {
|
|
@@ -9238,72 +9535,6 @@ function resolveRepoProject(projects, opt, routeAreaId) {
|
|
|
9238
9535
|
);
|
|
9239
9536
|
}
|
|
9240
9537
|
|
|
9241
|
-
// src/claude-mcp.ts
|
|
9242
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
9243
|
-
import { readFileSync as readFileSync10 } from "fs";
|
|
9244
|
-
import { homedir as homedir4 } from "os";
|
|
9245
|
-
import { join as join14 } from "path";
|
|
9246
|
-
var MCP_SERVER_NAME = "tablation";
|
|
9247
|
-
var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
|
|
9248
|
-
function mcpUrlFor(baseUrl) {
|
|
9249
|
-
return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
|
|
9250
|
-
}
|
|
9251
|
-
function runClaude(bin, args) {
|
|
9252
|
-
const r = spawnSync2(bin, args, { encoding: "utf8", timeout: 3e4 });
|
|
9253
|
-
const missing = r.error?.code === "ENOENT";
|
|
9254
|
-
return { status: r.status, missing, stderr: r.stderr ?? "" };
|
|
9255
|
-
}
|
|
9256
|
-
function userScopeEntry(home) {
|
|
9257
|
-
try {
|
|
9258
|
-
const j = JSON.parse(readFileSync10(join14(home, ".claude.json"), "utf8"));
|
|
9259
|
-
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
9260
|
-
} catch {
|
|
9261
|
-
return void 0;
|
|
9262
|
-
}
|
|
9263
|
-
}
|
|
9264
|
-
function inspectClaudeMcp(o) {
|
|
9265
|
-
const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
|
|
9266
|
-
if (got.missing) return { state: "no-claude" };
|
|
9267
|
-
if (got.status !== 0) return { state: "missing" };
|
|
9268
|
-
const entry = userScopeEntry(o.home ?? homedir4());
|
|
9269
|
-
if (!entry) {
|
|
9270
|
-
return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
|
|
9271
|
-
}
|
|
9272
|
-
if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
|
|
9273
|
-
const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
|
|
9274
|
-
if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
|
|
9275
|
-
return { state: "registered", url: o.url };
|
|
9276
|
-
}
|
|
9277
|
-
function ensureClaudeMcp(o) {
|
|
9278
|
-
const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
|
|
9279
|
-
const st = inspectClaudeMcp(o);
|
|
9280
|
-
if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
|
|
9281
|
-
if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
|
|
9282
|
-
if (st.state === "different" && !o.replace) {
|
|
9283
|
-
return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
9284
|
-
}
|
|
9285
|
-
if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
|
|
9286
|
-
if (st.state === "different") {
|
|
9287
|
-
runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
9288
|
-
}
|
|
9289
|
-
const add = runClaude(o.bin, [
|
|
9290
|
-
"mcp",
|
|
9291
|
-
"add",
|
|
9292
|
-
"--transport",
|
|
9293
|
-
"http",
|
|
9294
|
-
MCP_SERVER_NAME,
|
|
9295
|
-
o.url,
|
|
9296
|
-
"--header",
|
|
9297
|
-
`Authorization: Bearer ${o.key}`,
|
|
9298
|
-
"--scope",
|
|
9299
|
-
"user"
|
|
9300
|
-
]);
|
|
9301
|
-
if (add.status !== 0) {
|
|
9302
|
-
return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
|
|
9303
|
-
}
|
|
9304
|
-
return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
|
|
9305
|
-
}
|
|
9306
|
-
|
|
9307
9538
|
// src/doctor-fix.ts
|
|
9308
9539
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
9309
9540
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
@@ -9465,7 +9696,7 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
9465
9696
|
}
|
|
9466
9697
|
|
|
9467
9698
|
// src/stream-sweep.ts
|
|
9468
|
-
import { existsSync as existsSync16, readdirSync as readdirSync3, rmSync as
|
|
9699
|
+
import { existsSync as existsSync16, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync7 } from "fs";
|
|
9469
9700
|
import { join as join15 } from "path";
|
|
9470
9701
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
9471
9702
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
@@ -9474,7 +9705,7 @@ function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
9474
9705
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
9475
9706
|
return readdirSync3(dir).map((name) => join15(dir, name)).filter((path) => {
|
|
9476
9707
|
try {
|
|
9477
|
-
return
|
|
9708
|
+
return statSync7(path).mtimeMs < cutoff;
|
|
9478
9709
|
} catch {
|
|
9479
9710
|
return false;
|
|
9480
9711
|
}
|
|
@@ -9488,7 +9719,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
9488
9719
|
continue;
|
|
9489
9720
|
}
|
|
9490
9721
|
try {
|
|
9491
|
-
|
|
9722
|
+
rmSync4(path, { force: true });
|
|
9492
9723
|
removed++;
|
|
9493
9724
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
9494
9725
|
} catch (e) {
|
|
@@ -9497,9 +9728,20 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
9497
9728
|
}
|
|
9498
9729
|
return removed;
|
|
9499
9730
|
}
|
|
9731
|
+
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
9732
|
+
if (!existsSync16(stateDir)) return [];
|
|
9733
|
+
const cutoff = now - retentionDays * DAY_MS2;
|
|
9734
|
+
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join15(stateDir, name)).filter((path) => {
|
|
9735
|
+
try {
|
|
9736
|
+
return statSync7(path).mtimeMs < cutoff;
|
|
9737
|
+
} catch {
|
|
9738
|
+
return false;
|
|
9739
|
+
}
|
|
9740
|
+
}).sort();
|
|
9741
|
+
}
|
|
9500
9742
|
|
|
9501
9743
|
// src/cli.ts
|
|
9502
|
-
import { readFileSync as readFileSync14, existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync } from "fs";
|
|
9744
|
+
import { readFileSync as readFileSync14, existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
9503
9745
|
import { createInterface } from "readline/promises";
|
|
9504
9746
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
9505
9747
|
|
|
@@ -10237,7 +10479,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
10237
10479
|
}
|
|
10238
10480
|
|
|
10239
10481
|
// src/daemon.ts
|
|
10240
|
-
import { readdirSync as readdirSync6, statSync as
|
|
10482
|
+
import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
10241
10483
|
import { join as join19 } from "path";
|
|
10242
10484
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
10243
10485
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
@@ -10269,7 +10511,7 @@ async function runDaemonLoop(o) {
|
|
|
10269
10511
|
function latestMtime(path) {
|
|
10270
10512
|
let stat;
|
|
10271
10513
|
try {
|
|
10272
|
-
stat =
|
|
10514
|
+
stat = statSync8(path);
|
|
10273
10515
|
} catch {
|
|
10274
10516
|
return 0;
|
|
10275
10517
|
}
|
|
@@ -10866,6 +11108,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
10866
11108
|
const emit = new Emitter({
|
|
10867
11109
|
route: route?.route ?? "",
|
|
10868
11110
|
eventFile: dryRun ? void 0 : eventFileFor(cfg.ship.stateDir),
|
|
11111
|
+
rotateBytes: cfg.ship.eventsRotateBytes,
|
|
10869
11112
|
logFile: dryRun ? void 0 : cfg.ship.logFile,
|
|
10870
11113
|
console: (l) => process.stderr.write(`${l}
|
|
10871
11114
|
`)
|
|
@@ -10968,7 +11211,10 @@ if (command === "passenger-sync-daemon") {
|
|
|
10968
11211
|
emitReleaseSummary(emit, summaries);
|
|
10969
11212
|
if (!dryRun) state.releaseHeartbeat().touch();
|
|
10970
11213
|
try {
|
|
10971
|
-
const paths =
|
|
11214
|
+
const paths = [
|
|
11215
|
+
...planStreamSweep(cfg.ship.stateDir, cfg.ship.streamRetentionDays),
|
|
11216
|
+
...planEventsSweep(cfg.ship.stateDir, cfg.ship.streamRetentionDays)
|
|
11217
|
+
];
|
|
10972
11218
|
if (paths.length) {
|
|
10973
11219
|
emit.enter("worktree");
|
|
10974
11220
|
const removed = applyStreamSweep(paths, dryRun, emit);
|
|
@@ -12020,6 +12266,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12020
12266
|
newEmitter: () => new Emitter({
|
|
12021
12267
|
route: route.route,
|
|
12022
12268
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
12269
|
+
rotateBytes: cfg.ship.eventsRotateBytes,
|
|
12023
12270
|
logFile: cfg.ship.logFile,
|
|
12024
12271
|
console: (l) => process.stderr.write(`${l}
|
|
12025
12272
|
`)
|
|
@@ -12629,7 +12876,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12629
12876
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
12630
12877
|
writeFileSync9(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
12631
12878
|
`);
|
|
12632
|
-
|
|
12879
|
+
renameSync2(tmpPath, resolvedPath);
|
|
12633
12880
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
12634
12881
|
`);
|
|
12635
12882
|
}
|
|
@@ -12673,8 +12920,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12673
12920
|
if (!wroteConfig) process.stdout.write(routeBlock);
|
|
12674
12921
|
if (!flag("no-mcp") && apiKey) {
|
|
12675
12922
|
const replace = value("mcp") === "replace";
|
|
12676
|
-
|
|
12923
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12924
|
+
for (const tc of shipToolchains(cfg.ship)) {
|
|
12925
|
+
const id = `${tc.harness}\0${tc.bin}`;
|
|
12926
|
+
if (seen.has(id)) continue;
|
|
12927
|
+
seen.add(id);
|
|
12928
|
+
process.stderr.write(`${adapterFor(tc.harness).ensureMcp({ bin: tc.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
|
|
12677
12929
|
`);
|
|
12930
|
+
}
|
|
12678
12931
|
}
|
|
12679
12932
|
if (found.holds.length) {
|
|
12680
12933
|
process.stderr.write(
|
|
@@ -12846,7 +13099,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
12846
13099
|
{ step: "worktree" }
|
|
12847
13100
|
);
|
|
12848
13101
|
}
|
|
12849
|
-
|
|
13102
|
+
if (!dryRun) {
|
|
13103
|
+
const rotated = rotateEventsFile(eventFileFor(cfg.ship.stateDir), cfg.ship.eventsRotateBytes);
|
|
13104
|
+
if (rotated) {
|
|
13105
|
+
anything = true;
|
|
13106
|
+
emit.emit(`rotated oversized event log to ${rotated}`, { step: "worktree" });
|
|
13107
|
+
}
|
|
13108
|
+
}
|
|
13109
|
+
const streamPaths = [
|
|
13110
|
+
...planStreamSweep(cfg.ship.stateDir, cfg.ship.streamRetentionDays),
|
|
13111
|
+
...planEventsSweep(cfg.ship.stateDir, cfg.ship.streamRetentionDays)
|
|
13112
|
+
];
|
|
12850
13113
|
if (streamPaths.length) {
|
|
12851
13114
|
anything = true;
|
|
12852
13115
|
const removed = applyStreamSweep(streamPaths, dryRun, emit);
|
|
@@ -13220,6 +13483,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13220
13483
|
name: cfg.ship.name,
|
|
13221
13484
|
platform: cfg.ship.platform,
|
|
13222
13485
|
agent: { bin: cfg.ship.agent.bin, model: cfg.ship.agent.model },
|
|
13486
|
+
// Additive (CREW-1511): the configured toolchains and the ship's
|
|
13487
|
+
// default. `agent` above stays for older crew-macos builds.
|
|
13488
|
+
toolchain: resolveToolchain(cfg.ship).name,
|
|
13489
|
+
toolchains: shipToolchains(cfg.ship).map((x) => ({
|
|
13490
|
+
name: x.name,
|
|
13491
|
+
harness: x.harness,
|
|
13492
|
+
bin: x.bin,
|
|
13493
|
+
launcher: x.launcher ?? null,
|
|
13494
|
+
model: x.model
|
|
13495
|
+
})),
|
|
13223
13496
|
stateDir: cfg.ship.stateDir,
|
|
13224
13497
|
logFile: cfg.ship.logFile,
|
|
13225
13498
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
@@ -13245,6 +13518,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13245
13518
|
dir: route.dir,
|
|
13246
13519
|
enabled: route.enabled,
|
|
13247
13520
|
area: route.area ?? null,
|
|
13521
|
+
// Additive (CREW-1511): the toolchain this route's sessions run on.
|
|
13522
|
+
toolchain: resolveToolchain(cfg.ship, route).name,
|
|
13248
13523
|
baseUrl: route.baseUrl,
|
|
13249
13524
|
repos: repos.map((r) => ({
|
|
13250
13525
|
name: r.name,
|
|
@@ -13349,6 +13624,15 @@ watermark: ${state.watermark()}
|
|
|
13349
13624
|
routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
13350
13625
|
`
|
|
13351
13626
|
);
|
|
13627
|
+
try {
|
|
13628
|
+
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
13629
|
+
const evSize = existsSync20(evFile) ? statSync9(evFile).size : 0;
|
|
13630
|
+
process.stdout.write(
|
|
13631
|
+
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
13632
|
+
` : "events log: ok\n"
|
|
13633
|
+
);
|
|
13634
|
+
} catch {
|
|
13635
|
+
}
|
|
13352
13636
|
const tracker = new Tracker(route, cfg.ship);
|
|
13353
13637
|
const contractProblems = validateContract(tracker.contract);
|
|
13354
13638
|
process.stdout.write(
|
|
@@ -13461,14 +13745,24 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
13461
13745
|
}
|
|
13462
13746
|
}
|
|
13463
13747
|
const schedulerPath = pathFor(cfg.ship);
|
|
13464
|
-
const agentBinPath = findOnPath(cfg.ship.agent.bin, schedulerPath);
|
|
13465
13748
|
const gitPath = findOnPath("git", schedulerPath);
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13749
|
+
const defaultToolchain = resolveToolchain(cfg.ship).name;
|
|
13750
|
+
let allBinsFound = true;
|
|
13751
|
+
for (const tc of shipToolchains(cfg.ship)) {
|
|
13752
|
+
const adapter = adapterFor(tc.harness);
|
|
13753
|
+
const binPath = findOnPath(tc.launcher ?? tc.bin, schedulerPath);
|
|
13754
|
+
if (!binPath) allBinsFound = false;
|
|
13755
|
+
const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
|
|
13756
|
+
process.stdout.write(
|
|
13757
|
+
`${label} ${tc.launcher ?? tc.bin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
|
|
13469
13758
|
`
|
|
13470
|
-
|
|
13471
|
-
|
|
13759
|
+
);
|
|
13760
|
+
for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
|
|
13761
|
+
`);
|
|
13762
|
+
}
|
|
13763
|
+
process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
|
|
13764
|
+
`);
|
|
13765
|
+
if (!allBinsFound || !gitPath) {
|
|
13472
13766
|
process.stdout.write(
|
|
13473
13767
|
` scheduler PATH is ${schedulerPath} \u2014 add the directory holding the missing binary to ship.extraPath in crew.yaml, then re-run \`crew doctor\`.
|
|
13474
13768
|
`
|
|
@@ -13476,14 +13770,21 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
13476
13770
|
}
|
|
13477
13771
|
if (route.apiKey) {
|
|
13478
13772
|
const mcpUrl = mcpUrlFor(route.baseUrl);
|
|
13479
|
-
const
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13773
|
+
const seenMcp = /* @__PURE__ */ new Set();
|
|
13774
|
+
for (const tc of shipToolchains(cfg.ship)) {
|
|
13775
|
+
const id = `${tc.harness}\0${tc.bin}`;
|
|
13776
|
+
if (seenMcp.has(id)) continue;
|
|
13777
|
+
seenMcp.add(id);
|
|
13778
|
+
const adapter = adapterFor(tc.harness);
|
|
13779
|
+
const m = adapter.inspectMcp({ bin: tc.bin, url: mcpUrl, key: route.apiKey });
|
|
13780
|
+
process.stdout.write(
|
|
13781
|
+
m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
|
|
13782
|
+
` : m.state === "no-binary" ? `mcp: ${tc.bin} not found \u2014 ${adapter.displayName} was not checked
|
|
13483
13783
|
` : m.state === "different" ? `mcp: tablation registered but not for this route \u2014 ${m.detail}
|
|
13484
13784
|
` : `mcp: not registered \u2014 run crew connect
|
|
13485
13785
|
`
|
|
13486
|
-
|
|
13786
|
+
);
|
|
13787
|
+
}
|
|
13487
13788
|
}
|
|
13488
13789
|
for (const c of cfg.routes.filter((r) => r.enabled)) {
|
|
13489
13790
|
let repos;
|