@tablation/crew 0.73.2 → 0.74.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/dist/cli.js +496 -438
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -128,176 +128,10 @@ function formatStaleOwnLock(scope, holder) {
|
|
|
128
128
|
`;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
// src/
|
|
132
|
-
import {
|
|
133
|
-
import {
|
|
134
|
-
import {
|
|
135
|
-
import { join as join19 } from "path";
|
|
136
|
-
|
|
137
|
-
// src/agent.ts
|
|
138
|
-
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
139
|
-
import { readFileSync as readFileSync15, existsSync as existsSync18, statSync as statSync5, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
140
|
-
import { StringDecoder } from "string_decoder";
|
|
141
|
-
import { finished } from "stream/promises";
|
|
142
|
-
import { join as join18, dirname as dirname7, basename as basename7 } from "path";
|
|
143
|
-
import { createHash as createHash3 } from "crypto";
|
|
144
|
-
|
|
145
|
-
// src/config.ts
|
|
146
|
-
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
147
|
-
import { resolve, dirname as dirname3, isAbsolute, join as join8, basename as basename3 } from "path";
|
|
148
|
-
import { homedir as homedir5, tmpdir } from "os";
|
|
149
|
-
import { parse as parse2 } from "yaml";
|
|
150
|
-
|
|
151
|
-
// src/events.ts
|
|
152
|
-
import {
|
|
153
|
-
appendFileSync,
|
|
154
|
-
mkdirSync,
|
|
155
|
-
existsSync as existsSync2,
|
|
156
|
-
statSync,
|
|
157
|
-
renameSync,
|
|
158
|
-
openSync,
|
|
159
|
-
closeSync,
|
|
160
|
-
rmSync
|
|
161
|
-
} from "fs";
|
|
162
|
-
import { join, dirname } from "path";
|
|
163
|
-
function render(e) {
|
|
164
|
-
const scope = e.role ? `${e.step}[${e.role}]` : e.step;
|
|
165
|
-
const ticket = e.ticket ? ` ${e.ticket}` : "";
|
|
166
|
-
const level = e.level === "info" ? "" : `${e.level.toUpperCase()}: `;
|
|
167
|
-
return `${e.at} ${scope}:${ticket} ${level}${e.message}`;
|
|
168
|
-
}
|
|
169
|
-
var DEFAULT_EVENTS_ROTATE_BYTES = 64 * 1024 * 1024;
|
|
170
|
-
var ROTATED_EVENTS_RE = /^events\.\d{8}T\d{6}Z(?:-\d+)?\.jsonl$/;
|
|
171
|
-
var ROTATE_LOCK_STALE_MS = 6e4;
|
|
172
|
-
function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
|
|
173
|
-
try {
|
|
174
|
-
if (!existsSync2(file) || statSync(file).size <= maxBytes) return void 0;
|
|
175
|
-
} catch {
|
|
176
|
-
return void 0;
|
|
177
|
-
}
|
|
178
|
-
const lock = `${file}.rotate.lock`;
|
|
179
|
-
try {
|
|
180
|
-
try {
|
|
181
|
-
closeSync(openSync(lock, "wx"));
|
|
182
|
-
} catch {
|
|
183
|
-
try {
|
|
184
|
-
if (Date.now() - statSync(lock).mtimeMs < ROTATE_LOCK_STALE_MS) return void 0;
|
|
185
|
-
rmSync(lock, { force: true });
|
|
186
|
-
closeSync(openSync(lock, "wx"));
|
|
187
|
-
} catch {
|
|
188
|
-
return void 0;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
if (statSync(file).size <= maxBytes) return void 0;
|
|
193
|
-
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
194
|
-
let dest = join(dirname(file), `events.${stamp}.jsonl`);
|
|
195
|
-
for (let n = 1; existsSync2(dest); n++) dest = join(dirname(file), `events.${stamp}-${n}.jsonl`);
|
|
196
|
-
renameSync(file, dest);
|
|
197
|
-
return dest;
|
|
198
|
-
} finally {
|
|
199
|
-
rmSync(lock, { force: true });
|
|
200
|
-
}
|
|
201
|
-
} catch {
|
|
202
|
-
return void 0;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
var Emitter = class _Emitter {
|
|
206
|
-
cycle;
|
|
207
|
-
opts;
|
|
208
|
-
now;
|
|
209
|
-
step = "poll";
|
|
210
|
-
role;
|
|
211
|
-
constructor(opts) {
|
|
212
|
-
this.opts = opts;
|
|
213
|
-
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
214
|
-
this.cycle = opts.cycleId ?? this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
215
|
-
for (const f of [opts.eventFile, opts.logFile]) {
|
|
216
|
-
if (!f) continue;
|
|
217
|
-
try {
|
|
218
|
-
mkdirSync(dirname(f), { recursive: true });
|
|
219
|
-
} catch {
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
/** Everything emitted after this belongs to `step`, until the next one. */
|
|
224
|
-
enter(step, role) {
|
|
225
|
-
this.step = step;
|
|
226
|
-
this.role = role;
|
|
227
|
-
}
|
|
228
|
-
emit(message, extra = {}) {
|
|
229
|
-
const e = {
|
|
230
|
-
at: this.now().toISOString(),
|
|
231
|
-
cycle: this.cycle,
|
|
232
|
-
route: this.opts.route,
|
|
233
|
-
step: extra.step ?? this.step,
|
|
234
|
-
level: extra.level ?? "info",
|
|
235
|
-
message,
|
|
236
|
-
...extra.role ?? this.role ? { role: extra.role ?? this.role } : {},
|
|
237
|
-
...extra.ticket ? { ticket: extra.ticket } : {},
|
|
238
|
-
...extra.data ? { data: extra.data } : {}
|
|
239
|
-
};
|
|
240
|
-
const line = render(e);
|
|
241
|
-
try {
|
|
242
|
-
if (this.opts.eventFile && this.opts.rotateBytes) rotateEventsFile(this.opts.eventFile, this.opts.rotateBytes, this.now());
|
|
243
|
-
} catch {
|
|
244
|
-
}
|
|
245
|
-
try {
|
|
246
|
-
if (this.opts.eventFile) appendFileSync(this.opts.eventFile, `${JSON.stringify(e)}
|
|
247
|
-
`);
|
|
248
|
-
} catch {
|
|
249
|
-
}
|
|
250
|
-
try {
|
|
251
|
-
if (this.opts.logFile) appendFileSync(this.opts.logFile, `${line}
|
|
252
|
-
`);
|
|
253
|
-
} catch {
|
|
254
|
-
}
|
|
255
|
-
try {
|
|
256
|
-
(this.opts.console ?? ((l) => process.stderr.write(`${l}
|
|
257
|
-
`)))(line);
|
|
258
|
-
} catch {
|
|
259
|
-
}
|
|
260
|
-
return e;
|
|
261
|
-
}
|
|
262
|
-
warn(message, extra = {}) {
|
|
263
|
-
return this.emit(message, { ...extra, level: "warn" });
|
|
264
|
-
}
|
|
265
|
-
error(message, extra = {}) {
|
|
266
|
-
return this.emit(message, { ...extra, level: "error" });
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* A view of this emitter labelling every event with a different route,
|
|
270
|
-
* sharing the same sinks and cycle id. For a fleet-wide run, the top-level
|
|
271
|
-
* Emitter is stamped with just one route (see cli.ts) — a phase that
|
|
272
|
-
* actually knows which route it's working (release, per-repo poll) uses
|
|
273
|
-
* this instead, so its events aren't all attributed to `routes[0]`.
|
|
274
|
-
*/
|
|
275
|
-
forRoute(route) {
|
|
276
|
-
return new _Emitter({ ...this.opts, route, cycleId: this.cycle });
|
|
277
|
-
}
|
|
278
|
-
/** Times a step and emits its outcome, so a view can show duration. */
|
|
279
|
-
async timed(step, message, fn, role) {
|
|
280
|
-
this.enter(step, role);
|
|
281
|
-
const started = this.now().getTime();
|
|
282
|
-
try {
|
|
283
|
-
const result = await fn();
|
|
284
|
-
this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
|
|
285
|
-
return result;
|
|
286
|
-
} catch (err) {
|
|
287
|
-
this.error(`${message} \u2014 ${err.message}`, {
|
|
288
|
-
data: { ms: this.now().getTime() - started, ok: false }
|
|
289
|
-
});
|
|
290
|
-
throw err;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
295
|
-
|
|
296
|
-
// src/pair-install.ts
|
|
297
|
-
import { existsSync as existsSync5, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
298
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
299
|
-
import { homedir as homedir4 } from "os";
|
|
300
|
-
import { join as join6 } from "path";
|
|
131
|
+
// src/adapters/claude.ts
|
|
132
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
133
|
+
import { homedir as homedir3 } from "os";
|
|
134
|
+
import { join as join4 } from "path";
|
|
301
135
|
|
|
302
136
|
// src/stream.ts
|
|
303
137
|
function toolTarget(name, input) {
|
|
@@ -349,19 +183,19 @@ function extractResult(line) {
|
|
|
349
183
|
|
|
350
184
|
// src/claude-mcp.ts
|
|
351
185
|
import { spawnSync } from "child_process";
|
|
352
|
-
import { readFileSync as
|
|
186
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
353
187
|
import { homedir } from "os";
|
|
354
|
-
import { join as
|
|
188
|
+
import { join as join2 } from "path";
|
|
355
189
|
|
|
356
190
|
// src/mcp-registrations.ts
|
|
357
191
|
import { createHash } from "crypto";
|
|
358
|
-
import { mkdirSync
|
|
359
|
-
import { join
|
|
360
|
-
var fileFor = (stateDir) =>
|
|
192
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
193
|
+
import { join } from "path";
|
|
194
|
+
var fileFor = (stateDir) => join(stateDir, "mcp-registrations.json");
|
|
361
195
|
var keyFingerprint = (key) => createHash("sha256").update(key).digest("hex").slice(0, 8);
|
|
362
196
|
function readMcpRegistrations(stateDir) {
|
|
363
197
|
try {
|
|
364
|
-
const j = JSON.parse(
|
|
198
|
+
const j = JSON.parse(readFileSync(fileFor(stateDir), "utf8"));
|
|
365
199
|
return Array.isArray(j) ? j : [];
|
|
366
200
|
} catch {
|
|
367
201
|
return [];
|
|
@@ -369,7 +203,7 @@ function readMcpRegistrations(stateDir) {
|
|
|
369
203
|
}
|
|
370
204
|
function recordMcpRegistration(stateDir, r, now = /* @__PURE__ */ new Date()) {
|
|
371
205
|
const rest = readMcpRegistrations(stateDir).filter((x) => !(x.harness === r.harness && x.scope === r.scope && x.name === r.name));
|
|
372
|
-
|
|
206
|
+
mkdirSync(stateDir, { recursive: true });
|
|
373
207
|
writeFileSync(fileFor(stateDir), `${JSON.stringify([...rest, { ...r, at: now.toISOString() }], null, 2)}
|
|
374
208
|
`, { mode: 384 });
|
|
375
209
|
}
|
|
@@ -393,7 +227,7 @@ function runClaude(bin, args) {
|
|
|
393
227
|
}
|
|
394
228
|
function userScopeEntry(home) {
|
|
395
229
|
try {
|
|
396
|
-
const j = JSON.parse(
|
|
230
|
+
const j = JSON.parse(readFileSync2(join2(home, ".claude.json"), "utf8"));
|
|
397
231
|
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
398
232
|
} catch {
|
|
399
233
|
return void 0;
|
|
@@ -477,8 +311,8 @@ function removeClaudeMcp(o) {
|
|
|
477
311
|
}
|
|
478
312
|
|
|
479
313
|
// src/launcher.ts
|
|
480
|
-
import { basename, join as
|
|
481
|
-
import { existsSync as
|
|
314
|
+
import { basename, join as join3 } from "path";
|
|
315
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
482
316
|
import { homedir as homedir2 } from "os";
|
|
483
317
|
function isOriLauncher(spec) {
|
|
484
318
|
return !!spec.launcher && basename(spec.launcher) === "ori";
|
|
@@ -487,7 +321,7 @@ function isOmlxLauncher(spec) {
|
|
|
487
321
|
return !!spec.launcher && basename(spec.launcher) === "omlx";
|
|
488
322
|
}
|
|
489
323
|
function defaultOmlxBin(home = homedir2()) {
|
|
490
|
-
return
|
|
324
|
+
return join3(home, ".omlx", "bin", "omlx");
|
|
491
325
|
}
|
|
492
326
|
function launcherExecutable(spec, home) {
|
|
493
327
|
if (!spec.launcher) return void 0;
|
|
@@ -540,8 +374,8 @@ function omlxModelPresent(model, dirs) {
|
|
|
540
374
|
});
|
|
541
375
|
}
|
|
542
376
|
function readOmlxSettings(home = homedir2()) {
|
|
543
|
-
const p =
|
|
544
|
-
return
|
|
377
|
+
const p = join3(home, ".omlx", "settings.json");
|
|
378
|
+
return existsSync2(p) ? parseOmlxSettings(readFileSync3(p, "utf8")) : void 0;
|
|
545
379
|
}
|
|
546
380
|
|
|
547
381
|
// src/capability.ts
|
|
@@ -592,6 +426,24 @@ var claudePair = {
|
|
|
592
426
|
},
|
|
593
427
|
removePair: (o) => removeClaudeMcp({ bin: o.bin ?? "claude", dryRun: o.dryRun })
|
|
594
428
|
};
|
|
429
|
+
function subscriptionSettings() {
|
|
430
|
+
return JSON.stringify({ env: Object.fromEntries(BILLING_VARS_TO_UNSET.map((v) => [v, ""])) });
|
|
431
|
+
}
|
|
432
|
+
function describeClaudeAuth(spec, env = process.env, home = homedir3()) {
|
|
433
|
+
if (spec.auth === "subscription") return "subscription (settings override)";
|
|
434
|
+
let fromSettings;
|
|
435
|
+
try {
|
|
436
|
+
const parsed = JSON.parse(readFileSync4(join4(home, ".claude", "settings.json"), "utf8"));
|
|
437
|
+
const e = parsed.env ?? {};
|
|
438
|
+
const on = (k) => e[k] !== void 0 && e[k] !== "" && e[k] !== "0";
|
|
439
|
+
if (on("CLAUDE_CODE_USE_VERTEX")) fromSettings = "vertex from ~/.claude/settings.json";
|
|
440
|
+
else if (on("ANTHROPIC_API_KEY") || on("ANTHROPIC_AUTH_TOKEN")) fromSettings = "API key from ~/.claude/settings.json";
|
|
441
|
+
} catch {
|
|
442
|
+
}
|
|
443
|
+
if (fromSettings) return `inherit: ${fromSettings}`;
|
|
444
|
+
const shell = BILLING_VARS_TO_UNSET.find((v) => v.startsWith("ANTHROPIC_API") && env[v]);
|
|
445
|
+
return shell ? `inherit: ${shell} in env (unset at exec)` : "inherit: subscription login";
|
|
446
|
+
}
|
|
595
447
|
var claudeAdapter = {
|
|
596
448
|
harness: "claude",
|
|
597
449
|
displayName: "Claude Code",
|
|
@@ -646,6 +498,7 @@ var claudeAdapter = {
|
|
|
646
498
|
}
|
|
647
499
|
}
|
|
648
500
|
})] : [],
|
|
501
|
+
...spec.auth === "subscription" ? ["--settings", subscriptionSettings()] : [],
|
|
649
502
|
...spec.args
|
|
650
503
|
];
|
|
651
504
|
return wrapInvocation(spec, { model: o.model, args });
|
|
@@ -672,14 +525,185 @@ var claudeAdapter = {
|
|
|
672
525
|
ensureMcpResult: ensureClaudeMcpResult
|
|
673
526
|
};
|
|
674
527
|
|
|
528
|
+
// src/selftest.ts
|
|
529
|
+
import { spawn as spawn3 } from "child_process";
|
|
530
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, mkdtempSync, readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
531
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
532
|
+
import { join as join20 } from "path";
|
|
533
|
+
|
|
534
|
+
// src/agent.ts
|
|
535
|
+
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
536
|
+
import { readFileSync as readFileSync16, existsSync as existsSync18, statSync as statSync5, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
537
|
+
import { StringDecoder } from "string_decoder";
|
|
538
|
+
import { finished } from "stream/promises";
|
|
539
|
+
import { join as join19, dirname as dirname7, basename as basename7 } from "path";
|
|
540
|
+
import { createHash as createHash3 } from "crypto";
|
|
541
|
+
|
|
542
|
+
// src/config.ts
|
|
543
|
+
import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
|
|
544
|
+
import { resolve, dirname as dirname3, isAbsolute, join as join9, basename as basename3 } from "path";
|
|
545
|
+
import { homedir as homedir6, tmpdir } from "os";
|
|
546
|
+
import { parse as parse2 } from "yaml";
|
|
547
|
+
|
|
548
|
+
// src/events.ts
|
|
549
|
+
import {
|
|
550
|
+
appendFileSync,
|
|
551
|
+
mkdirSync as mkdirSync2,
|
|
552
|
+
existsSync as existsSync3,
|
|
553
|
+
statSync,
|
|
554
|
+
renameSync,
|
|
555
|
+
openSync,
|
|
556
|
+
closeSync,
|
|
557
|
+
rmSync
|
|
558
|
+
} from "fs";
|
|
559
|
+
import { join as join5, dirname } from "path";
|
|
560
|
+
function render(e) {
|
|
561
|
+
const scope = e.role ? `${e.step}[${e.role}]` : e.step;
|
|
562
|
+
const ticket = e.ticket ? ` ${e.ticket}` : "";
|
|
563
|
+
const level = e.level === "info" ? "" : `${e.level.toUpperCase()}: `;
|
|
564
|
+
return `${e.at} ${scope}:${ticket} ${level}${e.message}`;
|
|
565
|
+
}
|
|
566
|
+
var DEFAULT_EVENTS_ROTATE_BYTES = 64 * 1024 * 1024;
|
|
567
|
+
var ROTATED_EVENTS_RE = /^events\.\d{8}T\d{6}Z(?:-\d+)?\.jsonl$/;
|
|
568
|
+
var ROTATE_LOCK_STALE_MS = 6e4;
|
|
569
|
+
function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
|
|
570
|
+
try {
|
|
571
|
+
if (!existsSync3(file) || statSync(file).size <= maxBytes) return void 0;
|
|
572
|
+
} catch {
|
|
573
|
+
return void 0;
|
|
574
|
+
}
|
|
575
|
+
const lock = `${file}.rotate.lock`;
|
|
576
|
+
try {
|
|
577
|
+
try {
|
|
578
|
+
closeSync(openSync(lock, "wx"));
|
|
579
|
+
} catch {
|
|
580
|
+
try {
|
|
581
|
+
if (Date.now() - statSync(lock).mtimeMs < ROTATE_LOCK_STALE_MS) return void 0;
|
|
582
|
+
rmSync(lock, { force: true });
|
|
583
|
+
closeSync(openSync(lock, "wx"));
|
|
584
|
+
} catch {
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
if (statSync(file).size <= maxBytes) return void 0;
|
|
590
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
591
|
+
let dest = join5(dirname(file), `events.${stamp}.jsonl`);
|
|
592
|
+
for (let n = 1; existsSync3(dest); n++) dest = join5(dirname(file), `events.${stamp}-${n}.jsonl`);
|
|
593
|
+
renameSync(file, dest);
|
|
594
|
+
return dest;
|
|
595
|
+
} finally {
|
|
596
|
+
rmSync(lock, { force: true });
|
|
597
|
+
}
|
|
598
|
+
} catch {
|
|
599
|
+
return void 0;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
var Emitter = class _Emitter {
|
|
603
|
+
cycle;
|
|
604
|
+
opts;
|
|
605
|
+
now;
|
|
606
|
+
step = "poll";
|
|
607
|
+
role;
|
|
608
|
+
constructor(opts) {
|
|
609
|
+
this.opts = opts;
|
|
610
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
611
|
+
this.cycle = opts.cycleId ?? this.now().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
612
|
+
for (const f of [opts.eventFile, opts.logFile]) {
|
|
613
|
+
if (!f) continue;
|
|
614
|
+
try {
|
|
615
|
+
mkdirSync2(dirname(f), { recursive: true });
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/** Everything emitted after this belongs to `step`, until the next one. */
|
|
621
|
+
enter(step, role) {
|
|
622
|
+
this.step = step;
|
|
623
|
+
this.role = role;
|
|
624
|
+
}
|
|
625
|
+
emit(message, extra = {}) {
|
|
626
|
+
const e = {
|
|
627
|
+
at: this.now().toISOString(),
|
|
628
|
+
cycle: this.cycle,
|
|
629
|
+
route: this.opts.route,
|
|
630
|
+
step: extra.step ?? this.step,
|
|
631
|
+
level: extra.level ?? "info",
|
|
632
|
+
message,
|
|
633
|
+
...extra.role ?? this.role ? { role: extra.role ?? this.role } : {},
|
|
634
|
+
...extra.ticket ? { ticket: extra.ticket } : {},
|
|
635
|
+
...extra.data ? { data: extra.data } : {}
|
|
636
|
+
};
|
|
637
|
+
const line = render(e);
|
|
638
|
+
try {
|
|
639
|
+
if (this.opts.eventFile && this.opts.rotateBytes) rotateEventsFile(this.opts.eventFile, this.opts.rotateBytes, this.now());
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
if (this.opts.eventFile) appendFileSync(this.opts.eventFile, `${JSON.stringify(e)}
|
|
644
|
+
`);
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
if (this.opts.logFile) appendFileSync(this.opts.logFile, `${line}
|
|
649
|
+
`);
|
|
650
|
+
} catch {
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
(this.opts.console ?? ((l) => process.stderr.write(`${l}
|
|
654
|
+
`)))(line);
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
return e;
|
|
658
|
+
}
|
|
659
|
+
warn(message, extra = {}) {
|
|
660
|
+
return this.emit(message, { ...extra, level: "warn" });
|
|
661
|
+
}
|
|
662
|
+
error(message, extra = {}) {
|
|
663
|
+
return this.emit(message, { ...extra, level: "error" });
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* A view of this emitter labelling every event with a different route,
|
|
667
|
+
* sharing the same sinks and cycle id. For a fleet-wide run, the top-level
|
|
668
|
+
* Emitter is stamped with just one route (see cli.ts) — a phase that
|
|
669
|
+
* actually knows which route it's working (release, per-repo poll) uses
|
|
670
|
+
* this instead, so its events aren't all attributed to `routes[0]`.
|
|
671
|
+
*/
|
|
672
|
+
forRoute(route) {
|
|
673
|
+
return new _Emitter({ ...this.opts, route, cycleId: this.cycle });
|
|
674
|
+
}
|
|
675
|
+
/** Times a step and emits its outcome, so a view can show duration. */
|
|
676
|
+
async timed(step, message, fn, role) {
|
|
677
|
+
this.enter(step, role);
|
|
678
|
+
const started = this.now().getTime();
|
|
679
|
+
try {
|
|
680
|
+
const result = await fn();
|
|
681
|
+
this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
|
|
682
|
+
return result;
|
|
683
|
+
} catch (err) {
|
|
684
|
+
this.error(`${message} \u2014 ${err.message}`, {
|
|
685
|
+
data: { ms: this.now().getTime() - started, ok: false }
|
|
686
|
+
});
|
|
687
|
+
throw err;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
var eventFileFor = (stateDir) => join5(stateDir, "events.jsonl");
|
|
692
|
+
|
|
693
|
+
// src/pair-install.ts
|
|
694
|
+
import { existsSync as existsSync5, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
695
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
696
|
+
import { homedir as homedir5 } from "os";
|
|
697
|
+
import { join as join7 } from "path";
|
|
698
|
+
|
|
675
699
|
// src/adapters/codex.ts
|
|
676
700
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
677
|
-
import { homedir as
|
|
678
|
-
import { join as
|
|
701
|
+
import { homedir as homedir4 } from "os";
|
|
702
|
+
import { join as join6 } from "path";
|
|
679
703
|
import { rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
680
704
|
|
|
681
705
|
// src/pair-files.ts
|
|
682
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as
|
|
706
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
|
|
683
707
|
import { dirname as dirname2 } from "path";
|
|
684
708
|
var PAIR_BLOCK_START = "<!-- crew:pair start -->";
|
|
685
709
|
var PAIR_BLOCK_END = "<!-- crew:pair end -->";
|
|
@@ -720,7 +744,7 @@ function removePairBlock(text) {
|
|
|
720
744
|
return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
721
745
|
}
|
|
722
746
|
function readIfExists(path) {
|
|
723
|
-
return existsSync4(path) ?
|
|
747
|
+
return existsSync4(path) ? readFileSync6(path, "utf8") : void 0;
|
|
724
748
|
}
|
|
725
749
|
function writeFileMkdir(path, text) {
|
|
726
750
|
mkdirSync3(dirname2(path), { recursive: true });
|
|
@@ -867,8 +891,8 @@ function codexTable(url) {
|
|
|
867
891
|
return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
|
|
868
892
|
}
|
|
869
893
|
var codexPaths = (home) => ({
|
|
870
|
-
instructions:
|
|
871
|
-
config:
|
|
894
|
+
instructions: join6(home, ".codex", "AGENTS.md"),
|
|
895
|
+
config: join6(home, ".codex", "config.toml")
|
|
872
896
|
});
|
|
873
897
|
function codexMcpState(config, url) {
|
|
874
898
|
const lines = config.split("\n");
|
|
@@ -884,7 +908,7 @@ var codexPair = {
|
|
|
884
908
|
harness: "codex",
|
|
885
909
|
displayName: "Codex",
|
|
886
910
|
installPair(o) {
|
|
887
|
-
const p = codexPaths(o.home ??
|
|
911
|
+
const p = codexPaths(o.home ?? homedir4());
|
|
888
912
|
const config = readIfExists(p.config) ?? "";
|
|
889
913
|
const st = codexMcpState(config, o.url);
|
|
890
914
|
if (st.state === "different" && !o.replace) {
|
|
@@ -901,7 +925,7 @@ var codexPair = {
|
|
|
901
925
|
return `pair(codex): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair block in ${p.instructions} and ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
|
|
902
926
|
},
|
|
903
927
|
verifyPair(o) {
|
|
904
|
-
const p = codexPaths(o.home ??
|
|
928
|
+
const p = codexPaths(o.home ?? homedir4());
|
|
905
929
|
const b = blockVerdict(p.instructions, "codex");
|
|
906
930
|
if (!b.ok) return b;
|
|
907
931
|
const st = codexMcpState(readIfExists(p.config) ?? "", o.url);
|
|
@@ -910,7 +934,7 @@ var codexPair = {
|
|
|
910
934
|
return { ok: true, detail: `${p.instructions}, ${p.config}` };
|
|
911
935
|
},
|
|
912
936
|
removePair(o) {
|
|
913
|
-
const p = codexPaths(o.home ??
|
|
937
|
+
const p = codexPaths(o.home ?? homedir4());
|
|
914
938
|
const hadBlock = removeBlockFile(p.instructions, o.dryRun);
|
|
915
939
|
const config = readIfExists(p.config);
|
|
916
940
|
const st = codexMcpState(config ?? "", o.url);
|
|
@@ -996,11 +1020,11 @@ var codexAdapter = {
|
|
|
996
1020
|
extractTurnTokens: extractCodexTurnTokens,
|
|
997
1021
|
inspectMcp(o) {
|
|
998
1022
|
if (binMissing(o.bin)) return { state: "no-binary" };
|
|
999
|
-
const st = codexMcpState(readIfExists(codexPaths(o.home ??
|
|
1023
|
+
const st = codexMcpState(readIfExists(codexPaths(o.home ?? homedir4()).config) ?? "", o.url);
|
|
1000
1024
|
return st.state === "registered" ? { state: "registered", url: o.url } : st;
|
|
1001
1025
|
},
|
|
1002
1026
|
ensureMcp(o) {
|
|
1003
|
-
const p = codexPaths(o.home ??
|
|
1027
|
+
const p = codexPaths(o.home ?? homedir4());
|
|
1004
1028
|
const config = readIfExists(p.config) ?? "";
|
|
1005
1029
|
const st = codexMcpState(config, o.url);
|
|
1006
1030
|
if (st.state === "registered") return `mcp: ${SERVER} already registered with Codex \u2192 ${o.url}`;
|
|
@@ -1015,8 +1039,8 @@ var codexAdapter = {
|
|
|
1015
1039
|
|
|
1016
1040
|
// src/pair-install.ts
|
|
1017
1041
|
var geminiPaths = (home) => ({
|
|
1018
|
-
instructions:
|
|
1019
|
-
config:
|
|
1042
|
+
instructions: join7(home, ".gemini", "GEMINI.md"),
|
|
1043
|
+
config: join7(home, ".gemini", "settings.json")
|
|
1020
1044
|
});
|
|
1021
1045
|
function readGeminiSettings(path) {
|
|
1022
1046
|
const text = readIfExists(path);
|
|
@@ -1037,14 +1061,14 @@ function geminiMcpState(s, url) {
|
|
|
1037
1061
|
return { state: "registered" };
|
|
1038
1062
|
}
|
|
1039
1063
|
function inspectGeminiMcp(o) {
|
|
1040
|
-
const p = geminiPaths(o.home ??
|
|
1064
|
+
const p = geminiPaths(o.home ?? homedir5());
|
|
1041
1065
|
const settings = readGeminiSettings(p.config);
|
|
1042
1066
|
if (!settings) return { state: "different", detail: `${p.config} is not a JSON object` };
|
|
1043
1067
|
const st = geminiMcpState(settings, o.url);
|
|
1044
1068
|
return st.state === "registered" ? { state: "registered", url: o.url } : st;
|
|
1045
1069
|
}
|
|
1046
1070
|
function ensureGeminiMcp(o) {
|
|
1047
|
-
const p = geminiPaths(o.home ??
|
|
1071
|
+
const p = geminiPaths(o.home ?? homedir5());
|
|
1048
1072
|
const settings = readGeminiSettings(p.config);
|
|
1049
1073
|
if (!settings) return `gemini: ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
1050
1074
|
const st = geminiMcpState(settings, o.url);
|
|
@@ -1062,7 +1086,7 @@ var geminiPair = {
|
|
|
1062
1086
|
harness: "gemini",
|
|
1063
1087
|
displayName: "Gemini CLI",
|
|
1064
1088
|
installPair(o) {
|
|
1065
|
-
const p = geminiPaths(o.home ??
|
|
1089
|
+
const p = geminiPaths(o.home ?? homedir5());
|
|
1066
1090
|
const settings = readGeminiSettings(p.config);
|
|
1067
1091
|
if (!settings) return `pair(gemini): ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
1068
1092
|
const st = geminiMcpState(settings, o.url);
|
|
@@ -1081,7 +1105,7 @@ var geminiPair = {
|
|
|
1081
1105
|
return `pair(gemini): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair block in ${p.instructions} and ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
|
|
1082
1106
|
},
|
|
1083
1107
|
verifyPair(o) {
|
|
1084
|
-
const p = geminiPaths(o.home ??
|
|
1108
|
+
const p = geminiPaths(o.home ?? homedir5());
|
|
1085
1109
|
const b = blockVerdict(p.instructions, "gemini");
|
|
1086
1110
|
if (!b.ok) return b;
|
|
1087
1111
|
const settings = readGeminiSettings(p.config);
|
|
@@ -1092,7 +1116,7 @@ var geminiPair = {
|
|
|
1092
1116
|
return { ok: true, detail: `${p.instructions}, ${p.config}` };
|
|
1093
1117
|
},
|
|
1094
1118
|
removePair(o) {
|
|
1095
|
-
const p = geminiPaths(o.home ??
|
|
1119
|
+
const p = geminiPaths(o.home ?? homedir5());
|
|
1096
1120
|
const hadBlock = removeBlockFile(p.instructions, o.dryRun);
|
|
1097
1121
|
const settings = readGeminiSettings(p.config);
|
|
1098
1122
|
const st = settings ? geminiMcpState(settings, o.url) : { state: "missing" };
|
|
@@ -1113,10 +1137,10 @@ var geminiPair = {
|
|
|
1113
1137
|
};
|
|
1114
1138
|
var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
|
|
1115
1139
|
function vscodeUserDirs(o) {
|
|
1116
|
-
const home = o.home ??
|
|
1140
|
+
const home = o.home ?? homedir5();
|
|
1117
1141
|
const platform2 = o.platform ?? process.platform;
|
|
1118
|
-
const root = platform2 === "darwin" ?
|
|
1119
|
-
const all = VSCODE_FLAVOURS.map((f) =>
|
|
1142
|
+
const root = platform2 === "darwin" ? join7(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join7(home, "AppData", "Roaming") : join7(home, ".config");
|
|
1143
|
+
const all = VSCODE_FLAVOURS.map((f) => join7(root, f, "User"));
|
|
1120
1144
|
const present = all.filter((d) => existsSync5(d));
|
|
1121
1145
|
return present.length > 0 ? present : [all[0]];
|
|
1122
1146
|
}
|
|
@@ -1164,8 +1188,8 @@ var copilotPair = {
|
|
|
1164
1188
|
installPair(o) {
|
|
1165
1189
|
const lines = [];
|
|
1166
1190
|
for (const dir of vscodeUserDirs(o)) {
|
|
1167
|
-
const mcpPath =
|
|
1168
|
-
const instrPath =
|
|
1191
|
+
const mcpPath = join7(dir, "mcp.json");
|
|
1192
|
+
const instrPath = join7(dir, "prompts", "crew-pair.instructions.md");
|
|
1169
1193
|
const settings = readVscodeMcp(mcpPath);
|
|
1170
1194
|
if (!settings) {
|
|
1171
1195
|
lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
|
|
@@ -1193,8 +1217,8 @@ var copilotPair = {
|
|
|
1193
1217
|
},
|
|
1194
1218
|
verifyPair(o) {
|
|
1195
1219
|
const verdicts = vscodeUserDirs(o).map((dir) => {
|
|
1196
|
-
const instrPath =
|
|
1197
|
-
const mcpPath =
|
|
1220
|
+
const instrPath = join7(dir, "prompts", "crew-pair.instructions.md");
|
|
1221
|
+
const mcpPath = join7(dir, "mcp.json");
|
|
1198
1222
|
const b = blockVerdict(instrPath, "copilot");
|
|
1199
1223
|
if (!b.ok) return b;
|
|
1200
1224
|
const settings = readVscodeMcp(mcpPath);
|
|
@@ -1209,8 +1233,8 @@ var copilotPair = {
|
|
|
1209
1233
|
removePair(o) {
|
|
1210
1234
|
const lines = [];
|
|
1211
1235
|
for (const dir of vscodeUserDirs(o)) {
|
|
1212
|
-
const mcpPath =
|
|
1213
|
-
const hadBlock = removeCopilotInstructions(
|
|
1236
|
+
const mcpPath = join7(dir, "mcp.json");
|
|
1237
|
+
const hadBlock = removeCopilotInstructions(join7(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
|
|
1214
1238
|
const settings = readVscodeMcp(mcpPath);
|
|
1215
1239
|
const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
|
|
1216
1240
|
const hadMcp = st.state === "registered";
|
|
@@ -1509,8 +1533,8 @@ function explain(ship, required) {
|
|
|
1509
1533
|
}
|
|
1510
1534
|
|
|
1511
1535
|
// src/repo-config.ts
|
|
1512
|
-
import { readFileSync as
|
|
1513
|
-
import { join as
|
|
1536
|
+
import { readFileSync as readFileSync7, existsSync as existsSync6 } from "fs";
|
|
1537
|
+
import { join as join8, basename as basename2 } from "path";
|
|
1514
1538
|
import { parse } from "yaml";
|
|
1515
1539
|
var SPEC_VERSION = 1;
|
|
1516
1540
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -1593,7 +1617,7 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
1593
1617
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
1594
1618
|
function findRepoConfig(dir) {
|
|
1595
1619
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
1596
|
-
const p =
|
|
1620
|
+
const p = join8(dir, name);
|
|
1597
1621
|
if (existsSync6(p)) return p;
|
|
1598
1622
|
}
|
|
1599
1623
|
return null;
|
|
@@ -1832,7 +1856,7 @@ function parseRepoConfig(text, file) {
|
|
|
1832
1856
|
function loadRepoConfig(dir) {
|
|
1833
1857
|
const file = findRepoConfig(dir);
|
|
1834
1858
|
if (!file) return null;
|
|
1835
|
-
return parseRepoConfig(
|
|
1859
|
+
return parseRepoConfig(readFileSync7(file, "utf8"), file);
|
|
1836
1860
|
}
|
|
1837
1861
|
function hookLabel(cfg, hook2) {
|
|
1838
1862
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -2281,16 +2305,16 @@ function mergeRouteRelease(route, override) {
|
|
|
2281
2305
|
};
|
|
2282
2306
|
}
|
|
2283
2307
|
function expand(p, base) {
|
|
2284
|
-
let out = p.startsWith("~") ? p.replace(/^~/,
|
|
2285
|
-
out = out.replace(/\$\{?HOME\}?/g,
|
|
2308
|
+
let out = p.startsWith("~") ? p.replace(/^~/, homedir6()) : p;
|
|
2309
|
+
out = out.replace(/\$\{?HOME\}?/g, homedir6());
|
|
2286
2310
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
2287
2311
|
}
|
|
2288
2312
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
2289
|
-
if (!promptSet) return
|
|
2313
|
+
if (!promptSet) return join9(crewHome, "prompts", "default");
|
|
2290
2314
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
2291
2315
|
return expand(promptSet, base);
|
|
2292
2316
|
}
|
|
2293
|
-
return
|
|
2317
|
+
return join9(crewHome, "prompts", promptSet);
|
|
2294
2318
|
}
|
|
2295
2319
|
var Missing = class {
|
|
2296
2320
|
paths = [];
|
|
@@ -2314,7 +2338,7 @@ var Missing = class {
|
|
|
2314
2338
|
};
|
|
2315
2339
|
function crewVersion(crewHome) {
|
|
2316
2340
|
try {
|
|
2317
|
-
const pkg = JSON.parse(
|
|
2341
|
+
const pkg = JSON.parse(readFileSync8(join9(crewHome, "package.json"), "utf8"));
|
|
2318
2342
|
return pkg.version ?? "0.0.0";
|
|
2319
2343
|
} catch {
|
|
2320
2344
|
return "0.0.0";
|
|
@@ -2324,8 +2348,8 @@ function configSearchPath(crewHome) {
|
|
|
2324
2348
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
2325
2349
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2326
2350
|
return [
|
|
2327
|
-
...xdg ? [
|
|
2328
|
-
|
|
2351
|
+
...xdg ? [join9(xdg, "crew", "crew.yaml")] : [],
|
|
2352
|
+
join9(homedir6(), ".config", "crew", "crew.yaml"),
|
|
2329
2353
|
resolve(crewHome, "crew.yaml")
|
|
2330
2354
|
];
|
|
2331
2355
|
}
|
|
@@ -2333,17 +2357,17 @@ function findConfigFile(crewHome) {
|
|
|
2333
2357
|
const candidates = configSearchPath(crewHome);
|
|
2334
2358
|
return candidates.find((c) => existsSync7(c)) ?? candidates[candidates.length - 1];
|
|
2335
2359
|
}
|
|
2336
|
-
var defaultConfigPath = () =>
|
|
2360
|
+
var defaultConfigPath = () => join9(process.env.XDG_CONFIG_HOME ?? join9(homedir6(), ".config"), "crew", "crew.yaml");
|
|
2337
2361
|
function routeSlug(route) {
|
|
2338
2362
|
return route.replace(/\//g, "-");
|
|
2339
2363
|
}
|
|
2340
2364
|
function resolvedPathFor(stateDir, route) {
|
|
2341
2365
|
const [workspace, project] = splitRoute(route);
|
|
2342
|
-
return
|
|
2366
|
+
return join9(stateDir, "resolved", workspace, `${project}.json`);
|
|
2343
2367
|
}
|
|
2344
2368
|
function apiKeyPathFor(stateDir, route) {
|
|
2345
2369
|
const [workspace, project] = splitRoute(route);
|
|
2346
|
-
return
|
|
2370
|
+
return join9(stateDir, "keys", workspace, `${project}.env`);
|
|
2347
2371
|
}
|
|
2348
2372
|
function splitRoute(route) {
|
|
2349
2373
|
const parts = route.split("/");
|
|
@@ -2356,7 +2380,7 @@ function readResolvedFile(stateDir, route) {
|
|
|
2356
2380
|
const path = resolvedPathFor(stateDir, route);
|
|
2357
2381
|
if (!existsSync7(path)) return void 0;
|
|
2358
2382
|
try {
|
|
2359
|
-
return JSON.parse(
|
|
2383
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
2360
2384
|
} catch (e) {
|
|
2361
2385
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
2362
2386
|
}
|
|
@@ -2421,12 +2445,12 @@ function loadConfig(crewHome, configFile, opts = {}) {
|
|
|
2421
2445
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
2422
2446
|
|
|
2423
2447
|
Create the first of those:
|
|
2424
|
-
mkdir -p ${dirname3(defaultConfigPath())} && cp ${
|
|
2448
|
+
mkdir -p ${dirname3(defaultConfigPath())} && cp ${join9(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
2425
2449
|
);
|
|
2426
2450
|
}
|
|
2427
2451
|
let raw;
|
|
2428
2452
|
try {
|
|
2429
|
-
raw = parse2(opts.text ??
|
|
2453
|
+
raw = parse2(opts.text ?? readFileSync8(file, "utf8"));
|
|
2430
2454
|
} catch (e) {
|
|
2431
2455
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
2432
2456
|
}
|
|
@@ -2504,7 +2528,7 @@ Create the first of those:
|
|
|
2504
2528
|
useNvm: shipRaw.useNvm !== false,
|
|
2505
2529
|
nvmSh: shipRaw.nvmSh,
|
|
2506
2530
|
stateDir,
|
|
2507
|
-
logFile: expand(shipRaw.logFile ??
|
|
2531
|
+
logFile: expand(shipRaw.logFile ?? join9(tmpdir(), "crew.log"), base),
|
|
2508
2532
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
2509
2533
|
maxConcurrentAgents,
|
|
2510
2534
|
streamRetentionDays,
|
|
@@ -2526,7 +2550,7 @@ Create the first of those:
|
|
|
2526
2550
|
function expandCommand(cmd, base) {
|
|
2527
2551
|
return cmd.includes("/") || cmd.startsWith("~") ? expand(cmd, base) : cmd;
|
|
2528
2552
|
}
|
|
2529
|
-
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "launcherBin", "model", "tiers", "args", "env", "maxThinkingTokens"]);
|
|
2553
|
+
var TOOLCHAIN_KEYS = /* @__PURE__ */ new Set(["harness", "bin", "launcher", "launcherBin", "model", "tiers", "args", "env", "maxThinkingTokens", "auth"]);
|
|
2530
2554
|
function parsePair(raw, missing) {
|
|
2531
2555
|
if (raw === void 0) return void 0;
|
|
2532
2556
|
if (!Array.isArray(raw)) {
|
|
@@ -2583,9 +2607,20 @@ function parseToolchains(raw, base, missing, file) {
|
|
|
2583
2607
|
if (!Number.isInteger(maxThinkingTokens) || maxThinkingTokens < 0) {
|
|
2584
2608
|
missing.add(`${where}.maxThinkingTokens must be a non-negative integer (got "${entryRaw.maxThinkingTokens}")`);
|
|
2585
2609
|
}
|
|
2610
|
+
let auth;
|
|
2611
|
+
if (entryRaw.auth !== void 0) {
|
|
2612
|
+
if (entryRaw.auth !== "subscription" && entryRaw.auth !== "inherit") {
|
|
2613
|
+
missing.add(`${where}.auth must be subscription|inherit (got "${entryRaw.auth}")`);
|
|
2614
|
+
} else if (harness !== "claude") {
|
|
2615
|
+
missing.add(`${where}.auth is only supported on the claude harness`);
|
|
2616
|
+
} else {
|
|
2617
|
+
auth = entryRaw.auth;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2586
2620
|
out.push({
|
|
2587
2621
|
name,
|
|
2588
2622
|
harness,
|
|
2623
|
+
...auth ? { auth } : {},
|
|
2589
2624
|
bin: expandCommand(String(entryRaw.bin ?? adapter.defaultBin), base),
|
|
2590
2625
|
...entryRaw.launcher ? { launcher: expandCommand(String(entryRaw.launcher), base) } : {},
|
|
2591
2626
|
...entryRaw.launcher && entryRaw.launcherBin ? { launcherBin: expandCommand(String(entryRaw.launcherBin), base) } : {},
|
|
@@ -2645,7 +2680,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
2645
2680
|
const hasRepos = Object.keys(repoDirs).length > 0;
|
|
2646
2681
|
const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
|
|
2647
2682
|
const dirIsCloneBase = !c?.dir && !hasRepos && c?.repos && typeof c.repos === "object" && !Array.isArray(c.repos);
|
|
2648
|
-
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ?
|
|
2683
|
+
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join9(reposBasePath, splitRoute(route)[0]) : missing.req(void 0, `${where}.dir (or ${where}.repos)`) ?? "";
|
|
2649
2684
|
const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
|
|
2650
2685
|
if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
|
|
2651
2686
|
missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
|
|
@@ -2729,7 +2764,7 @@ function repoIdForName(r, name) {
|
|
|
2729
2764
|
function resolveApiKey(r) {
|
|
2730
2765
|
if (r.apiKey) return r.apiKey;
|
|
2731
2766
|
if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
|
|
2732
|
-
for (const line of
|
|
2767
|
+
for (const line of readFileSync8(r.apiKeyFile, "utf8").split("\n")) {
|
|
2733
2768
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
2734
2769
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
2735
2770
|
}
|
|
@@ -2744,7 +2779,7 @@ function credentialSource(r) {
|
|
|
2744
2779
|
if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
|
|
2745
2780
|
if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
|
|
2746
2781
|
const re = new RegExp(`^${r.apiKeyVar}=`);
|
|
2747
|
-
if (
|
|
2782
|
+
if (readFileSync8(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
|
|
2748
2783
|
}
|
|
2749
2784
|
return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
|
|
2750
2785
|
}
|
|
@@ -2800,7 +2835,7 @@ function configuredMembers(rt) {
|
|
|
2800
2835
|
return out;
|
|
2801
2836
|
}
|
|
2802
2837
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
2803
|
-
return
|
|
2838
|
+
return join9(basePath, workspace, repoName);
|
|
2804
2839
|
}
|
|
2805
2840
|
function reposOf(r) {
|
|
2806
2841
|
const named = Object.entries(r.repos);
|
|
@@ -2833,7 +2868,7 @@ function passengerRepoTargets(r) {
|
|
|
2833
2868
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
2834
2869
|
}
|
|
2835
2870
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
2836
|
-
return
|
|
2871
|
+
return join9(basePath, workspace, ".passenger-checkouts", repoName);
|
|
2837
2872
|
}
|
|
2838
2873
|
function passengerCheckoutTargets(r) {
|
|
2839
2874
|
if (!r.hostPassengers) return [];
|
|
@@ -3181,8 +3216,8 @@ function renderEnvironment(i) {
|
|
|
3181
3216
|
}
|
|
3182
3217
|
|
|
3183
3218
|
// src/agent-log.ts
|
|
3184
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as
|
|
3185
|
-
import { join as
|
|
3219
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync9, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3220
|
+
import { join as join10 } from "path";
|
|
3186
3221
|
|
|
3187
3222
|
// src/pending-backoff.ts
|
|
3188
3223
|
var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
|
|
@@ -3249,20 +3284,20 @@ async function reportAgentRun(target, run2) {
|
|
|
3249
3284
|
}
|
|
3250
3285
|
return { agentLogId: entry.id };
|
|
3251
3286
|
}
|
|
3252
|
-
var queueDir = (stateDir) =>
|
|
3287
|
+
var queueDir = (stateDir) => join10(stateDir, "pending-agent-logs");
|
|
3253
3288
|
function queueAgentRun(stateDir, name, q) {
|
|
3254
3289
|
mkdirSync4(queueDir(stateDir), { recursive: true });
|
|
3255
|
-
writeFileSync5(
|
|
3290
|
+
writeFileSync5(join10(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
3256
3291
|
}
|
|
3257
3292
|
async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
3258
3293
|
const dir = queueDir(stateDir);
|
|
3259
3294
|
if (!existsSync8(dir)) return 0;
|
|
3260
3295
|
let sent = 0;
|
|
3261
3296
|
for (const f of readdirSync2(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
3262
|
-
const path =
|
|
3297
|
+
const path = join10(dir, f);
|
|
3263
3298
|
let q;
|
|
3264
3299
|
try {
|
|
3265
|
-
q = JSON.parse(
|
|
3300
|
+
q = JSON.parse(readFileSync9(path, "utf8"));
|
|
3266
3301
|
} catch {
|
|
3267
3302
|
continue;
|
|
3268
3303
|
}
|
|
@@ -3462,8 +3497,8 @@ var HandoffTranscriptTracker = class {
|
|
|
3462
3497
|
};
|
|
3463
3498
|
|
|
3464
3499
|
// src/pending-handoff.ts
|
|
3465
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync8, readdirSync as readdirSync5, readFileSync as
|
|
3466
|
-
import { join as
|
|
3500
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync8, readdirSync as readdirSync5, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
3501
|
+
import { join as join16 } from "path";
|
|
3467
3502
|
|
|
3468
3503
|
// src/git.ts
|
|
3469
3504
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -3475,12 +3510,12 @@ import {
|
|
|
3475
3510
|
writeSync,
|
|
3476
3511
|
statSync as statSync2,
|
|
3477
3512
|
unlinkSync,
|
|
3478
|
-
readFileSync as
|
|
3513
|
+
readFileSync as readFileSync10,
|
|
3479
3514
|
writeFileSync as writeFileSync6,
|
|
3480
3515
|
readdirSync as readdirSync3,
|
|
3481
3516
|
rmSync as rmSync6
|
|
3482
3517
|
} from "fs";
|
|
3483
|
-
import { dirname as dirname4, basename as basename5, join as
|
|
3518
|
+
import { dirname as dirname4, basename as basename5, join as join11 } from "path";
|
|
3484
3519
|
var GitError = class extends Error {
|
|
3485
3520
|
};
|
|
3486
3521
|
function streamText(x) {
|
|
@@ -3878,11 +3913,11 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
3878
3913
|
}
|
|
3879
3914
|
for (const name of names) {
|
|
3880
3915
|
if (!name.startsWith("crew-release-")) continue;
|
|
3881
|
-
const root =
|
|
3916
|
+
const root = join11(tmp, name);
|
|
3882
3917
|
try {
|
|
3883
3918
|
const st = statSync2(root);
|
|
3884
3919
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
3885
|
-
if (existsSync9(
|
|
3920
|
+
if (existsSync9(join11(root, "checkout"))) continue;
|
|
3886
3921
|
rmSync6(root, { recursive: true, force: true });
|
|
3887
3922
|
removed.push(name);
|
|
3888
3923
|
} catch {
|
|
@@ -3996,8 +4031,8 @@ function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {
|
|
|
3996
4031
|
if (!existsSync9(`${dir}/.git`) && githubRemote) {
|
|
3997
4032
|
const url = cloneUrlFor(githubRemote, clone);
|
|
3998
4033
|
const marker = cloneFailureMarker(dir);
|
|
3999
|
-
if (existsSync9(marker) &&
|
|
4000
|
-
return { action: "clone-failed", detail:
|
|
4034
|
+
if (existsSync9(marker) && readFileSync10(marker, "utf8").split("\n")[0] === url) {
|
|
4035
|
+
return { action: "clone-failed", detail: readFileSync10(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
|
|
4001
4036
|
}
|
|
4002
4037
|
try {
|
|
4003
4038
|
ensureRepoCheckout(dir, githubRemote, clone);
|
|
@@ -4239,7 +4274,7 @@ function describeComposition(c) {
|
|
|
4239
4274
|
|
|
4240
4275
|
${rows}
|
|
4241
4276
|
|
|
4242
|
-
The gate runs on the base plus every verified branch above, not on the base alone: reproduce on that composition.`;
|
|
4277
|
+
The gate runs on the base plus every verified branch above, not on the base alone: reproduce on that composition. A fix that compiles only on the composition belongs on one of those branches (the one owning the failing file, after merging the others into it), not on a new branch cut from the base: a separate fix branch conflicts with the branches it contains when the release squash-merges them all. Re-queue the touched ticket to QA and close this alert with a note.`;
|
|
4243
4278
|
}
|
|
4244
4279
|
|
|
4245
4280
|
// src/shell.ts
|
|
@@ -4424,9 +4459,9 @@ function recoveredFailureHeadlines(o, route) {
|
|
|
4424
4459
|
}
|
|
4425
4460
|
|
|
4426
4461
|
// src/install.ts
|
|
4427
|
-
import { dirname as dirname5, join as
|
|
4428
|
-
import { homedir as
|
|
4429
|
-
import { accessSync, constants, existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as
|
|
4462
|
+
import { dirname as dirname5, join as join12, extname } from "path";
|
|
4463
|
+
import { homedir as homedir7 } from "os";
|
|
4464
|
+
import { accessSync, constants, existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync11, statSync as statSync3, writeFileSync as writeFileSync7, unlinkSync as unlinkSync2, realpathSync } from "fs";
|
|
4430
4465
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
4431
4466
|
import { createHash as createHash2 } from "crypto";
|
|
4432
4467
|
|
|
@@ -4438,7 +4473,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
4438
4473
|
// src/install.ts
|
|
4439
4474
|
function crewInvocation(crewHome) {
|
|
4440
4475
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
4441
|
-
return [process.execPath,
|
|
4476
|
+
return [process.execPath, join12(crewHome, "bin", "crew")];
|
|
4442
4477
|
}
|
|
4443
4478
|
var INTERVAL_SECONDS = 120;
|
|
4444
4479
|
function labelFor(crewHome, job) {
|
|
@@ -4476,7 +4511,7 @@ function pathFor(ship) {
|
|
|
4476
4511
|
return parts.join(":");
|
|
4477
4512
|
}
|
|
4478
4513
|
function findOnPath(bin, searchPath) {
|
|
4479
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
4514
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join12(dir, bin));
|
|
4480
4515
|
for (const candidate of candidates) {
|
|
4481
4516
|
try {
|
|
4482
4517
|
accessSync(candidate, constants.X_OK);
|
|
@@ -4486,7 +4521,7 @@ function findOnPath(bin, searchPath) {
|
|
|
4486
4521
|
}
|
|
4487
4522
|
return void 0;
|
|
4488
4523
|
}
|
|
4489
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
4524
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join12(homedir7(), ".docker", "bin")].join(":");
|
|
4490
4525
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
4491
4526
|
const schedulerPath = pathFor(ship);
|
|
4492
4527
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -4498,7 +4533,7 @@ function xmlEscape(s) {
|
|
|
4498
4533
|
}
|
|
4499
4534
|
function planLaunchd(ship, crewHome, job) {
|
|
4500
4535
|
const label = labelFor(crewHome, job);
|
|
4501
|
-
const unitPath =
|
|
4536
|
+
const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
|
|
4502
4537
|
const crewLog = ship.logFile;
|
|
4503
4538
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
4504
4539
|
const persistent = job === "run";
|
|
@@ -4552,8 +4587,8 @@ ${scheduleKeys}
|
|
|
4552
4587
|
}
|
|
4553
4588
|
function planSystemd(ship, crewHome, job) {
|
|
4554
4589
|
const label = labelFor(crewHome, job);
|
|
4555
|
-
const unitDir =
|
|
4556
|
-
const servicePath =
|
|
4590
|
+
const unitDir = join12(homedir7(), ".config", "systemd", "user");
|
|
4591
|
+
const servicePath = join12(unitDir, `${label}.service`);
|
|
4557
4592
|
const crewLog = ship.logFile;
|
|
4558
4593
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
4559
4594
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -4593,7 +4628,7 @@ WantedBy=default.target
|
|
|
4593
4628
|
schedulerLog
|
|
4594
4629
|
};
|
|
4595
4630
|
}
|
|
4596
|
-
const timerPath =
|
|
4631
|
+
const timerPath = join12(unitDir, `${label}.timer`);
|
|
4597
4632
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
4598
4633
|
[Unit]
|
|
4599
4634
|
Description=${description}
|
|
@@ -4667,7 +4702,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
4667
4702
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
4668
4703
|
const label = labelFor(crewHome, job);
|
|
4669
4704
|
if (hostShip === "macos") {
|
|
4670
|
-
const unitPath =
|
|
4705
|
+
const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
|
|
4671
4706
|
return {
|
|
4672
4707
|
mechanism: "launchd",
|
|
4673
4708
|
unitPaths: [unitPath],
|
|
@@ -4677,18 +4712,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
4677
4712
|
};
|
|
4678
4713
|
}
|
|
4679
4714
|
if (hostShip === "linux" && hasSystemd) {
|
|
4680
|
-
const unitDir =
|
|
4715
|
+
const unitDir = join12(homedir7(), ".config", "systemd", "user");
|
|
4681
4716
|
if (job === "run") {
|
|
4682
4717
|
return {
|
|
4683
4718
|
mechanism: "systemd",
|
|
4684
|
-
unitPaths: [
|
|
4719
|
+
unitPaths: [join12(unitDir, `${label}.service`)],
|
|
4685
4720
|
crontabLine: null,
|
|
4686
4721
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
4687
4722
|
};
|
|
4688
4723
|
}
|
|
4689
4724
|
return {
|
|
4690
4725
|
mechanism: "systemd",
|
|
4691
|
-
unitPaths: [
|
|
4726
|
+
unitPaths: [join12(unitDir, `${label}.service`), join12(unitDir, `${label}.timer`)],
|
|
4692
4727
|
crontabLine: null,
|
|
4693
4728
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
4694
4729
|
};
|
|
@@ -4706,7 +4741,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
4706
4741
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
4707
4742
|
const label = labelFor(crewHome, "run");
|
|
4708
4743
|
if (hostShip === "macos") {
|
|
4709
|
-
const unitPath =
|
|
4744
|
+
const unitPath = join12(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
|
|
4710
4745
|
return {
|
|
4711
4746
|
mechanism: "launchd",
|
|
4712
4747
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -4820,7 +4855,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
4820
4855
|
}
|
|
4821
4856
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
4822
4857
|
function launchAgentsDir() {
|
|
4823
|
-
return
|
|
4858
|
+
return join12(homedir7(), "Library", "LaunchAgents");
|
|
4824
4859
|
}
|
|
4825
4860
|
function xmlUnescape(s) {
|
|
4826
4861
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -4839,10 +4874,10 @@ function loadedCrewLabels() {
|
|
|
4839
4874
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
4840
4875
|
const files = existsSync10(dir) ? readdirSync4(dir) : [];
|
|
4841
4876
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
4842
|
-
const plistPath =
|
|
4877
|
+
const plistPath = join12(dir, f);
|
|
4843
4878
|
let program = null;
|
|
4844
4879
|
try {
|
|
4845
|
-
program = programFromPlist(
|
|
4880
|
+
program = programFromPlist(readFileSync11(plistPath, "utf8"));
|
|
4846
4881
|
} catch {
|
|
4847
4882
|
}
|
|
4848
4883
|
return {
|
|
@@ -4855,7 +4890,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
|
4855
4890
|
const seen = new Set(units.map((u) => u.label));
|
|
4856
4891
|
for (const label of [...loaded].sort()) {
|
|
4857
4892
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
4858
|
-
units.push({ label, plistPath:
|
|
4893
|
+
units.push({ label, plistPath: join12(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
4859
4894
|
}
|
|
4860
4895
|
return units;
|
|
4861
4896
|
}
|
|
@@ -5201,8 +5236,8 @@ var SHELL_WORDS = /* @__PURE__ */ new Set([
|
|
|
5201
5236
|
]);
|
|
5202
5237
|
|
|
5203
5238
|
// src/state.ts
|
|
5204
|
-
import { readFileSync as
|
|
5205
|
-
import { join as
|
|
5239
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync7, statSync as statSync4 } from "fs";
|
|
5240
|
+
import { join as join13 } from "path";
|
|
5206
5241
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
5207
5242
|
var State = class _State {
|
|
5208
5243
|
dir;
|
|
@@ -5211,7 +5246,7 @@ var State = class _State {
|
|
|
5211
5246
|
mkdirSync7(dir, { recursive: true });
|
|
5212
5247
|
}
|
|
5213
5248
|
path(...parts) {
|
|
5214
|
-
return
|
|
5249
|
+
return join13(this.dir, ...parts);
|
|
5215
5250
|
}
|
|
5216
5251
|
/**
|
|
5217
5252
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -5222,7 +5257,7 @@ var State = class _State {
|
|
|
5222
5257
|
*/
|
|
5223
5258
|
watermark() {
|
|
5224
5259
|
try {
|
|
5225
|
-
const v =
|
|
5260
|
+
const v = readFileSync12(this.path(".poll-watermark"), "utf8").trim();
|
|
5226
5261
|
return v || EPOCH;
|
|
5227
5262
|
} catch {
|
|
5228
5263
|
return EPOCH;
|
|
@@ -5308,7 +5343,7 @@ var State = class _State {
|
|
|
5308
5343
|
const name = _State.safe(rawName);
|
|
5309
5344
|
const file = this.path(`.${name}.lock`);
|
|
5310
5345
|
try {
|
|
5311
|
-
const existing = Number.parseInt(
|
|
5346
|
+
const existing = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
|
|
5312
5347
|
if (Number.isFinite(existing) && existing > 0) {
|
|
5313
5348
|
try {
|
|
5314
5349
|
process.kill(existing, 0);
|
|
@@ -5402,7 +5437,7 @@ var State = class _State {
|
|
|
5402
5437
|
const heldReported = `.release-held-${suffix}`;
|
|
5403
5438
|
const read = (f) => {
|
|
5404
5439
|
try {
|
|
5405
|
-
return
|
|
5440
|
+
return readFileSync12(this.path(f), "utf8").trim() || null;
|
|
5406
5441
|
} catch {
|
|
5407
5442
|
return null;
|
|
5408
5443
|
}
|
|
@@ -5452,7 +5487,7 @@ var State = class _State {
|
|
|
5452
5487
|
/** Whether the board was already told about this head's red gate. */
|
|
5453
5488
|
heldReported: () => {
|
|
5454
5489
|
try {
|
|
5455
|
-
return
|
|
5490
|
+
return readFileSync12(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
5456
5491
|
} catch {
|
|
5457
5492
|
return null;
|
|
5458
5493
|
}
|
|
@@ -5489,7 +5524,7 @@ var State = class _State {
|
|
|
5489
5524
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
5490
5525
|
const read = (f) => {
|
|
5491
5526
|
try {
|
|
5492
|
-
return
|
|
5527
|
+
return readFileSync12(this.path(f), "utf8").trim() || null;
|
|
5493
5528
|
} catch {
|
|
5494
5529
|
return null;
|
|
5495
5530
|
}
|
|
@@ -5554,7 +5589,7 @@ var State = class _State {
|
|
|
5554
5589
|
return {
|
|
5555
5590
|
previous: () => {
|
|
5556
5591
|
try {
|
|
5557
|
-
const parsed = JSON.parse(
|
|
5592
|
+
const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
|
|
5558
5593
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5559
5594
|
} catch {
|
|
5560
5595
|
return {};
|
|
@@ -5575,7 +5610,7 @@ var State = class _State {
|
|
|
5575
5610
|
return {
|
|
5576
5611
|
previous: () => {
|
|
5577
5612
|
try {
|
|
5578
|
-
const parsed = JSON.parse(
|
|
5613
|
+
const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
|
|
5579
5614
|
return Array.isArray(parsed) ? parsed : [];
|
|
5580
5615
|
} catch {
|
|
5581
5616
|
return [];
|
|
@@ -5608,7 +5643,7 @@ var State = class _State {
|
|
|
5608
5643
|
}
|
|
5609
5644
|
readInt(file) {
|
|
5610
5645
|
try {
|
|
5611
|
-
return Number.parseInt(
|
|
5646
|
+
return Number.parseInt(readFileSync12(this.path(file), "utf8").trim(), 10) || 0;
|
|
5612
5647
|
} catch {
|
|
5613
5648
|
return 0;
|
|
5614
5649
|
}
|
|
@@ -6611,12 +6646,12 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
|
|
|
6611
6646
|
|
|
6612
6647
|
// src/run-worktree.ts
|
|
6613
6648
|
import { existsSync as existsSync14 } from "fs";
|
|
6614
|
-
import { dirname as dirname6, join as
|
|
6649
|
+
import { dirname as dirname6, join as join15 } from "path";
|
|
6615
6650
|
|
|
6616
6651
|
// src/ports.ts
|
|
6617
6652
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
6618
6653
|
import { existsSync as existsSync13 } from "fs";
|
|
6619
|
-
import { join as
|
|
6654
|
+
import { join as join14 } from "path";
|
|
6620
6655
|
var run = (cmd, args) => {
|
|
6621
6656
|
try {
|
|
6622
6657
|
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -6661,7 +6696,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
6661
6696
|
}
|
|
6662
6697
|
function worktreeExists(parent, prefix, n) {
|
|
6663
6698
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
6664
|
-
if (existsSync13(
|
|
6699
|
+
if (existsSync13(join14(parent, name, ".git"))) return true;
|
|
6665
6700
|
}
|
|
6666
6701
|
return false;
|
|
6667
6702
|
}
|
|
@@ -6753,7 +6788,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
6753
6788
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
6754
6789
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
6755
6790
|
}
|
|
6756
|
-
const path =
|
|
6791
|
+
const path = join15(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
|
|
6757
6792
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
6758
6793
|
return { action: "cut-remote", path, branch: located.branch };
|
|
6759
6794
|
}
|
|
@@ -7116,12 +7151,12 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
7116
7151
|
|
|
7117
7152
|
// src/pending-handoff.ts
|
|
7118
7153
|
var PENDING_MAX_AGE_MS = 48 * 36e5;
|
|
7119
|
-
var pendingDirFor = (stateDir) =>
|
|
7120
|
-
var stateDirOfStream = (streamPath) =>
|
|
7154
|
+
var pendingDirFor = (stateDir) => join16(stateDir, "pending-handoffs");
|
|
7155
|
+
var stateDirOfStream = (streamPath) => join16(streamPath, "..", "..");
|
|
7121
7156
|
function writePendingHandoff(stateDir, rec) {
|
|
7122
7157
|
const dir = pendingDirFor(stateDir);
|
|
7123
7158
|
mkdirSync8(dir, { recursive: true });
|
|
7124
|
-
writeFileSync9(
|
|
7159
|
+
writeFileSync9(join16(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
|
|
7125
7160
|
}
|
|
7126
7161
|
function listPendingHandoffs(stateDir, route) {
|
|
7127
7162
|
const dir = pendingDirFor(stateDir);
|
|
@@ -7129,7 +7164,7 @@ function listPendingHandoffs(stateDir, route) {
|
|
|
7129
7164
|
const out = [];
|
|
7130
7165
|
for (const f of readdirSync5(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
7131
7166
|
try {
|
|
7132
|
-
const rec = JSON.parse(
|
|
7167
|
+
const rec = JSON.parse(readFileSync13(join16(dir, f), "utf8"));
|
|
7133
7168
|
if (rec.version === 1 && rec.ticketRecordId && (!route || rec.route === route)) out.push(rec);
|
|
7134
7169
|
} catch {
|
|
7135
7170
|
}
|
|
@@ -7137,7 +7172,7 @@ function listPendingHandoffs(stateDir, route) {
|
|
|
7137
7172
|
return out;
|
|
7138
7173
|
}
|
|
7139
7174
|
function removePendingHandoff(stateDir, ticketRecordId) {
|
|
7140
|
-
rmSync8(
|
|
7175
|
+
rmSync8(join16(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
|
|
7141
7176
|
}
|
|
7142
7177
|
function pendingHandoffFor(stateDir, ticketRecordId) {
|
|
7143
7178
|
return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
|
|
@@ -7247,8 +7282,8 @@ function expandSkillMarkers(text, lookup, opts = {}) {
|
|
|
7247
7282
|
}
|
|
7248
7283
|
|
|
7249
7284
|
// src/skills.ts
|
|
7250
|
-
import { readFileSync as
|
|
7251
|
-
import { join as
|
|
7285
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync6, existsSync as existsSync17 } from "fs";
|
|
7286
|
+
import { join as join18 } from "path";
|
|
7252
7287
|
import { parse as parseYaml } from "yaml";
|
|
7253
7288
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
7254
7289
|
|
|
@@ -7264,8 +7299,8 @@ ${additions}`;
|
|
|
7264
7299
|
}
|
|
7265
7300
|
|
|
7266
7301
|
// src/agents.ts
|
|
7267
|
-
import { readFileSync as
|
|
7268
|
-
import { join as
|
|
7302
|
+
import { readFileSync as readFileSync14, existsSync as existsSync16 } from "fs";
|
|
7303
|
+
import { join as join17 } from "path";
|
|
7269
7304
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
|
|
7270
7305
|
var PERSONA_NAME = {
|
|
7271
7306
|
dev: "Developer",
|
|
@@ -7279,12 +7314,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
7279
7314
|
var AgentsSyncError = class extends Error {
|
|
7280
7315
|
};
|
|
7281
7316
|
function personaDefaultPrompt(promptsDir, role) {
|
|
7282
|
-
const briefPath =
|
|
7317
|
+
const briefPath = join17(promptsDir, "personas", `lane-${role}.md`);
|
|
7283
7318
|
if (!existsSync16(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
7284
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
7285
|
-
const commonPath =
|
|
7286
|
-
if (!existsSync16(commonPath)) return
|
|
7287
|
-
return `${
|
|
7319
|
+
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync14(briefPath, "utf8");
|
|
7320
|
+
const commonPath = join17(promptsDir, "personas", "common.md");
|
|
7321
|
+
if (!existsSync16(commonPath)) return readFileSync14(briefPath, "utf8");
|
|
7322
|
+
return `${readFileSync14(commonPath, "utf8")}${readFileSync14(briefPath, "utf8")}`;
|
|
7288
7323
|
}
|
|
7289
7324
|
async function resolveAgentId(route, role, opts) {
|
|
7290
7325
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -7731,7 +7766,7 @@ function describeCrewLink(o) {
|
|
|
7731
7766
|
var SkillSyncError = class extends Error {
|
|
7732
7767
|
};
|
|
7733
7768
|
function parseSkillFile(path) {
|
|
7734
|
-
const text =
|
|
7769
|
+
const text = readFileSync15(path, "utf8");
|
|
7735
7770
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
7736
7771
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
7737
7772
|
const frontmatter = match[1];
|
|
@@ -7744,9 +7779,9 @@ function parseSkillFile(path) {
|
|
|
7744
7779
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
7745
7780
|
}
|
|
7746
7781
|
function listLocalSkills(promptsDir) {
|
|
7747
|
-
const dir =
|
|
7782
|
+
const dir = join18(promptsDir, "skills");
|
|
7748
7783
|
if (!existsSync17(dir)) return [];
|
|
7749
|
-
return readdirSync6(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
7784
|
+
return readdirSync6(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join18(dir, f)));
|
|
7750
7785
|
}
|
|
7751
7786
|
async function syncSkills(route, opts) {
|
|
7752
7787
|
if (!route.resolved) {
|
|
@@ -7949,23 +7984,23 @@ var AgentError = class extends Error {
|
|
|
7949
7984
|
var AgentSpawnError = class extends AgentError {
|
|
7950
7985
|
};
|
|
7951
7986
|
function planAgentRun(o) {
|
|
7952
|
-
const briefPath =
|
|
7987
|
+
const briefPath = join19(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
7953
7988
|
if (!existsSync18(briefPath)) {
|
|
7954
7989
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
7955
7990
|
}
|
|
7956
|
-
const commonPath =
|
|
7991
|
+
const commonPath = join19(o.route.promptsDir, "personas", "common.md");
|
|
7957
7992
|
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync18(commonPath);
|
|
7958
7993
|
const now = o.now ?? (() => Date.now());
|
|
7959
7994
|
const routeLabel = routeSlug(o.route.route);
|
|
7960
|
-
const digestPath2 =
|
|
7995
|
+
const digestPath2 = join19(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
7961
7996
|
let digest;
|
|
7962
7997
|
let ageSeconds;
|
|
7963
7998
|
if (existsSync18(digestPath2)) {
|
|
7964
7999
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
7965
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
8000
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync16(digestPath2, "utf8");
|
|
7966
8001
|
}
|
|
7967
|
-
const common = usesCommon ?
|
|
7968
|
-
const brief =
|
|
8002
|
+
const common = usesCommon ? readFileSync16(commonPath, "utf8") : "";
|
|
8003
|
+
const brief = readFileSync16(briefPath, "utf8");
|
|
7969
8004
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
7970
8005
|
const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
|
|
7971
8006
|
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
@@ -7987,7 +8022,7 @@ function planAgentRun(o) {
|
|
|
7987
8022
|
digest
|
|
7988
8023
|
});
|
|
7989
8024
|
const promptSha = createHash3("sha256").update(expanded.text).digest("hex");
|
|
7990
|
-
const streamsDir =
|
|
8025
|
+
const streamsDir = join19(o.stateDir, "streams");
|
|
7991
8026
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
7992
8027
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
7993
8028
|
const { toolchain, model } = seat;
|
|
@@ -8043,8 +8078,8 @@ function planAgentRun(o) {
|
|
|
8043
8078
|
digestAttached: digest !== void 0,
|
|
8044
8079
|
digestAgeSeconds: ageSeconds,
|
|
8045
8080
|
model,
|
|
8046
|
-
streamPath:
|
|
8047
|
-
eventsPath:
|
|
8081
|
+
streamPath: join19(streamsDir, `${base}.jsonl`),
|
|
8082
|
+
eventsPath: join19(streamsDir, `${base}.events.jsonl`),
|
|
8048
8083
|
ticket: o.ticket,
|
|
8049
8084
|
ticketRecordId: o.ticketRecordId ?? null,
|
|
8050
8085
|
agentLog,
|
|
@@ -8073,7 +8108,7 @@ function describePlan(p) {
|
|
|
8073
8108
|
var pidPathFor = (streamPath) => `${streamPath}.pid`;
|
|
8074
8109
|
function readRunSidecar(streamPath) {
|
|
8075
8110
|
try {
|
|
8076
|
-
const sc = JSON.parse(
|
|
8111
|
+
const sc = JSON.parse(readFileSync16(pidPathFor(streamPath), "utf8"));
|
|
8077
8112
|
return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
|
|
8078
8113
|
} catch {
|
|
8079
8114
|
return null;
|
|
@@ -8657,7 +8692,7 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
8657
8692
|
...adapter.runEnv(spec),
|
|
8658
8693
|
[KEY_VAR]: i.apiKey
|
|
8659
8694
|
};
|
|
8660
|
-
const cwd = mkdtempSync(
|
|
8695
|
+
const cwd = mkdtempSync(join20(tmpdir2(), "crew-selftest-"));
|
|
8661
8696
|
try {
|
|
8662
8697
|
const run2 = await deps.run({
|
|
8663
8698
|
bin: invocation.bin,
|
|
@@ -8672,7 +8707,8 @@ async function runSelftest(i, deps = realDeps) {
|
|
|
8672
8707
|
const answer = [seen.finalText ?? "", ...seen.texts].join("\n");
|
|
8673
8708
|
const usage2 = { ...seen.tokens !== void 0 ? { tokens: seen.tokens } : {}, ...seen.costUsd !== void 0 ? { costUsd: seen.costUsd } : {} };
|
|
8674
8709
|
if (!run2.spawnError && !run2.timedOut && answer.toLowerCase().includes(i.workspaceName.toLowerCase())) {
|
|
8675
|
-
|
|
8710
|
+
const auth = spec.harness === "claude" ? ` on ${describeClaudeAuth(spec)}` : "";
|
|
8711
|
+
return finish({ ok: true, workspace: i.workspaceName, detail: `read workspace "${i.workspaceName}"${auth}`, ...usage2 });
|
|
8676
8712
|
}
|
|
8677
8713
|
const { failure, detail } = classify(spec, run2, seen);
|
|
8678
8714
|
return finish({ ok: false, failure, detail, ...usage2 });
|
|
@@ -8689,11 +8725,11 @@ function formatResult(r) {
|
|
|
8689
8725
|
const secs = `${(r.ms / 1e3).toFixed(1)}s`;
|
|
8690
8726
|
return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
|
|
8691
8727
|
}
|
|
8692
|
-
var fileFor2 = (stateDir) =>
|
|
8728
|
+
var fileFor2 = (stateDir) => join20(stateDir, "selftest.json");
|
|
8693
8729
|
function readSelftest(stateDir) {
|
|
8694
8730
|
try {
|
|
8695
8731
|
const p = fileFor2(stateDir);
|
|
8696
|
-
return existsSync19(p) ? JSON.parse(
|
|
8732
|
+
return existsSync19(p) ? JSON.parse(readFileSync17(p, "utf8")) : {};
|
|
8697
8733
|
} catch {
|
|
8698
8734
|
return {};
|
|
8699
8735
|
}
|
|
@@ -8714,9 +8750,9 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
8714
8750
|
}
|
|
8715
8751
|
|
|
8716
8752
|
// src/cli.ts
|
|
8717
|
-
import { resolve as resolve5, dirname as dirname20, join as
|
|
8753
|
+
import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename11 } from "path";
|
|
8718
8754
|
import { fileURLToPath } from "url";
|
|
8719
|
-
import { hostname as hostname2, homedir as
|
|
8755
|
+
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
8720
8756
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
8721
8757
|
import { parse as parseYaml2 } from "yaml";
|
|
8722
8758
|
|
|
@@ -8894,14 +8930,14 @@ function rosterMarkdown(roster, meId) {
|
|
|
8894
8930
|
}
|
|
8895
8931
|
|
|
8896
8932
|
// src/connect-resume.ts
|
|
8897
|
-
import { mkdirSync as mkdirSync11, readFileSync as
|
|
8898
|
-
import { dirname as dirname8, join as
|
|
8933
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "fs";
|
|
8934
|
+
import { dirname as dirname8, join as join21 } from "path";
|
|
8899
8935
|
function lastSessionFile(stateDir) {
|
|
8900
|
-
return
|
|
8936
|
+
return join21(stateDir, "connect-session.json");
|
|
8901
8937
|
}
|
|
8902
8938
|
function readPointers(stateDir) {
|
|
8903
8939
|
try {
|
|
8904
|
-
const parsed = JSON.parse(
|
|
8940
|
+
const parsed = JSON.parse(readFileSync18(lastSessionFile(stateDir), "utf8"));
|
|
8905
8941
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
8906
8942
|
const out = {};
|
|
8907
8943
|
for (const [host, v] of Object.entries(parsed)) {
|
|
@@ -8972,15 +9008,15 @@ async function signOutSessions(o) {
|
|
|
8972
9008
|
}
|
|
8973
9009
|
|
|
8974
9010
|
// src/reset.ts
|
|
8975
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync12, readFileSync as
|
|
8976
|
-
import { dirname as dirname9, join as
|
|
9011
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync7, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync13 } from "fs";
|
|
9012
|
+
import { dirname as dirname9, join as join22 } from "path";
|
|
8977
9013
|
import { hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
8978
9014
|
function clonedCheckoutsFile(stateDir) {
|
|
8979
|
-
return
|
|
9015
|
+
return join22(stateDir, "cloned-checkouts.json");
|
|
8980
9016
|
}
|
|
8981
9017
|
function readClonedCheckouts(stateDir) {
|
|
8982
9018
|
try {
|
|
8983
|
-
const parsed = JSON.parse(
|
|
9019
|
+
const parsed = JSON.parse(readFileSync19(clonedCheckoutsFile(stateDir), "utf8"));
|
|
8984
9020
|
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
8985
9021
|
} catch {
|
|
8986
9022
|
return [];
|
|
@@ -9065,10 +9101,10 @@ function removeCheckout(dir, dryRun, fx) {
|
|
|
9065
9101
|
}
|
|
9066
9102
|
function removeState(stateDir, dryRun) {
|
|
9067
9103
|
const out = [];
|
|
9068
|
-
const targets = [
|
|
9104
|
+
const targets = [join22(stateDir, "resolved"), join22(stateDir, "keys"), clonedCheckoutsFile(stateDir), join22(stateDir, "connect-session.json"), join22(stateDir, "reload-requested"), join22(stateDir, "daemon.started")];
|
|
9069
9105
|
try {
|
|
9070
9106
|
for (const name of readdirSync7(stateDir)) {
|
|
9071
|
-
if (name.startsWith(".") && statSync6(
|
|
9107
|
+
if (name.startsWith(".") && statSync6(join22(stateDir, name)).isFile()) targets.push(join22(stateDir, name));
|
|
9072
9108
|
}
|
|
9073
9109
|
} catch {
|
|
9074
9110
|
}
|
|
@@ -9854,7 +9890,7 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
|
|
|
9854
9890
|
|
|
9855
9891
|
// src/poll.ts
|
|
9856
9892
|
import { writeFileSync as writeFileSync14 } from "fs";
|
|
9857
|
-
import { join as
|
|
9893
|
+
import { join as join23 } from "path";
|
|
9858
9894
|
|
|
9859
9895
|
// src/stalled.ts
|
|
9860
9896
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -10145,7 +10181,7 @@ async function decideCycle(o) {
|
|
|
10145
10181
|
}
|
|
10146
10182
|
return decision;
|
|
10147
10183
|
}
|
|
10148
|
-
var digestPath = (stateDir, route, role) =>
|
|
10184
|
+
var digestPath = (stateDir, route, role) => join23(stateDir, `digest-${route}-${role}.md`);
|
|
10149
10185
|
function writeDigest(o, d, role, stateDir) {
|
|
10150
10186
|
const me = o.route.resolved?.seats[role];
|
|
10151
10187
|
if (!me) return false;
|
|
@@ -10331,7 +10367,8 @@ var lastActedHandBack = (ticket, events) => {
|
|
|
10331
10367
|
return mine.filter((e) => e.event_type === "conflict_handed_back" && pushes.some((p) => p > stampOf(e))).sort((a, b) => stampOf(b).localeCompare(stampOf(a)))[0];
|
|
10332
10368
|
};
|
|
10333
10369
|
var MAX_ABSORBED_HANDBACKS = 2;
|
|
10334
|
-
function planConflictBounce(cwd, ticket, branch, base, paths, events) {
|
|
10370
|
+
function planConflictBounce(cwd, ticket, branch, base, paths, events, merged = []) {
|
|
10371
|
+
const siblings = merged.filter((m) => m.branch !== branch && paths.some((p) => (gitOk(cwd, ["diff", "--name-only", `${base}...${m.branch}`, "--", p]) ?? "").trim() !== ""));
|
|
10335
10372
|
return {
|
|
10336
10373
|
ticket,
|
|
10337
10374
|
branch,
|
|
@@ -10341,7 +10378,8 @@ function planConflictBounce(cwd, ticket, branch, base, paths, events) {
|
|
|
10341
10378
|
priorBounces: priorBounces(ticket, events),
|
|
10342
10379
|
cwd,
|
|
10343
10380
|
baseSha: gitOk(cwd, ["rev-parse", "--verify", "-q", base]) || null,
|
|
10344
|
-
priorBaseSha: baseShaOf(lastActedHandBack(ticket, events) ?? { record_id: "", event_type: "" })
|
|
10381
|
+
priorBaseSha: baseShaOf(lastActedHandBack(ticket, events) ?? { record_id: "", event_type: "" }),
|
|
10382
|
+
siblings
|
|
10345
10383
|
};
|
|
10346
10384
|
}
|
|
10347
10385
|
function refreshBranch(worktree, base) {
|
|
@@ -10370,7 +10408,7 @@ function handBackComment(b, merged) {
|
|
|
10370
10408
|
|
|
10371
10409
|
**Back with the dev lane \u2014 this branch no longer merges.**
|
|
10372
10410
|
|
|
10373
|
-
\`${b.branch}\` was verified, but squash-merging it into \`${b.base}\` conflicts in ${list}. Work that landed on \`${b.base}\` after this branch was cut has changed the same lines
|
|
10411
|
+
\`${b.branch}\` was verified, but squash-merging it into \`${b.base}\` conflicts in ${list}. ${b.siblings?.length ? `This conflicts with ${b.siblings.map((x) => `${x.key} (\`${x.branch}\`)`).join(", ")}, merged earlier in the same release, not with a \`${b.base}\` that moved: merging \`${b.base}\` in will change nothing. Merge ${b.siblings.length === 1 ? "that branch" : "those branches"} into this one (or move this change onto ${b.siblings.length === 1 ? "it" : "one of them"}), resolve, and push.` : `Work that landed on \`${b.base}\` after this branch was cut has changed the same lines.`}
|
|
10374
10412
|
|
|
10375
10413
|
${merged ? `\`${b.base}\` has been merged into \`${b.branch}\` in its worktree and the conflict is left in place, so \`git status\` there shows exactly what disagrees. Resolve it, commit, push as a plain fast-forward (the branch is pushed, so rebasing it is not an option) and hand back to QA as usual \u2014 \`git merge --abort\` backs the merge out if you would rather start differently.` : `This ship has no worktree for \`${b.branch}\`, so nothing has been merged for you. Merge \`${b.base}\` into the branch wherever it lives, resolve the conflict, and commit.`}
|
|
10376
10414
|
|
|
@@ -10632,7 +10670,7 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
|
|
|
10632
10670
|
var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
|
|
10633
10671
|
|
|
10634
10672
|
// src/connect-wizard.ts
|
|
10635
|
-
import { delimiter, dirname as dirname10, join as
|
|
10673
|
+
import { delimiter, dirname as dirname10, join as join24 } from "path";
|
|
10636
10674
|
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync21, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15 } from "fs";
|
|
10637
10675
|
import { stringify } from "yaml";
|
|
10638
10676
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
@@ -10655,7 +10693,7 @@ function defaultIsExecutable(p) {
|
|
|
10655
10693
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
10656
10694
|
for (const dir of pathEnv.split(delimiter)) {
|
|
10657
10695
|
if (!dir) continue;
|
|
10658
|
-
const candidate =
|
|
10696
|
+
const candidate = join24(dir, name);
|
|
10659
10697
|
if (isExecutable(candidate)) return candidate;
|
|
10660
10698
|
}
|
|
10661
10699
|
return void 0;
|
|
@@ -10751,8 +10789,8 @@ Wrote ${o.configPath}.
|
|
|
10751
10789
|
|
|
10752
10790
|
// src/connect-json.ts
|
|
10753
10791
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
10754
|
-
import { homedir as
|
|
10755
|
-
import { dirname as dirname11, join as
|
|
10792
|
+
import { homedir as homedir8 } from "os";
|
|
10793
|
+
import { dirname as dirname11, join as join25 } from "path";
|
|
10756
10794
|
import { delimiter as delimiter2 } from "path";
|
|
10757
10795
|
var CHOICE_EXIT_CODE = 3;
|
|
10758
10796
|
function createEmitter(json, write) {
|
|
@@ -10791,17 +10829,17 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
|
|
|
10791
10829
|
const dirs = [
|
|
10792
10830
|
...pathEnv.split(delimiter2),
|
|
10793
10831
|
...(extraPath ?? "").split(delimiter2),
|
|
10794
|
-
|
|
10795
|
-
|
|
10832
|
+
join25(home, ".local/bin"),
|
|
10833
|
+
join25(home, ".claude/local"),
|
|
10796
10834
|
"/opt/homebrew/bin",
|
|
10797
10835
|
"/usr/local/bin",
|
|
10798
|
-
|
|
10799
|
-
|
|
10836
|
+
join25(home, ".npm-global/bin"),
|
|
10837
|
+
join25(home, ".volta/bin")
|
|
10800
10838
|
];
|
|
10801
10839
|
return [...new Set(dirs.filter(Boolean))];
|
|
10802
10840
|
}
|
|
10803
10841
|
function findAgentBin(name, env, extraPath, isExec) {
|
|
10804
|
-
const searched = agentBinSearchDirs(env.home ??
|
|
10842
|
+
const searched = agentBinSearchDirs(env.home ?? homedir8(), env.pathEnv, extraPath);
|
|
10805
10843
|
return { bin: findOnPath2(name, searched.join(delimiter2), isExec), searched };
|
|
10806
10844
|
}
|
|
10807
10845
|
function flagWizardAnswers(f, env) {
|
|
@@ -10832,8 +10870,8 @@ function flagWizardAnswers(f, env) {
|
|
|
10832
10870
|
}
|
|
10833
10871
|
|
|
10834
10872
|
// src/release-run.ts
|
|
10835
|
-
import { readFileSync as
|
|
10836
|
-
import { dirname as dirname12, join as
|
|
10873
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync16, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync2, rmSync as rmSync12 } from "fs";
|
|
10874
|
+
import { dirname as dirname12, join as join26 } from "path";
|
|
10837
10875
|
import { tmpdir as tmpdir3 } from "os";
|
|
10838
10876
|
|
|
10839
10877
|
// src/release.ts
|
|
@@ -11551,13 +11589,13 @@ async function bump(o, size) {
|
|
|
11551
11589
|
}
|
|
11552
11590
|
return printed;
|
|
11553
11591
|
}
|
|
11554
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
11592
|
+
const files = o.repo.release.versionFiles.map((f) => join26(o.cwd, f));
|
|
11555
11593
|
const first = files[0];
|
|
11556
11594
|
if (!existsSync22(first)) {
|
|
11557
11595
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
11558
11596
|
return null;
|
|
11559
11597
|
}
|
|
11560
|
-
const current = JSON.parse(
|
|
11598
|
+
const current = JSON.parse(readFileSync20(first, "utf8")).version;
|
|
11561
11599
|
if (!current) {
|
|
11562
11600
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
11563
11601
|
return null;
|
|
@@ -11570,7 +11608,7 @@ async function bump(o, size) {
|
|
|
11570
11608
|
return version;
|
|
11571
11609
|
}
|
|
11572
11610
|
for (const f of files) {
|
|
11573
|
-
const src =
|
|
11611
|
+
const src = readFileSync20(f, "utf8");
|
|
11574
11612
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
11575
11613
|
if (out === src) {
|
|
11576
11614
|
o.emit.error(`no version field in ${f}`);
|
|
@@ -11583,13 +11621,13 @@ async function bump(o, size) {
|
|
|
11583
11621
|
function writeChangelog(o, version, entries) {
|
|
11584
11622
|
const file = o.repo.release.changelog;
|
|
11585
11623
|
if (!file || entries.length === 0) return;
|
|
11586
|
-
const path =
|
|
11624
|
+
const path = join26(o.cwd, file);
|
|
11587
11625
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
11588
11626
|
if (o.dryRun) {
|
|
11589
11627
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
11590
11628
|
return;
|
|
11591
11629
|
}
|
|
11592
|
-
const existing = existsSync22(path) ?
|
|
11630
|
+
const existing = existsSync22(path) ? readFileSync20(path, "utf8") : "# Changelog\n";
|
|
11593
11631
|
writeFileSync16(path, insertChangelogSection(existing, section));
|
|
11594
11632
|
}
|
|
11595
11633
|
async function confirm(o, expected) {
|
|
@@ -11683,8 +11721,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
11683
11721
|
function openReleaseCheckout(o) {
|
|
11684
11722
|
const { remote, base } = o.repo.branch;
|
|
11685
11723
|
const branch = `crew/release-${Date.now()}`;
|
|
11686
|
-
const root = mkdtempSync2(
|
|
11687
|
-
const dir =
|
|
11724
|
+
const root = mkdtempSync2(join26(tmpdir3(), "crew-release-"));
|
|
11725
|
+
const dir = join26(root, "checkout");
|
|
11688
11726
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
11689
11727
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
11690
11728
|
try {
|
|
@@ -11694,10 +11732,10 @@ function openReleaseCheckout(o) {
|
|
|
11694
11732
|
throw e;
|
|
11695
11733
|
}
|
|
11696
11734
|
for (const f of o.repo.worktrees.copy) {
|
|
11697
|
-
const src =
|
|
11735
|
+
const src = join26(o.cwd, f);
|
|
11698
11736
|
if (!existsSync22(src)) continue;
|
|
11699
|
-
mkdirSync14(dirname12(
|
|
11700
|
-
copyFileSync(src,
|
|
11737
|
+
mkdirSync14(dirname12(join26(dir, f)), { recursive: true });
|
|
11738
|
+
copyFileSync(src, join26(dir, f));
|
|
11701
11739
|
}
|
|
11702
11740
|
return { dir, branch, close };
|
|
11703
11741
|
}
|
|
@@ -12778,17 +12816,17 @@ function nextRoles(decision, ran) {
|
|
|
12778
12816
|
}
|
|
12779
12817
|
|
|
12780
12818
|
// src/ship-name.ts
|
|
12781
|
-
import { existsSync as existsSync25, mkdirSync as mkdirSync16, readFileSync as
|
|
12819
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync16, readFileSync as readFileSync22, renameSync as renameSync2, writeFileSync as writeFileSync18 } from "fs";
|
|
12782
12820
|
import { dirname as dirname14 } from "path";
|
|
12783
12821
|
import { parseDocument } from "yaml";
|
|
12784
12822
|
|
|
12785
12823
|
// src/machine-id.ts
|
|
12786
12824
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
12787
12825
|
import { createHash as createHash4 } from "crypto";
|
|
12788
|
-
import { existsSync as existsSync24, mkdirSync as mkdirSync15, readFileSync as
|
|
12789
|
-
import { dirname as dirname13, join as
|
|
12826
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "fs";
|
|
12827
|
+
import { dirname as dirname13, join as join27 } from "path";
|
|
12790
12828
|
function machineIdPathFor(stateDir) {
|
|
12791
|
-
return
|
|
12829
|
+
return join27(stateDir, "machine-id");
|
|
12792
12830
|
}
|
|
12793
12831
|
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
12794
12832
|
function readRawMachineId(platform2 = process.platform) {
|
|
@@ -12802,7 +12840,7 @@ function readRawMachineId(platform2 = process.platform) {
|
|
|
12802
12840
|
}
|
|
12803
12841
|
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
12804
12842
|
if (existsSync24(p)) {
|
|
12805
|
-
const id =
|
|
12843
|
+
const id = readFileSync21(p, "utf8").trim();
|
|
12806
12844
|
if (id) return id;
|
|
12807
12845
|
}
|
|
12808
12846
|
}
|
|
@@ -12814,7 +12852,7 @@ function ensureMachineId(stateDir, opts = {}) {
|
|
|
12814
12852
|
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
12815
12853
|
const path = machineIdPathFor(stateDir);
|
|
12816
12854
|
if (existsSync24(path)) {
|
|
12817
|
-
const cached =
|
|
12855
|
+
const cached = readFileSync21(path, "utf8").trim();
|
|
12818
12856
|
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
12819
12857
|
}
|
|
12820
12858
|
const raw = (opts.read ?? readRawMachineId)();
|
|
@@ -12894,7 +12932,7 @@ async function syncRouteName(i) {
|
|
|
12894
12932
|
function readSyncedName(resolvedFile) {
|
|
12895
12933
|
if (!existsSync25(resolvedFile)) return void 0;
|
|
12896
12934
|
try {
|
|
12897
|
-
const v = JSON.parse(
|
|
12935
|
+
const v = JSON.parse(readFileSync22(resolvedFile, "utf8")).shipName;
|
|
12898
12936
|
return typeof v === "string" ? v : void 0;
|
|
12899
12937
|
} catch {
|
|
12900
12938
|
return void 0;
|
|
@@ -12904,7 +12942,7 @@ function writeSyncedName(resolvedFile, name) {
|
|
|
12904
12942
|
if (!existsSync25(resolvedFile)) return;
|
|
12905
12943
|
let raw;
|
|
12906
12944
|
try {
|
|
12907
|
-
raw = JSON.parse(
|
|
12945
|
+
raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
|
|
12908
12946
|
} catch {
|
|
12909
12947
|
return;
|
|
12910
12948
|
}
|
|
@@ -12919,7 +12957,7 @@ function writeSyncedName(resolvedFile, name) {
|
|
|
12919
12957
|
async function reconcileShipName(i) {
|
|
12920
12958
|
let local = i.ship.name;
|
|
12921
12959
|
try {
|
|
12922
|
-
local = shipNameOfYaml(
|
|
12960
|
+
local = shipNameOfYaml(readFileSync22(i.configFile, "utf8")) ?? local;
|
|
12923
12961
|
} catch {
|
|
12924
12962
|
}
|
|
12925
12963
|
const resolvedFile = resolvedPathFor(i.stateDir, i.route);
|
|
@@ -12943,7 +12981,7 @@ async function reconcileShipName(i) {
|
|
|
12943
12981
|
}
|
|
12944
12982
|
if (result.status !== "ok") return result;
|
|
12945
12983
|
if (result.action === "adopt" || result.action === "row-wins") {
|
|
12946
|
-
const text =
|
|
12984
|
+
const text = readFileSync22(i.configFile, "utf8");
|
|
12947
12985
|
writeFileSync18(i.configFile, setShipNameInYaml(text, result.name));
|
|
12948
12986
|
i.ship.name = result.name;
|
|
12949
12987
|
if (result.action === "row-wins") {
|
|
@@ -13852,7 +13890,7 @@ function describeCapability(i) {
|
|
|
13852
13890
|
}
|
|
13853
13891
|
|
|
13854
13892
|
// src/agents-sync-run.ts
|
|
13855
|
-
import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as
|
|
13893
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "fs";
|
|
13856
13894
|
import { dirname as dirname15 } from "path";
|
|
13857
13895
|
async function runAgentsSync(target, o) {
|
|
13858
13896
|
const dryRun = o.dryRun ?? false;
|
|
@@ -13886,7 +13924,7 @@ async function runAgentsSync(target, o) {
|
|
|
13886
13924
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
13887
13925
|
if (!dryRun) {
|
|
13888
13926
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
13889
|
-
const raw = existsSync26(resolvedPath) ? JSON.parse(
|
|
13927
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
|
|
13890
13928
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
13891
13929
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
13892
13930
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
@@ -13997,8 +14035,8 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
13997
14035
|
import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
|
|
13998
14036
|
|
|
13999
14037
|
// src/repos-cmd.ts
|
|
14000
|
-
import { homedir as
|
|
14001
|
-
import { basename as basename8, join as
|
|
14038
|
+
import { homedir as homedir9 } from "os";
|
|
14039
|
+
import { basename as basename8, join as join28, resolve as resolve3 } from "path";
|
|
14002
14040
|
import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
|
|
14003
14041
|
var ReposError = class extends Error {
|
|
14004
14042
|
};
|
|
@@ -14044,7 +14082,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
14044
14082
|
return { route, path: "." };
|
|
14045
14083
|
}
|
|
14046
14084
|
function expandDir(p, base) {
|
|
14047
|
-
const home = p === "~" ?
|
|
14085
|
+
const home = p === "~" ? homedir9() : p.startsWith("~/") ? `${homedir9()}/${p.slice(2)}` : p;
|
|
14048
14086
|
return resolve3(base, home);
|
|
14049
14087
|
}
|
|
14050
14088
|
function routeMapOf(doc, route) {
|
|
@@ -14276,7 +14314,7 @@ function planRepoClone(i) {
|
|
|
14276
14314
|
}
|
|
14277
14315
|
const ship = doc.get("ship", true);
|
|
14278
14316
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
14279
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
14317
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join28(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
14280
14318
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
|
|
14281
14319
|
return { ...add, remote, target };
|
|
14282
14320
|
}
|
|
@@ -14675,7 +14713,7 @@ ${run2.err}` });
|
|
|
14675
14713
|
}
|
|
14676
14714
|
|
|
14677
14715
|
// src/sample-ticket.ts
|
|
14678
|
-
import { basename as basename9, dirname as dirname17, join as
|
|
14716
|
+
import { basename as basename9, dirname as dirname17, join as join29 } from "path";
|
|
14679
14717
|
var SAMPLE_REPO = "crew-sample";
|
|
14680
14718
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
14681
14719
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -14698,8 +14736,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
14698
14736
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
14699
14737
|
`;
|
|
14700
14738
|
function sampleDir(existing, reposBase, workspace) {
|
|
14701
|
-
if (existing.length > 0) return
|
|
14702
|
-
return
|
|
14739
|
+
if (existing.length > 0) return join29(dirname17(existing[0]), SAMPLE_REPO);
|
|
14740
|
+
return join29(reposBase, workspace, SAMPLE_REPO);
|
|
14703
14741
|
}
|
|
14704
14742
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
14705
14743
|
function sampleDescription() {
|
|
@@ -14749,7 +14787,15 @@ function planFix(f) {
|
|
|
14749
14787
|
const actions = [];
|
|
14750
14788
|
const notes = [];
|
|
14751
14789
|
for (const r of f.routes) {
|
|
14752
|
-
if (!r.enabled && sound(r))
|
|
14790
|
+
if (!r.enabled && sound(r)) {
|
|
14791
|
+
actions.push({ kind: "enable", route: r.route });
|
|
14792
|
+
const gaps = r.readiness ?? [];
|
|
14793
|
+
if (gaps.length) {
|
|
14794
|
+
notes.push(`${r.route}: ${gaps.length} release-readiness gap${gaps.length === 1 ? "" : "s"} (${gaps[0]}${gaps.length > 1 ? ", ..." : ""}) \u2014 the route will run, but releases stop at the first missing hook until it is added`);
|
|
14795
|
+
}
|
|
14796
|
+
} else if (!sound(r) && !r.enabled) {
|
|
14797
|
+
notes.push(`${r.route}: not enabled \u2014 ${r.hasRepos ? r.problems[0] : "no checkout is configured"}`);
|
|
14798
|
+
}
|
|
14753
14799
|
}
|
|
14754
14800
|
if (f.schedulerAbsent && f.routes.some(sound)) {
|
|
14755
14801
|
if (f.host === "windows") notes.push("scheduler support is not built for Windows yet \u2014 nothing to install; run `crew run` by hand");
|
|
@@ -14759,12 +14805,15 @@ function planFix(f) {
|
|
|
14759
14805
|
}
|
|
14760
14806
|
async function gatherHealth(c, check) {
|
|
14761
14807
|
let problems;
|
|
14808
|
+
let readiness = [];
|
|
14762
14809
|
try {
|
|
14763
|
-
|
|
14810
|
+
const r = await check();
|
|
14811
|
+
if (Array.isArray(r)) problems = r;
|
|
14812
|
+
else ({ problems, readiness } = r);
|
|
14764
14813
|
} catch (e) {
|
|
14765
14814
|
problems = [`checks could not run: ${e instanceof Error ? e.message : String(e)}`];
|
|
14766
14815
|
}
|
|
14767
|
-
return { route: c.route, enabled: c.enabled, hasRepos: c.hasRepos, problems };
|
|
14816
|
+
return { route: c.route, enabled: c.enabled, hasRepos: c.hasRepos, problems, readiness };
|
|
14768
14817
|
}
|
|
14769
14818
|
function routesInScope(routes, named) {
|
|
14770
14819
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
@@ -14780,8 +14829,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
14780
14829
|
}
|
|
14781
14830
|
|
|
14782
14831
|
// src/worktree-sweep.ts
|
|
14783
|
-
import { existsSync as existsSync27, readFileSync as
|
|
14784
|
-
import { basename as basename10, dirname as dirname18, join as
|
|
14832
|
+
import { existsSync as existsSync27, readFileSync as readFileSync24, readdirSync as readdirSync8 } from "fs";
|
|
14833
|
+
import { basename as basename10, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
|
|
14785
14834
|
function planWorktreeSweep(target, tickets, contract) {
|
|
14786
14835
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
14787
14836
|
const verified = contract.statuses.verified;
|
|
@@ -14848,7 +14897,7 @@ function siblingWorktreeDirs(target) {
|
|
|
14848
14897
|
for (const name of names) {
|
|
14849
14898
|
if (!name.startsWith(stem)) continue;
|
|
14850
14899
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
14851
|
-
const path =
|
|
14900
|
+
const path = join30(parent, name);
|
|
14852
14901
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
14853
14902
|
}
|
|
14854
14903
|
return out;
|
|
@@ -14951,7 +15000,7 @@ function isHandBackMerge(path, base) {
|
|
|
14951
15000
|
if (!msgPath) return false;
|
|
14952
15001
|
let first;
|
|
14953
15002
|
try {
|
|
14954
|
-
first =
|
|
15003
|
+
first = readFileSync24(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
14955
15004
|
} catch {
|
|
14956
15005
|
return false;
|
|
14957
15006
|
}
|
|
@@ -15057,13 +15106,13 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
15057
15106
|
|
|
15058
15107
|
// src/stream-sweep.ts
|
|
15059
15108
|
import { existsSync as existsSync28, readdirSync as readdirSync9, rmSync as rmSync13, statSync as statSync8 } from "fs";
|
|
15060
|
-
import { join as
|
|
15109
|
+
import { join as join31 } from "path";
|
|
15061
15110
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
15062
15111
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
15063
|
-
const dir =
|
|
15112
|
+
const dir = join31(stateDir, "streams");
|
|
15064
15113
|
if (!existsSync28(dir)) return [];
|
|
15065
15114
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
15066
|
-
return readdirSync9(dir).map((name) =>
|
|
15115
|
+
return readdirSync9(dir).map((name) => join31(dir, name)).filter((path) => {
|
|
15067
15116
|
try {
|
|
15068
15117
|
return statSync8(path).mtimeMs < cutoff;
|
|
15069
15118
|
} catch {
|
|
@@ -15091,7 +15140,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
15091
15140
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
15092
15141
|
if (!existsSync28(stateDir)) return [];
|
|
15093
15142
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
15094
|
-
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
15143
|
+
return readdirSync9(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join31(stateDir, name)).filter((path) => {
|
|
15095
15144
|
try {
|
|
15096
15145
|
return statSync8(path).mtimeMs < cutoff;
|
|
15097
15146
|
} catch {
|
|
@@ -15101,18 +15150,18 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
15101
15150
|
}
|
|
15102
15151
|
|
|
15103
15152
|
// src/cli.ts
|
|
15104
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
15153
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync30, existsSync as existsSync34, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
|
|
15105
15154
|
import { createInterface } from "readline/promises";
|
|
15106
15155
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
15107
15156
|
|
|
15108
15157
|
// src/ssh-keys.ts
|
|
15109
15158
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
15110
|
-
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as
|
|
15111
|
-
import { dirname as dirname19, join as
|
|
15159
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, chmodSync } from "fs";
|
|
15160
|
+
import { dirname as dirname19, join as join32 } from "path";
|
|
15112
15161
|
var SshKeyError = class extends Error {
|
|
15113
15162
|
};
|
|
15114
15163
|
function shipSshKeyPathFor(stateDir) {
|
|
15115
|
-
return
|
|
15164
|
+
return join32(stateDir, "ssh", "id_ed25519");
|
|
15116
15165
|
}
|
|
15117
15166
|
function ensureShipSshKeypair(stateDir) {
|
|
15118
15167
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
@@ -15141,7 +15190,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
15141
15190
|
if (!existsSync29(publicKeyPath)) {
|
|
15142
15191
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
15143
15192
|
}
|
|
15144
|
-
const publicKey =
|
|
15193
|
+
const publicKey = readFileSync25(publicKeyPath, "utf8").trim();
|
|
15145
15194
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
15146
15195
|
}
|
|
15147
15196
|
function sshKeygenAvailable() {
|
|
@@ -15357,9 +15406,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
15357
15406
|
|
|
15358
15407
|
// src/tunnel.ts
|
|
15359
15408
|
import { spawn as spawn5 } from "child_process";
|
|
15360
|
-
import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as
|
|
15409
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync26, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
|
|
15361
15410
|
import { randomInt } from "crypto";
|
|
15362
|
-
import { join as
|
|
15411
|
+
import { join as join33 } from "path";
|
|
15363
15412
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
15364
15413
|
var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
15365
15414
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -15426,20 +15475,20 @@ function startTunnel(opts) {
|
|
|
15426
15475
|
};
|
|
15427
15476
|
}
|
|
15428
15477
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
15429
|
-
return
|
|
15478
|
+
return join33(stateDir, "passengers", `${workspaceId}.json`);
|
|
15430
15479
|
}
|
|
15431
15480
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
15432
15481
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
15433
15482
|
if (!existsSync30(path)) return void 0;
|
|
15434
15483
|
try {
|
|
15435
|
-
return JSON.parse(
|
|
15484
|
+
return JSON.parse(readFileSync26(path, "utf8"));
|
|
15436
15485
|
} catch {
|
|
15437
15486
|
return void 0;
|
|
15438
15487
|
}
|
|
15439
15488
|
}
|
|
15440
15489
|
function writePersistedTunnel(stateDir, state) {
|
|
15441
15490
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
15442
|
-
mkdirSync19(
|
|
15491
|
+
mkdirSync19(join33(stateDir, "passengers"), { recursive: true });
|
|
15443
15492
|
writeFileSync20(path, `${JSON.stringify(state, null, 2)}
|
|
15444
15493
|
`);
|
|
15445
15494
|
}
|
|
@@ -15537,7 +15586,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
15537
15586
|
}
|
|
15538
15587
|
}
|
|
15539
15588
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
15540
|
-
const dir =
|
|
15589
|
+
const dir = join33(stateDir, "passengers");
|
|
15541
15590
|
if (!existsSync30(dir)) return [];
|
|
15542
15591
|
return readdirSync10(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
15543
15592
|
}
|
|
@@ -15659,8 +15708,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
15659
15708
|
import { createServer as createHttpServer } from "http";
|
|
15660
15709
|
import { spawn as spawn6 } from "child_process";
|
|
15661
15710
|
import { randomBytes } from "crypto";
|
|
15662
|
-
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as
|
|
15663
|
-
import { join as
|
|
15711
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync27, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
|
|
15712
|
+
import { join as join34 } from "path";
|
|
15664
15713
|
|
|
15665
15714
|
// src/secret-compare.ts
|
|
15666
15715
|
import { timingSafeEqual } from "crypto";
|
|
@@ -15749,20 +15798,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
15749
15798
|
return child;
|
|
15750
15799
|
};
|
|
15751
15800
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
15752
|
-
return
|
|
15801
|
+
return join34(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
15753
15802
|
}
|
|
15754
15803
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
15755
15804
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
15756
15805
|
if (!existsSync31(path)) return void 0;
|
|
15757
15806
|
try {
|
|
15758
|
-
return JSON.parse(
|
|
15807
|
+
return JSON.parse(readFileSync27(path, "utf8"));
|
|
15759
15808
|
} catch {
|
|
15760
15809
|
return void 0;
|
|
15761
15810
|
}
|
|
15762
15811
|
}
|
|
15763
15812
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
15764
15813
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
15765
|
-
mkdirSync20(
|
|
15814
|
+
mkdirSync20(join34(stateDir, "passengers"), { recursive: true });
|
|
15766
15815
|
writeFileSync21(path, `${JSON.stringify(state, null, 2)}
|
|
15767
15816
|
`);
|
|
15768
15817
|
}
|
|
@@ -15783,7 +15832,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
15783
15832
|
}
|
|
15784
15833
|
}
|
|
15785
15834
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
15786
|
-
const dir =
|
|
15835
|
+
const dir = join34(stateDir, "passengers");
|
|
15787
15836
|
if (!existsSync31(dir)) return [];
|
|
15788
15837
|
const suffix = "-sync.json";
|
|
15789
15838
|
return readdirSync11(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -15872,12 +15921,12 @@ function describeCause(err) {
|
|
|
15872
15921
|
}
|
|
15873
15922
|
|
|
15874
15923
|
// src/daemon.ts
|
|
15875
|
-
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as
|
|
15876
|
-
import { join as
|
|
15924
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync29, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
|
|
15925
|
+
import { join as join36 } from "path";
|
|
15877
15926
|
|
|
15878
15927
|
// src/interrupted.ts
|
|
15879
|
-
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as
|
|
15880
|
-
import { join as
|
|
15928
|
+
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync28 } from "fs";
|
|
15929
|
+
import { join as join35 } from "path";
|
|
15881
15930
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
15882
15931
|
function interruptedComment(b) {
|
|
15883
15932
|
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
@@ -15898,19 +15947,19 @@ function runFinished(streamPath) {
|
|
|
15898
15947
|
const path = `${streamPath}.meta.json`;
|
|
15899
15948
|
if (!existsSync32(path)) return false;
|
|
15900
15949
|
try {
|
|
15901
|
-
return JSON.parse(
|
|
15950
|
+
return JSON.parse(readFileSync28(path, "utf8")).running !== true;
|
|
15902
15951
|
} catch {
|
|
15903
15952
|
return true;
|
|
15904
15953
|
}
|
|
15905
15954
|
}
|
|
15906
15955
|
function orphanedStreams(o) {
|
|
15907
|
-
const dir =
|
|
15956
|
+
const dir = join35(o.stateDir, "streams");
|
|
15908
15957
|
if (!existsSync32(dir)) return [];
|
|
15909
15958
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
15910
15959
|
const out = [];
|
|
15911
15960
|
for (const name of readdirSync12(dir)) {
|
|
15912
15961
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
15913
|
-
const streamPath =
|
|
15962
|
+
const streamPath = join35(dir, name);
|
|
15914
15963
|
if (runFinished(streamPath)) continue;
|
|
15915
15964
|
const sidecar = readRunSidecar(streamPath);
|
|
15916
15965
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -16030,7 +16079,7 @@ function latestMtime(path) {
|
|
|
16030
16079
|
if (!stat.isDirectory()) return 0;
|
|
16031
16080
|
let latest = stat.mtimeMs;
|
|
16032
16081
|
for (const entry of readdirSync13(path, { withFileTypes: true })) {
|
|
16033
|
-
latest = Math.max(latest, latestMtime(
|
|
16082
|
+
latest = Math.max(latest, latestMtime(join36(path, entry.name)));
|
|
16034
16083
|
}
|
|
16035
16084
|
return latest;
|
|
16036
16085
|
}
|
|
@@ -16042,10 +16091,10 @@ function makeStaleChecker(paths, opts = {}) {
|
|
|
16042
16091
|
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync33(opts.reloadSentinel);
|
|
16043
16092
|
}
|
|
16044
16093
|
function reloadSentinelFor(stateDir) {
|
|
16045
|
-
return
|
|
16094
|
+
return join36(stateDir, "reload-requested");
|
|
16046
16095
|
}
|
|
16047
16096
|
function daemonStampFor(stateDir) {
|
|
16048
|
-
return
|
|
16097
|
+
return join36(stateDir, "daemon.started");
|
|
16049
16098
|
}
|
|
16050
16099
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
16051
16100
|
mkdirSync21(stateDir, { recursive: true });
|
|
@@ -16055,7 +16104,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
16055
16104
|
}
|
|
16056
16105
|
function readDaemonStarted(stateDir) {
|
|
16057
16106
|
try {
|
|
16058
|
-
const v = JSON.parse(
|
|
16107
|
+
const v = JSON.parse(readFileSync29(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
16059
16108
|
return typeof v === "number" ? v : null;
|
|
16060
16109
|
} catch {
|
|
16061
16110
|
return null;
|
|
@@ -17076,7 +17125,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17076
17125
|
}
|
|
17077
17126
|
if (host === "linux" && hasSystemd) {
|
|
17078
17127
|
const label = labelFor(CREW_HOME, "run");
|
|
17079
|
-
const staleTimer =
|
|
17128
|
+
const staleTimer = join37(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
17080
17129
|
if (existsSync34(staleTimer)) {
|
|
17081
17130
|
process.stdout.write(
|
|
17082
17131
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
@@ -17096,6 +17145,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17096
17145
|
async function routeProblems(c) {
|
|
17097
17146
|
const host = hostPlatform();
|
|
17098
17147
|
const problems = [];
|
|
17148
|
+
const readiness = [];
|
|
17099
17149
|
if (c.hostPassengers) {
|
|
17100
17150
|
if (!dockerAvailable()) problems.push("host passengers: docker is not available");
|
|
17101
17151
|
if (!sshKeygenAvailable()) problems.push("host passengers: ssh-keygen is not available");
|
|
@@ -17105,7 +17155,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17105
17155
|
for (const r of await resolvedRepos(c)) {
|
|
17106
17156
|
if (!existsSync34(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
17107
17157
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
17108
|
-
|
|
17158
|
+
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
17109
17159
|
}
|
|
17110
17160
|
try {
|
|
17111
17161
|
const ships = await tracker.shipRows();
|
|
@@ -17117,7 +17167,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17117
17167
|
} catch (e) {
|
|
17118
17168
|
problems.push(`tracker unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
17119
17169
|
}
|
|
17120
|
-
return problems;
|
|
17170
|
+
return { problems, readiness };
|
|
17121
17171
|
}
|
|
17122
17172
|
async function doctorFix(o) {
|
|
17123
17173
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
@@ -17138,7 +17188,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17138
17188
|
for (const a of plan.actions) {
|
|
17139
17189
|
if (a.kind === "enable") {
|
|
17140
17190
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
17141
|
-
writeFileSync23(cfg.configFile, planEnable(
|
|
17191
|
+
writeFileSync23(cfg.configFile, planEnable(readFileSync30(cfg.configFile, "utf8"), a.route));
|
|
17142
17192
|
enabledSound.add(a.route);
|
|
17143
17193
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
17144
17194
|
} else {
|
|
@@ -17559,7 +17609,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
17559
17609
|
f.candidate.branch,
|
|
17560
17610
|
repo.branch.base,
|
|
17561
17611
|
f.paths,
|
|
17562
|
-
handedBack
|
|
17612
|
+
handedBack,
|
|
17613
|
+
outcome.merged.filter((m) => m.branch).map((m) => ({ key: m.ticket.issue_id, branch: m.branch }))
|
|
17563
17614
|
);
|
|
17564
17615
|
const r = await applyConflictBounce(tracker, bounce, tracker.contract, seat, remit, dryRun);
|
|
17565
17616
|
if (r.kind === "failed") {
|
|
@@ -18159,7 +18210,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18159
18210
|
process.exit(1);
|
|
18160
18211
|
}
|
|
18161
18212
|
requireArmed("run the daemon loop");
|
|
18162
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
18213
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join37(CREW_HOME, "src"), join37(CREW_HOME, "dist"), join37(CREW_HOME, "bin")];
|
|
18163
18214
|
const configPaths = daemonConfigPaths(cfg);
|
|
18164
18215
|
watchPaths.push(...configPaths);
|
|
18165
18216
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -18378,7 +18429,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18378
18429
|
`);
|
|
18379
18430
|
if (!dryRun) {
|
|
18380
18431
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
18381
|
-
const raw = existsSync34(resolvedPath) ? JSON.parse(
|
|
18432
|
+
const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync30(resolvedPath, "utf8")) : {};
|
|
18382
18433
|
raw.agentSkills = result.agentSkills;
|
|
18383
18434
|
mkdirSync22(dirOf(resolvedPath), { recursive: true });
|
|
18384
18435
|
writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -18404,7 +18455,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18404
18455
|
throw e;
|
|
18405
18456
|
}
|
|
18406
18457
|
const oldName = cfg.ship.name;
|
|
18407
|
-
writeFileSync23(cfg.configFile, setShipNameInYaml(
|
|
18458
|
+
writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync30(cfg.configFile, "utf8"), newName));
|
|
18408
18459
|
cfg.ship.name = newName;
|
|
18409
18460
|
let renameMachineId;
|
|
18410
18461
|
try {
|
|
@@ -18451,7 +18502,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18451
18502
|
process.exit(2);
|
|
18452
18503
|
};
|
|
18453
18504
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
18454
|
-
const configText =
|
|
18505
|
+
const configText = readFileSync30(cfg.configFile, "utf8");
|
|
18455
18506
|
if (sub === "hooks") {
|
|
18456
18507
|
try {
|
|
18457
18508
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
@@ -18465,7 +18516,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18465
18516
|
}
|
|
18466
18517
|
}, read: (r) => {
|
|
18467
18518
|
try {
|
|
18468
|
-
return
|
|
18519
|
+
return readFileSync30(resolvePath(dir, r), "utf8");
|
|
18469
18520
|
} catch {
|
|
18470
18521
|
return void 0;
|
|
18471
18522
|
}
|
|
@@ -18575,7 +18626,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18575
18626
|
if (sub === "list" && !routeName) usage2();
|
|
18576
18627
|
if (sub === "add") {
|
|
18577
18628
|
try {
|
|
18578
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync34(resolvePath(p.replace(/^~(?=\/|$)/,
|
|
18629
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync34(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
18579
18630
|
routeName = a.route;
|
|
18580
18631
|
addPath = a.path;
|
|
18581
18632
|
addName = a.name;
|
|
@@ -18606,7 +18657,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18606
18657
|
let resolvedRemotes;
|
|
18607
18658
|
if (existsSync34(resolvedFile)) {
|
|
18608
18659
|
try {
|
|
18609
|
-
resolvedRemotes = JSON.parse(
|
|
18660
|
+
resolvedRemotes = JSON.parse(readFileSync30(resolvedFile, "utf8"));
|
|
18610
18661
|
} catch {
|
|
18611
18662
|
}
|
|
18612
18663
|
}
|
|
@@ -18627,7 +18678,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18627
18678
|
isCheckout,
|
|
18628
18679
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
18629
18680
|
try {
|
|
18630
|
-
return Object.keys(parseRepoConfig(
|
|
18681
|
+
return Object.keys(parseRepoConfig(readFileSync30(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
18631
18682
|
} catch {
|
|
18632
18683
|
return false;
|
|
18633
18684
|
}
|
|
@@ -18682,7 +18733,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18682
18733
|
say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
|
|
18683
18734
|
break;
|
|
18684
18735
|
}
|
|
18685
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
18736
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
18686
18737
|
const top = existsSync34(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
18687
18738
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
18688
18739
|
const origin = originOf(top);
|
|
@@ -18736,7 +18787,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18736
18787
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
18737
18788
|
`);
|
|
18738
18789
|
if (existsSync34(resolvedFile)) {
|
|
18739
|
-
const raw = JSON.parse(
|
|
18790
|
+
const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
|
|
18740
18791
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
18741
18792
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
18742
18793
|
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -19094,7 +19145,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19094
19145
|
process.exit(2);
|
|
19095
19146
|
}
|
|
19096
19147
|
if (found.shipNameAdopted && !dryRun) {
|
|
19097
|
-
writeFileSync23(cfg.configFile, setShipNameInYaml(
|
|
19148
|
+
writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync30(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
19098
19149
|
cfg.ship.name = found.shipNameAdopted;
|
|
19099
19150
|
}
|
|
19100
19151
|
if (found.shipRow) {
|
|
@@ -19140,7 +19191,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19140
19191
|
const hadPreviousFile = existsSync34(resolvedPath);
|
|
19141
19192
|
if (hadPreviousFile) {
|
|
19142
19193
|
try {
|
|
19143
|
-
previous = JSON.parse(
|
|
19194
|
+
previous = JSON.parse(readFileSync30(resolvedPath, "utf8"));
|
|
19144
19195
|
} catch {
|
|
19145
19196
|
}
|
|
19146
19197
|
}
|
|
@@ -19998,7 +20049,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19998
20049
|
};
|
|
19999
20050
|
const droppedRoutes = [];
|
|
20000
20051
|
try {
|
|
20001
|
-
const rawCfg = parseYaml2(
|
|
20052
|
+
const rawCfg = parseYaml2(readFileSync30(cfg.configFile, "utf8"));
|
|
20002
20053
|
for (const r of rawCfg?.routes ?? []) {
|
|
20003
20054
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
20004
20055
|
try {
|
|
@@ -20092,7 +20143,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20092
20143
|
}
|
|
20093
20144
|
case "log": {
|
|
20094
20145
|
try {
|
|
20095
|
-
const text =
|
|
20146
|
+
const text = readFileSync30(cfg.ship.logFile, "utf8").split("\n");
|
|
20096
20147
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
20097
20148
|
`);
|
|
20098
20149
|
} catch {
|
|
@@ -20115,7 +20166,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20115
20166
|
};
|
|
20116
20167
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
20117
20168
|
const tracker = new Tracker(route, cfg.ship);
|
|
20118
|
-
const configText =
|
|
20169
|
+
const configText = readFileSync30(cfg.configFile, "utf8");
|
|
20119
20170
|
const cfgBase = dirname20(cfg.configFile);
|
|
20120
20171
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
20121
20172
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
@@ -20153,19 +20204,19 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20153
20204
|
if (row) {
|
|
20154
20205
|
await tracker.deleteRepoRow(row.id);
|
|
20155
20206
|
if (existsSync34(resolvedFile)) {
|
|
20156
|
-
const raw = JSON.parse(
|
|
20207
|
+
const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
|
|
20157
20208
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
20158
20209
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
20159
20210
|
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
20160
20211
|
`);
|
|
20161
20212
|
}
|
|
20162
20213
|
}
|
|
20163
|
-
if (existsSync34(
|
|
20214
|
+
if (existsSync34(join37(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
|
|
20164
20215
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
20165
20216
|
break;
|
|
20166
20217
|
}
|
|
20167
20218
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
20168
|
-
const fresh = !existsSync34(
|
|
20219
|
+
const fresh = !existsSync34(join37(dir, ".git"));
|
|
20169
20220
|
if (existsSync34(dir) && fresh && readdirSync14(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
20170
20221
|
if (dryRun) {
|
|
20171
20222
|
say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
|
|
@@ -20173,8 +20224,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20173
20224
|
}
|
|
20174
20225
|
if (fresh) {
|
|
20175
20226
|
mkdirSync22(dir, { recursive: true });
|
|
20176
|
-
writeFileSync23(
|
|
20177
|
-
writeFileSync23(
|
|
20227
|
+
writeFileSync23(join37(dir, "README.md"), SAMPLE_README);
|
|
20228
|
+
writeFileSync23(join37(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
20178
20229
|
const g = (...a) => {
|
|
20179
20230
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
20180
20231
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -20193,7 +20244,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20193
20244
|
});
|
|
20194
20245
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
20195
20246
|
if (existsSync34(resolvedFile)) {
|
|
20196
|
-
const raw = JSON.parse(
|
|
20247
|
+
const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
|
|
20197
20248
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
20198
20249
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
20199
20250
|
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -20404,6 +20455,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20404
20455
|
// Additive (CREW-1379): `unsafe` means a ship must not cut a
|
|
20405
20456
|
// worktree or release from this base. `version` stays 1.
|
|
20406
20457
|
baseBranch: baseHealth(r),
|
|
20458
|
+
// Additive (CREW-2537): gaps that only matter once a ticket reaches
|
|
20459
|
+
// release (no deploy hook, no CI provider...). They never stop a poll.
|
|
20460
|
+
releaseReadiness: validateEffective(r.config),
|
|
20407
20461
|
// Additive (CREW-2051): worktrees the sweep refuses to remove.
|
|
20408
20462
|
heldWorktrees: openShipAttention(state).filter((i) => i.kind === "worktree_held" && i.held?.repo === r.name).map((i) => ({
|
|
20409
20463
|
path: i.held.path,
|
|
@@ -20556,7 +20610,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
20556
20610
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
20557
20611
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
20558
20612
|
try {
|
|
20559
|
-
return
|
|
20613
|
+
return readFileSync30(p, "utf8");
|
|
20560
20614
|
} catch {
|
|
20561
20615
|
return null;
|
|
20562
20616
|
}
|
|
@@ -20613,7 +20667,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
20613
20667
|
);
|
|
20614
20668
|
}
|
|
20615
20669
|
for (const p2 of validateEffective(config)) {
|
|
20616
|
-
process.stdout.write(` - ${p2}
|
|
20670
|
+
process.stdout.write(` - release readiness: ${p2}
|
|
20617
20671
|
`);
|
|
20618
20672
|
}
|
|
20619
20673
|
const dh = doctorBaseReport(r, config.branch.base, baseHealth);
|
|
@@ -20944,6 +20998,10 @@ ${foreign.join("\n")}
|
|
|
20944
20998
|
}
|
|
20945
20999
|
for (const l of doctorSelftestLines(readSelftest(cfg.ship.stateDir), shipToolchains(cfg.ship).map((t) => t.name))) {
|
|
20946
21000
|
process.stdout.write(`${l}
|
|
21001
|
+
`);
|
|
21002
|
+
}
|
|
21003
|
+
for (const t of shipToolchains(cfg.ship).filter((t2) => t2.harness === "claude")) {
|
|
21004
|
+
process.stdout.write(`auth: ${t.name}: ${describeClaudeAuth(t)}
|
|
20947
21005
|
`);
|
|
20948
21006
|
}
|
|
20949
21007
|
const interactive = !!(process.stdin.isTTY && process.stdout.isTTY);
|