@tablation/crew 0.91.0 → 0.92.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 +1219 -786
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -352,8 +352,8 @@ var Emitter = class _Emitter {
|
|
|
352
352
|
* actually knows which route it's working (release, per-repo poll) uses
|
|
353
353
|
* this instead, so its events aren't all attributed to `routes[0]`.
|
|
354
354
|
*/
|
|
355
|
-
forRoute(
|
|
356
|
-
return new _Emitter({ ...this.opts, route, cycleId: this.cycle });
|
|
355
|
+
forRoute(route2) {
|
|
356
|
+
return new _Emitter({ ...this.opts, route: route2, cycleId: this.cycle });
|
|
357
357
|
}
|
|
358
358
|
/** Times a step and emits its outcome, so a view can show duration. */
|
|
359
359
|
async timed(step, message, fn, role) {
|
|
@@ -363,11 +363,11 @@ var Emitter = class _Emitter {
|
|
|
363
363
|
const result = await fn();
|
|
364
364
|
this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
|
|
365
365
|
return result;
|
|
366
|
-
} catch (
|
|
367
|
-
this.error(`${message} \u2014 ${
|
|
366
|
+
} catch (err2) {
|
|
367
|
+
this.error(`${message} \u2014 ${err2.message}`, {
|
|
368
368
|
data: { ms: this.now().getTime() - started, ok: false }
|
|
369
369
|
});
|
|
370
|
-
throw
|
|
370
|
+
throw err2;
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
};
|
|
@@ -1521,8 +1521,8 @@ function shipToolchains(ship) {
|
|
|
1521
1521
|
};
|
|
1522
1522
|
return [legacy, ...explicit];
|
|
1523
1523
|
}
|
|
1524
|
-
function resolveToolchain(ship,
|
|
1525
|
-
const name = (repo &&
|
|
1524
|
+
function resolveToolchain(ship, route2, repo) {
|
|
1525
|
+
const name = (repo && route2?.repoOverrides?.[repo]?.toolchain) ?? route2?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
|
|
1526
1526
|
const found = shipToolchains(ship).find((t) => t.name === name);
|
|
1527
1527
|
if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
|
|
1528
1528
|
return found;
|
|
@@ -1537,12 +1537,12 @@ function toolchainProvider(spec) {
|
|
|
1537
1537
|
if (isOmlxLauncher(spec)) return "local";
|
|
1538
1538
|
return adapterFor(spec.harness).provider(spec);
|
|
1539
1539
|
}
|
|
1540
|
-
function resolveSeat(ship,
|
|
1540
|
+
function resolveSeat(ship, route2, role, persona = {}, repo) {
|
|
1541
1541
|
const warnings = [];
|
|
1542
|
-
const base = resolveToolchain(ship,
|
|
1542
|
+
const base = resolveToolchain(ship, route2, repo);
|
|
1543
1543
|
const baseVendor = toolchainVendor(base);
|
|
1544
|
-
const repoToolchain = repo ?
|
|
1545
|
-
const baseWhy = repoToolchain ? `repo ${repo} toolchain ${base.name}` :
|
|
1544
|
+
const repoToolchain = repo ? route2?.repoOverrides?.[repo]?.toolchain : void 0;
|
|
1545
|
+
const baseWhy = repoToolchain ? `repo ${repo} toolchain ${base.name}` : route2?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
|
|
1546
1546
|
let chosen = base;
|
|
1547
1547
|
let toolchainWhy = baseWhy;
|
|
1548
1548
|
if (persona.vendor && persona.vendor !== baseVendor) {
|
|
@@ -2626,23 +2626,23 @@ function parseBranchOverride(raw, where, file) {
|
|
|
2626
2626
|
if (base === void 0 && name === void 0 && push === void 0 && remote === void 0 && lowercase === void 0) return void 0;
|
|
2627
2627
|
return { base, name, push, remote, ...lowercase !== void 0 ? { lowercase } : {} };
|
|
2628
2628
|
}
|
|
2629
|
-
function mergeRouteRelease(
|
|
2630
|
-
const provider = override?.ci?.provider ??
|
|
2631
|
-
const ref = override?.ci?.ref ??
|
|
2632
|
-
const match = override?.verify?.match ??
|
|
2633
|
-
const timeoutSeconds = override?.verify?.timeoutSeconds ??
|
|
2634
|
-
const intervalSeconds = override?.verify?.intervalSeconds ??
|
|
2635
|
-
const isolateMax = override?.isolateFailures?.maxCandidates ??
|
|
2629
|
+
function mergeRouteRelease(route2, override) {
|
|
2630
|
+
const provider = override?.ci?.provider ?? route2.ci?.provider;
|
|
2631
|
+
const ref = override?.ci?.ref ?? route2.ci?.ref;
|
|
2632
|
+
const match = override?.verify?.match ?? route2.verify?.match;
|
|
2633
|
+
const timeoutSeconds = override?.verify?.timeoutSeconds ?? route2.verify?.timeoutSeconds;
|
|
2634
|
+
const intervalSeconds = override?.verify?.intervalSeconds ?? route2.verify?.intervalSeconds;
|
|
2635
|
+
const isolateMax = override?.isolateFailures?.maxCandidates ?? route2.isolateFailures?.maxCandidates;
|
|
2636
2636
|
return {
|
|
2637
|
-
mode: override?.mode ??
|
|
2637
|
+
mode: override?.mode ?? route2.mode,
|
|
2638
2638
|
ci: provider !== void 0 || ref !== void 0 ? { provider, ref } : void 0,
|
|
2639
2639
|
verify: match !== void 0 || timeoutSeconds !== void 0 || intervalSeconds !== void 0 ? { match, timeoutSeconds, intervalSeconds } : void 0,
|
|
2640
2640
|
...isolateMax === void 0 ? {} : { isolateFailures: { maxCandidates: isolateMax } },
|
|
2641
|
-
versioning: override?.versioning ??
|
|
2642
|
-
versionFiles: override?.versionFiles ??
|
|
2643
|
-
changelog: override?.changelog ??
|
|
2644
|
-
tag: override?.tag ??
|
|
2645
|
-
tagPattern: override?.tagPattern ??
|
|
2641
|
+
versioning: override?.versioning ?? route2.versioning,
|
|
2642
|
+
versionFiles: override?.versionFiles ?? route2.versionFiles,
|
|
2643
|
+
changelog: override?.changelog ?? route2.changelog,
|
|
2644
|
+
tag: override?.tag ?? route2.tag,
|
|
2645
|
+
tagPattern: override?.tagPattern ?? route2.tagPattern
|
|
2646
2646
|
};
|
|
2647
2647
|
}
|
|
2648
2648
|
function expand(p, base) {
|
|
@@ -2695,30 +2695,30 @@ function configSearchPath(crewHome) {
|
|
|
2695
2695
|
];
|
|
2696
2696
|
}
|
|
2697
2697
|
function findConfigFile(crewHome) {
|
|
2698
|
-
const
|
|
2699
|
-
return
|
|
2698
|
+
const candidates2 = configSearchPath(crewHome);
|
|
2699
|
+
return candidates2.find((c) => existsSync9(c)) ?? candidates2[candidates2.length - 1];
|
|
2700
2700
|
}
|
|
2701
2701
|
var defaultConfigPath = () => join11(process.env.XDG_CONFIG_HOME ?? join11(homedir6(), ".config"), "crew", "crew.yaml");
|
|
2702
|
-
function routeSlug(
|
|
2703
|
-
return
|
|
2702
|
+
function routeSlug(route2) {
|
|
2703
|
+
return route2.replace(/\//g, "-");
|
|
2704
2704
|
}
|
|
2705
|
-
function resolvedPathFor(stateDir,
|
|
2706
|
-
const [workspace, project] = splitRoute(
|
|
2705
|
+
function resolvedPathFor(stateDir, route2) {
|
|
2706
|
+
const [workspace, project] = splitRoute(route2);
|
|
2707
2707
|
return join11(stateDir, "resolved", workspace, `${project}.json`);
|
|
2708
2708
|
}
|
|
2709
|
-
function apiKeyPathFor(stateDir,
|
|
2710
|
-
const [workspace, project] = splitRoute(
|
|
2709
|
+
function apiKeyPathFor(stateDir, route2) {
|
|
2710
|
+
const [workspace, project] = splitRoute(route2);
|
|
2711
2711
|
return join11(stateDir, "keys", workspace, `${project}.env`);
|
|
2712
2712
|
}
|
|
2713
|
-
function splitRoute(
|
|
2714
|
-
const parts =
|
|
2713
|
+
function splitRoute(route2) {
|
|
2714
|
+
const parts = route2.split("/");
|
|
2715
2715
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
2716
|
-
throw new ConfigError(`route must be "workspace/project" (got "${
|
|
2716
|
+
throw new ConfigError(`route must be "workspace/project" (got "${route2}")`);
|
|
2717
2717
|
}
|
|
2718
2718
|
return [parts[0], parts[1]];
|
|
2719
2719
|
}
|
|
2720
|
-
function readResolvedFile(stateDir,
|
|
2721
|
-
const path = resolvedPathFor(stateDir,
|
|
2720
|
+
function readResolvedFile(stateDir, route2) {
|
|
2721
|
+
const path = resolvedPathFor(stateDir, route2);
|
|
2722
2722
|
if (!existsSync9(path)) return void 0;
|
|
2723
2723
|
try {
|
|
2724
2724
|
return JSON.parse(readFileSync10(path, "utf8"));
|
|
@@ -3019,14 +3019,14 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
3019
3019
|
if (toolchain !== void 0 && !toolchainNames.has(toolchain)) {
|
|
3020
3020
|
missing.add(`${where}.toolchain "${toolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
|
|
3021
3021
|
}
|
|
3022
|
-
const
|
|
3022
|
+
const route2 = missing.req(c?.route, `${where}.route`);
|
|
3023
3023
|
if (c?.hostPeriscopes === true && c?.hostPassengers !== true) {
|
|
3024
|
-
missing.add(`${where} (route "${
|
|
3024
|
+
missing.add(`${where} (route "${route2}") sets hostPeriscopes: true without hostPassengers: true \u2014 a Passenger must be able to board before they can open a periscope`);
|
|
3025
3025
|
}
|
|
3026
|
-
const routeParts =
|
|
3026
|
+
const routeParts = route2 ? route2.split("/") : [];
|
|
3027
3027
|
const routeWellFormed = routeParts.length === 2 && routeParts.every(Boolean);
|
|
3028
|
-
if (
|
|
3029
|
-
missing.add(`${where}.route must be "workspace/project" (got "${
|
|
3028
|
+
if (route2 && !routeWellFormed) {
|
|
3029
|
+
missing.add(`${where}.route must be "workspace/project" (got "${route2}")`);
|
|
3030
3030
|
}
|
|
3031
3031
|
const repoDirs = {};
|
|
3032
3032
|
const repoOverrides = {};
|
|
@@ -3063,13 +3063,13 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
3063
3063
|
const hasRepos = Object.keys(repoDirs).length > 0;
|
|
3064
3064
|
const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
|
|
3065
3065
|
const dirIsCloneBase = !c?.dir && !hasRepos && !Array.isArray(c?.repos);
|
|
3066
|
-
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join11(reposBasePath, splitRoute(
|
|
3066
|
+
const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join11(reposBasePath, splitRoute(route2)[0]) : "";
|
|
3067
3067
|
const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
|
|
3068
3068
|
if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
|
|
3069
3069
|
missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
|
|
3070
3070
|
}
|
|
3071
3071
|
const result = {
|
|
3072
|
-
route,
|
|
3072
|
+
route: route2,
|
|
3073
3073
|
enabled: c?.enabled === true,
|
|
3074
3074
|
area: c?.area,
|
|
3075
3075
|
dir,
|
|
@@ -3100,7 +3100,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
3100
3100
|
hostPassengers: c?.hostPassengers === true,
|
|
3101
3101
|
hostPeriscopes: c?.hostPeriscopes === true
|
|
3102
3102
|
};
|
|
3103
|
-
const resolvedFile = routeWellFormed ? readResolvedFile(stateDir,
|
|
3103
|
+
const resolvedFile = routeWellFormed ? readResolvedFile(stateDir, route2) : void 0;
|
|
3104
3104
|
result.resolved = parseResolved(resolvedFile ?? c?.resolved, missing, where);
|
|
3105
3105
|
result.contract = resolvedFile?.contract ?? c?.contract ?? null;
|
|
3106
3106
|
missing.throwIfAny(file);
|
|
@@ -3115,9 +3115,9 @@ function locateDir(cfg, dir, commonDirOf) {
|
|
|
3115
3115
|
return target2 === b || target2.startsWith(`${b}/`);
|
|
3116
3116
|
};
|
|
3117
3117
|
const find = (target2) => {
|
|
3118
|
-
for (const
|
|
3119
|
-
const repo = reposOf(
|
|
3120
|
-
if (repo) return { route, repo, checkout: resolve(repo.dir) };
|
|
3118
|
+
for (const route2 of cfg.routes) {
|
|
3119
|
+
const repo = reposOf(route2).find((x) => within(x.dir, target2));
|
|
3120
|
+
if (repo) return { route: route2, repo, checkout: resolve(repo.dir) };
|
|
3121
3121
|
}
|
|
3122
3122
|
return void 0;
|
|
3123
3123
|
};
|
|
@@ -3127,8 +3127,8 @@ function locateDir(cfg, dir, commonDirOf) {
|
|
|
3127
3127
|
const common = commonDirOf?.(target);
|
|
3128
3128
|
return common ? find(dirname4(resolve(target, common))) : void 0;
|
|
3129
3129
|
}
|
|
3130
|
-
function findRoute(cfg,
|
|
3131
|
-
if (!
|
|
3130
|
+
function findRoute(cfg, route2) {
|
|
3131
|
+
if (!route2) {
|
|
3132
3132
|
const enabled = cfg.routes.filter((r) => r.enabled);
|
|
3133
3133
|
if (enabled.length === 1) return enabled[0];
|
|
3134
3134
|
if (cfg.routes.length === 1) return cfg.routes[0];
|
|
@@ -3136,9 +3136,9 @@ function findRoute(cfg, route) {
|
|
|
3136
3136
|
`this ship has ${cfg.routes.length} routes \u2014 name one: ${cfg.routes.map((r) => r.route).join(", ")}`
|
|
3137
3137
|
);
|
|
3138
3138
|
}
|
|
3139
|
-
const found = cfg.routes.find((r) => r.route ===
|
|
3139
|
+
const found = cfg.routes.find((r) => r.route === route2);
|
|
3140
3140
|
if (!found) {
|
|
3141
|
-
throw new ConfigError(`no route "${
|
|
3141
|
+
throw new ConfigError(`no route "${route2}" (have: ${cfg.routes.map((r) => r.route).join(", ")})`);
|
|
3142
3142
|
}
|
|
3143
3143
|
return found;
|
|
3144
3144
|
}
|
|
@@ -3184,13 +3184,13 @@ var KEYCHAIN_ELIGIBLE_ROUTES = /* @__PURE__ */ new WeakSet();
|
|
|
3184
3184
|
async function hydrateApiKeys(routes, getStore = getSessionStore) {
|
|
3185
3185
|
let store;
|
|
3186
3186
|
let storeUnavailable = false;
|
|
3187
|
-
for (const
|
|
3188
|
-
if (!KEYCHAIN_ELIGIBLE_ROUTES.has(
|
|
3187
|
+
for (const route2 of routes) {
|
|
3188
|
+
if (!KEYCHAIN_ELIGIBLE_ROUTES.has(route2)) {
|
|
3189
3189
|
try {
|
|
3190
|
-
resolveApiKey(
|
|
3190
|
+
resolveApiKey(route2);
|
|
3191
3191
|
continue;
|
|
3192
3192
|
} catch {
|
|
3193
|
-
KEYCHAIN_ELIGIBLE_ROUTES.add(
|
|
3193
|
+
KEYCHAIN_ELIGIBLE_ROUTES.add(route2);
|
|
3194
3194
|
}
|
|
3195
3195
|
}
|
|
3196
3196
|
if (storeUnavailable) continue;
|
|
@@ -3204,13 +3204,13 @@ async function hydrateApiKeys(routes, getStore = getSessionStore) {
|
|
|
3204
3204
|
}
|
|
3205
3205
|
let workspaceSlug;
|
|
3206
3206
|
try {
|
|
3207
|
-
[workspaceSlug] = splitRoute(
|
|
3207
|
+
[workspaceSlug] = splitRoute(route2.route);
|
|
3208
3208
|
} catch {
|
|
3209
3209
|
continue;
|
|
3210
3210
|
}
|
|
3211
3211
|
try {
|
|
3212
|
-
const session = await store.get(hostFromUrl(
|
|
3213
|
-
if (session?.apiKey)
|
|
3212
|
+
const session = await store.get(hostFromUrl(route2.baseUrl), workspaceSlug);
|
|
3213
|
+
if (session?.apiKey) route2.apiKey = session.apiKey;
|
|
3214
3214
|
} catch {
|
|
3215
3215
|
}
|
|
3216
3216
|
}
|
|
@@ -3404,10 +3404,10 @@ function runScript(script, opts) {
|
|
|
3404
3404
|
child.stdout.on("data", absorb);
|
|
3405
3405
|
child.stderr.on("data", absorb);
|
|
3406
3406
|
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : void 0;
|
|
3407
|
-
child.on("error", (
|
|
3407
|
+
child.on("error", (err2) => {
|
|
3408
3408
|
if (timer) clearTimeout(timer);
|
|
3409
3409
|
resolvePromise({ code: 127, output: `${output}
|
|
3410
|
-
crew: cannot run ${shell.bin}: ${
|
|
3410
|
+
crew: cannot run ${shell.bin}: ${err2.message}` });
|
|
3411
3411
|
});
|
|
3412
3412
|
child.on("close", (code) => {
|
|
3413
3413
|
if (timer) clearTimeout(timer);
|
|
@@ -3418,8 +3418,8 @@ crew: cannot run ${shell.bin}: ${err.message}` });
|
|
|
3418
3418
|
}
|
|
3419
3419
|
|
|
3420
3420
|
// src/notify.ts
|
|
3421
|
-
async function notify(
|
|
3422
|
-
const script =
|
|
3421
|
+
async function notify(route2, ship, n, emit, dryRun = false) {
|
|
3422
|
+
const script = route2.hooks.notify;
|
|
3423
3423
|
if (!script) return false;
|
|
3424
3424
|
if (dryRun) {
|
|
3425
3425
|
emit.emit(`would notify: [${n.level}] ${n.headline}`);
|
|
@@ -3427,12 +3427,12 @@ async function notify(route, ship, n, emit, dryRun = false) {
|
|
|
3427
3427
|
}
|
|
3428
3428
|
try {
|
|
3429
3429
|
const r = await runScript(script, {
|
|
3430
|
-
cwd:
|
|
3430
|
+
cwd: route2.dir,
|
|
3431
3431
|
env: {
|
|
3432
3432
|
CREW_LEVEL: n.level,
|
|
3433
3433
|
CREW_HEADLINE: n.headline,
|
|
3434
3434
|
CREW_DETAIL: n.detail ?? "",
|
|
3435
|
-
CREW_ROUTE:
|
|
3435
|
+
CREW_ROUTE: route2.route,
|
|
3436
3436
|
CREW_SHIP: ship.name
|
|
3437
3437
|
},
|
|
3438
3438
|
shell: resolveShell(ship.shell),
|
|
@@ -3457,18 +3457,18 @@ The \`${f.hook}\` hook (\`${f.label}\`) printed, last lines:
|
|
|
3457
3457
|
${f.tail}
|
|
3458
3458
|
\`\`\``;
|
|
3459
3459
|
}
|
|
3460
|
-
function describeRelease(o,
|
|
3460
|
+
function describeRelease(o, route2) {
|
|
3461
3461
|
if (o.integrated) {
|
|
3462
3462
|
return {
|
|
3463
3463
|
level: "ok",
|
|
3464
|
-
headline: `${
|
|
3464
|
+
headline: `${route2}: integrated ${o.version ?? o.merged.length + " ticket(s)"}`,
|
|
3465
3465
|
detail: "merged to the base branch \u2014 this repo has no deploy step"
|
|
3466
3466
|
};
|
|
3467
3467
|
}
|
|
3468
3468
|
if (o.deployed) {
|
|
3469
3469
|
return {
|
|
3470
3470
|
level: o.confirmed === false ? "warn" : "ok",
|
|
3471
|
-
headline: `${
|
|
3471
|
+
headline: `${route2}: released ${o.version ?? "a new version"}`,
|
|
3472
3472
|
detail: o.confirmed === false ? "deploy ran, but the target did not confirm the new commit in time" : `${o.merged.length} ticket(s) shipped`
|
|
3473
3473
|
};
|
|
3474
3474
|
}
|
|
@@ -3476,7 +3476,7 @@ function describeRelease(o, route) {
|
|
|
3476
3476
|
if (o.stopped === "tests failed") {
|
|
3477
3477
|
return {
|
|
3478
3478
|
level: "fail",
|
|
3479
|
-
headline: `${
|
|
3479
|
+
headline: `${route2}: release blocked \u2014 tests failed`,
|
|
3480
3480
|
detail: withHookTail(
|
|
3481
3481
|
(o.composition ? `the target stays on the previous release
|
|
3482
3482
|
|
|
@@ -3491,7 +3491,7 @@ ${o.isolation}` : ""),
|
|
|
3491
3491
|
if (o.stopped === "setup failed") {
|
|
3492
3492
|
return {
|
|
3493
3493
|
level: "fail",
|
|
3494
|
-
headline: `${
|
|
3494
|
+
headline: `${route2}: release blocked \u2014 setup failed`,
|
|
3495
3495
|
detail: withHookTail(
|
|
3496
3496
|
"the setup hook failed in the release checkout; the target stays on the previous release",
|
|
3497
3497
|
o.hookFailure
|
|
@@ -3501,7 +3501,7 @@ ${o.isolation}` : ""),
|
|
|
3501
3501
|
if (o.stopped === "deploy failed") {
|
|
3502
3502
|
return {
|
|
3503
3503
|
level: "fail",
|
|
3504
|
-
headline: `${
|
|
3504
|
+
headline: `${route2}: deploy FAILED`,
|
|
3505
3505
|
detail: withHookTail(
|
|
3506
3506
|
"the version is merged to the base branch but not live \u2014 if the deploy target was the problem, run `crew deploy` to retry (do not re-release); if the merged code itself failed to build, fix forward on a ticket branch, and merging it clears the not-retrying guard",
|
|
3507
3507
|
o.hookFailure
|
|
@@ -3511,7 +3511,7 @@ ${o.isolation}` : ""),
|
|
|
3511
3511
|
if (o.stopped === "build failed") {
|
|
3512
3512
|
return {
|
|
3513
3513
|
level: "fail",
|
|
3514
|
-
headline: `${
|
|
3514
|
+
headline: `${route2}: build FAILED`,
|
|
3515
3515
|
detail: withHookTail(`the target stays on the previous release \u2014 nothing was deployed${o.isolation ? `
|
|
3516
3516
|
|
|
3517
3517
|
${o.isolation}` : ""}`, o.hookFailure)
|
|
@@ -3520,25 +3520,25 @@ ${o.isolation}` : ""}`, o.hookFailure)
|
|
|
3520
3520
|
if (o.blockedCycles && o.blockedCycles >= 2) {
|
|
3521
3521
|
return {
|
|
3522
3522
|
level: "fail",
|
|
3523
|
-
headline: `${
|
|
3523
|
+
headline: `${route2}: release blocked \u2014 ${o.blockKind === "branch" ? "checkout is not on the base branch" : "working tree is not clean"}`,
|
|
3524
3524
|
detail: `${o.stopped ?? "blocked"} (blocked ${o.blockedCycles} cycles running) \u2014 a person has to clear it`
|
|
3525
3525
|
};
|
|
3526
3526
|
}
|
|
3527
3527
|
return null;
|
|
3528
3528
|
}
|
|
3529
|
-
function recoveredFailureHeadlines(o,
|
|
3530
|
-
const setup = `${
|
|
3531
|
-
const tests = `${
|
|
3532
|
-
const build = `${
|
|
3533
|
-
const deploy = `${
|
|
3529
|
+
function recoveredFailureHeadlines(o, route2) {
|
|
3530
|
+
const setup = `${route2}: release blocked \u2014 setup failed`;
|
|
3531
|
+
const tests = `${route2}: release blocked \u2014 tests failed`;
|
|
3532
|
+
const build = `${route2}: build FAILED`;
|
|
3533
|
+
const deploy = `${route2}: deploy FAILED`;
|
|
3534
3534
|
if (o.deployed || o.integrated) {
|
|
3535
3535
|
return [
|
|
3536
3536
|
setup,
|
|
3537
3537
|
tests,
|
|
3538
3538
|
build,
|
|
3539
3539
|
deploy,
|
|
3540
|
-
`${
|
|
3541
|
-
`${
|
|
3540
|
+
`${route2}: release blocked \u2014 checkout is not on the base branch`,
|
|
3541
|
+
`${route2}: release blocked \u2014 working tree is not clean`
|
|
3542
3542
|
];
|
|
3543
3543
|
}
|
|
3544
3544
|
switch (o.stopped) {
|
|
@@ -3604,8 +3604,8 @@ function pathFor(ship) {
|
|
|
3604
3604
|
return parts.join(":");
|
|
3605
3605
|
}
|
|
3606
3606
|
function findOnPath(bin, searchPath) {
|
|
3607
|
-
const
|
|
3608
|
-
for (const candidate of
|
|
3607
|
+
const candidates2 = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join12(dir, bin));
|
|
3608
|
+
for (const candidate of candidates2) {
|
|
3609
3609
|
try {
|
|
3610
3610
|
accessSync(candidate, constants.X_OK);
|
|
3611
3611
|
if (statSync3(candidate).isFile()) return candidate;
|
|
@@ -4103,18 +4103,18 @@ function gitRaw(cwd, args) {
|
|
|
4103
4103
|
try {
|
|
4104
4104
|
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4105
4105
|
} catch (e) {
|
|
4106
|
-
const
|
|
4107
|
-
const why = [
|
|
4108
|
-
throw new GitError(`git ${args.join(" ")}: ${why ||
|
|
4106
|
+
const err2 = e;
|
|
4107
|
+
const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
4108
|
+
throw new GitError(`git ${args.join(" ")}: ${why || err2.message.trim()}`);
|
|
4109
4109
|
}
|
|
4110
4110
|
}
|
|
4111
4111
|
function git(cwd, args) {
|
|
4112
4112
|
try {
|
|
4113
4113
|
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
4114
4114
|
} catch (e) {
|
|
4115
|
-
const
|
|
4116
|
-
const why = [
|
|
4117
|
-
throw new GitError(`git ${args.join(" ")}: ${why ||
|
|
4115
|
+
const err2 = e;
|
|
4116
|
+
const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
4117
|
+
throw new GitError(`git ${args.join(" ")}: ${why || err2.message.trim()}`);
|
|
4118
4118
|
}
|
|
4119
4119
|
}
|
|
4120
4120
|
function gitOk(cwd, args) {
|
|
@@ -4153,19 +4153,19 @@ function branchForIssue(cwd, issueKey, templates = { name: "issue-{number}" }, r
|
|
|
4153
4153
|
function branchCandidates(issueKey, templates, render2) {
|
|
4154
4154
|
const num = issueKey.replace(/^\D+/, "");
|
|
4155
4155
|
const unpadded = String(Number.parseInt(num, 10));
|
|
4156
|
-
const
|
|
4156
|
+
const candidates2 = [];
|
|
4157
4157
|
for (const template of [templates.name, templates.push].filter(Boolean)) {
|
|
4158
4158
|
const exact = render2(template);
|
|
4159
|
-
|
|
4160
|
-
if (num !== unpadded)
|
|
4159
|
+
candidates2.push(exact);
|
|
4160
|
+
if (num !== unpadded) candidates2.push(exact.replace(num, unpadded));
|
|
4161
4161
|
if (template.includes("{slug}")) {
|
|
4162
4162
|
const prefix = render2(template.slice(0, template.indexOf("{slug}")));
|
|
4163
|
-
if (prefix)
|
|
4163
|
+
if (prefix) candidates2.push(`${prefix}*`);
|
|
4164
4164
|
}
|
|
4165
4165
|
}
|
|
4166
|
-
|
|
4167
|
-
if (num !== unpadded)
|
|
4168
|
-
return [...new Set(
|
|
4166
|
+
candidates2.push(`issue-${num}`);
|
|
4167
|
+
if (num !== unpadded) candidates2.push(`issue-${unpadded}`);
|
|
4168
|
+
return [...new Set(candidates2)];
|
|
4169
4169
|
}
|
|
4170
4170
|
function remoteBranchForIssue(cwd, remote, issueKey, templates = { name: "issue-{number}" }, render2 = (t) => t.replace(/\{number\}/g, issueKey.replace(/^\D+/, ""))) {
|
|
4171
4171
|
const prefix = `refs/remotes/${remote}/`;
|
|
@@ -4303,11 +4303,11 @@ function findSquashByBranchTip(cwd, key, branch, range) {
|
|
|
4303
4303
|
function findSquashOfBranch(cwd, key, branch, base, range) {
|
|
4304
4304
|
const byTip = findSquashByBranchTip(cwd, key, branch, range);
|
|
4305
4305
|
if (byTip) return byTip;
|
|
4306
|
-
const
|
|
4307
|
-
if (
|
|
4306
|
+
const candidates2 = keyCommitsInRange(cwd, key, range);
|
|
4307
|
+
if (candidates2.length === 0) return null;
|
|
4308
4308
|
const mb = gitOk(cwd, ["merge-base", base, branch]);
|
|
4309
4309
|
const branchPatch = mb ? patchIdOf(cwd, mb, branch) : null;
|
|
4310
|
-
for (const sha of
|
|
4310
|
+
for (const sha of candidates2) {
|
|
4311
4311
|
if (gitOk(cwd, ["diff", "--quiet", branch, sha]) !== null) return sha;
|
|
4312
4312
|
if (branchPatch && gitOk(cwd, ["rev-parse", "--verify", "-q", `${sha}^`]) !== null && patchIdOf(cwd, `${sha}^`, sha) === branchPatch) return sha;
|
|
4313
4313
|
}
|
|
@@ -4556,9 +4556,9 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
|
4556
4556
|
try {
|
|
4557
4557
|
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4558
4558
|
} catch (e) {
|
|
4559
|
-
const
|
|
4560
|
-
const why = [
|
|
4561
|
-
throw new GitError(`git clone ${url} ${dir}: ${why ||
|
|
4559
|
+
const err2 = e;
|
|
4560
|
+
const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
4561
|
+
throw new GitError(`git clone ${url} ${dir}: ${why || err2.message.trim()}`);
|
|
4562
4562
|
}
|
|
4563
4563
|
cloneRecorder?.(dir);
|
|
4564
4564
|
return true;
|
|
@@ -4591,8 +4591,8 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
4591
4591
|
}
|
|
4592
4592
|
};
|
|
4593
4593
|
} catch (e) {
|
|
4594
|
-
const
|
|
4595
|
-
if (
|
|
4594
|
+
const err2 = e;
|
|
4595
|
+
if (err2.code !== "EEXIST") throw err2;
|
|
4596
4596
|
try {
|
|
4597
4597
|
if (Date.now() - statSync4(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
|
|
4598
4598
|
try {
|
|
@@ -4689,13 +4689,13 @@ function baseAttention(repo, outcome, remote, base, nowIso = (/* @__PURE__ */ ne
|
|
|
4689
4689
|
if (outcome.action === "level" || outcome.action === "fast-forwarded") return { clear: baseUnsafeKey(repo) };
|
|
4690
4690
|
return null;
|
|
4691
4691
|
}
|
|
4692
|
-
var agentLogRefusedKey = (
|
|
4693
|
-
function agentLogRefusedItem(
|
|
4692
|
+
var agentLogRefusedKey = (route2) => `agent_log_refused:${route2}`;
|
|
4693
|
+
function agentLogRefusedItem(route2, role, status2, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4694
4694
|
return {
|
|
4695
4695
|
kind: "agent_log_refused",
|
|
4696
|
-
key: agentLogRefusedKey(
|
|
4696
|
+
key: agentLogRefusedKey(route2),
|
|
4697
4697
|
since: nowIso,
|
|
4698
|
-
message: `${
|
|
4698
|
+
message: `${route2}: the tracker refused the ${role} seat's run report (${status2}) \u2014 runs and ticket spend are not being recorded; check the seat key's role`
|
|
4699
4699
|
};
|
|
4700
4700
|
}
|
|
4701
4701
|
var diskLowKey = (repo) => `disk_low:${repo}`;
|
|
@@ -4737,25 +4737,25 @@ function parseHookMissing(key, routeName) {
|
|
|
4737
4737
|
if (slash <= 0) return null;
|
|
4738
4738
|
return { repo: rest.slice(0, slash), hook: rest.slice(slash + 1) };
|
|
4739
4739
|
}
|
|
4740
|
-
function repoStopFor(items,
|
|
4740
|
+
function repoStopFor(items, route2) {
|
|
4741
4741
|
const stops = /* @__PURE__ */ new Map();
|
|
4742
4742
|
for (const i of items) {
|
|
4743
4743
|
let repo = null;
|
|
4744
4744
|
if (i.kind === "base_unsafe") repo = i.key.slice("base_unsafe:".length);
|
|
4745
4745
|
else if (i.kind === "disk_low") repo = i.key.slice("disk_low:".length);
|
|
4746
4746
|
else if (i.kind === "hook_missing") {
|
|
4747
|
-
const h = parseHookMissing(i.key,
|
|
4747
|
+
const h = parseHookMissing(i.key, route2.route);
|
|
4748
4748
|
if (h?.hook === "setup") repo = h.repo;
|
|
4749
4749
|
}
|
|
4750
4750
|
if (repo && !stops.has(repo)) stops.set(repo, i.message);
|
|
4751
4751
|
}
|
|
4752
4752
|
if (stops.size === 0) return () => null;
|
|
4753
4753
|
return (t) => {
|
|
4754
|
-
const name = repoTargetFor(
|
|
4754
|
+
const name = repoTargetFor(route2, t.repo_id)?.name;
|
|
4755
4755
|
return name ? stops.get(name) ?? null : null;
|
|
4756
4756
|
};
|
|
4757
4757
|
}
|
|
4758
|
-
function recheckEnvironmentStops(state,
|
|
4758
|
+
function recheckEnvironmentStops(state, route2, ship, deps = {}) {
|
|
4759
4759
|
const onPath = deps.onPath ?? findOnPath;
|
|
4760
4760
|
const dockerProblem = deps.dockerProblem ?? dockerPathProblem;
|
|
4761
4761
|
const cleared = [];
|
|
@@ -4768,8 +4768,8 @@ function recheckEnvironmentStops(state, route, ship, deps = {}) {
|
|
|
4768
4768
|
continue;
|
|
4769
4769
|
}
|
|
4770
4770
|
if (item.kind !== "hook_missing") continue;
|
|
4771
|
-
const h = parseHookMissing(item.key,
|
|
4772
|
-
const target = h && reposOf(
|
|
4771
|
+
const h = parseHookMissing(item.key, route2.route);
|
|
4772
|
+
const target = h && reposOf(route2).find((t) => t.name === h.repo);
|
|
4773
4773
|
if (!h || !target) continue;
|
|
4774
4774
|
let cfg;
|
|
4775
4775
|
try {
|
|
@@ -4786,11 +4786,11 @@ function recheckEnvironmentStops(state, route, ship, deps = {}) {
|
|
|
4786
4786
|
}
|
|
4787
4787
|
return cleared;
|
|
4788
4788
|
}
|
|
4789
|
-
function recheckBaseStops(state,
|
|
4789
|
+
function recheckBaseStops(state, route2) {
|
|
4790
4790
|
const open = state.shipAttention().previous().filter((i) => i.kind === "base_unsafe");
|
|
4791
4791
|
const cleared = [];
|
|
4792
4792
|
for (const item of open) {
|
|
4793
|
-
const target = reposOf(
|
|
4793
|
+
const target = reposOf(route2).find((t) => baseUnsafeKey(t.name) === item.key);
|
|
4794
4794
|
if (!target) continue;
|
|
4795
4795
|
const cfg = resolveRepoConfig(loadRepoConfig(target.dir), void 0, target.dir);
|
|
4796
4796
|
const outcome = refreshBaseBranch(target.dir, cfg.branch.remote, cfg.branch.base, true);
|
|
@@ -4975,8 +4975,8 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
|
|
|
4975
4975
|
run2(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
|
|
4976
4976
|
return { state: "ok" };
|
|
4977
4977
|
} catch (e) {
|
|
4978
|
-
const
|
|
4979
|
-
const raw = (
|
|
4978
|
+
const err2 = e;
|
|
4979
|
+
const raw = (err2.stderr ? String(err2.stderr) : err2.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
|
|
4980
4980
|
const detail = raw || "push dry-run failed";
|
|
4981
4981
|
const f = o.forge;
|
|
4982
4982
|
if (!f) return { state: "failed", detail, hint: pushAuthHint(o.remoteUrl) };
|
|
@@ -5350,8 +5350,8 @@ var State = class _State {
|
|
|
5350
5350
|
* release blocked in one repo counted cycles for all of them — a dirty tree
|
|
5351
5351
|
* in one checkout would have silently held up an unrelated project.
|
|
5352
5352
|
*/
|
|
5353
|
-
release(
|
|
5354
|
-
const suffix = _State.safe(
|
|
5353
|
+
release(route2) {
|
|
5354
|
+
const suffix = _State.safe(route2);
|
|
5355
5355
|
const blocked = `.release-blocked-${suffix}`;
|
|
5356
5356
|
const failed = `.deploy-failed-sha-${suffix}`;
|
|
5357
5357
|
const gateFailed = `.test-gate-failed-${suffix}`;
|
|
@@ -5438,8 +5438,8 @@ var State = class _State {
|
|
|
5438
5438
|
* cycle, so "how long has this been waiting" only exists if it survives
|
|
5439
5439
|
* between them.
|
|
5440
5440
|
*/
|
|
5441
|
-
fairness(
|
|
5442
|
-
const suffix = _State.safe(
|
|
5441
|
+
fairness(route2) {
|
|
5442
|
+
const suffix = _State.safe(route2);
|
|
5443
5443
|
const streakFile = `.fairness-streak-${suffix}`;
|
|
5444
5444
|
const ticketFile = `.fairness-ticket-${suffix}`;
|
|
5445
5445
|
const sinceFile = `.fairness-since-${suffix}`;
|
|
@@ -5506,8 +5506,8 @@ var State = class _State {
|
|
|
5506
5506
|
* currently needing a person emits once, rather than being silently
|
|
5507
5507
|
* skipped as though already notified.
|
|
5508
5508
|
*/
|
|
5509
|
-
attention(
|
|
5510
|
-
const file = `.attention-${_State.safe(
|
|
5509
|
+
attention(route2) {
|
|
5510
|
+
const file = `.attention-${_State.safe(route2)}.json`;
|
|
5511
5511
|
return {
|
|
5512
5512
|
previous: () => {
|
|
5513
5513
|
try {
|
|
@@ -5529,8 +5529,8 @@ var State = class _State {
|
|
|
5529
5529
|
* caller persists the new set beside `attention(route).persist(...)`. A
|
|
5530
5530
|
* missing or corrupt file reads as empty, so a fresh install warns once.
|
|
5531
5531
|
*/
|
|
5532
|
-
unroutable(
|
|
5533
|
-
const file = `.unroutable-${_State.safe(
|
|
5532
|
+
unroutable(route2) {
|
|
5533
|
+
const file = `.unroutable-${_State.safe(route2)}.json`;
|
|
5534
5534
|
return {
|
|
5535
5535
|
previous: () => {
|
|
5536
5536
|
try {
|
|
@@ -5709,11 +5709,11 @@ function repoSection(r, key, heading) {
|
|
|
5709
5709
|
return lines;
|
|
5710
5710
|
}
|
|
5711
5711
|
function renderEnvironment(i) {
|
|
5712
|
-
const { route, contract } = i;
|
|
5712
|
+
const { route: route2, contract } = i;
|
|
5713
5713
|
const repos = i.repos;
|
|
5714
5714
|
const only = repos.length === 1 ? repos[0] : void 0;
|
|
5715
|
-
const models =
|
|
5716
|
-
const epicsModelId =
|
|
5715
|
+
const models = route2.resolved?.models;
|
|
5716
|
+
const epicsModelId = route2.resolved?.epicsModelId;
|
|
5717
5717
|
const mediaLibraryModelId = i.attachments?.mediaLibraryModelId ?? null;
|
|
5718
5718
|
const key = "ISSUE-000";
|
|
5719
5719
|
const lines = [
|
|
@@ -5725,7 +5725,7 @@ function renderEnvironment(i) {
|
|
|
5725
5725
|
"",
|
|
5726
5726
|
"## The board",
|
|
5727
5727
|
"",
|
|
5728
|
-
`- Base URL: \`${
|
|
5728
|
+
`- Base URL: \`${route2.baseUrl}\``,
|
|
5729
5729
|
`- Your key is in the \`${API_KEY_VAR}\` environment variable. Send it as`,
|
|
5730
5730
|
` \`Authorization: Bearer $${API_KEY_VAR}\`. **Do not read it out of any file**,`,
|
|
5731
5731
|
" and never write it into a comment, a commit or a log line.",
|
|
@@ -5743,9 +5743,9 @@ function renderEnvironment(i) {
|
|
|
5743
5743
|
...mediaLibraryModelId ? [`- Media Library: \`${mediaLibraryModelId}\` (what a ticket's \`${contract.columns.attachments}\` ids point into)`] : []
|
|
5744
5744
|
);
|
|
5745
5745
|
}
|
|
5746
|
-
const workspaceId =
|
|
5746
|
+
const workspaceId = route2.resolved?.workspaceId;
|
|
5747
5747
|
if (workspaceId) {
|
|
5748
|
-
const base =
|
|
5748
|
+
const base = route2.baseUrl.replace(/\/+$/, "");
|
|
5749
5749
|
lines.push(
|
|
5750
5750
|
"",
|
|
5751
5751
|
`- Workspace id: \`${workspaceId}\``,
|
|
@@ -5801,7 +5801,7 @@ function renderEnvironment(i) {
|
|
|
5801
5801
|
"another (the digest prints other ships by name) \u2014 write its name, never a Ships row id."
|
|
5802
5802
|
);
|
|
5803
5803
|
}
|
|
5804
|
-
const seatIds = Object.entries(
|
|
5804
|
+
const seatIds = Object.entries(route2.resolved?.seats ?? {}).filter(([, id]) => !!id);
|
|
5805
5805
|
if (seatIds.length > 0) {
|
|
5806
5806
|
lines.push(
|
|
5807
5807
|
"",
|
|
@@ -5854,7 +5854,7 @@ function renderEnvironment(i) {
|
|
|
5854
5854
|
"",
|
|
5855
5855
|
`\`${contract.statuses.verified}\` and the closed statuses are **never yours to set**.`
|
|
5856
5856
|
);
|
|
5857
|
-
const areaId =
|
|
5857
|
+
const areaId = route2.resolved?.areaId;
|
|
5858
5858
|
if (areaId || repos.some((r) => r.id)) {
|
|
5859
5859
|
lines.push(
|
|
5860
5860
|
"",
|
|
@@ -6249,7 +6249,7 @@ function queueAgentRun(stateDir, name, q) {
|
|
|
6249
6249
|
mkdirSync9(queueDir(stateDir), { recursive: true });
|
|
6250
6250
|
writeFileSync10(join15(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
6251
6251
|
}
|
|
6252
|
-
async function replayQueuedAgentRuns(stateDir,
|
|
6252
|
+
async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
6253
6253
|
const dir = queueDir(stateDir);
|
|
6254
6254
|
if (!existsSync14(dir)) return 0;
|
|
6255
6255
|
let sent = 0;
|
|
@@ -6261,7 +6261,7 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
|
|
|
6261
6261
|
} catch {
|
|
6262
6262
|
continue;
|
|
6263
6263
|
}
|
|
6264
|
-
if (q.version !== 1 || q.route !==
|
|
6264
|
+
if (q.version !== 1 || q.route !== route2 || now.toISOString() < q.nextAttemptAt) continue;
|
|
6265
6265
|
try {
|
|
6266
6266
|
const apiKey = await keyFor(q.role);
|
|
6267
6267
|
if (!apiKey) continue;
|
|
@@ -6302,7 +6302,7 @@ function queueTicketCounters(stateDir, name, q) {
|
|
|
6302
6302
|
mkdirSync9(countersDir(stateDir), { recursive: true });
|
|
6303
6303
|
writeFileSync10(join15(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
6304
6304
|
}
|
|
6305
|
-
async function replayQueuedTicketCounters(stateDir,
|
|
6305
|
+
async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
6306
6306
|
const dir = countersDir(stateDir);
|
|
6307
6307
|
if (!existsSync14(dir)) return 0;
|
|
6308
6308
|
let sent = 0;
|
|
@@ -6314,7 +6314,7 @@ async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /*
|
|
|
6314
6314
|
} catch {
|
|
6315
6315
|
continue;
|
|
6316
6316
|
}
|
|
6317
|
-
if (q.version !== 1 || q.route !==
|
|
6317
|
+
if (q.version !== 1 || q.route !== route2 || now.toISOString() < q.nextAttemptAt) continue;
|
|
6318
6318
|
try {
|
|
6319
6319
|
const apiKey = await keyFor(q.role);
|
|
6320
6320
|
if (!apiKey) continue;
|
|
@@ -6594,8 +6594,8 @@ function projectModeLabels(rows) {
|
|
|
6594
6594
|
|
|
6595
6595
|
// src/board-lock.ts
|
|
6596
6596
|
import { StaleWriteError } from "@tablation/client";
|
|
6597
|
-
function releaseLockScope(
|
|
6598
|
-
return `${
|
|
6597
|
+
function releaseLockScope(route2, repo) {
|
|
6598
|
+
return `${route2}/${repo}`;
|
|
6599
6599
|
}
|
|
6600
6600
|
async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs, log = {}) {
|
|
6601
6601
|
if (!modelId) return { ok: true, release: async () => {
|
|
@@ -6723,21 +6723,21 @@ var Tracker = class {
|
|
|
6723
6723
|
*/
|
|
6724
6724
|
/** Told once per failed best-effort read (`events`); poll.ts points it at the pass's emitter. */
|
|
6725
6725
|
onReadFailure;
|
|
6726
|
-
constructor(
|
|
6727
|
-
this.route =
|
|
6728
|
-
if (!
|
|
6726
|
+
constructor(route2, ship, opts = {}) {
|
|
6727
|
+
this.route = route2;
|
|
6728
|
+
if (!route2.resolved) {
|
|
6729
6729
|
throw new ConfigError(
|
|
6730
|
-
`route "${
|
|
6730
|
+
`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` to discover them from the project`
|
|
6731
6731
|
);
|
|
6732
6732
|
}
|
|
6733
|
-
this.models =
|
|
6734
|
-
this.contract = resolveContract(
|
|
6733
|
+
this.models = route2.resolved.models;
|
|
6734
|
+
this.contract = resolveContract(route2.contract);
|
|
6735
6735
|
this.userAgent = ship.userAgent;
|
|
6736
6736
|
this.shipName = ship.name;
|
|
6737
6737
|
this.stateDir = ship.stateDir;
|
|
6738
6738
|
this.client = new TablationClient({
|
|
6739
|
-
baseUrl: `${
|
|
6740
|
-
apiKey: opts.apiKey ?? resolveApiKey(
|
|
6739
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
6740
|
+
apiKey: opts.apiKey ?? resolveApiKey(route2),
|
|
6741
6741
|
// Cloudflare 403s (error 1010) default agents on this host.
|
|
6742
6742
|
headers: { "User-Agent": ship.userAgent }
|
|
6743
6743
|
});
|
|
@@ -7205,8 +7205,8 @@ var Tracker = class {
|
|
|
7205
7205
|
if (res.status === 404) return [];
|
|
7206
7206
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
7207
7207
|
rows = await res.json();
|
|
7208
|
-
} catch (
|
|
7209
|
-
this.onReadFailure?.(`reading ${type} events failed: ${
|
|
7208
|
+
} catch (err2) {
|
|
7209
|
+
this.onReadFailure?.(`reading ${type} events failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
7210
7210
|
return [];
|
|
7211
7211
|
}
|
|
7212
7212
|
return rows.filter((e) => e.event_type === type).sort((a, b) => Date.parse(b.occurred_at ?? b.created_at ?? "") - Date.parse(a.occurred_at ?? a.created_at ?? ""));
|
|
@@ -8059,14 +8059,14 @@ async function enforceHandoff(i) {
|
|
|
8059
8059
|
return { kind: "failed", why: e.message };
|
|
8060
8060
|
}
|
|
8061
8061
|
}
|
|
8062
|
-
async function guardRunHandoff(
|
|
8063
|
-
const replayed = await replayRoutePending(
|
|
8062
|
+
async function guardRunHandoff(route2, ship, tracker, role, workingId, log, since2) {
|
|
8063
|
+
const replayed = await replayRoutePending(route2, ship, tracker, log, role);
|
|
8064
8064
|
if (!workingId || role !== "dev" && role !== "design" && role !== "qa") return null;
|
|
8065
|
-
const seatId =
|
|
8065
|
+
const seatId = route2.resolved?.seats[role];
|
|
8066
8066
|
if (!seatId) return null;
|
|
8067
8067
|
try {
|
|
8068
8068
|
const t = await tracker.ticket(workingId);
|
|
8069
|
-
const resolved = repoConfigFor(
|
|
8069
|
+
const resolved = repoConfigFor(route2, t);
|
|
8070
8070
|
if (!resolved) return null;
|
|
8071
8071
|
const { target, cfg } = resolved;
|
|
8072
8072
|
if (role === "qa") {
|
|
@@ -8094,7 +8094,7 @@ async function guardRunHandoff(route, ship, tracker, role, workingId, log, since
|
|
|
8094
8094
|
log.emit(`${t.issue_id}: dropped ${branch} \u2014 the run made no commits beyond ${cfg.branch.base}`, { ticket: t.issue_id, step: "handoff" });
|
|
8095
8095
|
}
|
|
8096
8096
|
}
|
|
8097
|
-
if (since2) await guardRunStrays(
|
|
8097
|
+
if (since2) await guardRunStrays(route2, ship, tracker, role, workingId, since2, log);
|
|
8098
8098
|
return outcome;
|
|
8099
8099
|
} catch (e) {
|
|
8100
8100
|
log.warn(`could not check the hand-off: ${e.message}`, { step: "handoff" });
|
|
@@ -8117,29 +8117,29 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
8117
8117
|
log.warn(`could not retire the run's worktree: ${e.message}`, { step: "handoff" });
|
|
8118
8118
|
}
|
|
8119
8119
|
}
|
|
8120
|
-
function repoConfigFor(
|
|
8121
|
-
const target = repoTargetFor(
|
|
8120
|
+
function repoConfigFor(route2, t) {
|
|
8121
|
+
const target = repoTargetFor(route2, t.repo_id);
|
|
8122
8122
|
if (!target) return null;
|
|
8123
|
-
const o =
|
|
8123
|
+
const o = route2.repoOverrides[target.name];
|
|
8124
8124
|
const cfg = resolveRepoConfig(loadRepoConfig(target.dir), {
|
|
8125
|
-
hooks: { ...
|
|
8126
|
-
labels: { ...
|
|
8127
|
-
branch: { ...
|
|
8125
|
+
hooks: { ...route2.hooks, ...o?.hooks },
|
|
8126
|
+
labels: { ...route2.labels, ...o?.labels },
|
|
8127
|
+
branch: { ...route2.branch, ...o?.branch },
|
|
8128
8128
|
worktrees: { nameOverride: o?.worktreeName }
|
|
8129
8129
|
}, target.dir);
|
|
8130
8130
|
return { target, cfg };
|
|
8131
8131
|
}
|
|
8132
|
-
async function replayRoutePending(
|
|
8132
|
+
async function replayRoutePending(route2, ship, tracker, log, role) {
|
|
8133
8133
|
try {
|
|
8134
8134
|
return await replayPendingHandoffs({
|
|
8135
8135
|
writer: tracker,
|
|
8136
8136
|
contract: tracker.contract,
|
|
8137
8137
|
stateDir: ship.stateDir,
|
|
8138
|
-
route: routeSlug(
|
|
8138
|
+
route: routeSlug(route2.route),
|
|
8139
8139
|
role,
|
|
8140
|
-
seats:
|
|
8140
|
+
seats: route2.resolved?.seats ?? {},
|
|
8141
8141
|
checkout: (t) => {
|
|
8142
|
-
const r = repoConfigFor(
|
|
8142
|
+
const r = repoConfigFor(route2, t);
|
|
8143
8143
|
return r ? { dir: r.target.dir, cfg: r.cfg } : null;
|
|
8144
8144
|
},
|
|
8145
8145
|
log
|
|
@@ -8195,23 +8195,23 @@ ${i.shipName ? `${i.shipName}'s ` : "A "}${i.role} run set it to \`${st.handoff}
|
|
|
8195
8195
|
}
|
|
8196
8196
|
}
|
|
8197
8197
|
}
|
|
8198
|
-
async function guardRunStrays(
|
|
8199
|
-
const seatId =
|
|
8198
|
+
async function guardRunStrays(route2, ship, tracker, role, workingId, since2, log) {
|
|
8199
|
+
const seatId = route2.resolved?.seats[role];
|
|
8200
8200
|
if (!seatId) return;
|
|
8201
8201
|
try {
|
|
8202
8202
|
const affinity = await tracker.claimAffinity();
|
|
8203
|
-
const
|
|
8204
|
-
if (
|
|
8203
|
+
const candidates2 = await tracker.handedOffSince(since2, workingId);
|
|
8204
|
+
if (candidates2.length === 0) return;
|
|
8205
8205
|
await guardStrayHandoffs({
|
|
8206
8206
|
writer: tracker,
|
|
8207
8207
|
contract: tracker.contract,
|
|
8208
|
-
candidates,
|
|
8208
|
+
candidates: candidates2,
|
|
8209
8209
|
seatId,
|
|
8210
8210
|
role,
|
|
8211
8211
|
myShipId: affinity?.shipId ?? null,
|
|
8212
8212
|
shipName: ship.name,
|
|
8213
8213
|
checkout: (t) => {
|
|
8214
|
-
const r = repoConfigFor(
|
|
8214
|
+
const r = repoConfigFor(route2, t);
|
|
8215
8215
|
return r ? { dir: r.target.dir, cfg: r.cfg } : null;
|
|
8216
8216
|
},
|
|
8217
8217
|
log
|
|
@@ -8230,14 +8230,14 @@ function writePendingHandoff(stateDir, rec) {
|
|
|
8230
8230
|
mkdirSync10(dir, { recursive: true });
|
|
8231
8231
|
writeFileSync11(join18(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
|
|
8232
8232
|
}
|
|
8233
|
-
function listPendingHandoffs(stateDir,
|
|
8233
|
+
function listPendingHandoffs(stateDir, route2) {
|
|
8234
8234
|
const dir = pendingDirFor(stateDir);
|
|
8235
8235
|
if (!existsSync17(dir)) return [];
|
|
8236
8236
|
const out = [];
|
|
8237
8237
|
for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
8238
8238
|
try {
|
|
8239
8239
|
const rec = JSON.parse(readFileSync15(join18(dir, f), "utf8"));
|
|
8240
|
-
if (rec.version === 1 && rec.ticketRecordId && (!
|
|
8240
|
+
if (rec.version === 1 && rec.ticketRecordId && (!route2 || rec.route === route2)) out.push(rec);
|
|
8241
8241
|
} catch {
|
|
8242
8242
|
}
|
|
8243
8243
|
}
|
|
@@ -8393,61 +8393,61 @@ function personaDefaultPrompt(promptsDir, role) {
|
|
|
8393
8393
|
if (!existsSync18(commonPath)) return readFileSync16(briefPath, "utf8");
|
|
8394
8394
|
return `${readFileSync16(commonPath, "utf8")}${readFileSync16(briefPath, "utf8")}`;
|
|
8395
8395
|
}
|
|
8396
|
-
async function resolveAgentId(
|
|
8397
|
-
const cached =
|
|
8396
|
+
async function resolveAgentId(route2, role, opts) {
|
|
8397
|
+
const cached = route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
8398
8398
|
if (cached) return cached;
|
|
8399
|
-
const seatId =
|
|
8400
|
-
if (!seatId || !
|
|
8399
|
+
const seatId = route2.resolved?.seats?.[role];
|
|
8400
|
+
if (!seatId || !route2.resolved) return void 0;
|
|
8401
8401
|
const client2 = new TablationClient2({
|
|
8402
|
-
baseUrl: `${
|
|
8403
|
-
apiKey: resolveApiKey(
|
|
8402
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
8403
|
+
apiKey: resolveApiKey(route2),
|
|
8404
8404
|
headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
8405
8405
|
});
|
|
8406
8406
|
try {
|
|
8407
|
-
const row = await client2.records.get(
|
|
8407
|
+
const row = await client2.records.get(route2.resolved.models.crew, seatId);
|
|
8408
8408
|
return row.agent_id ?? void 0;
|
|
8409
8409
|
} catch {
|
|
8410
8410
|
return void 0;
|
|
8411
8411
|
}
|
|
8412
8412
|
}
|
|
8413
|
-
async function fetchPersonaPrompt(
|
|
8414
|
-
const agentId = await resolveAgentId(
|
|
8415
|
-
if (!agentId || !
|
|
8413
|
+
async function fetchPersonaPrompt(route2, role, opts) {
|
|
8414
|
+
const agentId = await resolveAgentId(route2, role, opts);
|
|
8415
|
+
if (!agentId || !route2.resolved) return {};
|
|
8416
8416
|
const client2 = new TablationClient2({
|
|
8417
|
-
baseUrl: `${
|
|
8418
|
-
apiKey: resolveApiKey(
|
|
8417
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
8418
|
+
apiKey: resolveApiKey(route2),
|
|
8419
8419
|
headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
8420
8420
|
});
|
|
8421
8421
|
let row;
|
|
8422
8422
|
let agentsModelId;
|
|
8423
8423
|
try {
|
|
8424
|
-
const agentsModel = await client2.dataModels.get("agents",
|
|
8424
|
+
const agentsModel = await client2.dataModels.get("agents", route2.resolved.workspaceId);
|
|
8425
8425
|
agentsModelId = agentsModel.id;
|
|
8426
8426
|
row = await client2.records.get(agentsModelId, agentId);
|
|
8427
8427
|
} catch {
|
|
8428
8428
|
return {};
|
|
8429
8429
|
}
|
|
8430
8430
|
const additions = typeof row.prompt_additions === "string" && row.prompt_additions.trim() !== "" ? row.prompt_additions : void 0;
|
|
8431
|
-
const divergedPrompt = await divergedRowPrompt(
|
|
8431
|
+
const divergedPrompt = await divergedRowPrompt(route2, role, row, agentId, agentsModelId, opts);
|
|
8432
8432
|
return { divergedPrompt, additions };
|
|
8433
8433
|
}
|
|
8434
8434
|
function personaPromptOptions(p) {
|
|
8435
8435
|
return { divergedPrompt: p.divergedPrompt, personaAdditions: p.additions };
|
|
8436
8436
|
}
|
|
8437
|
-
async function divergedRowPrompt(
|
|
8438
|
-
if (!
|
|
8439
|
-
const defaultPrompt = personaDefaultPrompt(
|
|
8437
|
+
async function divergedRowPrompt(route2, role, row, agentId, agentsModelId, opts) {
|
|
8438
|
+
if (!route2.resolved) return void 0;
|
|
8439
|
+
const defaultPrompt = personaDefaultPrompt(route2.promptsDir, role);
|
|
8440
8440
|
if (row.prompt === defaultPrompt) return void 0;
|
|
8441
|
-
const cached =
|
|
8441
|
+
const cached = route2.resolved.agentPersonas?.[role];
|
|
8442
8442
|
if (cached?.agentId === agentId && cached.historyId) {
|
|
8443
|
-
const lastSynced = await fetchLastSyncedPrompt(
|
|
8443
|
+
const lastSynced = await fetchLastSyncedPrompt(route2, agentsModelId, agentId, cached.historyId, opts.userAgent);
|
|
8444
8444
|
if (lastSynced !== void 0 && lastSynced === row.prompt) return void 0;
|
|
8445
8445
|
}
|
|
8446
8446
|
return row.prompt;
|
|
8447
8447
|
}
|
|
8448
|
-
async function fetchLastSyncedPrompt(
|
|
8448
|
+
async function fetchLastSyncedPrompt(route2, agentsModelId, recordId, historyId, userAgent) {
|
|
8449
8449
|
try {
|
|
8450
|
-
const entries = await fetchHistoryEntries(
|
|
8450
|
+
const entries = await fetchHistoryEntries(route2, agentsModelId, recordId, userAgent);
|
|
8451
8451
|
return entries.find((e) => e.history_id === historyId)?.prompt;
|
|
8452
8452
|
} catch {
|
|
8453
8453
|
return void 0;
|
|
@@ -8457,16 +8457,16 @@ function choiceValue(v) {
|
|
|
8457
8457
|
const raw = typeof v === "string" ? v : v && typeof v === "object" ? v.value ?? v.key : void 0;
|
|
8458
8458
|
return typeof raw === "string" && raw.trim() ? raw.trim().toLowerCase() : void 0;
|
|
8459
8459
|
}
|
|
8460
|
-
async function fetchSeatPersona(
|
|
8461
|
-
const agentId = await resolveAgentId(
|
|
8462
|
-
if (!agentId || !
|
|
8460
|
+
async function fetchSeatPersona(route2, role, opts) {
|
|
8461
|
+
const agentId = await resolveAgentId(route2, role, opts);
|
|
8462
|
+
if (!agentId || !route2.resolved) return void 0;
|
|
8463
8463
|
const client2 = new TablationClient2({
|
|
8464
|
-
baseUrl: `${
|
|
8465
|
-
apiKey: resolveApiKey(
|
|
8464
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
8465
|
+
apiKey: resolveApiKey(route2),
|
|
8466
8466
|
headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
8467
8467
|
});
|
|
8468
8468
|
try {
|
|
8469
|
-
const agentsModel = await client2.dataModels.get("agents",
|
|
8469
|
+
const agentsModel = await client2.dataModels.get("agents", route2.resolved.workspaceId);
|
|
8470
8470
|
const row = await client2.records.get(agentsModel.id, agentId);
|
|
8471
8471
|
const tier = choiceValue(row.tier);
|
|
8472
8472
|
const vendor = choiceValue(row.vendor);
|
|
@@ -8480,24 +8480,24 @@ async function fetchSeatPersona(route, role, opts) {
|
|
|
8480
8480
|
return void 0;
|
|
8481
8481
|
}
|
|
8482
8482
|
}
|
|
8483
|
-
async function currentPersonaPrompt(
|
|
8484
|
-
const { divergedPrompt, additions } = await fetchPersonaPrompt(
|
|
8485
|
-
return appendAdditions(divergedPrompt ?? personaDefaultPrompt(
|
|
8483
|
+
async function currentPersonaPrompt(route2, role, opts) {
|
|
8484
|
+
const { divergedPrompt, additions } = await fetchPersonaPrompt(route2, role, opts);
|
|
8485
|
+
return appendAdditions(divergedPrompt ?? personaDefaultPrompt(route2.promptsDir, role), additions);
|
|
8486
8486
|
}
|
|
8487
|
-
async function fetchHistoryEntries(
|
|
8488
|
-
const url = `${
|
|
8487
|
+
async function fetchHistoryEntries(route2, agentsModelId, recordId, userAgent, limit = 50) {
|
|
8488
|
+
const url = `${route2.baseUrl.replace(/\/+$/, "")}/api/data-models/${agentsModelId}/records/${recordId}/history?limit=${limit}`;
|
|
8489
8489
|
const res = await fetch(url, {
|
|
8490
8490
|
headers: {
|
|
8491
|
-
Authorization: `Bearer ${resolveApiKey(
|
|
8491
|
+
Authorization: `Bearer ${resolveApiKey(route2)}`,
|
|
8492
8492
|
"User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
|
|
8493
8493
|
}
|
|
8494
8494
|
});
|
|
8495
8495
|
if (!res.ok) throw new AgentsSyncError(`history lookup for ${recordId}: ${res.status} ${res.statusText}`);
|
|
8496
8496
|
return await res.json();
|
|
8497
8497
|
}
|
|
8498
|
-
async function fetchLatestHistoryId(
|
|
8498
|
+
async function fetchLatestHistoryId(route2, agentsModelId, recordId, userAgent) {
|
|
8499
8499
|
try {
|
|
8500
|
-
const entries = await fetchHistoryEntries(
|
|
8500
|
+
const entries = await fetchHistoryEntries(route2, agentsModelId, recordId, userAgent, 1);
|
|
8501
8501
|
return entries[0]?.history_id;
|
|
8502
8502
|
} catch {
|
|
8503
8503
|
return void 0;
|
|
@@ -8506,11 +8506,11 @@ async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
|
|
|
8506
8506
|
function isAdminRole(role) {
|
|
8507
8507
|
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
8508
8508
|
}
|
|
8509
|
-
async function fetchKeyRole(
|
|
8510
|
-
if (!
|
|
8509
|
+
async function fetchKeyRole(route2, userAgent) {
|
|
8510
|
+
if (!route2.resolved) return void 0;
|
|
8511
8511
|
try {
|
|
8512
|
-
const res = await fetch(`${
|
|
8513
|
-
headers: { Authorization: `Bearer ${resolveApiKey(
|
|
8512
|
+
const res = await fetch(`${route2.baseUrl.replace(/\/+$/, "")}/api/auth/me?workspaceId=${route2.resolved.workspaceId}`, {
|
|
8513
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
8514
8514
|
});
|
|
8515
8515
|
if (!res.ok) return void 0;
|
|
8516
8516
|
const me = await res.json();
|
|
@@ -8536,14 +8536,14 @@ function isPromptGateRejection(e) {
|
|
|
8536
8536
|
}
|
|
8537
8537
|
var RELEASE_AGENT_NAME = "Release agent";
|
|
8538
8538
|
var RELEASE_AGENT_PROMPT = "Principal for the crew release phase (merge, deploy, release comments). It runs no model; this row only gives those tracker writes an agent identity.";
|
|
8539
|
-
async function syncPersonas(
|
|
8540
|
-
if (!
|
|
8541
|
-
throw new AgentsSyncError(`route "${
|
|
8539
|
+
async function syncPersonas(route2, opts) {
|
|
8540
|
+
if (!route2.resolved) {
|
|
8541
|
+
throw new AgentsSyncError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
|
|
8542
8542
|
}
|
|
8543
|
-
const workspaceId =
|
|
8543
|
+
const workspaceId = route2.resolved.workspaceId;
|
|
8544
8544
|
const client2 = new TablationClient2({
|
|
8545
|
-
baseUrl: `${
|
|
8546
|
-
apiKey: resolveApiKey(
|
|
8545
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
8546
|
+
apiKey: resolveApiKey(route2),
|
|
8547
8547
|
headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
8548
8548
|
});
|
|
8549
8549
|
let agentsModel;
|
|
@@ -8558,12 +8558,12 @@ async function syncPersonas(route, opts) {
|
|
|
8558
8558
|
throw e;
|
|
8559
8559
|
}
|
|
8560
8560
|
const rows = await client2.records.list(agentsModel.id, { limit: 200 });
|
|
8561
|
-
const cache =
|
|
8561
|
+
const cache = route2.resolved.agentPersonas ?? {};
|
|
8562
8562
|
const outcomes = [];
|
|
8563
8563
|
const agentPersonas = { ...cache };
|
|
8564
8564
|
for (const role of ROLE_NAMES) {
|
|
8565
8565
|
const name = PERSONA_NAME[role];
|
|
8566
|
-
const defaultPrompt = personaDefaultPrompt(
|
|
8566
|
+
const defaultPrompt = personaDefaultPrompt(route2.promptsDir, role);
|
|
8567
8567
|
const row = rows.find((r) => r.name === name);
|
|
8568
8568
|
if (opts.readOnly) {
|
|
8569
8569
|
if (!row) {
|
|
@@ -8581,7 +8581,7 @@ async function syncPersonas(route, opts) {
|
|
|
8581
8581
|
continue;
|
|
8582
8582
|
}
|
|
8583
8583
|
const created = await client2.records.create(agentsModel.id, { name, prompt: defaultPrompt, ...promptSourceFields(agentsModel, defaultPrompt) });
|
|
8584
|
-
const historyId = await fetchLatestHistoryId(
|
|
8584
|
+
const historyId = await fetchLatestHistoryId(route2, agentsModel.id, created.id, opts.userAgent);
|
|
8585
8585
|
agentPersonas[role] = { agentId: created.id, lastSyncedUpdatedAt: created.updated_at, ...historyId ? { historyId } : {} };
|
|
8586
8586
|
outcomes.push({ role, action: "created", agentId: created.id });
|
|
8587
8587
|
continue;
|
|
@@ -8595,7 +8595,7 @@ async function syncPersonas(route, opts) {
|
|
|
8595
8595
|
if (!(e instanceof StaleWriteError2) && !isPromptGateRejection(e)) throw e;
|
|
8596
8596
|
}
|
|
8597
8597
|
}
|
|
8598
|
-
const historyId = await fetchLatestHistoryId(
|
|
8598
|
+
const historyId = await fetchLatestHistoryId(route2, agentsModel.id, row.id, opts.userAgent);
|
|
8599
8599
|
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: anchoredAt, ...historyId ? { historyId } : {} };
|
|
8600
8600
|
outcomes.push({ role, action: "unchanged", agentId: row.id });
|
|
8601
8601
|
continue;
|
|
@@ -8620,7 +8620,7 @@ async function syncPersonas(route, opts) {
|
|
|
8620
8620
|
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, ...cache[role]?.agentId === row.id && cache[role]?.historyId ? { historyId: cache[role].historyId } : {} };
|
|
8621
8621
|
continue;
|
|
8622
8622
|
}
|
|
8623
|
-
const historyId = await fetchLatestHistoryId(
|
|
8623
|
+
const historyId = await fetchLatestHistoryId(route2, agentsModel.id, row.id, opts.userAgent);
|
|
8624
8624
|
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
|
|
8625
8625
|
outcomes.push({ role, action: "forced", agentId: row.id, previousPrompt: row.prompt });
|
|
8626
8626
|
continue;
|
|
@@ -8639,7 +8639,7 @@ async function syncPersonas(route, opts) {
|
|
|
8639
8639
|
}
|
|
8640
8640
|
let lastSyncedPrompt;
|
|
8641
8641
|
try {
|
|
8642
|
-
const entries = await fetchHistoryEntries(
|
|
8642
|
+
const entries = await fetchHistoryEntries(route2, agentsModel.id, row.id, opts.userAgent);
|
|
8643
8643
|
lastSyncedPrompt = entries.find((e) => e.history_id === cached.historyId)?.prompt;
|
|
8644
8644
|
} catch {
|
|
8645
8645
|
lastSyncedPrompt = void 0;
|
|
@@ -8660,7 +8660,7 @@ async function syncPersonas(route, opts) {
|
|
|
8660
8660
|
{ prompt: defaultPrompt, ...promptSourceFields(agentsModel, defaultPrompt) },
|
|
8661
8661
|
row.updated_at
|
|
8662
8662
|
);
|
|
8663
|
-
const historyId = await fetchLatestHistoryId(
|
|
8663
|
+
const historyId = await fetchLatestHistoryId(route2, agentsModel.id, row.id, opts.userAgent);
|
|
8664
8664
|
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
|
|
8665
8665
|
outcomes.push({ role, action: "updated", agentId: row.id });
|
|
8666
8666
|
} catch (e) {
|
|
@@ -8676,8 +8676,8 @@ async function syncPersonas(route, opts) {
|
|
|
8676
8676
|
throw e;
|
|
8677
8677
|
}
|
|
8678
8678
|
}
|
|
8679
|
-
const crewLinks = opts.dryRun ? [] : await linkCrewSeats(
|
|
8680
|
-
const release = opts.dryRun ? { agentId:
|
|
8679
|
+
const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route2, client2, agentsModel.id, agentPersonas, opts.userAgent, opts.readOnly === true);
|
|
8680
|
+
const release = opts.dryRun ? { agentId: route2.resolved.releaseAgentId } : await linkReleaseSeat(route2, client2, agentsModel, opts.userAgent, opts.readOnly === true);
|
|
8681
8681
|
return {
|
|
8682
8682
|
outcomes,
|
|
8683
8683
|
agentPersonas,
|
|
@@ -8687,8 +8687,8 @@ async function syncPersonas(route, opts) {
|
|
|
8687
8687
|
...opts.readOnly ? { readOnly: true } : {}
|
|
8688
8688
|
};
|
|
8689
8689
|
}
|
|
8690
|
-
async function linkReleaseSeat(
|
|
8691
|
-
const seatId =
|
|
8690
|
+
async function linkReleaseSeat(route2, client2, agentsModel, userAgent, readOnly) {
|
|
8691
|
+
const seatId = route2.resolved.releaseSeat;
|
|
8692
8692
|
if (!seatId) return {};
|
|
8693
8693
|
const rows = await client2.records.list(agentsModel.id, { limit: 200 });
|
|
8694
8694
|
let agentId = rows.find((r) => r.name === RELEASE_AGENT_NAME)?.id;
|
|
@@ -8705,11 +8705,11 @@ async function linkReleaseSeat(route, client2, agentsModel, userAgent, readOnly)
|
|
|
8705
8705
|
throw e;
|
|
8706
8706
|
}
|
|
8707
8707
|
}
|
|
8708
|
-
const crewModelId =
|
|
8708
|
+
const crewModelId = route2.resolved.models.crew;
|
|
8709
8709
|
const crewModel = await client2.dataModels.get(crewModelId);
|
|
8710
8710
|
let columnName;
|
|
8711
8711
|
try {
|
|
8712
|
-
columnName = await ensureCrewAgentField(
|
|
8712
|
+
columnName = await ensureCrewAgentField(route2, client2, crewModel, agentsModel.id, userAgent);
|
|
8713
8713
|
} catch (e) {
|
|
8714
8714
|
if (e instanceof CrewAgentFieldMissing) return { note: `Release: ${CREW_FIELD_MISSING_NOTE}` };
|
|
8715
8715
|
throw e;
|
|
@@ -8722,18 +8722,18 @@ var NO_WRITE_NOTE = "this key has no write access to the Agents table";
|
|
|
8722
8722
|
var CREW_FIELD_MISSING_NOTE = "Crew table has no Agent field yet \u2014 a workspace admin's `crew agents sync` adds it";
|
|
8723
8723
|
var CrewAgentFieldMissing = class extends Error {
|
|
8724
8724
|
};
|
|
8725
|
-
async function linkCrewSeats(
|
|
8726
|
-
const seats2 =
|
|
8725
|
+
async function linkCrewSeats(route2, client2, agentsModelId, agentPersonas, userAgent, readOnly) {
|
|
8726
|
+
const seats2 = route2.resolved.seats;
|
|
8727
8727
|
const rolesWithSeats = ROLE_NAMES.filter((role) => seats2[role] && agentPersonas[role]);
|
|
8728
8728
|
if (rolesWithSeats.length === 0) {
|
|
8729
8729
|
if (readOnly) return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agents_row" : "no_seat" }));
|
|
8730
8730
|
return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "persona_diverged" : "no_seat" }));
|
|
8731
8731
|
}
|
|
8732
|
-
const crewModelId =
|
|
8732
|
+
const crewModelId = route2.resolved.models.crew;
|
|
8733
8733
|
const crewModel = await client2.dataModels.get(crewModelId);
|
|
8734
8734
|
let columnName;
|
|
8735
8735
|
try {
|
|
8736
|
-
columnName = await ensureCrewAgentField(
|
|
8736
|
+
columnName = await ensureCrewAgentField(route2, client2, crewModel, agentsModelId, userAgent);
|
|
8737
8737
|
} catch (e) {
|
|
8738
8738
|
if (e instanceof CrewAgentFieldMissing) {
|
|
8739
8739
|
return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agent_field" : "no_seat" }));
|
|
@@ -8762,14 +8762,14 @@ async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userA
|
|
|
8762
8762
|
}
|
|
8763
8763
|
return outcomes;
|
|
8764
8764
|
}
|
|
8765
|
-
async function ensureCrewAgentField(
|
|
8765
|
+
async function ensureCrewAgentField(route2, client2, crewModel, agentsModelId, userAgent) {
|
|
8766
8766
|
const existing = crewModel.fields.find(
|
|
8767
8767
|
(f) => f.fieldType?.kind === "REFERENCE" && f.fieldType.targetModelId === agentsModelId && f.fieldType.isMultiple === false
|
|
8768
8768
|
);
|
|
8769
8769
|
if (existing) return existing.columnName;
|
|
8770
|
-
const workspaceId =
|
|
8771
|
-
const baseUrl =
|
|
8772
|
-
const apiKey = resolveApiKey(
|
|
8770
|
+
const workspaceId = route2.resolved.workspaceId;
|
|
8771
|
+
const baseUrl = route2.baseUrl.replace(/\/+$/, "");
|
|
8772
|
+
const apiKey = resolveApiKey(route2);
|
|
8773
8773
|
const headers = { Authorization: `Bearer ${apiKey}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" };
|
|
8774
8774
|
const typesRes = await fetch(`${baseUrl}/api/workspaces/${workspaceId}/field-types`, { headers });
|
|
8775
8775
|
if (!typesRes.ok) throw new AgentsSyncError(`field-types lookup failed: ${typesRes.status} ${typesRes.statusText}`);
|
|
@@ -8855,14 +8855,14 @@ function listLocalSkills(promptsDir) {
|
|
|
8855
8855
|
if (!existsSync19(dir)) return [];
|
|
8856
8856
|
return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join20(dir, f)));
|
|
8857
8857
|
}
|
|
8858
|
-
async function syncSkills(
|
|
8859
|
-
if (!
|
|
8860
|
-
throw new SkillSyncError(`route "${
|
|
8858
|
+
async function syncSkills(route2, opts) {
|
|
8859
|
+
if (!route2.resolved) {
|
|
8860
|
+
throw new SkillSyncError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
|
|
8861
8861
|
}
|
|
8862
|
-
const workspaceId =
|
|
8862
|
+
const workspaceId = route2.resolved.workspaceId;
|
|
8863
8863
|
const client2 = new TablationClient3({
|
|
8864
|
-
baseUrl: `${
|
|
8865
|
-
apiKey: resolveApiKey(
|
|
8864
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
8865
|
+
apiKey: resolveApiKey(route2),
|
|
8866
8866
|
headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
8867
8867
|
});
|
|
8868
8868
|
let skillsModel;
|
|
@@ -8876,9 +8876,9 @@ async function syncSkills(route, opts) {
|
|
|
8876
8876
|
}
|
|
8877
8877
|
throw e;
|
|
8878
8878
|
}
|
|
8879
|
-
const local = listLocalSkills(
|
|
8879
|
+
const local = listLocalSkills(route2.promptsDir);
|
|
8880
8880
|
const rows = await client2.records.list(skillsModel.id, { limit: 200 });
|
|
8881
|
-
const cache =
|
|
8881
|
+
const cache = route2.resolved.agentSkills ?? {};
|
|
8882
8882
|
const outcomes = [];
|
|
8883
8883
|
const agentSkills = { ...cache };
|
|
8884
8884
|
for (const skill of local) {
|
|
@@ -8994,22 +8994,22 @@ function describeSkillSyncOutcome(o) {
|
|
|
8994
8994
|
return `${o.name}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.skillId} in the app before syncing again`;
|
|
8995
8995
|
}
|
|
8996
8996
|
}
|
|
8997
|
-
async function fetchSkillOverrides(
|
|
8998
|
-
if (!
|
|
8997
|
+
async function fetchSkillOverrides(route2, opts) {
|
|
8998
|
+
if (!route2.resolved) return {};
|
|
8999
8999
|
const client2 = new TablationClient3({
|
|
9000
|
-
baseUrl: `${
|
|
9001
|
-
apiKey: resolveApiKey(
|
|
9000
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
9001
|
+
apiKey: resolveApiKey(route2),
|
|
9002
9002
|
headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
9003
9003
|
});
|
|
9004
9004
|
let rows;
|
|
9005
9005
|
try {
|
|
9006
|
-
const model = await client2.dataModels.get("agent_skills",
|
|
9006
|
+
const model = await client2.dataModels.get("agent_skills", route2.resolved.workspaceId);
|
|
9007
9007
|
rows = await client2.records.list(model.id, { limit: 200 });
|
|
9008
9008
|
} catch {
|
|
9009
9009
|
return {};
|
|
9010
9010
|
}
|
|
9011
|
-
const local = new Map(listLocalSkills(
|
|
9012
|
-
const cache =
|
|
9011
|
+
const local = new Map(listLocalSkills(route2.promptsDir).map((s) => [s.name, s]));
|
|
9012
|
+
const cache = route2.resolved.agentSkills ?? {};
|
|
9013
9013
|
const out = {};
|
|
9014
9014
|
for (const row of rows) {
|
|
9015
9015
|
const name = row.name;
|
|
@@ -9625,10 +9625,10 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9625
9625
|
const tail3 = new FileTail(plan.streamPath);
|
|
9626
9626
|
const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
|
|
9627
9627
|
let spawnFailed = false;
|
|
9628
|
-
child.on("error", (
|
|
9628
|
+
child.on("error", (err2) => {
|
|
9629
9629
|
spawnFailed = true;
|
|
9630
9630
|
clearInterval(timer);
|
|
9631
|
-
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${
|
|
9631
|
+
reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err2.message}`));
|
|
9632
9632
|
});
|
|
9633
9633
|
child.on("close", async (code) => {
|
|
9634
9634
|
clearInterval(timer);
|
|
@@ -9639,8 +9639,8 @@ function spawnAgent(plan, emit, hintMoved) {
|
|
|
9639
9639
|
const { ms: ms3, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey, hintMoved);
|
|
9640
9640
|
resolve6({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {} });
|
|
9641
9641
|
});
|
|
9642
|
-
child.stdin?.on("error", (
|
|
9643
|
-
emit.warn(`agent closed stdin before the prompt was delivered: ${
|
|
9642
|
+
child.stdin?.on("error", (err2) => {
|
|
9643
|
+
emit.warn(`agent closed stdin before the prompt was delivered: ${err2.message}`, { step: "agent" });
|
|
9644
9644
|
});
|
|
9645
9645
|
child.stdin?.end(plan.prompt);
|
|
9646
9646
|
});
|
|
@@ -9682,8 +9682,8 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
9682
9682
|
// src/seat-keys.ts
|
|
9683
9683
|
import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
9684
9684
|
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
9685
|
-
function keyIsAdmin(
|
|
9686
|
-
const role =
|
|
9685
|
+
function keyIsAdmin(route2) {
|
|
9686
|
+
const role = route2.resolved?.role;
|
|
9687
9687
|
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
9688
9688
|
}
|
|
9689
9689
|
var MEMBER_NO_ROW = "has no linked Agents row \u2014 a workspace admin's `crew agents sync` creates the persona rows; run it here afterwards to link this ship's seats";
|
|
@@ -9692,41 +9692,41 @@ var FORBIDDEN_LINE = 'seat keys: not minted \u2014 your role does not grant "Min
|
|
|
9692
9692
|
function seatKeySlug(workspaceSlug, agentId) {
|
|
9693
9693
|
return `${workspaceSlug}#agent:${agentId}`;
|
|
9694
9694
|
}
|
|
9695
|
-
function slugFor(
|
|
9696
|
-
const [workspaceSlug] = splitRoute(
|
|
9695
|
+
function slugFor(route2, agentId) {
|
|
9696
|
+
const [workspaceSlug] = splitRoute(route2.route);
|
|
9697
9697
|
return seatKeySlug(workspaceSlug, agentId);
|
|
9698
9698
|
}
|
|
9699
|
-
function seatAgentId(
|
|
9700
|
-
return
|
|
9699
|
+
function seatAgentId(route2, role) {
|
|
9700
|
+
return route2.resolved?.agentPersonas?.[role]?.agentId;
|
|
9701
9701
|
}
|
|
9702
|
-
async function resolveReleaseKey(
|
|
9703
|
-
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(
|
|
9704
|
-
const agentId =
|
|
9705
|
-
if (!agentId) return ship(keyIsAdmin(
|
|
9702
|
+
async function resolveReleaseKey(route2, getStore = getSessionStore2) {
|
|
9703
|
+
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
9704
|
+
const agentId = route2.resolved?.releaseAgentId;
|
|
9705
|
+
if (!agentId) return ship(keyIsAdmin(route2) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
9706
9706
|
try {
|
|
9707
|
-
const session = await getStore().get(hostFromUrl2(
|
|
9707
|
+
const session = await getStore().get(hostFromUrl2(route2.baseUrl), slugFor(route2, agentId));
|
|
9708
9708
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
9709
|
-
return ship(keyIsAdmin(
|
|
9709
|
+
return ship(keyIsAdmin(route2) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
9710
9710
|
} catch (e) {
|
|
9711
9711
|
return ship(describeReadError(e));
|
|
9712
9712
|
}
|
|
9713
9713
|
}
|
|
9714
|
-
async function resolveSeatKey(
|
|
9715
|
-
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(
|
|
9714
|
+
async function resolveSeatKey(route2, role, getStore = getSessionStore2) {
|
|
9715
|
+
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
|
|
9716
9716
|
if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
|
|
9717
|
-
const agentId = seatAgentId(
|
|
9718
|
-
if (!agentId) return ship(keyIsAdmin(
|
|
9717
|
+
const agentId = seatAgentId(route2, role);
|
|
9718
|
+
if (!agentId) return ship(keyIsAdmin(route2) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
9719
9719
|
try {
|
|
9720
|
-
const session = await getStore().get(hostFromUrl2(
|
|
9720
|
+
const session = await getStore().get(hostFromUrl2(route2.baseUrl), slugFor(route2, agentId));
|
|
9721
9721
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
9722
|
-
return ship(keyIsAdmin(
|
|
9722
|
+
return ship(keyIsAdmin(route2) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
9723
9723
|
} catch (e) {
|
|
9724
9724
|
return ship(describeReadError(e));
|
|
9725
9725
|
}
|
|
9726
9726
|
}
|
|
9727
9727
|
function safeStderrLine(e) {
|
|
9728
|
-
const
|
|
9729
|
-
const raw = typeof
|
|
9728
|
+
const err2 = e;
|
|
9729
|
+
const raw = typeof err2?.stderr === "string" && err2.stderr.trim() ? err2.stderr : typeof err2?.message === "string" ? err2.message : "";
|
|
9730
9730
|
for (const line of raw.split("\n")) {
|
|
9731
9731
|
const t = line.trim();
|
|
9732
9732
|
if (!t || /^Command failed:/i.test(t) || /(^|\s)-w(\s|$)/.test(t)) continue;
|
|
@@ -9752,13 +9752,13 @@ function describeStoreError(e) {
|
|
|
9752
9752
|
}
|
|
9753
9753
|
return "keychain unavailable (could not write to the OS keychain)";
|
|
9754
9754
|
}
|
|
9755
|
-
async function mintMissingSeatKeys(
|
|
9755
|
+
async function mintMissingSeatKeys(route2, o) {
|
|
9756
9756
|
const out = [];
|
|
9757
|
-
if (!
|
|
9758
|
-
const resolved =
|
|
9757
|
+
if (!route2.resolved) return out;
|
|
9758
|
+
const resolved = route2.resolved;
|
|
9759
9759
|
const doFetch = o.fetchImpl ?? fetch;
|
|
9760
9760
|
const targets = [
|
|
9761
|
-
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(
|
|
9761
|
+
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
|
|
9762
9762
|
...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
|
|
9763
9763
|
];
|
|
9764
9764
|
let store;
|
|
@@ -9771,10 +9771,10 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
9771
9771
|
detail: describeStoreError(e)
|
|
9772
9772
|
}));
|
|
9773
9773
|
}
|
|
9774
|
-
const host = hostFromUrl2(
|
|
9775
|
-
const base =
|
|
9774
|
+
const host = hostFromUrl2(route2.baseUrl);
|
|
9775
|
+
const base = route2.baseUrl.replace(/\/+$/, "");
|
|
9776
9776
|
if (!o.dryRun) {
|
|
9777
|
-
const probeSlug = seatKeySlug(splitRoute(
|
|
9777
|
+
const probeSlug = seatKeySlug(splitRoute(route2.route)[0], "_probe");
|
|
9778
9778
|
try {
|
|
9779
9779
|
await store.set(host, probeSlug, {
|
|
9780
9780
|
apiKey: "probe",
|
|
@@ -9793,7 +9793,7 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
9793
9793
|
out.push({ role, action: "no_agent" });
|
|
9794
9794
|
continue;
|
|
9795
9795
|
}
|
|
9796
|
-
const slug = slugFor(
|
|
9796
|
+
const slug = slugFor(route2, agentId);
|
|
9797
9797
|
try {
|
|
9798
9798
|
if ((await store.get(host, slug))?.apiKey) {
|
|
9799
9799
|
out.push({ role, action: "already_stored" });
|
|
@@ -9806,7 +9806,7 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
9806
9806
|
const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
|
|
9807
9807
|
method: "POST",
|
|
9808
9808
|
headers: {
|
|
9809
|
-
Authorization: `Bearer ${resolveApiKey(
|
|
9809
|
+
Authorization: `Bearer ${resolveApiKey(route2)}`,
|
|
9810
9810
|
"Content-Type": "application/json",
|
|
9811
9811
|
"User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
|
|
9812
9812
|
},
|
|
@@ -9840,7 +9840,7 @@ async function mintMissingSeatKeys(route, o) {
|
|
|
9840
9840
|
try {
|
|
9841
9841
|
const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${body.id}`, {
|
|
9842
9842
|
method: "DELETE",
|
|
9843
|
-
headers: { Authorization: `Bearer ${resolveApiKey(
|
|
9843
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
9844
9844
|
});
|
|
9845
9845
|
revoked = del2.ok;
|
|
9846
9846
|
} catch {
|
|
@@ -9875,13 +9875,13 @@ function describeMintOutcome(o) {
|
|
|
9875
9875
|
return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
|
|
9876
9876
|
}
|
|
9877
9877
|
}
|
|
9878
|
-
async function revokeSeatKeys(
|
|
9878
|
+
async function revokeSeatKeys(route2, o) {
|
|
9879
9879
|
const out = [];
|
|
9880
|
-
if (!
|
|
9881
|
-
const resolved =
|
|
9880
|
+
if (!route2.resolved) return out;
|
|
9881
|
+
const resolved = route2.resolved;
|
|
9882
9882
|
const doFetch = o.fetchImpl ?? fetch;
|
|
9883
9883
|
const targets = [
|
|
9884
|
-
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(
|
|
9884
|
+
...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
|
|
9885
9885
|
...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
|
|
9886
9886
|
];
|
|
9887
9887
|
let store;
|
|
@@ -9890,14 +9890,14 @@ async function revokeSeatKeys(route, o) {
|
|
|
9890
9890
|
} catch (e) {
|
|
9891
9891
|
return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
|
|
9892
9892
|
}
|
|
9893
|
-
const host = hostFromUrl2(
|
|
9894
|
-
const base =
|
|
9893
|
+
const host = hostFromUrl2(route2.baseUrl);
|
|
9894
|
+
const base = route2.baseUrl.replace(/\/+$/, "");
|
|
9895
9895
|
for (const { role, agentId } of targets) {
|
|
9896
9896
|
if (!agentId) {
|
|
9897
9897
|
out.push({ role, action: "no_agent" });
|
|
9898
9898
|
continue;
|
|
9899
9899
|
}
|
|
9900
|
-
const slug = slugFor(
|
|
9900
|
+
const slug = slugFor(route2, agentId);
|
|
9901
9901
|
try {
|
|
9902
9902
|
const session = await store.get(host, slug);
|
|
9903
9903
|
if (!session?.apiKey) {
|
|
@@ -9914,7 +9914,7 @@ async function revokeSeatKeys(route, o) {
|
|
|
9914
9914
|
}
|
|
9915
9915
|
const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${session.keyId}`, {
|
|
9916
9916
|
method: "DELETE",
|
|
9917
|
-
headers: { Authorization: `Bearer ${resolveApiKey(
|
|
9917
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
9918
9918
|
});
|
|
9919
9919
|
if (!del2.ok && del2.status !== 404) {
|
|
9920
9920
|
out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
|
|
@@ -9940,14 +9940,14 @@ function describeRevokeOutcome(o) {
|
|
|
9940
9940
|
return `${o.role}: could not revoke the agent key (${o.detail ?? "unknown"}) \u2014 left in the keychain`;
|
|
9941
9941
|
}
|
|
9942
9942
|
}
|
|
9943
|
-
async function canMintAgentKeys(
|
|
9944
|
-
const resolved =
|
|
9943
|
+
async function canMintAgentKeys(route2, o = {}) {
|
|
9944
|
+
const resolved = route2.resolved;
|
|
9945
9945
|
if (!resolved) return void 0;
|
|
9946
|
-
const agentId = seatAgentId(
|
|
9946
|
+
const agentId = seatAgentId(route2, "dev") ?? SEAT_KEY_ROLES.map((r) => seatAgentId(route2, r)).find(Boolean);
|
|
9947
9947
|
if (!agentId) return void 0;
|
|
9948
9948
|
try {
|
|
9949
|
-
const res = await (o.fetchImpl ?? fetch)(`${
|
|
9950
|
-
headers: { Authorization: `Bearer ${resolveApiKey(
|
|
9949
|
+
const res = await (o.fetchImpl ?? fetch)(`${route2.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
|
|
9950
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
9951
9951
|
});
|
|
9952
9952
|
if (res.ok) return true;
|
|
9953
9953
|
return res.status === 403 ? false : void 0;
|
|
@@ -10175,7 +10175,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
10175
10175
|
}
|
|
10176
10176
|
|
|
10177
10177
|
// src/cli.ts
|
|
10178
|
-
import { resolve as resolve5, dirname as
|
|
10178
|
+
import { resolve as resolve5, dirname as dirname22, join as join43, basename as basename13 } from "path";
|
|
10179
10179
|
import { fileURLToPath } from "url";
|
|
10180
10180
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
10181
10181
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
@@ -10246,12 +10246,12 @@ function splitWorktreeArgs(positional2, routeNames) {
|
|
|
10246
10246
|
if (a !== void 0 && isTicket(a) && !routeNames.includes(a)) return { ticket: a };
|
|
10247
10247
|
return { route: a, ticket: void 0 };
|
|
10248
10248
|
}
|
|
10249
|
-
function pickWorktreeRoute(ticketNumber,
|
|
10250
|
-
for (const c of
|
|
10251
|
-
if (c.hit && (
|
|
10249
|
+
function pickWorktreeRoute(ticketNumber, candidates2) {
|
|
10250
|
+
for (const c of candidates2) {
|
|
10251
|
+
if (c.hit && (candidates2.length === 1 || c.servedRepoIds.includes(c.hit.repoId ?? ""))) return { route: c.route };
|
|
10252
10252
|
}
|
|
10253
|
-
const looked =
|
|
10254
|
-
const found =
|
|
10253
|
+
const looked = candidates2.map((c) => c.route).join(", ");
|
|
10254
|
+
const found = candidates2.filter((c) => c.hit).map((c) => `${c.route} (repo ${c.hit?.repoId ?? "none"})`);
|
|
10255
10255
|
return {
|
|
10256
10256
|
error: `no open ticket ISSUE-${ticketNumber} served by this ship \u2014 looked on ${looked}` + (found.length ? `; found it only on ${found.join(", ")}, which has no checkout for its repo` : "")
|
|
10257
10257
|
};
|
|
@@ -10502,22 +10502,22 @@ async function runReset(o, fx) {
|
|
|
10502
10502
|
for (const url of urls) {
|
|
10503
10503
|
for (const line of fx.removePair(url, o.dryRun)) steps.push({ step: "pair", target: line, action: mark(o.dryRun) });
|
|
10504
10504
|
}
|
|
10505
|
-
for (const
|
|
10506
|
-
for (const r of await fx.revokeSeatKeys(
|
|
10507
|
-
steps.push({ step: "seat-keys", target: `${
|
|
10505
|
+
for (const route2 of o.routes) {
|
|
10506
|
+
for (const r of await fx.revokeSeatKeys(route2, o.dryRun)) {
|
|
10507
|
+
steps.push({ step: "seat-keys", target: `${route2.route}: ${r.line}`, action: r.ok ? mark(o.dryRun) : "failed" });
|
|
10508
10508
|
}
|
|
10509
10509
|
}
|
|
10510
|
-
for (const
|
|
10510
|
+
for (const route2 of [...o.routes, ...o.droppedRoutes ?? []]) {
|
|
10511
10511
|
try {
|
|
10512
|
-
const cred = await fx.credential(
|
|
10512
|
+
const cred = await fx.credential(route2);
|
|
10513
10513
|
if (!cred.stored) {
|
|
10514
|
-
steps.push({ step: "credential", target:
|
|
10514
|
+
steps.push({ step: "credential", target: route2.route, action: "absent", detail: "no workspace credential in the keychain" });
|
|
10515
10515
|
continue;
|
|
10516
10516
|
}
|
|
10517
10517
|
if (!o.dryRun) await cred.clear();
|
|
10518
|
-
steps.push({ step: "credential", target:
|
|
10518
|
+
steps.push({ step: "credential", target: route2.route, action: mark(o.dryRun), detail: "workspace credential in the keychain" });
|
|
10519
10519
|
} catch (e) {
|
|
10520
|
-
steps.push({ step: "credential", target:
|
|
10520
|
+
steps.push({ step: "credential", target: route2.route, action: "failed", detail: e.message });
|
|
10521
10521
|
}
|
|
10522
10522
|
}
|
|
10523
10523
|
const covered = new Set([...o.routes, ...o.droppedRoutes ?? []].map((r) => `${hostFromUrl3(r.baseUrl)}/${splitRoute(r.route)[0]}`));
|
|
@@ -10618,12 +10618,12 @@ function outdatedItem(have, floor, nowIso = (/* @__PURE__ */ new Date()).toISOSt
|
|
|
10618
10618
|
function sentVersion(ship) {
|
|
10619
10619
|
return /CrewAgent\/(\d+\.\d+\.\d+\S*)/.exec(ship.userAgent)?.[1] ?? null;
|
|
10620
10620
|
}
|
|
10621
|
-
async function readCrewFloor(
|
|
10622
|
-
const r =
|
|
10621
|
+
async function readCrewFloor(route2, ship) {
|
|
10622
|
+
const r = route2.resolved;
|
|
10623
10623
|
if (!r?.projectId) return void 0;
|
|
10624
10624
|
try {
|
|
10625
|
-
const res = await fetch(`${
|
|
10626
|
-
headers: { Authorization: `Bearer ${resolveApiKey(
|
|
10625
|
+
const res = await fetch(`${route2.baseUrl}/api/projects/${encodeURIComponent(r.projectId)}?workspaceId=${r.workspaceId}`, {
|
|
10626
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": ship.userAgent }
|
|
10627
10627
|
});
|
|
10628
10628
|
if (!res.ok) return void 0;
|
|
10629
10629
|
const body = await res.json();
|
|
@@ -10648,12 +10648,12 @@ async function checkCrewFloor(o, deps = {}) {
|
|
|
10648
10648
|
function openOutdated(state) {
|
|
10649
10649
|
return state.shipAttention().previous().find((i) => i.key === CREW_OUTDATED_KEY)?.message ?? null;
|
|
10650
10650
|
}
|
|
10651
|
-
function isUpgradeRequired(
|
|
10652
|
-
return
|
|
10651
|
+
function isUpgradeRequired(err2) {
|
|
10652
|
+
return err2 instanceof TablationApiError3 && err2.status === 426;
|
|
10653
10653
|
}
|
|
10654
|
-
async function raiseFromUpgradeRequired(o,
|
|
10654
|
+
async function raiseFromUpgradeRequired(o, err2) {
|
|
10655
10655
|
const have = sentVersion(o.ship) ?? "unknown";
|
|
10656
|
-
const floor = /(\d+\.\d+\.\d+)/.exec(
|
|
10656
|
+
const floor = /(\d+\.\d+\.\d+)/.exec(err2.message.replace(have, ""))?.[1] ?? "a newer version";
|
|
10657
10657
|
await raiseShipAttention(o, outdatedItem(have, floor));
|
|
10658
10658
|
}
|
|
10659
10659
|
|
|
@@ -10963,10 +10963,10 @@ function rankedCandidates(role, i) {
|
|
|
10963
10963
|
}
|
|
10964
10964
|
function roleTopRank(role, i) {
|
|
10965
10965
|
if (role !== "qa" && role !== "triage" && !i.seats[role]) return NOTHING_ACTIONABLE;
|
|
10966
|
-
const
|
|
10967
|
-
if (
|
|
10966
|
+
const candidates2 = roleCandidates(role, i);
|
|
10967
|
+
if (candidates2.length === 0) return NOTHING_ACTIONABLE;
|
|
10968
10968
|
const c = i.contract ?? DEFAULT_CONTRACT;
|
|
10969
|
-
return Math.min(...
|
|
10969
|
+
return Math.min(...candidates2.map((t) => rankScalar(t, c)));
|
|
10970
10970
|
}
|
|
10971
10971
|
function actionableSummary(i, pending) {
|
|
10972
10972
|
const c = i.contract ?? DEFAULT_CONTRACT;
|
|
@@ -11461,15 +11461,15 @@ async function runStalledSweep(o, stalled) {
|
|
|
11461
11461
|
|
|
11462
11462
|
// src/poll.ts
|
|
11463
11463
|
async function decideCycle(o) {
|
|
11464
|
-
const { route, ship, state, emit } = o;
|
|
11465
|
-
const tracker = new Tracker(
|
|
11464
|
+
const { route: route2, ship, state, emit } = o;
|
|
11465
|
+
const tracker = new Tracker(route2, ship);
|
|
11466
11466
|
tracker.onReadFailure = (m) => emit.warn(m);
|
|
11467
11467
|
emit.enter("poll");
|
|
11468
11468
|
const optional = async (what, read, fallback) => {
|
|
11469
11469
|
try {
|
|
11470
11470
|
return await read;
|
|
11471
|
-
} catch (
|
|
11472
|
-
emit.warn(`reading ${what} failed: ${
|
|
11471
|
+
} catch (err2) {
|
|
11472
|
+
emit.warn(`reading ${what} failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
11473
11473
|
return fallback;
|
|
11474
11474
|
}
|
|
11475
11475
|
};
|
|
@@ -11493,13 +11493,13 @@ async function decideCycle(o) {
|
|
|
11493
11493
|
const epicStatusById = new Map(epicRows.map((e) => [e.id, e[epicStatusColumn] ?? null]));
|
|
11494
11494
|
let epicSync = [];
|
|
11495
11495
|
try {
|
|
11496
|
-
const
|
|
11497
|
-
const outstanding = await tracker.epicTickets(
|
|
11496
|
+
const candidates2 = trackedEpics(epicRows, tracker.contract);
|
|
11497
|
+
const outstanding = await tracker.epicTickets(candidates2.map((e) => e.id), { closed: false });
|
|
11498
11498
|
const closed = await tracker.epicTickets(
|
|
11499
|
-
epicsNeedingClosedCheck(
|
|
11499
|
+
epicsNeedingClosedCheck(candidates2, outstanding, tracker.contract),
|
|
11500
11500
|
{ closed: true }
|
|
11501
11501
|
);
|
|
11502
|
-
epicSync = planEpicSync(
|
|
11502
|
+
epicSync = planEpicSync(candidates2, outstanding, closed, tracker.contract);
|
|
11503
11503
|
for (const s of epicSync) epicStatusById.set(s.epic.id, s.to);
|
|
11504
11504
|
} catch (e) {
|
|
11505
11505
|
emit.warn(`could not take stock of epics this cycle: ${e.message}`);
|
|
@@ -11515,7 +11515,7 @@ async function decideCycle(o) {
|
|
|
11515
11515
|
const info = blockerInfoMap(tickets, extra);
|
|
11516
11516
|
const blocked = computeBlockedIds(tickets, info);
|
|
11517
11517
|
emit.enter("sweep");
|
|
11518
|
-
const roster = buildRoster(configuredMembers(
|
|
11518
|
+
const roster = buildRoster(configuredMembers(route2), crewRows);
|
|
11519
11519
|
const holds2 = new Set(holdIds(roster));
|
|
11520
11520
|
const workModes = { modes: projectModes, agentAssignees: agentAssigneeIds(crewRows, (id) => holds2.has(id)) };
|
|
11521
11521
|
const diag = sweepDiagnostics(tickets, info);
|
|
@@ -11558,7 +11558,7 @@ async function decideCycle(o) {
|
|
|
11558
11558
|
const unroutable = unroutableTickets.map((t) => t.id);
|
|
11559
11559
|
const { added, gained } = unroutableChanges(
|
|
11560
11560
|
unroutableTickets,
|
|
11561
|
-
state.unroutable(
|
|
11561
|
+
state.unroutable(route2.route).previous(),
|
|
11562
11562
|
tickets,
|
|
11563
11563
|
tracker.contract.columns.externalId
|
|
11564
11564
|
);
|
|
@@ -11580,14 +11580,14 @@ async function decideCycle(o) {
|
|
|
11580
11580
|
const reasons = attentionReasons(t, tracker.contract);
|
|
11581
11581
|
if (reasons.length) attentionCurrent[t.id] = reasons;
|
|
11582
11582
|
}
|
|
11583
|
-
const attentionPrevious = state.attention(
|
|
11583
|
+
const attentionPrevious = state.attention(route2.route).previous();
|
|
11584
11584
|
const transitions2 = attentionTransitions(attentionPrevious, attentionCurrent);
|
|
11585
11585
|
let mintFailed = false;
|
|
11586
11586
|
for (const tr of transitions2) {
|
|
11587
11587
|
const ticket = tickets.find((t) => t.id === tr.ticketId);
|
|
11588
11588
|
if (!ticket) continue;
|
|
11589
11589
|
const url = await tracker.recordLinkUrl(ticket.id);
|
|
11590
|
-
if (!url &&
|
|
11590
|
+
if (!url && route2.resolved?.recordLinkViewId) mintFailed = true;
|
|
11591
11591
|
emit.emit(
|
|
11592
11592
|
`needs a person: ${tr.reasons.join(", ")} \u2014 ${ticket.title}`,
|
|
11593
11593
|
{
|
|
@@ -11606,15 +11606,15 @@ async function decideCycle(o) {
|
|
|
11606
11606
|
}
|
|
11607
11607
|
emit.enter("select");
|
|
11608
11608
|
const watermark = state.watermark();
|
|
11609
|
-
for (const repo of recheckBaseStops(state,
|
|
11609
|
+
for (const repo of recheckBaseStops(state, route2)) {
|
|
11610
11610
|
emit.emit(`${repo}: primary checkout is level again \u2014 building lanes resume for that repo`, { step: "select" });
|
|
11611
11611
|
}
|
|
11612
|
-
for (const what of recheckEnvironmentStops(state,
|
|
11612
|
+
for (const what of recheckEnvironmentStops(state, route2, ship)) {
|
|
11613
11613
|
emit.emit(`${what}: no longer missing from the scheduler PATH \u2014 stop cleared`, { step: "select" });
|
|
11614
11614
|
}
|
|
11615
|
-
await checkRepoDisks({ state, emit, route, ship });
|
|
11616
|
-
const floorStop = await checkCrewFloor({ state, emit, route, ship });
|
|
11617
|
-
const itemStop = repoStopFor(openShipAttention(state),
|
|
11615
|
+
await checkRepoDisks({ state, emit, route: route2, ship });
|
|
11616
|
+
const floorStop = await checkCrewFloor({ state, emit, route: route2, ship });
|
|
11617
|
+
const itemStop = repoStopFor(openShipAttention(state), route2);
|
|
11618
11618
|
const selectionInput = {
|
|
11619
11619
|
repoStop: floorStop ? () => floorStop : itemStop,
|
|
11620
11620
|
workModes,
|
|
@@ -11623,7 +11623,7 @@ async function decideCycle(o) {
|
|
|
11623
11623
|
watermark,
|
|
11624
11624
|
blocked,
|
|
11625
11625
|
holds: holds2,
|
|
11626
|
-
seats:
|
|
11626
|
+
seats: route2.resolved.seats,
|
|
11627
11627
|
paused: state.pausedRoles(["dev", "design", "qa"]),
|
|
11628
11628
|
contract: tracker.contract,
|
|
11629
11629
|
ships: { myShipId: shipIdByName(ships, ship.name), rows: ships }
|
|
@@ -11669,7 +11669,7 @@ async function decideCycle(o) {
|
|
|
11669
11669
|
}
|
|
11670
11670
|
return decision;
|
|
11671
11671
|
}
|
|
11672
|
-
var digestPath = (stateDir,
|
|
11672
|
+
var digestPath = (stateDir, route2, role, runId) => join25(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
|
|
11673
11673
|
function otherRunsRemoved(tickets, own) {
|
|
11674
11674
|
if (!own) return tickets;
|
|
11675
11675
|
return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
|
|
@@ -11757,8 +11757,8 @@ ${body}` : body;
|
|
|
11757
11757
|
return false;
|
|
11758
11758
|
}
|
|
11759
11759
|
}
|
|
11760
|
-
function rosterFor(d,
|
|
11761
|
-
return rosterMarkdown(d.roster,
|
|
11760
|
+
function rosterFor(d, route2, role) {
|
|
11761
|
+
return rosterMarkdown(d.roster, route2.resolved.seats[role] ?? null);
|
|
11762
11762
|
}
|
|
11763
11763
|
|
|
11764
11764
|
// src/claim.ts
|
|
@@ -11787,11 +11787,11 @@ async function stampResumption(tracker, t, affinity) {
|
|
|
11787
11787
|
}
|
|
11788
11788
|
return { outcome: "unknown" };
|
|
11789
11789
|
}
|
|
11790
|
-
async function resolveTopCandidate(tracker,
|
|
11790
|
+
async function resolveTopCandidate(tracker, candidates2, seat, approvedStatus, buildingStatus, isServable, affinity, ticketLock) {
|
|
11791
11791
|
const contended = [];
|
|
11792
11792
|
const unservable = [];
|
|
11793
11793
|
const held = [];
|
|
11794
|
-
for (const t of
|
|
11794
|
+
for (const t of candidates2) {
|
|
11795
11795
|
if (affinity?.isHeldElsewhere?.(t)) {
|
|
11796
11796
|
held.push(t.issue_id);
|
|
11797
11797
|
continue;
|
|
@@ -12047,9 +12047,9 @@ function failingPaths(output, cwd) {
|
|
|
12047
12047
|
}
|
|
12048
12048
|
return [...found];
|
|
12049
12049
|
}
|
|
12050
|
-
function culpritByFiles(
|
|
12050
|
+
function culpritByFiles(candidates2, paths) {
|
|
12051
12051
|
if (paths.length === 0) return null;
|
|
12052
|
-
const hits =
|
|
12052
|
+
const hits = candidates2.filter((c) => paths.some((p) => c.changed.has(p)));
|
|
12053
12053
|
return hits.length === 1 ? hits[0] : null;
|
|
12054
12054
|
}
|
|
12055
12055
|
function planBuildFailureBounce(cwd, c, base, remote) {
|
|
@@ -12942,9 +12942,9 @@ ${h.prFailure.tail}
|
|
|
12942
12942
|
function emitReleaseSummary(emit, summaries) {
|
|
12943
12943
|
if (summaries.length <= 1) return;
|
|
12944
12944
|
const routes = [...new Set(summaries.map((s) => s.route))];
|
|
12945
|
-
for (const
|
|
12946
|
-
const rows = summaries.filter((s) => s.route ===
|
|
12947
|
-
const remit = emit.forRoute(
|
|
12945
|
+
for (const route2 of routes) {
|
|
12946
|
+
const rows = summaries.filter((s) => s.route === route2);
|
|
12947
|
+
const remit = emit.forRoute(route2);
|
|
12948
12948
|
remit.enter("release");
|
|
12949
12949
|
remit.emit(`release summary \u2014 ${rows.length} repo(s):`);
|
|
12950
12950
|
for (const s of rows) {
|
|
@@ -13024,7 +13024,7 @@ var hook = async (o, name, env = {}) => {
|
|
|
13024
13024
|
}
|
|
13025
13025
|
return result;
|
|
13026
13026
|
};
|
|
13027
|
-
async function detectExternalClosures(o,
|
|
13027
|
+
async function detectExternalClosures(o, candidates2) {
|
|
13028
13028
|
const script = o.repo.hooks.merged;
|
|
13029
13029
|
const mergedHook = script ? async (env) => (await runScript(script, {
|
|
13030
13030
|
cwd: o.cwd,
|
|
@@ -13035,7 +13035,7 @@ async function detectExternalClosures(o, candidates) {
|
|
|
13035
13035
|
}
|
|
13036
13036
|
})).code : void 0;
|
|
13037
13037
|
const out = [];
|
|
13038
|
-
for (const c of
|
|
13038
|
+
for (const c of candidates2) {
|
|
13039
13039
|
if (!c.branch) continue;
|
|
13040
13040
|
const prRef = c.ticket[o.contract.columns.prRef];
|
|
13041
13041
|
const closure = await detectClosure({
|
|
@@ -13913,14 +13913,14 @@ var FAILURE_ALERT_MARKER = calloutHeader("CAUTION", "Failure Alert");
|
|
|
13913
13913
|
function findOpenFailureAlert(tickets, headline) {
|
|
13914
13914
|
return tickets.find((t) => t.title === headline);
|
|
13915
13915
|
}
|
|
13916
|
-
function ticketBody(n,
|
|
13916
|
+
function ticketBody(n, route2) {
|
|
13917
13917
|
return `${FAILURE_ALERT_MARKER}
|
|
13918
13918
|
|
|
13919
13919
|
${n.detail ?? n.headline}
|
|
13920
13920
|
|
|
13921
|
-
Filed automatically \u2014 no \`notify\` hook is configured for route \`${
|
|
13921
|
+
Filed automatically \u2014 no \`notify\` hook is configured for route \`${route2}\`, so this is the crew's built-in fallback rather than a silent log line. Configure \`hooks.notify\` on this route to replace it.`;
|
|
13922
13922
|
}
|
|
13923
|
-
async function applyFailureAlert(writer, n,
|
|
13923
|
+
async function applyFailureAlert(writer, n, route2, openTickets, memberId, log, dryRun, placement = {}) {
|
|
13924
13924
|
const existing = findOpenFailureAlert(openTickets, n.headline);
|
|
13925
13925
|
if (existing) {
|
|
13926
13926
|
const label = displayKey(existing);
|
|
@@ -13951,7 +13951,7 @@ Still failing: ${body}`, memberId, "failure_alert");
|
|
|
13951
13951
|
try {
|
|
13952
13952
|
const created = await writer.fileTicket({
|
|
13953
13953
|
title: n.headline,
|
|
13954
|
-
description: ticketBody(n,
|
|
13954
|
+
description: ticketBody(n, route2),
|
|
13955
13955
|
report_type: "bug",
|
|
13956
13956
|
severity: "s2",
|
|
13957
13957
|
reporter_name: "Crew release",
|
|
@@ -14153,15 +14153,15 @@ async function settleRunEnd(o) {
|
|
|
14153
14153
|
emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
|
|
14154
14154
|
}
|
|
14155
14155
|
}
|
|
14156
|
-
function describeRun(run2,
|
|
14156
|
+
function describeRun(run2, route2, seatIds, worktreeOf = (d, n) => {
|
|
14157
14157
|
try {
|
|
14158
14158
|
return worktreeForNumber(d, n)?.path ?? null;
|
|
14159
14159
|
} catch {
|
|
14160
14160
|
return null;
|
|
14161
14161
|
}
|
|
14162
14162
|
}) {
|
|
14163
|
-
const ownRoute = run2.route === routeSlug(
|
|
14164
|
-
const repoDir = run2.repo ? reposOf(
|
|
14163
|
+
const ownRoute = run2.route === routeSlug(route2.route);
|
|
14164
|
+
const repoDir = run2.repo ? reposOf(route2).find((t) => t.name === run2.repo)?.dir : void 0;
|
|
14165
14165
|
const num = run2.ticket?.replace(/^\D+/, "") ?? "";
|
|
14166
14166
|
return {
|
|
14167
14167
|
role: run2.role,
|
|
@@ -14282,29 +14282,29 @@ function runForge(a, io) {
|
|
|
14282
14282
|
// src/whoami.ts
|
|
14283
14283
|
async function buildWhoami(routes, deps = {}) {
|
|
14284
14284
|
const doFetch = deps.fetch ?? fetch;
|
|
14285
|
-
return Promise.all(routes.map(async (
|
|
14286
|
-
const credential = credentialSource(
|
|
14285
|
+
return Promise.all(routes.map(async (route2) => {
|
|
14286
|
+
const credential = credentialSource(route2);
|
|
14287
14287
|
try {
|
|
14288
|
-
const key = resolveApiKey(
|
|
14289
|
-
const q =
|
|
14290
|
-
const res = await doFetch(`${
|
|
14288
|
+
const key = resolveApiKey(route2);
|
|
14289
|
+
const q = route2.resolved ? `?workspaceId=${encodeURIComponent(route2.resolved.workspaceId)}` : "";
|
|
14290
|
+
const res = await doFetch(`${route2.baseUrl.replace(/\/+$/, "")}/api/auth/me${q}`, {
|
|
14291
14291
|
headers: { Authorization: `Bearer ${key}`, "User-Agent": deps.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
14292
14292
|
});
|
|
14293
14293
|
if (res.status === 401 || res.status === 403) {
|
|
14294
|
-
return { route:
|
|
14294
|
+
return { route: route2.route, ok: false, credential, error: `key rejected by ${route2.baseUrl} (HTTP ${res.status}): expired or revoked, re-run crew connect` };
|
|
14295
14295
|
}
|
|
14296
|
-
if (!res.ok) return { route:
|
|
14296
|
+
if (!res.ok) return { route: route2.route, ok: false, credential, error: `/auth/me answered HTTP ${res.status}` };
|
|
14297
14297
|
const me = await res.json();
|
|
14298
|
-
const operatorId =
|
|
14298
|
+
const operatorId = route2.resolved?.operator ?? null;
|
|
14299
14299
|
let operatorName = null;
|
|
14300
14300
|
if (operatorId && deps.operatorName) {
|
|
14301
14301
|
try {
|
|
14302
|
-
operatorName = await deps.operatorName(
|
|
14302
|
+
operatorName = await deps.operatorName(route2, operatorId) ?? null;
|
|
14303
14303
|
} catch {
|
|
14304
14304
|
}
|
|
14305
14305
|
}
|
|
14306
14306
|
return {
|
|
14307
|
-
route:
|
|
14307
|
+
route: route2.route,
|
|
14308
14308
|
ok: true,
|
|
14309
14309
|
credential,
|
|
14310
14310
|
identity: { id: me.id ?? null, email: me.email ?? null, name: me.name ?? null },
|
|
@@ -14312,7 +14312,7 @@ async function buildWhoami(routes, deps = {}) {
|
|
|
14312
14312
|
operator: { id: operatorId, name: operatorName }
|
|
14313
14313
|
};
|
|
14314
14314
|
} catch (e) {
|
|
14315
|
-
return { route:
|
|
14315
|
+
return { route: route2.route, ok: false, credential, error: e.message };
|
|
14316
14316
|
}
|
|
14317
14317
|
}));
|
|
14318
14318
|
}
|
|
@@ -14336,10 +14336,10 @@ function formatWhoami(report) {
|
|
|
14336
14336
|
|
|
14337
14337
|
// src/seat-read-probe.ts
|
|
14338
14338
|
var SEAT_NO_READ_REMEDY = "the project has no default role for agent keys \u2014 update the Issues app's template, then re-mint with `crew agents sync --revoke-seat-keys`, or assign the Crew seat role to the agent members";
|
|
14339
|
-
function probeTables(
|
|
14340
|
-
const m =
|
|
14339
|
+
function probeTables(route2) {
|
|
14340
|
+
const m = route2.resolved?.models;
|
|
14341
14341
|
if (!m) return [];
|
|
14342
|
-
const r =
|
|
14342
|
+
const r = route2.resolved;
|
|
14343
14343
|
return [
|
|
14344
14344
|
{ name: "Issues", modelId: m.issues },
|
|
14345
14345
|
{ name: "Comments", modelId: m.comments },
|
|
@@ -14347,8 +14347,8 @@ function probeTables(route) {
|
|
|
14347
14347
|
{ name: "Ships", modelId: r?.shipsModelId ?? "" }
|
|
14348
14348
|
].filter((t) => !!t.modelId);
|
|
14349
14349
|
}
|
|
14350
|
-
async function probeTableRead(
|
|
14351
|
-
const url = `${
|
|
14350
|
+
async function probeTableRead(route2, modelId, apiKey, o = {}) {
|
|
14351
|
+
const url = `${route2.baseUrl.replace(/\/+$/, "")}/api/data-models/${modelId}/records?limit=1`;
|
|
14352
14352
|
try {
|
|
14353
14353
|
const res = await (o.fetchImpl ?? fetch)(url, {
|
|
14354
14354
|
headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
@@ -14360,25 +14360,25 @@ async function probeTableRead(route, modelId, apiKey, o = {}) {
|
|
|
14360
14360
|
return { state: "unknown", detail: e.message };
|
|
14361
14361
|
}
|
|
14362
14362
|
}
|
|
14363
|
-
async function probeSeatReads(
|
|
14363
|
+
async function probeSeatReads(route2, o = {}) {
|
|
14364
14364
|
const out = [];
|
|
14365
|
-
const roles = [...SEAT_KEY_ROLES.filter((r) =>
|
|
14365
|
+
const roles = [...SEAT_KEY_ROLES.filter((r) => route2.resolved?.seats[r]), ...route2.resolved?.releaseSeat ? ["release"] : []];
|
|
14366
14366
|
for (const role of roles) {
|
|
14367
|
-
const id = role === "release" ? await resolveReleaseKey(
|
|
14367
|
+
const id = role === "release" ? await resolveReleaseKey(route2, o.getStore) : await resolveSeatKey(route2, role, o.getStore);
|
|
14368
14368
|
if (id.kind !== "agent") continue;
|
|
14369
14369
|
const tables = [];
|
|
14370
|
-
for (const t of probeTables(
|
|
14370
|
+
for (const t of probeTables(route2)) tables.push({ table: t.name, ...await probeTableRead(route2, t.modelId, id.apiKey, o) });
|
|
14371
14371
|
const bad = tables.find((t) => t.state === "forbidden") ?? tables.find((t) => t.state === "unknown");
|
|
14372
14372
|
out.push({ role, state: bad?.state ?? (tables.length ? "ok" : "unknown"), detail: bad?.detail ?? (tables.length ? void 0 : "route has no resolved tables"), tables });
|
|
14373
14373
|
}
|
|
14374
14374
|
return out;
|
|
14375
14375
|
}
|
|
14376
|
-
function forbiddenDetail(e,
|
|
14376
|
+
function forbiddenDetail(e, route2) {
|
|
14377
14377
|
const msg = e instanceof Error ? e.message : String(e);
|
|
14378
14378
|
const m = /^([A-Z]+) (\S+) -> 403/.exec(msg);
|
|
14379
14379
|
const verb = /No (READ|WRITE) access/i.exec(msg)?.[1]?.toUpperCase();
|
|
14380
14380
|
const modelId = /\/data-models\/([\w-]+)/i.exec(m?.[2] ?? "")?.[1];
|
|
14381
|
-
const r =
|
|
14381
|
+
const r = route2?.resolved;
|
|
14382
14382
|
const known = [
|
|
14383
14383
|
[r?.models?.issues, "Issues"],
|
|
14384
14384
|
[r?.models?.comments, "Comments"],
|
|
@@ -14414,8 +14414,8 @@ function isForbiddenError(e) {
|
|
|
14414
14414
|
return /\b403\b/.test(e instanceof Error ? e.message : String(e));
|
|
14415
14415
|
}
|
|
14416
14416
|
var SEAT_FORBIDDEN_KEY = "seat_forbidden:release";
|
|
14417
|
-
function seatForbiddenItem(now = /* @__PURE__ */ new Date(), e,
|
|
14418
|
-
const d = e === void 0 ? {} : forbiddenDetail(e,
|
|
14417
|
+
function seatForbiddenItem(now = /* @__PURE__ */ new Date(), e, route2) {
|
|
14418
|
+
const d = e === void 0 ? {} : forbiddenDetail(e, route2);
|
|
14419
14419
|
const what = d.method && d.table ? `${d.method} ${d.table}${d.verb ? ` -> 403 No ${d.verb} access` : " -> 403"}` : "cannot read the Issues table (403)";
|
|
14420
14420
|
return {
|
|
14421
14421
|
kind: "seat_forbidden",
|
|
@@ -14461,8 +14461,8 @@ var red = paintWith(31);
|
|
|
14461
14461
|
var cyan = paintWith(36);
|
|
14462
14462
|
var STEP_ORDER = ["poll", "sweep", "select", "worktree", "agent", "reconcile", "merge", "release", "idle"];
|
|
14463
14463
|
var WORKING_STEPS = /* @__PURE__ */ new Set(["worktree", "agent", "reconcile", "merge", "release"]);
|
|
14464
|
-
function byRoute(events,
|
|
14465
|
-
return
|
|
14464
|
+
function byRoute(events, route2) {
|
|
14465
|
+
return route2 ? events.filter((e) => e.route === route2) : events;
|
|
14466
14466
|
}
|
|
14467
14467
|
function matches(e, f) {
|
|
14468
14468
|
if (f.role && e.role !== f.role) return false;
|
|
@@ -14482,9 +14482,9 @@ function formatLine(e, showRoute = false) {
|
|
|
14482
14482
|
const t = dim(localTime(e.at));
|
|
14483
14483
|
const scope = e.role ? `${e.step}[${e.role}]` : e.step;
|
|
14484
14484
|
const paint = e.level === "error" ? red : e.level === "warn" ? yellow : cyan;
|
|
14485
|
-
const
|
|
14485
|
+
const route2 = showRoute ? `${dim(e.route)} ` : "";
|
|
14486
14486
|
const ticket = e.ticket ? ` ${bold(e.ticket)}` : "";
|
|
14487
|
-
return `${t} ${
|
|
14487
|
+
return `${t} ${route2}${paint(scope.padEnd(14))}${ticket} ${e.message}`;
|
|
14488
14488
|
}
|
|
14489
14489
|
function foldCycle(events) {
|
|
14490
14490
|
const last = events[events.length - 1];
|
|
@@ -14640,23 +14640,23 @@ async function gatherInbox(routes, ship, opts = {}) {
|
|
|
14640
14640
|
const items = [];
|
|
14641
14641
|
const errors = [];
|
|
14642
14642
|
const resolved = [];
|
|
14643
|
-
await Promise.all(routes.map(async (
|
|
14643
|
+
await Promise.all(routes.map(async (route2) => {
|
|
14644
14644
|
try {
|
|
14645
|
-
const tracker = new Tracker(
|
|
14645
|
+
const tracker = new Tracker(route2, ship);
|
|
14646
14646
|
const [tickets, crewRows] = await Promise.all([tracker.openTickets(), tracker.crewRows()]);
|
|
14647
|
-
const roster = buildRoster(configuredMembers(
|
|
14647
|
+
const roster = buildRoster(configuredMembers(route2), crewRows);
|
|
14648
14648
|
let mine;
|
|
14649
14649
|
if (opts.memberQuery) {
|
|
14650
14650
|
const hits = matchMembers(crewRows, opts.memberQuery);
|
|
14651
14651
|
resolved.push({
|
|
14652
|
-
route:
|
|
14652
|
+
route: route2.route,
|
|
14653
14653
|
names: hits.map((h) => h.name),
|
|
14654
14654
|
emails: hits.map((h) => h.email).filter(Boolean),
|
|
14655
14655
|
by: hits[0]?.by ?? null
|
|
14656
14656
|
});
|
|
14657
14657
|
mine = new Set(hits.map((h) => h.id));
|
|
14658
14658
|
} else {
|
|
14659
|
-
mine = opts.memberIds ?? new Set([
|
|
14659
|
+
mine = opts.memberIds ?? new Set([route2.resolved?.operator].filter(Boolean));
|
|
14660
14660
|
}
|
|
14661
14661
|
const c = tracker.contract;
|
|
14662
14662
|
for (const t of tickets) {
|
|
@@ -14664,7 +14664,7 @@ async function gatherInbox(routes, ship, opts = {}) {
|
|
|
14664
14664
|
if (opts.statuses && !opts.statuses.has(t.status)) continue;
|
|
14665
14665
|
if (!opts.includeClosed && c.statuses.resolved.includes(t.status)) continue;
|
|
14666
14666
|
items.push({
|
|
14667
|
-
route:
|
|
14667
|
+
route: route2.route,
|
|
14668
14668
|
ticket: t,
|
|
14669
14669
|
assignee: crewLabel(roster.get(t.assignee_id)),
|
|
14670
14670
|
rank: rankScalar(t, c),
|
|
@@ -14673,7 +14673,7 @@ async function gatherInbox(routes, ship, opts = {}) {
|
|
|
14673
14673
|
});
|
|
14674
14674
|
}
|
|
14675
14675
|
} catch (e) {
|
|
14676
|
-
errors.push({ route:
|
|
14676
|
+
errors.push({ route: route2.route, error: e.message });
|
|
14677
14677
|
}
|
|
14678
14678
|
}));
|
|
14679
14679
|
items.sort((a, b) => a.rank - b.rank);
|
|
@@ -14708,29 +14708,29 @@ function renderInbox(items, byConnection = false, subject) {
|
|
|
14708
14708
|
}
|
|
14709
14709
|
|
|
14710
14710
|
// src/fleet.ts
|
|
14711
|
-
function agedConnectionRank(rawRank, role,
|
|
14711
|
+
function agedConnectionRank(rawRank, role, route2, decision, state, now) {
|
|
14712
14712
|
if (role !== "dev" && role !== "design") return rawRank;
|
|
14713
|
-
const waiting = state.fairness(
|
|
14713
|
+
const waiting = state.fairness(route2.route).waiting();
|
|
14714
14714
|
if (!waiting || waiting.ticket !== decision.actionable.top?.issue_id) return rawRank;
|
|
14715
|
-
return agedRankScalar(rawRank, { since: waiting.since, weight:
|
|
14715
|
+
return agedRankScalar(rawRank, { since: waiting.since, weight: route2.weight }, now);
|
|
14716
14716
|
}
|
|
14717
14717
|
async function decideFleet(o) {
|
|
14718
14718
|
const now = o.now ?? Date.now();
|
|
14719
14719
|
const eligible = o.routes.filter((c) => o.enabledOnly === false ? true : c.enabled);
|
|
14720
|
-
const entries = await Promise.all(eligible.map(async (
|
|
14720
|
+
const entries = await Promise.all(eligible.map(async (route2) => {
|
|
14721
14721
|
try {
|
|
14722
|
-
const decision = await decideCycle({ route, ship: o.ship, state: o.state, emit: o.emit.forRoute(
|
|
14722
|
+
const decision = await decideCycle({ route: route2, ship: o.ship, state: o.state, emit: o.emit.forRoute(route2.route) });
|
|
14723
14723
|
const sel = decision.selection;
|
|
14724
|
-
if (!sel.selected) return { route, decision, rank: NOTHING_ACTIONABLE };
|
|
14724
|
+
if (!sel.selected) return { route: route2, decision, rank: NOTHING_ACTIONABLE };
|
|
14725
14725
|
const rawRank = sel.ranks[sel.selected] ?? NOTHING_ACTIONABLE;
|
|
14726
14726
|
return {
|
|
14727
|
-
route,
|
|
14727
|
+
route: route2,
|
|
14728
14728
|
decision,
|
|
14729
14729
|
role: sel.selected,
|
|
14730
|
-
rank: agedConnectionRank(rawRank, sel.selected,
|
|
14730
|
+
rank: agedConnectionRank(rawRank, sel.selected, route2, decision, o.state, now)
|
|
14731
14731
|
};
|
|
14732
14732
|
} catch (e) {
|
|
14733
|
-
return { route, rank: NOTHING_ACTIONABLE, error: e.message };
|
|
14733
|
+
return { route: route2, rank: NOTHING_ACTIONABLE, error: e.message };
|
|
14734
14734
|
}
|
|
14735
14735
|
}));
|
|
14736
14736
|
for (const e of entries) {
|
|
@@ -15062,8 +15062,8 @@ async function del(o, path) {
|
|
|
15062
15062
|
});
|
|
15063
15063
|
if (!res.ok) throw new ConnectHttpError(res.status, path, res.statusText);
|
|
15064
15064
|
}
|
|
15065
|
-
async function provisionReleaseLocks(o, locksModelId,
|
|
15066
|
-
const scopes = [...new Set(repoNames)].sort().map((r) => releaseLockScope(
|
|
15065
|
+
async function provisionReleaseLocks(o, locksModelId, route2, repoNames) {
|
|
15066
|
+
const scopes = [...new Set(repoNames)].sort().map((r) => releaseLockScope(route2, r));
|
|
15067
15067
|
if (scopes.length === 0) return [];
|
|
15068
15068
|
const client2 = {
|
|
15069
15069
|
// The Locks table is a handful of rows, so read it whole and match in
|
|
@@ -15235,14 +15235,14 @@ async function discover(o) {
|
|
|
15235
15235
|
out.locksModelId = find("Locks");
|
|
15236
15236
|
try {
|
|
15237
15237
|
const views = await get(o, `/views?workspaceId=${ws.id}`);
|
|
15238
|
-
const
|
|
15239
|
-
if (
|
|
15240
|
-
out.recordLinkViewId =
|
|
15241
|
-
} else if (
|
|
15238
|
+
const candidates2 = views.filter((v) => v.recordLinkOnly === true && v.published === true && !!v.publicSlug && (v.pages ?? []).some((p) => (p.components ?? []).some((c) => c.dataModelId === out.models.issues)));
|
|
15239
|
+
if (candidates2.length === 1) {
|
|
15240
|
+
out.recordLinkViewId = candidates2[0].id;
|
|
15241
|
+
} else if (candidates2.length === 0) {
|
|
15242
15242
|
problems.push("no record-link-only view bound to Issues \u2014 attention events will carry no link");
|
|
15243
15243
|
} else {
|
|
15244
15244
|
problems.push(
|
|
15245
|
-
`${
|
|
15245
|
+
`${candidates2.length} record-link-only views bound to Issues \u2014 set resolved.recordLinkViewId by hand`
|
|
15246
15246
|
);
|
|
15247
15247
|
}
|
|
15248
15248
|
} catch {
|
|
@@ -15312,8 +15312,8 @@ async function discover(o) {
|
|
|
15312
15312
|
const patchBody = {};
|
|
15313
15313
|
if (matchedBy === "machine_id") {
|
|
15314
15314
|
const rowName = (mine[0].name ?? "").trim();
|
|
15315
|
-
const
|
|
15316
|
-
const plan = planNameSync(o.ship.name.trim(), rowName, o.ship.lastSyncedName?.(
|
|
15315
|
+
const route2 = `${out.workspaceSlug}/${out.projectSlug ?? out.projectId}`;
|
|
15316
|
+
const plan = planNameSync(o.ship.name.trim(), rowName, o.ship.lastSyncedName?.(route2));
|
|
15317
15317
|
if (plan.action === "push") {
|
|
15318
15318
|
patchBody.name = o.ship.name.trim();
|
|
15319
15319
|
provisioning.push(`renamed Ships row "${rowName}" to "${o.ship.name.trim()}" (crew.yaml changed)`);
|
|
@@ -15477,11 +15477,11 @@ async function discover(o) {
|
|
|
15477
15477
|
else if (out.candidates.length === 1) out.operator = out.candidates[0].id;
|
|
15478
15478
|
}
|
|
15479
15479
|
} else {
|
|
15480
|
-
const
|
|
15480
|
+
const candidates2 = people.filter((r) => !r.user_id || isMine(r));
|
|
15481
15481
|
const mine = people.find(isMine);
|
|
15482
|
-
const byEmail = out.meEmail ?
|
|
15482
|
+
const byEmail = out.meEmail ? candidates2.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
|
|
15483
15483
|
const unlinked = people.filter((r) => !r.user_id);
|
|
15484
|
-
const only =
|
|
15484
|
+
const only = candidates2.length === 1 && unlinked.length === 1 ? candidates2[0] : void 0;
|
|
15485
15485
|
const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
|
|
15486
15486
|
if (pick) {
|
|
15487
15487
|
out.operator = pick.id;
|
|
@@ -15492,7 +15492,7 @@ async function discover(o) {
|
|
|
15492
15492
|
const m = r.user_id ? members.get(r.user_id) : void 0;
|
|
15493
15493
|
return `"${r.name || "unnamed row"}" <${r.email ?? "no email"}>` + (r.user_id ? ` linked to member ${m?.email ?? r.user_id}${m?.identityId ? ` (identity ${m.identityId})` : ""}` : " unlinked");
|
|
15494
15494
|
}).join("; "));
|
|
15495
|
-
if (o.ship && (!o.interactive ||
|
|
15495
|
+
if (o.ship && (!o.interactive || candidates2.length === 0)) await createOperatorRow(o, out);
|
|
15496
15496
|
}
|
|
15497
15497
|
}
|
|
15498
15498
|
}
|
|
@@ -15541,7 +15541,7 @@ async function createOperatorRow(o, found) {
|
|
|
15541
15541
|
found.provisioning.push("could not create your Crew row (no write access) \u2014 ask a workspace admin to add one with your user_id");
|
|
15542
15542
|
}
|
|
15543
15543
|
}
|
|
15544
|
-
function renderConnection(d,
|
|
15544
|
+
function renderConnection(d, route2, dir, opts = {}) {
|
|
15545
15545
|
const apiKeyFileLine = opts.keychainBacked ? "" : opts.apiKeyFile ? ` apiKeyFile: "${opts.apiKeyFile}"` : ` apiKeyFile: "REPLACE \u2014 a file holding this workspace's key"`;
|
|
15546
15546
|
const apiKeyVarLine = opts.keychainBacked ? "" : opts.apiKeyVar ? `
|
|
15547
15547
|
apiKeyVar: ${opts.apiKeyVar}` : `
|
|
@@ -15549,7 +15549,7 @@ function renderConnection(d, route, dir, opts = {}) {
|
|
|
15549
15549
|
const baseUrlLine = opts.baseUrl ? opts.baseUrl.replace(/\/+$/, "") === DEFAULT_BASE_URL ? "" : ` baseUrl: "${opts.baseUrl}"
|
|
15550
15550
|
` : ' baseUrl: "REPLACE \u2014 the same baseUrl as your other routes"\n';
|
|
15551
15551
|
const keyLines = [apiKeyFileLine, apiKeyVarLine.replace(/^\n/, "")].filter(Boolean).join("\n");
|
|
15552
|
-
return ` - route: ${
|
|
15552
|
+
return ` - route: ${route2}
|
|
15553
15553
|
enabled: false # set to true once \`crew doctor\` is green
|
|
15554
15554
|
${opts.area ? ` area: "${opts.area}"
|
|
15555
15555
|
` : ""}${dir === null ? "" : ` dir: "${dir}"
|
|
@@ -15668,26 +15668,26 @@ import { createHash as createHash5 } from "crypto";
|
|
|
15668
15668
|
import { TablationClient as TablationClient4 } from "@tablation/client";
|
|
15669
15669
|
var LogbookError = class extends Error {
|
|
15670
15670
|
};
|
|
15671
|
-
function client(
|
|
15671
|
+
function client(route2, userAgent) {
|
|
15672
15672
|
return new TablationClient4({
|
|
15673
|
-
baseUrl: `${
|
|
15674
|
-
apiKey: resolveApiKey(
|
|
15673
|
+
baseUrl: `${route2.baseUrl}/api`,
|
|
15674
|
+
apiKey: resolveApiKey(route2),
|
|
15675
15675
|
headers: { "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
15676
15676
|
});
|
|
15677
15677
|
}
|
|
15678
|
-
function requireWorkspace(
|
|
15679
|
-
if (!
|
|
15680
|
-
throw new LogbookError(`route "${
|
|
15678
|
+
function requireWorkspace(route2) {
|
|
15679
|
+
if (!route2.resolved) {
|
|
15680
|
+
throw new LogbookError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
|
|
15681
15681
|
}
|
|
15682
15682
|
}
|
|
15683
|
-
async function listLogEntries(
|
|
15684
|
-
requireWorkspace(
|
|
15685
|
-
const c = client(
|
|
15686
|
-
const agentLogModel = await c.dataModels.get("agent_log",
|
|
15683
|
+
async function listLogEntries(route2, opts = {}) {
|
|
15684
|
+
requireWorkspace(route2);
|
|
15685
|
+
const c = client(route2, opts.userAgent);
|
|
15686
|
+
const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
|
|
15687
15687
|
const filters = [];
|
|
15688
15688
|
if (opts.ticket) filters.push({ columnName: "ticket_reference", operator: "EQ", value: opts.ticket });
|
|
15689
15689
|
if (opts.role) {
|
|
15690
|
-
const agentsModel = await c.dataModels.get("agents",
|
|
15690
|
+
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
15691
15691
|
const rows = await c.records.list(agentsModel.id, { limit: 200 });
|
|
15692
15692
|
const match = rows.find((r) => r.name.toLowerCase() === opts.role.toLowerCase());
|
|
15693
15693
|
if (!match) return [];
|
|
@@ -15699,10 +15699,10 @@ async function listLogEntries(route, opts = {}) {
|
|
|
15699
15699
|
});
|
|
15700
15700
|
return [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
|
|
15701
15701
|
}
|
|
15702
|
-
async function showLogEntry(
|
|
15703
|
-
requireWorkspace(
|
|
15704
|
-
const c = client(
|
|
15705
|
-
const agentLogModel = await c.dataModels.get("agent_log",
|
|
15702
|
+
async function showLogEntry(route2, entryId, opts = {}) {
|
|
15703
|
+
requireWorkspace(route2);
|
|
15704
|
+
const c = client(route2, opts.userAgent);
|
|
15705
|
+
const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
|
|
15706
15706
|
let entry;
|
|
15707
15707
|
try {
|
|
15708
15708
|
entry = await c.records.get(agentLogModel.id, entryId);
|
|
@@ -15719,10 +15719,10 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
15719
15719
|
}
|
|
15720
15720
|
};
|
|
15721
15721
|
}
|
|
15722
|
-
const agentsModel = await c.dataModels.get("agents",
|
|
15722
|
+
const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
|
|
15723
15723
|
let historyEntries;
|
|
15724
15724
|
try {
|
|
15725
|
-
historyEntries = await fetchHistoryEntries(
|
|
15725
|
+
historyEntries = await fetchHistoryEntries(route2, agentsModel.id, entry.agent_id, opts.userAgent, 200);
|
|
15726
15726
|
} catch (e) {
|
|
15727
15727
|
return { entry, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
|
|
15728
15728
|
}
|
|
@@ -15882,37 +15882,37 @@ function routeNamesOf(text) {
|
|
|
15882
15882
|
}
|
|
15883
15883
|
function parseAddArgs(args, routes, isDir) {
|
|
15884
15884
|
const rest = [...args];
|
|
15885
|
-
let
|
|
15886
|
-
if (rest.length > 0 && routes.includes(rest[0]))
|
|
15885
|
+
let route2;
|
|
15886
|
+
if (rest.length > 0 && routes.includes(rest[0])) route2 = rest.shift();
|
|
15887
15887
|
else if (rest.length > 0 && /^[^/\s]+\/[^/\s]+$/.test(rest[0]) && !isDir(rest[0])) {
|
|
15888
15888
|
throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
|
|
15889
15889
|
}
|
|
15890
|
-
if (!
|
|
15891
|
-
if (routes.length === 1)
|
|
15890
|
+
if (!route2) {
|
|
15891
|
+
if (routes.length === 1) route2 = routes[0];
|
|
15892
15892
|
else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
|
|
15893
15893
|
}
|
|
15894
15894
|
if (rest.length > 2) throw new ReposError("too many arguments: expected [route] [name] [path]");
|
|
15895
|
-
if (rest.length === 2) return { route, name: rest[0], path: rest[1] };
|
|
15896
|
-
if (rest.length === 1) return isDir(rest[0]) ? { route, path: rest[0] } : { route, name: rest[0], path: "." };
|
|
15897
|
-
return { route, path: "." };
|
|
15895
|
+
if (rest.length === 2) return { route: route2, name: rest[0], path: rest[1] };
|
|
15896
|
+
if (rest.length === 1) return isDir(rest[0]) ? { route: route2, path: rest[0] } : { route: route2, name: rest[0], path: "." };
|
|
15897
|
+
return { route: route2, path: "." };
|
|
15898
15898
|
}
|
|
15899
15899
|
function expandDir(p, base) {
|
|
15900
15900
|
const home = p === "~" ? homedir9() : p.startsWith("~/") ? `${homedir9()}/${p.slice(2)}` : p;
|
|
15901
15901
|
return resolve3(base, home);
|
|
15902
15902
|
}
|
|
15903
|
-
function routeMapOf(doc,
|
|
15903
|
+
function routeMapOf(doc, route2) {
|
|
15904
15904
|
const routes = doc.get("routes", true);
|
|
15905
15905
|
if (isSeq(routes)) {
|
|
15906
15906
|
for (const item of routes.items) {
|
|
15907
|
-
if (isMap(item) && item.get("route") ===
|
|
15907
|
+
if (isMap(item) && item.get("route") === route2) return item;
|
|
15908
15908
|
}
|
|
15909
15909
|
}
|
|
15910
15910
|
const have = isSeq(routes) ? routes.items.map((i) => isMap(i) ? String(i.get("route")) : "?").join(", ") : "none";
|
|
15911
|
-
throw new ReposError(`no route "${
|
|
15911
|
+
throw new ReposError(`no route "${route2}" in the config (have: ${have})`);
|
|
15912
15912
|
}
|
|
15913
|
-
function configuredRepos(text,
|
|
15913
|
+
function configuredRepos(text, route2, base) {
|
|
15914
15914
|
const doc = parseDocument3(text);
|
|
15915
|
-
const m = routeMapOf(doc,
|
|
15915
|
+
const m = routeMapOf(doc, route2);
|
|
15916
15916
|
const repos = m.get("repos");
|
|
15917
15917
|
if (isMap(repos)) {
|
|
15918
15918
|
return repos.items.map((p) => {
|
|
@@ -15925,8 +15925,8 @@ function configuredRepos(text, route, base) {
|
|
|
15925
15925
|
const dir = m.get("dir");
|
|
15926
15926
|
return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
15927
15927
|
}
|
|
15928
|
-
function cloneBaseOf(text,
|
|
15929
|
-
const m = routeMapOf(parseDocument3(text),
|
|
15928
|
+
function cloneBaseOf(text, route2, base, isCheckout) {
|
|
15929
|
+
const m = routeMapOf(parseDocument3(text), route2);
|
|
15930
15930
|
const repos = m.get("repos", true);
|
|
15931
15931
|
if (isMap(repos) && repos.items.length > 0) return null;
|
|
15932
15932
|
const dir = m.get("dir");
|
|
@@ -15934,8 +15934,8 @@ function cloneBaseOf(text, route, base, isCheckout) {
|
|
|
15934
15934
|
const abs = expandDir(String(dir), base);
|
|
15935
15935
|
return isCheckout(abs) ? null : abs;
|
|
15936
15936
|
}
|
|
15937
|
-
function routeSetsHooks(text,
|
|
15938
|
-
const m = routeMapOf(parseDocument3(text),
|
|
15937
|
+
function routeSetsHooks(text, route2, name) {
|
|
15938
|
+
const m = routeMapOf(parseDocument3(text), route2);
|
|
15939
15939
|
const repos = m.get("repos", true);
|
|
15940
15940
|
let target = m;
|
|
15941
15941
|
if (isMap(repos)) target = repos.get(name, true);
|
|
@@ -16042,14 +16042,14 @@ function planRepoAdd(i) {
|
|
|
16042
16042
|
}
|
|
16043
16043
|
return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
|
|
16044
16044
|
}
|
|
16045
|
-
function planRepoRemove(text, base,
|
|
16045
|
+
function planRepoRemove(text, base, route2, path) {
|
|
16046
16046
|
const doc = parseDocument3(text);
|
|
16047
16047
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
16048
|
-
const m = routeMapOf(doc,
|
|
16048
|
+
const m = routeMapOf(doc, route2);
|
|
16049
16049
|
const target = expandDir(path, base);
|
|
16050
|
-
const existing = configuredRepos(text,
|
|
16050
|
+
const existing = configuredRepos(text, route2, base);
|
|
16051
16051
|
const hit = existing.find((e) => e.dir === target);
|
|
16052
|
-
if (!hit) throw new ReposError(`${target} is not configured on route ${
|
|
16052
|
+
if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
|
|
16053
16053
|
const repos = m.get("repos", true);
|
|
16054
16054
|
if (isMap(repos)) {
|
|
16055
16055
|
repos.delete(hit.name);
|
|
@@ -16097,15 +16097,15 @@ function resolveRepoProject(projects, opt, routeAreaId) {
|
|
|
16097
16097
|
}
|
|
16098
16098
|
function parseCloneArgs(args, routes) {
|
|
16099
16099
|
const rest = [...args];
|
|
16100
|
-
let
|
|
16101
|
-
if (rest.length > 1 && routes.includes(rest[0]))
|
|
16100
|
+
let route2;
|
|
16101
|
+
if (rest.length > 1 && routes.includes(rest[0])) route2 = rest.shift();
|
|
16102
16102
|
else if (rest.length > 1) throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
|
|
16103
|
-
if (!
|
|
16104
|
-
if (routes.length === 1)
|
|
16103
|
+
if (!route2) {
|
|
16104
|
+
if (routes.length === 1) route2 = routes[0];
|
|
16105
16105
|
else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
|
|
16106
16106
|
}
|
|
16107
16107
|
if (rest.length !== 1) throw new ReposError("expected [route] <name> \u2014 the Repos row to clone (see `crew repos list`)");
|
|
16108
|
-
return { route, name: rest[0] };
|
|
16108
|
+
return { route: route2, name: rest[0] };
|
|
16109
16109
|
}
|
|
16110
16110
|
function planRepoClone(i) {
|
|
16111
16111
|
const names = i.resolved?.repoNames ?? {};
|
|
@@ -16257,27 +16257,27 @@ function parseHookOverrides(specs) {
|
|
|
16257
16257
|
}
|
|
16258
16258
|
function parseHooksArgs(args, routes) {
|
|
16259
16259
|
const rest = [...args];
|
|
16260
|
-
let
|
|
16261
|
-
if (rest.length > 1 && routes.includes(rest[0]))
|
|
16262
|
-
if (!
|
|
16263
|
-
if (routes.length === 1)
|
|
16260
|
+
let route2;
|
|
16261
|
+
if (rest.length > 1 && routes.includes(rest[0])) route2 = rest.shift();
|
|
16262
|
+
if (!route2) {
|
|
16263
|
+
if (routes.length === 1) route2 = routes[0];
|
|
16264
16264
|
else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
|
|
16265
16265
|
}
|
|
16266
16266
|
if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--hook KEY=CMD] [--json]");
|
|
16267
|
-
return { route, name: rest[0] };
|
|
16267
|
+
return { route: route2, name: rest[0] };
|
|
16268
16268
|
}
|
|
16269
|
-
function repoDirOf(text, base,
|
|
16270
|
-
const repos = configuredRepos(text,
|
|
16269
|
+
function repoDirOf(text, base, route2, name) {
|
|
16270
|
+
const repos = configuredRepos(text, route2, base);
|
|
16271
16271
|
const hit = repos.find((r) => r.name === name);
|
|
16272
16272
|
if (!hit) {
|
|
16273
|
-
throw new ReposError(`route ${
|
|
16273
|
+
throw new ReposError(`route ${route2} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
|
|
16274
16274
|
}
|
|
16275
16275
|
return hit.dir;
|
|
16276
16276
|
}
|
|
16277
|
-
function planRouteHooks(text,
|
|
16277
|
+
function planRouteHooks(text, route2, name, hooks, opts = {}) {
|
|
16278
16278
|
const doc = parseDocument4(text);
|
|
16279
16279
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
16280
|
-
const m = routeMapOf(doc,
|
|
16280
|
+
const m = routeMapOf(doc, route2);
|
|
16281
16281
|
const repos = m.get("repos", true);
|
|
16282
16282
|
let target = m;
|
|
16283
16283
|
if (isMap2(repos)) {
|
|
@@ -16311,10 +16311,10 @@ function planRouteHooks(text, route, name, hooks, opts = {}) {
|
|
|
16311
16311
|
}
|
|
16312
16312
|
return { text: String(doc), written, kept, replaced };
|
|
16313
16313
|
}
|
|
16314
|
-
function clearRouteHooks(text,
|
|
16314
|
+
function clearRouteHooks(text, route2, name) {
|
|
16315
16315
|
const doc = parseDocument4(text);
|
|
16316
16316
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
16317
|
-
const m = routeMapOf(doc,
|
|
16317
|
+
const m = routeMapOf(doc, route2);
|
|
16318
16318
|
const repos = m.get("repos", true);
|
|
16319
16319
|
let target = m;
|
|
16320
16320
|
if (isMap2(repos)) {
|
|
@@ -16476,7 +16476,7 @@ async function runAgentSuggest(o) {
|
|
|
16476
16476
|
const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
|
|
16477
16477
|
const run2 = await new Promise((resolve6) => {
|
|
16478
16478
|
let out = "";
|
|
16479
|
-
let
|
|
16479
|
+
let err2 = "";
|
|
16480
16480
|
const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
16481
16481
|
let settled = false;
|
|
16482
16482
|
const settle = (r) => {
|
|
@@ -16493,7 +16493,7 @@ async function runAgentSuggest(o) {
|
|
|
16493
16493
|
}
|
|
16494
16494
|
child.stdout.destroy();
|
|
16495
16495
|
child.stderr.destroy();
|
|
16496
|
-
settle({ code: null, out, err, aborted: true });
|
|
16496
|
+
settle({ code: null, out, err: err2, aborted: true });
|
|
16497
16497
|
};
|
|
16498
16498
|
if (signal.aborted) onAbort();
|
|
16499
16499
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -16501,10 +16501,10 @@ async function runAgentSuggest(o) {
|
|
|
16501
16501
|
out += d.toString();
|
|
16502
16502
|
});
|
|
16503
16503
|
child.stderr.on("data", (d) => {
|
|
16504
|
-
|
|
16504
|
+
err2 += d.toString();
|
|
16505
16505
|
});
|
|
16506
|
-
child.on("error", (e) => settle({ code: null, out, err, spawnError: e, aborted: false }));
|
|
16507
|
-
child.on("close", (code) => settle({ code, out, err, aborted: false }));
|
|
16506
|
+
child.on("error", (e) => settle({ code: null, out, err: err2, spawnError: e, aborted: false }));
|
|
16507
|
+
child.on("close", (code) => settle({ code, out, err: err2, aborted: false }));
|
|
16508
16508
|
child.stdin.on("error", () => {
|
|
16509
16509
|
});
|
|
16510
16510
|
child.stdin.end(buildAgentPrompt(o.sniff));
|
|
@@ -16633,9 +16633,9 @@ async function gatherHealth(c, check) {
|
|
|
16633
16633
|
function routesInScope(routes, named) {
|
|
16634
16634
|
return named ? routes.filter((r) => r.route === named) : routes;
|
|
16635
16635
|
}
|
|
16636
|
-
function planEnable(text,
|
|
16636
|
+
function planEnable(text, route2) {
|
|
16637
16637
|
const doc = parseDocument5(text);
|
|
16638
|
-
routeMapOf(doc,
|
|
16638
|
+
routeMapOf(doc, route2).set("enabled", true);
|
|
16639
16639
|
return String(doc);
|
|
16640
16640
|
}
|
|
16641
16641
|
function schedulerInstalled(plan, exists, crontab) {
|
|
@@ -16975,7 +16975,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
16975
16975
|
}
|
|
16976
16976
|
|
|
16977
16977
|
// src/cli.ts
|
|
16978
|
-
import { realpathSync as realpathSync3, readFileSync as readFileSync34, existsSync as
|
|
16978
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync34, existsSync as existsSync39, mkdirSync as mkdirSync25, writeFileSync as writeFileSync26, rmSync as rmSync18, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync13, readdirSync as readdirSync18 } from "fs";
|
|
16979
16979
|
import { createInterface } from "readline/promises";
|
|
16980
16980
|
|
|
16981
16981
|
// src/periscope-cmd.ts
|
|
@@ -17513,8 +17513,8 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
17513
17513
|
"-q"
|
|
17514
17514
|
], { stdio: ["ignore", "ignore", "pipe"] });
|
|
17515
17515
|
} catch (e) {
|
|
17516
|
-
const
|
|
17517
|
-
throw new SshKeyError(`ssh-keygen failed: ${
|
|
17516
|
+
const err2 = e;
|
|
17517
|
+
throw new SshKeyError(`ssh-keygen failed: ${err2.stderr?.toString().trim() || err2.message}`);
|
|
17518
17518
|
}
|
|
17519
17519
|
chmodSync2(privateKeyPath, 384);
|
|
17520
17520
|
}
|
|
@@ -17540,9 +17540,9 @@ var realExec = (cmd, args, opts) => {
|
|
|
17540
17540
|
try {
|
|
17541
17541
|
return execFileSync9(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
|
|
17542
17542
|
} catch (e) {
|
|
17543
|
-
const
|
|
17544
|
-
const why = [
|
|
17545
|
-
throw new Error(`${cmd} ${args.join(" ")}: ${why ||
|
|
17543
|
+
const err2 = e;
|
|
17544
|
+
const why = [err2.stderr, err2.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
17545
|
+
throw new Error(`${cmd} ${args.join(" ")}: ${why || err2.message.trim()}`);
|
|
17546
17546
|
}
|
|
17547
17547
|
};
|
|
17548
17548
|
var DOCKER_INFO_TIMEOUT_MS = 1e4;
|
|
@@ -17583,11 +17583,11 @@ function mountsDigest(mounts, image) {
|
|
|
17583
17583
|
function planContainers(cfg) {
|
|
17584
17584
|
const byWorkspace = /* @__PURE__ */ new Map();
|
|
17585
17585
|
const image = passengerImageRef(cfg);
|
|
17586
|
-
for (const
|
|
17587
|
-
if (!
|
|
17588
|
-
const workspaceId =
|
|
17586
|
+
for (const route2 of cfg.routes) {
|
|
17587
|
+
if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
|
|
17588
|
+
const workspaceId = route2.resolved.workspaceId;
|
|
17589
17589
|
const mounts = byWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
|
|
17590
|
-
for (const target of passengerCheckoutTargets(
|
|
17590
|
+
for (const target of passengerCheckoutTargets(route2)) {
|
|
17591
17591
|
mounts.set(target.name, { hostPath: target.dir, containerPath: `/workspace/${target.name}` });
|
|
17592
17592
|
}
|
|
17593
17593
|
byWorkspace.set(workspaceId, mounts);
|
|
@@ -17701,17 +17701,17 @@ function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, sync
|
|
|
17701
17701
|
}
|
|
17702
17702
|
function passengerCheckoutSyncSpecs(cfg, workspaceId) {
|
|
17703
17703
|
const specs = [];
|
|
17704
|
-
for (const
|
|
17705
|
-
if (!
|
|
17706
|
-
if (workspaceId &&
|
|
17707
|
-
const routeWorkspaceId =
|
|
17708
|
-
const ordinaryByName = new Map(reposOf(
|
|
17709
|
-
for (const target of passengerCheckoutTargets(
|
|
17704
|
+
for (const route2 of cfg.routes) {
|
|
17705
|
+
if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
|
|
17706
|
+
if (workspaceId && route2.resolved.workspaceId !== workspaceId) continue;
|
|
17707
|
+
const routeWorkspaceId = route2.resolved.workspaceId;
|
|
17708
|
+
const ordinaryByName = new Map(reposOf(route2).map((t) => [t.name, t]));
|
|
17709
|
+
for (const target of passengerCheckoutTargets(route2)) {
|
|
17710
17710
|
const ordinary = ordinaryByName.get(target.name);
|
|
17711
|
-
const override =
|
|
17711
|
+
const override = route2.repoOverrides[target.name];
|
|
17712
17712
|
const effective = resolveRepoConfig(
|
|
17713
17713
|
ordinary ? loadRepoConfig(ordinary.dir) : null,
|
|
17714
|
-
{ branch: { ...
|
|
17714
|
+
{ branch: { ...route2.branch, ...override?.branch } },
|
|
17715
17715
|
ordinary?.dir ?? target.dir
|
|
17716
17716
|
);
|
|
17717
17717
|
specs.push({
|
|
@@ -18115,8 +18115,8 @@ function runSyncDaemonFromEnv(env = process.env) {
|
|
|
18115
18115
|
const secret = requireEnv(env, "PASSENGER_SYNC_SECRET");
|
|
18116
18116
|
const targets = JSON.parse(requireEnv(env, "PASSENGER_SYNC_TARGETS"));
|
|
18117
18117
|
const server = createSyncDaemonServer(targets, secret);
|
|
18118
|
-
server.on("error", (
|
|
18119
|
-
console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${
|
|
18118
|
+
server.on("error", (err2) => {
|
|
18119
|
+
console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
18120
18120
|
process.exit(1);
|
|
18121
18121
|
});
|
|
18122
18122
|
server.listen(port, "0.0.0.0", () => {
|
|
@@ -18233,22 +18233,22 @@ function codeOf(e) {
|
|
|
18233
18233
|
const code = e?.code;
|
|
18234
18234
|
return typeof code === "string" ? code : void 0;
|
|
18235
18235
|
}
|
|
18236
|
-
function isTransientError(
|
|
18237
|
-
if (!(
|
|
18238
|
-
const msg =
|
|
18236
|
+
function isTransientError(err2) {
|
|
18237
|
+
if (!(err2 instanceof Error)) return false;
|
|
18238
|
+
const msg = err2.message;
|
|
18239
18239
|
if (msg === "fetch failed" || msg.startsWith("fetch failed")) return true;
|
|
18240
|
-
for (let e =
|
|
18240
|
+
for (let e = err2, depth = 0; e && depth < 4; e = e.cause, depth++) {
|
|
18241
18241
|
const code = codeOf(e);
|
|
18242
18242
|
if (code && NETWORK_CODES.includes(code)) return true;
|
|
18243
18243
|
}
|
|
18244
18244
|
if (NETWORK_CODES.some((c) => msg.includes(c))) return true;
|
|
18245
18245
|
if (/\b(?:HTTP|status)\s*:?\s*(502|503|504)\b/i.test(msg) || /^(502|503|504)\b/.test(msg)) return true;
|
|
18246
|
-
if (
|
|
18246
|
+
if (err2 instanceof SyntaxError && /Unexpected token '?<|is not valid JSON|Unexpected end of JSON/.test(msg)) return true;
|
|
18247
18247
|
return false;
|
|
18248
18248
|
}
|
|
18249
|
-
function describeCause(
|
|
18250
|
-
const first = (
|
|
18251
|
-
const code = codeOf(
|
|
18249
|
+
function describeCause(err2) {
|
|
18250
|
+
const first = (err2 instanceof Error ? err2.message : String(err2)).split("\n")[0];
|
|
18251
|
+
const code = codeOf(err2?.cause);
|
|
18252
18252
|
return code && !first.includes(code) ? `${first} (${code})` : first;
|
|
18253
18253
|
}
|
|
18254
18254
|
|
|
@@ -18277,22 +18277,22 @@ async function runDaemonLoop(o) {
|
|
|
18277
18277
|
o.onRecovered?.({ downMs: now() - downSince });
|
|
18278
18278
|
transientRun = 0;
|
|
18279
18279
|
}
|
|
18280
|
-
} catch (
|
|
18281
|
-
if (isTransientError(
|
|
18280
|
+
} catch (err2) {
|
|
18281
|
+
if (isTransientError(err2)) {
|
|
18282
18282
|
passes++;
|
|
18283
18283
|
if (transientRun === 0) downSince = now();
|
|
18284
18284
|
transientRun++;
|
|
18285
18285
|
const wait = transientBackoffMs(transientRun);
|
|
18286
18286
|
const downMs = now() - downSince;
|
|
18287
|
-
o.onTransient?.({ err, consecutive: transientRun, backoffMs: wait, downMs });
|
|
18288
|
-
if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err, consecutive: transientRun, downMs });
|
|
18287
|
+
o.onTransient?.({ err: err2, consecutive: transientRun, backoffMs: wait, downMs });
|
|
18288
|
+
if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err: err2, consecutive: transientRun, downMs });
|
|
18289
18289
|
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
18290
18290
|
if (o.signal?.aborted) return "aborted";
|
|
18291
18291
|
await sleep(wait);
|
|
18292
18292
|
continue;
|
|
18293
18293
|
}
|
|
18294
18294
|
passes++;
|
|
18295
|
-
o.onPassError?.(
|
|
18295
|
+
o.onPassError?.(err2);
|
|
18296
18296
|
if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
|
|
18297
18297
|
if (o.signal?.aborted) return "aborted";
|
|
18298
18298
|
await sleep(backoff);
|
|
@@ -18411,7 +18411,7 @@ async function runOnePassUnguarded(o) {
|
|
|
18411
18411
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
18412
18412
|
const emit = o.newEmitter();
|
|
18413
18413
|
const decide = o.decide ?? decideCycle;
|
|
18414
|
-
const
|
|
18414
|
+
const spawn8 = o.spawnRoleAgent ?? runRoleAgent;
|
|
18415
18415
|
const breaker = o.spawnBreaker ?? new SpawnBreaker();
|
|
18416
18416
|
const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
|
|
18417
18417
|
if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
|
|
@@ -18515,7 +18515,7 @@ async function runOnePassUnguarded(o) {
|
|
|
18515
18515
|
const settled = new Promise((resolve6) => {
|
|
18516
18516
|
ready = resolve6;
|
|
18517
18517
|
});
|
|
18518
|
-
const run2 =
|
|
18518
|
+
const run2 = spawn8(role, decision, ctx, { ready }).then(() => {
|
|
18519
18519
|
breaker.recordSuccess(role);
|
|
18520
18520
|
clearShipAttention(o.state, `role_parked:${role}`);
|
|
18521
18521
|
}).catch(async (e) => {
|
|
@@ -18571,23 +18571,23 @@ async function publishShipAttention(o) {
|
|
|
18571
18571
|
}
|
|
18572
18572
|
}
|
|
18573
18573
|
async function replayPendingWrites(ctx) {
|
|
18574
|
-
const { route, ship, emit } = ctx;
|
|
18574
|
+
const { route: route2, ship, emit } = ctx;
|
|
18575
18575
|
try {
|
|
18576
|
-
const roles = [...new Set(listPendingHandoffs(ship.stateDir, routeSlug(
|
|
18576
|
+
const roles = [...new Set(listPendingHandoffs(ship.stateDir, routeSlug(route2.route)).map((r) => r.role))];
|
|
18577
18577
|
for (const role of roles) {
|
|
18578
|
-
const identity = await resolveSeatKey(
|
|
18579
|
-
await replayRoutePending(
|
|
18578
|
+
const identity = await resolveSeatKey(route2, role, ctx.getSessionStore);
|
|
18579
|
+
await replayRoutePending(route2, ship, new Tracker(route2, ship, { apiKey: identity.apiKey }), emit, role);
|
|
18580
18580
|
}
|
|
18581
18581
|
await replayQueuedAgentRuns(
|
|
18582
18582
|
ship.stateDir,
|
|
18583
|
-
routeSlug(
|
|
18584
|
-
async (role) => (await resolveSeatKey(
|
|
18583
|
+
routeSlug(route2.route),
|
|
18584
|
+
async (role) => (await resolveSeatKey(route2, role, ctx.getSessionStore)).apiKey,
|
|
18585
18585
|
emit
|
|
18586
18586
|
);
|
|
18587
18587
|
await replayQueuedTicketCounters(
|
|
18588
18588
|
ship.stateDir,
|
|
18589
|
-
routeSlug(
|
|
18590
|
-
async (role) => (await resolveSeatKey(
|
|
18589
|
+
routeSlug(route2.route),
|
|
18590
|
+
async (role) => (await resolveSeatKey(route2, role, ctx.getSessionStore)).apiKey,
|
|
18591
18591
|
emit
|
|
18592
18592
|
);
|
|
18593
18593
|
} catch (e) {
|
|
@@ -18595,27 +18595,27 @@ async function replayPendingWrites(ctx) {
|
|
|
18595
18595
|
}
|
|
18596
18596
|
}
|
|
18597
18597
|
async function reconcileForRoute(ctx, o) {
|
|
18598
|
-
const { route, ship, emit } = ctx;
|
|
18598
|
+
const { route: route2, ship, emit } = ctx;
|
|
18599
18599
|
await replayPendingWrites(ctx);
|
|
18600
18600
|
await reconcileRuns({
|
|
18601
18601
|
stateDir: ship.stateDir,
|
|
18602
|
-
routeLabel: routeSlug(
|
|
18602
|
+
routeLabel: routeSlug(route2.route),
|
|
18603
18603
|
emit,
|
|
18604
18604
|
state: o.state,
|
|
18605
18605
|
maxConcurrentAgents: o.maxConcurrentAgents,
|
|
18606
18606
|
roleConcurrency: o.roleConcurrency,
|
|
18607
18607
|
inFlight: o.inFlight,
|
|
18608
|
-
agentLogKey: async (role) => (await resolveSeatKey(
|
|
18608
|
+
agentLogKey: async (role) => (await resolveSeatKey(route2, role, ctx.getSessionStore)).apiKey,
|
|
18609
18609
|
afterRun: async (sc, result) => {
|
|
18610
|
-
const identity = await resolveSeatKey(
|
|
18611
|
-
const tracker = new Tracker(
|
|
18612
|
-
const memberId =
|
|
18610
|
+
const identity = await resolveSeatKey(route2, sc.role, ctx.getSessionStore);
|
|
18611
|
+
const tracker = new Tracker(route2, ship, { apiKey: identity.apiKey });
|
|
18612
|
+
const memberId = route2.resolved?.seats[sc.role];
|
|
18613
18613
|
try {
|
|
18614
18614
|
if (!sc.ticketRecordId) return;
|
|
18615
18615
|
if (result.interrupted) {
|
|
18616
18616
|
if (!memberId) return;
|
|
18617
18617
|
const t = await tracker.ticket(sc.ticketRecordId);
|
|
18618
|
-
const target = repoTargetFor(
|
|
18618
|
+
const target = repoTargetFor(route2, t.repo_id);
|
|
18619
18619
|
const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
|
|
18620
18620
|
const wt = target && num ? worktreeForNumber(target.dir, num) : null;
|
|
18621
18621
|
let dirty = null;
|
|
@@ -18629,13 +18629,13 @@ async function reconcileForRoute(ctx, o) {
|
|
|
18629
18629
|
reason: result.interrupted === "daemon-exit" ? "the crew daemon exited" : "it exited without a result"
|
|
18630
18630
|
}), memberId, "interrupted");
|
|
18631
18631
|
} else {
|
|
18632
|
-
await guardRunHandoff(
|
|
18632
|
+
await guardRunHandoff(route2, ship, tracker, sc.role, sc.ticketRecordId, emit, sc.spawnedAt);
|
|
18633
18633
|
}
|
|
18634
18634
|
} finally {
|
|
18635
18635
|
await settleRunEnd({
|
|
18636
18636
|
tracker,
|
|
18637
18637
|
shipName: ship.name,
|
|
18638
|
-
route,
|
|
18638
|
+
route: route2,
|
|
18639
18639
|
role: sc.role,
|
|
18640
18640
|
memberId,
|
|
18641
18641
|
ownTicketRecordId: sc.ticketRecordId,
|
|
@@ -18656,30 +18656,30 @@ async function runRoleAgent(role, decision, ctx, hooks) {
|
|
|
18656
18656
|
}
|
|
18657
18657
|
}
|
|
18658
18658
|
async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
18659
|
-
const { route, ship, emit } = ctx;
|
|
18659
|
+
const { route: route2, ship, emit } = ctx;
|
|
18660
18660
|
const runId = randomBytes2(3).toString("hex");
|
|
18661
|
-
const identity = await resolveSeatKey(
|
|
18661
|
+
const identity = await resolveSeatKey(route2, role, ctx.getSessionStore);
|
|
18662
18662
|
if (identity.kind === "ship" && SEAT_KEY_ROLES.includes(role)) {
|
|
18663
18663
|
emit.warn(`${role} writes as the ship, not its own agent: ${identity.reason}`, { step: "agent", role });
|
|
18664
18664
|
} else if (identity.kind === "agent") {
|
|
18665
18665
|
emit.emit(`${role} writes as its own agent principal`, { step: "agent", role });
|
|
18666
18666
|
}
|
|
18667
|
-
const tracker = new Tracker(
|
|
18667
|
+
const tracker = new Tracker(route2, ship, { apiKey: identity.apiKey });
|
|
18668
18668
|
let ticketHint = decision.actionable.top?.issue_id;
|
|
18669
|
-
let ticketRepo = repoTargetFor(
|
|
18669
|
+
let ticketRepo = repoTargetFor(route2, decision.actionable.top?.repo_id)?.name;
|
|
18670
18670
|
let workingId = decision.actionable.top?.id ?? null;
|
|
18671
18671
|
if (role === "dev" || role === "design" || role === "qa") {
|
|
18672
|
-
const seat =
|
|
18672
|
+
const seat = route2.resolved?.seats[role];
|
|
18673
18673
|
if (seat) {
|
|
18674
|
-
const
|
|
18674
|
+
const candidates2 = rankedCandidates(role, decision.selectionInput);
|
|
18675
18675
|
const contract = decision.selectionInput.contract ?? DEFAULT_CONTRACT;
|
|
18676
18676
|
const result = await resolveTopCandidate(
|
|
18677
18677
|
tracker,
|
|
18678
|
-
|
|
18678
|
+
candidates2,
|
|
18679
18679
|
seat,
|
|
18680
18680
|
claimStatuses(role, contract).approved,
|
|
18681
18681
|
claimStatuses(role, contract).building,
|
|
18682
|
-
(t) => dirForRepo(
|
|
18682
|
+
(t) => dirForRepo(route2, t.repo_id) !== null,
|
|
18683
18683
|
withHoldCheck(await tracker.claimAffinity(), decision.selectionInput),
|
|
18684
18684
|
(t) => {
|
|
18685
18685
|
const got = ctx.state.acquireTicket(t.id);
|
|
@@ -18710,7 +18710,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18710
18710
|
decision.selectionInput.ships?.rows ?? [],
|
|
18711
18711
|
ship.name,
|
|
18712
18712
|
(t) => {
|
|
18713
|
-
const d = dirForRepo(
|
|
18713
|
+
const d = dirForRepo(route2, t.repo_id);
|
|
18714
18714
|
return d ? originBranchSha(d, t, resolve2) : null;
|
|
18715
18715
|
}
|
|
18716
18716
|
);
|
|
@@ -18721,16 +18721,16 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18721
18721
|
if (result.claimed) {
|
|
18722
18722
|
const idx = decision.tickets.findIndex((t) => t.id === result.ticket.id);
|
|
18723
18723
|
if (idx >= 0) decision.tickets[idx] = result.ticket;
|
|
18724
|
-
writeDigest({ route, ship, state: ctx.state, emit }, decision, role, ship.stateDir);
|
|
18724
|
+
writeDigest({ route: route2, ship, state: ctx.state, emit }, decision, role, ship.stateDir);
|
|
18725
18725
|
}
|
|
18726
|
-
writeDigest({ route, ship, state: ctx.state, emit }, decision, role, ship.stateDir, runId, {
|
|
18726
|
+
writeDigest({ route: route2, ship, state: ctx.state, emit }, decision, role, ship.stateDir, runId, {
|
|
18727
18727
|
ticket: result.ticket,
|
|
18728
18728
|
inFlight: (id) => ctx.state.ticketInFlight(id)
|
|
18729
18729
|
});
|
|
18730
18730
|
hooks?.ready(true);
|
|
18731
18731
|
ticketHint = result.ticket.issue_id;
|
|
18732
18732
|
workingId = result.ticket.id;
|
|
18733
|
-
const target = repoTargetFor(
|
|
18733
|
+
const target = repoTargetFor(route2, result.ticket.repo_id);
|
|
18734
18734
|
ticketRepo = target?.name;
|
|
18735
18735
|
try {
|
|
18736
18736
|
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
@@ -18746,25 +18746,25 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18746
18746
|
}
|
|
18747
18747
|
const plan = planAgentRun({
|
|
18748
18748
|
role,
|
|
18749
|
-
route,
|
|
18749
|
+
route: route2,
|
|
18750
18750
|
ship,
|
|
18751
18751
|
stateDir: ship.stateDir,
|
|
18752
|
-
roster: rosterFor(decision,
|
|
18753
|
-
environment: await buildEnvironment(
|
|
18752
|
+
roster: rosterFor(decision, route2, role),
|
|
18753
|
+
environment: await buildEnvironment(route2, ship, ticketHint),
|
|
18754
18754
|
apiKey: identity.apiKey,
|
|
18755
18755
|
cycle: emit.cycle,
|
|
18756
18756
|
runId,
|
|
18757
18757
|
ticket: ticketHint,
|
|
18758
18758
|
repo: ticketRepo,
|
|
18759
18759
|
ticketRecordId: workingId,
|
|
18760
|
-
...personaPromptOptions(await fetchPersonaPrompt(
|
|
18761
|
-
skillOverrides: await fetchSkillOverrides(
|
|
18762
|
-
agentPersona: await fetchSeatPersona(
|
|
18763
|
-
resolvedAgentId: await resolveAgentId(
|
|
18760
|
+
...personaPromptOptions(await fetchPersonaPrompt(route2, role, { userAgent: ship.userAgent })),
|
|
18761
|
+
skillOverrides: await fetchSkillOverrides(route2, { userAgent: ship.userAgent }),
|
|
18762
|
+
agentPersona: await fetchSeatPersona(route2, role, { userAgent: ship.userAgent }),
|
|
18763
|
+
resolvedAgentId: await resolveAgentId(route2, role, { userAgent: ship.userAgent })
|
|
18764
18764
|
});
|
|
18765
18765
|
emit.enter("agent", role);
|
|
18766
|
-
emit.emit(`starting agent run for ${
|
|
18767
|
-
const memberId =
|
|
18766
|
+
emit.emit(`starting agent run for ${route2.route}`);
|
|
18767
|
+
const memberId = route2.resolved?.seats[role];
|
|
18768
18768
|
if (memberId) {
|
|
18769
18769
|
try {
|
|
18770
18770
|
await tracker.setCrewStatus(memberId, "working", workingId);
|
|
@@ -18773,7 +18773,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18773
18773
|
}
|
|
18774
18774
|
}
|
|
18775
18775
|
try {
|
|
18776
|
-
await tracker.beatEngaged(ship.name,
|
|
18776
|
+
await tracker.beatEngaged(ship.name, route2.route, workingId);
|
|
18777
18777
|
} catch (e) {
|
|
18778
18778
|
emit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
|
|
18779
18779
|
}
|
|
@@ -18792,12 +18792,12 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18792
18792
|
}
|
|
18793
18793
|
}
|
|
18794
18794
|
await spawnAgent(plan, emit, hintMoved);
|
|
18795
|
-
await guardRunHandoff(
|
|
18795
|
+
await guardRunHandoff(route2, ship, tracker, role, workingId, emit, runStart);
|
|
18796
18796
|
} finally {
|
|
18797
18797
|
await settleRunEnd({
|
|
18798
18798
|
tracker,
|
|
18799
18799
|
shipName: ship.name,
|
|
18800
|
-
route,
|
|
18800
|
+
route: route2,
|
|
18801
18801
|
role,
|
|
18802
18802
|
memberId,
|
|
18803
18803
|
ownTicketRecordId: workingId,
|
|
@@ -18806,31 +18806,441 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
|
|
|
18806
18806
|
});
|
|
18807
18807
|
}
|
|
18808
18808
|
}
|
|
18809
|
-
async function buildEnvironment(
|
|
18810
|
-
const tracker = new Tracker(
|
|
18809
|
+
async function buildEnvironment(route2, ship, ticket) {
|
|
18810
|
+
const tracker = new Tracker(route2, ship);
|
|
18811
18811
|
const contract = tracker.contract;
|
|
18812
|
-
const repos = reposOf(
|
|
18813
|
-
const id = repoIdForName(
|
|
18814
|
-
const o =
|
|
18812
|
+
const repos = reposOf(route2).map((t) => {
|
|
18813
|
+
const id = repoIdForName(route2, t.name);
|
|
18814
|
+
const o = route2.repoOverrides[t.name];
|
|
18815
18815
|
return {
|
|
18816
18816
|
name: t.name,
|
|
18817
18817
|
dir: t.dir,
|
|
18818
18818
|
id,
|
|
18819
18819
|
config: resolveRepoConfig(loadRepoConfig(t.dir), {
|
|
18820
|
-
hooks: { ...
|
|
18821
|
-
labels: { ...
|
|
18822
|
-
branch: { ...
|
|
18820
|
+
hooks: { ...route2.hooks, ...o?.hooks },
|
|
18821
|
+
labels: { ...route2.labels, ...o?.labels },
|
|
18822
|
+
branch: { ...route2.branch, ...o?.branch },
|
|
18823
18823
|
worktrees: { nameOverride: o?.worktreeName }
|
|
18824
18824
|
}, t.dir)
|
|
18825
18825
|
};
|
|
18826
18826
|
});
|
|
18827
18827
|
const authorship = await tracker.authorshipProbe().catch(() => null);
|
|
18828
18828
|
const attachments = await tracker.attachmentsProbe().catch(() => null);
|
|
18829
|
-
return renderEnvironment({ route, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name });
|
|
18829
|
+
return renderEnvironment({ route: route2, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name });
|
|
18830
|
+
}
|
|
18831
|
+
|
|
18832
|
+
// src/control-api.ts
|
|
18833
|
+
import { createHash as createHash8 } from "crypto";
|
|
18834
|
+
import {
|
|
18835
|
+
chmodSync as chmodSync3,
|
|
18836
|
+
closeSync as closeSync5,
|
|
18837
|
+
existsSync as existsSync38,
|
|
18838
|
+
fstatSync,
|
|
18839
|
+
openSync as openSync5,
|
|
18840
|
+
readSync as readSync3,
|
|
18841
|
+
readdirSync as readdirSync17,
|
|
18842
|
+
rmSync as rmSync17,
|
|
18843
|
+
statSync as statSync12,
|
|
18844
|
+
watch
|
|
18845
|
+
} from "fs";
|
|
18846
|
+
import { createConnection } from "net";
|
|
18847
|
+
import { createServer } from "http";
|
|
18848
|
+
import { basename as basename12, dirname as dirname21, join as join42 } from "path";
|
|
18849
|
+
import { execFile, spawn as spawn7 } from "child_process";
|
|
18850
|
+
var PAUSABLE = ["dev", "design", "qa"];
|
|
18851
|
+
var CONTROL_SOCKET_NAME = "control.sock";
|
|
18852
|
+
var controlSocketFor = (stateDir) => join42(stateDir, CONTROL_SOCKET_NAME);
|
|
18853
|
+
var MAX_SOCKET_PATH = 100;
|
|
18854
|
+
var MAX_TAIL_BYTES = 1024 * 1024;
|
|
18855
|
+
var MAX_WAIT_SECONDS = 30;
|
|
18856
|
+
var DEFAULT_TAIL_LINES = 100;
|
|
18857
|
+
var MAX_BODY_BYTES = 4096;
|
|
18858
|
+
var ControlAuth = class {
|
|
18859
|
+
cache = /* @__PURE__ */ new Map();
|
|
18860
|
+
o;
|
|
18861
|
+
constructor(o) {
|
|
18862
|
+
this.o = o;
|
|
18863
|
+
}
|
|
18864
|
+
async verify(token) {
|
|
18865
|
+
if (!token) return "unauthorized";
|
|
18866
|
+
for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
|
|
18867
|
+
const now = (this.o.now ?? Date.now)();
|
|
18868
|
+
const id = createHash8("sha256").update(token).digest("hex");
|
|
18869
|
+
const hit = this.cache.get(id);
|
|
18870
|
+
if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
|
|
18871
|
+
const verdict = await this.ask(token);
|
|
18872
|
+
if (verdict !== "unavailable") {
|
|
18873
|
+
this.cache.set(id, { verdict, at: now });
|
|
18874
|
+
if (this.cache.size > 256) {
|
|
18875
|
+
for (const [k, v] of this.cache) if (now - v.at >= (this.o.cacheMs ?? 6e4)) this.cache.delete(k);
|
|
18876
|
+
}
|
|
18877
|
+
}
|
|
18878
|
+
return verdict;
|
|
18879
|
+
}
|
|
18880
|
+
async ask(token) {
|
|
18881
|
+
const doFetch = this.o.fetch ?? fetch;
|
|
18882
|
+
const base = this.o.baseUrl.replace(/\/+$/, "");
|
|
18883
|
+
let forbidden = false;
|
|
18884
|
+
for (const ws of this.o.workspaceIds) {
|
|
18885
|
+
let res;
|
|
18886
|
+
try {
|
|
18887
|
+
res = await doFetch(`${base}/api/auth/me?workspaceId=${encodeURIComponent(ws)}`, {
|
|
18888
|
+
headers: { Authorization: `Bearer ${token}`, "User-Agent": this.o.userAgent ?? "Mozilla/5.0 CrewShipControl/1.0" },
|
|
18889
|
+
signal: AbortSignal.timeout(this.o.timeoutMs ?? 5e3)
|
|
18890
|
+
});
|
|
18891
|
+
} catch {
|
|
18892
|
+
return "unavailable";
|
|
18893
|
+
}
|
|
18894
|
+
if (res.status === 401) return "unauthorized";
|
|
18895
|
+
if (res.status === 403) {
|
|
18896
|
+
forbidden = true;
|
|
18897
|
+
continue;
|
|
18898
|
+
}
|
|
18899
|
+
if (res.status !== 200) return "unavailable";
|
|
18900
|
+
if (!(res.headers.get("content-type") ?? "").includes("application/json")) return "unavailable";
|
|
18901
|
+
return "ok";
|
|
18902
|
+
}
|
|
18903
|
+
return forbidden ? "forbidden" : "unauthorized";
|
|
18904
|
+
}
|
|
18905
|
+
};
|
|
18906
|
+
function encodeCursor(c) {
|
|
18907
|
+
return Buffer.from(JSON.stringify([c.dev, c.ino, c.off])).toString("base64url");
|
|
18908
|
+
}
|
|
18909
|
+
function decodeCursor(s) {
|
|
18910
|
+
try {
|
|
18911
|
+
const a = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
|
|
18912
|
+
if (Array.isArray(a) && a.length === 3 && a.every((n) => Number.isSafeInteger(n) && n >= 0)) {
|
|
18913
|
+
return { dev: a[0], ino: a[1], off: a[2] };
|
|
18914
|
+
}
|
|
18915
|
+
} catch {
|
|
18916
|
+
}
|
|
18917
|
+
return null;
|
|
18918
|
+
}
|
|
18919
|
+
function candidates(live, rotated) {
|
|
18920
|
+
const out = [live];
|
|
18921
|
+
if (!rotated) return out;
|
|
18922
|
+
try {
|
|
18923
|
+
for (const n of readdirSync17(dirname21(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join42(dirname21(live), n));
|
|
18924
|
+
} catch {
|
|
18925
|
+
}
|
|
18926
|
+
return out;
|
|
18927
|
+
}
|
|
18928
|
+
function readRange(path, from, max) {
|
|
18929
|
+
const fd = openSync5(path, "r");
|
|
18930
|
+
try {
|
|
18931
|
+
const size = fstatSync(fd).size;
|
|
18932
|
+
const len = Math.max(0, Math.min(size - from, max));
|
|
18933
|
+
const buf = Buffer.alloc(len);
|
|
18934
|
+
let got = 0;
|
|
18935
|
+
while (got < len) {
|
|
18936
|
+
const n = readSync3(fd, buf, got, len - got, from + got);
|
|
18937
|
+
if (n <= 0) break;
|
|
18938
|
+
got += n;
|
|
18939
|
+
}
|
|
18940
|
+
return { text: buf.subarray(0, got).toString("utf8"), size };
|
|
18941
|
+
} finally {
|
|
18942
|
+
closeSync5(fd);
|
|
18943
|
+
}
|
|
18944
|
+
}
|
|
18945
|
+
function lastLines2(path, n) {
|
|
18946
|
+
const st = statSync12(path);
|
|
18947
|
+
const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
|
|
18948
|
+
if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
|
|
18949
|
+
let start = st.size;
|
|
18950
|
+
let text = "";
|
|
18951
|
+
while (start > 0 && text.split("\n").length <= n + 1 && st.size - start < MAX_TAIL_BYTES) {
|
|
18952
|
+
const step = Math.min(64 * 1024, start);
|
|
18953
|
+
start -= step;
|
|
18954
|
+
text = readRange(path, start, step).text + text;
|
|
18955
|
+
}
|
|
18956
|
+
let lines = text.split("\n");
|
|
18957
|
+
if (start > 0) lines = lines.slice(1);
|
|
18958
|
+
let end = st.size;
|
|
18959
|
+
if (!text.endsWith("\n")) {
|
|
18960
|
+
const frag = lines.pop() ?? "";
|
|
18961
|
+
end -= Buffer.byteLength(frag);
|
|
18962
|
+
} else lines.pop();
|
|
18963
|
+
return { lines: lines.slice(-n).filter((l) => l.length), cursor: cursorAt(end), reset: false };
|
|
18964
|
+
}
|
|
18965
|
+
function readTail(live, opts) {
|
|
18966
|
+
const max = opts.maxBytes ?? MAX_TAIL_BYTES;
|
|
18967
|
+
if (!existsSync38(live)) {
|
|
18968
|
+
return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
|
|
18969
|
+
}
|
|
18970
|
+
const cur = opts.after ? decodeCursor(opts.after) : null;
|
|
18971
|
+
if (!opts.after) return lastLines2(live, opts.tail ?? DEFAULT_TAIL_LINES);
|
|
18972
|
+
let reset = false;
|
|
18973
|
+
let path = live;
|
|
18974
|
+
let off = 0;
|
|
18975
|
+
if (!cur) {
|
|
18976
|
+
reset = true;
|
|
18977
|
+
} else {
|
|
18978
|
+
const found = candidates(live, opts.rotated ?? false).find((p) => {
|
|
18979
|
+
try {
|
|
18980
|
+
const s = statSync12(p);
|
|
18981
|
+
return s.ino === cur.ino && s.dev === cur.dev;
|
|
18982
|
+
} catch {
|
|
18983
|
+
return false;
|
|
18984
|
+
}
|
|
18985
|
+
});
|
|
18986
|
+
if (found) {
|
|
18987
|
+
path = found;
|
|
18988
|
+
off = cur.off;
|
|
18989
|
+
} else reset = true;
|
|
18990
|
+
}
|
|
18991
|
+
let size = statSync12(path).size;
|
|
18992
|
+
if (off > size) {
|
|
18993
|
+
off = 0;
|
|
18994
|
+
reset = true;
|
|
18995
|
+
}
|
|
18996
|
+
if (off >= size && path !== live) {
|
|
18997
|
+
path = live;
|
|
18998
|
+
off = 0;
|
|
18999
|
+
size = statSync12(path).size;
|
|
19000
|
+
}
|
|
19001
|
+
const { text } = readRange(path, off, max);
|
|
19002
|
+
const cut = text.lastIndexOf("\n");
|
|
19003
|
+
const whole = cut < 0 ? "" : text.slice(0, cut + 1);
|
|
19004
|
+
const st = statSync12(path);
|
|
19005
|
+
return {
|
|
19006
|
+
lines: whole.split("\n").filter((l) => l.length),
|
|
19007
|
+
cursor: encodeCursor({ dev: st.dev, ino: st.ino, off: off + Buffer.byteLength(whole) }),
|
|
19008
|
+
reset
|
|
19009
|
+
};
|
|
19010
|
+
}
|
|
19011
|
+
function waitForChange(dir, ms3, signal) {
|
|
19012
|
+
return new Promise((resolve6) => {
|
|
19013
|
+
let w;
|
|
19014
|
+
const done = () => {
|
|
19015
|
+
clearTimeout(t);
|
|
19016
|
+
clearInterval(poll);
|
|
19017
|
+
signal.removeEventListener("abort", done);
|
|
19018
|
+
try {
|
|
19019
|
+
w?.close();
|
|
19020
|
+
} catch {
|
|
19021
|
+
}
|
|
19022
|
+
resolve6();
|
|
19023
|
+
};
|
|
19024
|
+
const t = setTimeout(done, ms3);
|
|
19025
|
+
const poll = setInterval(done, 500);
|
|
19026
|
+
signal.addEventListener("abort", done);
|
|
19027
|
+
try {
|
|
19028
|
+
w = watch(dir, done);
|
|
19029
|
+
w.on("error", () => {
|
|
19030
|
+
});
|
|
19031
|
+
} catch {
|
|
19032
|
+
}
|
|
19033
|
+
});
|
|
19034
|
+
}
|
|
19035
|
+
var reply = (status2, body) => ({ status: status2, body });
|
|
19036
|
+
var err = (status2, message) => reply(status2, { error: message });
|
|
19037
|
+
function pauseDoc(state) {
|
|
19038
|
+
return {
|
|
19039
|
+
crew: state.isPaused(),
|
|
19040
|
+
release: state.isReleasePaused(),
|
|
19041
|
+
roles: Object.fromEntries(PAUSABLE.map((r) => [r, state.isRolePaused(r)]))
|
|
19042
|
+
};
|
|
19043
|
+
}
|
|
19044
|
+
function describeStream(r) {
|
|
19045
|
+
return {
|
|
19046
|
+
run: basename12(r.streamPath).replace(/\.jsonl$/, ""),
|
|
19047
|
+
pid: r.pid,
|
|
19048
|
+
ticket: r.ticket,
|
|
19049
|
+
role: r.role,
|
|
19050
|
+
model: r.model,
|
|
19051
|
+
startedAt: r.spawnedAt,
|
|
19052
|
+
// No run-time limit is enforced today; the field is here so a reader needs no change when one is.
|
|
19053
|
+
deadline: null
|
|
19054
|
+
};
|
|
19055
|
+
}
|
|
19056
|
+
var RUN_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
19057
|
+
async function readJsonBody(req) {
|
|
19058
|
+
const chunks = [];
|
|
19059
|
+
let n = 0;
|
|
19060
|
+
for await (const c of req) {
|
|
19061
|
+
n += c.length;
|
|
19062
|
+
if (n > MAX_BODY_BYTES) return null;
|
|
19063
|
+
chunks.push(c);
|
|
19064
|
+
}
|
|
19065
|
+
if (!n) return {};
|
|
19066
|
+
try {
|
|
19067
|
+
const v = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
19068
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
19069
|
+
} catch {
|
|
19070
|
+
return null;
|
|
19071
|
+
}
|
|
19072
|
+
}
|
|
19073
|
+
async function tailReply(path, q, rotated, abort) {
|
|
19074
|
+
const after = q.get("after") ?? void 0;
|
|
19075
|
+
const tail3 = q.has("tail") ? Number(q.get("tail")) : void 0;
|
|
19076
|
+
if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return err(400, "tail must be an integer from 0 to 1000");
|
|
19077
|
+
const wait = q.has("wait") ? Number(q.get("wait")) : 0;
|
|
19078
|
+
if (!(Number.isFinite(wait) && wait >= 0)) return err(400, "wait must be a number of seconds");
|
|
19079
|
+
if (after !== void 0 && !decodeCursor(after)) return err(400, "after is not a cursor this API issued");
|
|
19080
|
+
const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
|
|
19081
|
+
let r = readTail(path, { after, tail: tail3, rotated });
|
|
19082
|
+
while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
|
|
19083
|
+
await waitForChange(dirname21(path), deadline - Date.now(), abort);
|
|
19084
|
+
r = readTail(path, { after: r.cursor, rotated });
|
|
19085
|
+
}
|
|
19086
|
+
return reply(200, r);
|
|
19087
|
+
}
|
|
19088
|
+
async function route(deps, method, url, req, abort) {
|
|
19089
|
+
const p = url.pathname.replace(/\/+$/, "") || "/";
|
|
19090
|
+
const get2 = method === "GET";
|
|
19091
|
+
const post3 = method === "POST";
|
|
19092
|
+
if (p === "/status" && get2) {
|
|
19093
|
+
const doc = await deps.status();
|
|
19094
|
+
if (!doc) return err(502, "could not produce the status document");
|
|
19095
|
+
return reply(200, {
|
|
19096
|
+
...doc,
|
|
19097
|
+
daemon: {
|
|
19098
|
+
alive: true,
|
|
19099
|
+
pid: process.pid,
|
|
19100
|
+
startedAt: deps.startedAt ?? readDaemonStarted(deps.stateDir),
|
|
19101
|
+
uptimeSeconds: Math.round(process.uptime())
|
|
19102
|
+
},
|
|
19103
|
+
pause: pauseDoc(deps.state)
|
|
19104
|
+
});
|
|
19105
|
+
}
|
|
19106
|
+
if (p === "/events" && get2) return tailReply(eventFileFor(deps.stateDir), url.searchParams, true, abort);
|
|
19107
|
+
if (p === "/streams" && get2) return reply(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
|
|
19108
|
+
if (p.startsWith("/streams/") && get2) {
|
|
19109
|
+
const run2 = decodeURIComponent(p.slice("/streams/".length));
|
|
19110
|
+
if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err(400, "not a run name");
|
|
19111
|
+
const file = join42(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
|
|
19112
|
+
if (!existsSync38(file)) return err(404, "no such run");
|
|
19113
|
+
return tailReply(file, url.searchParams, false, abort);
|
|
19114
|
+
}
|
|
19115
|
+
if ((p === "/pause" || p === "/resume") && post3) {
|
|
19116
|
+
const body = await readJsonBody(req);
|
|
19117
|
+
if (!body) return err(400, "body must be a small JSON object");
|
|
19118
|
+
const role = body.role;
|
|
19119
|
+
if (role !== void 0 && !PAUSABLE.includes(role)) return err(400, `role must be one of ${PAUSABLE.join(", ")}`);
|
|
19120
|
+
if (p === "/pause") deps.state.pause(role);
|
|
19121
|
+
else deps.state.resume(role);
|
|
19122
|
+
return reply(200, { ok: true, pause: pauseDoc(deps.state) });
|
|
19123
|
+
}
|
|
19124
|
+
if ((p === "/release/pause" || p === "/release/resume") && post3) {
|
|
19125
|
+
deps.state.setReleasePaused(p === "/release/pause");
|
|
19126
|
+
return reply(200, { ok: true, pause: pauseDoc(deps.state) });
|
|
19127
|
+
}
|
|
19128
|
+
const sched = /^\/scheduler\/(start|stop|restart)$/.exec(p);
|
|
19129
|
+
if (sched && post3) {
|
|
19130
|
+
const r = deps.scheduler(sched[1]);
|
|
19131
|
+
return r.accepted ? reply(202, { accepted: true, detail: r.detail }) : err(409, r.detail);
|
|
19132
|
+
}
|
|
19133
|
+
if (p === "/doctor" && get2) {
|
|
19134
|
+
const doc = await deps.doctor();
|
|
19135
|
+
return doc ? reply(200, doc) : err(502, "could not produce the doctor report");
|
|
19136
|
+
}
|
|
19137
|
+
const known = ["/status", "/events", "/streams", "/pause", "/resume", "/release/pause", "/release/resume", "/doctor"];
|
|
19138
|
+
if (known.includes(p) || p.startsWith("/streams/") || sched) return err(405, "method not allowed");
|
|
19139
|
+
return err(404, "not found");
|
|
19140
|
+
}
|
|
19141
|
+
function send(res, r) {
|
|
19142
|
+
const body = JSON.stringify(r.body);
|
|
19143
|
+
res.writeHead(r.status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), "Cache-Control": "no-store" });
|
|
19144
|
+
res.end(body);
|
|
19145
|
+
}
|
|
19146
|
+
async function socketIsStale(path) {
|
|
19147
|
+
if (!existsSync38(path)) return false;
|
|
19148
|
+
return new Promise((resolve6) => {
|
|
19149
|
+
const c = createConnection(path);
|
|
19150
|
+
c.once("connect", () => {
|
|
19151
|
+
c.destroy();
|
|
19152
|
+
resolve6(false);
|
|
19153
|
+
});
|
|
19154
|
+
c.once("error", () => resolve6(true));
|
|
19155
|
+
});
|
|
19156
|
+
}
|
|
19157
|
+
async function startControlServer(deps) {
|
|
19158
|
+
const path = controlSocketFor(deps.stateDir);
|
|
19159
|
+
const log = deps.log ?? (() => {
|
|
19160
|
+
});
|
|
19161
|
+
if (Buffer.byteLength(path) > MAX_SOCKET_PATH) {
|
|
19162
|
+
log(`control API not started: socket path is too long for a unix socket (${path})`);
|
|
19163
|
+
return null;
|
|
19164
|
+
}
|
|
19165
|
+
if (await socketIsStale(path)) rmSync17(path, { force: true });
|
|
19166
|
+
else if (existsSync38(path)) {
|
|
19167
|
+
log(`control API not started: ${path} is in use by another process`);
|
|
19168
|
+
return null;
|
|
19169
|
+
}
|
|
19170
|
+
const server = createServer((req, res) => {
|
|
19171
|
+
const abort = new AbortController();
|
|
19172
|
+
res.on("close", () => abort.abort());
|
|
19173
|
+
void (async () => {
|
|
19174
|
+
try {
|
|
19175
|
+
const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
|
|
19176
|
+
const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
|
|
19177
|
+
if (verdict === "unauthorized") return send(res, err(401, "invalid or revoked key"));
|
|
19178
|
+
if (verdict === "forbidden") return send(res, err(403, "this key does not belong to a workspace this ship serves"));
|
|
19179
|
+
if (verdict === "unavailable") return send(res, err(503, "could not verify the key right now"));
|
|
19180
|
+
send(res, await route(deps, req.method ?? "GET", new URL(req.url ?? "/", "http://ship"), req, abort.signal));
|
|
19181
|
+
} catch (e) {
|
|
19182
|
+
log(`control API request failed: ${e.message}`);
|
|
19183
|
+
if (!res.headersSent) send(res, err(500, "internal error"));
|
|
19184
|
+
else res.end();
|
|
19185
|
+
}
|
|
19186
|
+
})();
|
|
19187
|
+
});
|
|
19188
|
+
return new Promise((resolve6) => {
|
|
19189
|
+
server.once("error", (e) => {
|
|
19190
|
+
log(`control API not started: ${e.message}`);
|
|
19191
|
+
resolve6(null);
|
|
19192
|
+
});
|
|
19193
|
+
server.listen(path, () => {
|
|
19194
|
+
try {
|
|
19195
|
+
chmodSync3(path, 384);
|
|
19196
|
+
} catch {
|
|
19197
|
+
}
|
|
19198
|
+
server.on("error", (e) => log(`control API error: ${e.message}`));
|
|
19199
|
+
resolve6({
|
|
19200
|
+
server,
|
|
19201
|
+
path,
|
|
19202
|
+
close: () => new Promise((done) => {
|
|
19203
|
+
server.close(() => {
|
|
19204
|
+
rmSync17(path, { force: true });
|
|
19205
|
+
done();
|
|
19206
|
+
});
|
|
19207
|
+
})
|
|
19208
|
+
});
|
|
19209
|
+
});
|
|
19210
|
+
});
|
|
19211
|
+
}
|
|
19212
|
+
var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
19213
|
+
function runDetached(commands, spawnFn = spawn7) {
|
|
19214
|
+
const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
|
|
19215
|
+
const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
|
|
19216
|
+
child.on("error", () => {
|
|
19217
|
+
});
|
|
19218
|
+
child.unref();
|
|
19219
|
+
}
|
|
19220
|
+
function schedulerHandler(control, run2 = runDetached) {
|
|
19221
|
+
return (action) => {
|
|
19222
|
+
const { startCommand: start, stopCommand: stop } = control;
|
|
19223
|
+
if (!start || !stop) return { accepted: false, detail: "this host has no service manager for the daemon (cron or none)" };
|
|
19224
|
+
const commands = action === "start" ? [start] : action === "stop" ? [stop] : [stop, start];
|
|
19225
|
+
run2(commands);
|
|
19226
|
+
return { accepted: true, detail: `${action} requested` };
|
|
19227
|
+
};
|
|
19228
|
+
}
|
|
19229
|
+
function crewJson(args, timeoutMs = 6e4) {
|
|
19230
|
+
return new Promise((resolve6) => {
|
|
19231
|
+
execFile(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
|
|
19232
|
+
try {
|
|
19233
|
+
const v = JSON.parse(String(stdout));
|
|
19234
|
+
resolve6(v && typeof v === "object" && !Array.isArray(v) ? v : null);
|
|
19235
|
+
} catch {
|
|
19236
|
+
resolve6(null);
|
|
19237
|
+
}
|
|
19238
|
+
});
|
|
19239
|
+
});
|
|
18830
19240
|
}
|
|
18831
19241
|
|
|
18832
19242
|
// src/cli.ts
|
|
18833
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
19243
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname22(process.execPath) : resolve5(dirname22(fileURLToPath(import.meta.url)), "..");
|
|
18834
19244
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
18835
19245
|
var CYCLE_SECONDS2 = 120;
|
|
18836
19246
|
var STALE_CYCLES = 3;
|
|
@@ -19155,9 +19565,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
19155
19565
|
runSyncDaemonFromEnv();
|
|
19156
19566
|
} else {
|
|
19157
19567
|
let requireArmed = function(what) {
|
|
19158
|
-
if (dryRun ||
|
|
19568
|
+
if (dryRun || route2.enabled) return;
|
|
19159
19569
|
process.stderr.write(
|
|
19160
|
-
`crew: route "${
|
|
19570
|
+
`crew: route "${route2.route}" is not enabled in ${cfg.configFile} \u2014 refusing to ${what}.
|
|
19161
19571
|
Add --dry-run to see what it would do.
|
|
19162
19572
|
`
|
|
19163
19573
|
);
|
|
@@ -19268,7 +19678,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19268
19678
|
} else {
|
|
19269
19679
|
const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
|
|
19270
19680
|
const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
|
|
19271
|
-
if (configless && !
|
|
19681
|
+
if (configless && !existsSync39(findConfigFile(CREW_HOME))) {
|
|
19272
19682
|
cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
|
|
19273
19683
|
} else {
|
|
19274
19684
|
cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
|
|
@@ -19318,9 +19728,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
19318
19728
|
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
19319
19729
|
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
19320
19730
|
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
19321
|
-
let
|
|
19731
|
+
let route2;
|
|
19322
19732
|
try {
|
|
19323
|
-
|
|
19733
|
+
route2 = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
19324
19734
|
} catch (e) {
|
|
19325
19735
|
if (e instanceof ConfigError) {
|
|
19326
19736
|
process.stderr.write(`crew: ${e.message}
|
|
@@ -19335,7 +19745,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19335
19745
|
const force = flag("force");
|
|
19336
19746
|
const EXCLUSIVE = { passengers: "passengers" };
|
|
19337
19747
|
const emit = new Emitter({
|
|
19338
|
-
route:
|
|
19748
|
+
route: route2?.route ?? "",
|
|
19339
19749
|
eventFile: dryRun ? void 0 : eventFileFor(cfg.ship.stateDir),
|
|
19340
19750
|
rotateBytes: cfg.ship.eventsRotateBytes,
|
|
19341
19751
|
logFile: dryRun ? void 0 : cfg.ship.logFile,
|
|
@@ -19434,18 +19844,18 @@ if (command === "passenger-sync-daemon") {
|
|
|
19434
19844
|
if (releaseRefused()) return;
|
|
19435
19845
|
precheckTickets.clear();
|
|
19436
19846
|
const only = value("repo");
|
|
19437
|
-
const targets = reposOf(
|
|
19847
|
+
const targets = reposOf(route2);
|
|
19438
19848
|
const chosen = only ? targets.filter((t) => t.name === only) : targets;
|
|
19439
19849
|
if (only && chosen.length === 0) {
|
|
19440
19850
|
process.stderr.write(
|
|
19441
|
-
`crew: no repo named "${only}" on route "${
|
|
19851
|
+
`crew: no repo named "${only}" on route "${route2.route}" (have: ${targets.map((t) => t.name).join(", ")})
|
|
19442
19852
|
`
|
|
19443
19853
|
);
|
|
19444
19854
|
process.exit(2);
|
|
19445
19855
|
}
|
|
19446
19856
|
const summaries = [];
|
|
19447
19857
|
if (!dryRun) state.releaseHeartbeat().touch();
|
|
19448
|
-
for (const t of chosen) summaries.push({ ...await releasePhase(
|
|
19858
|
+
for (const t of chosen) summaries.push({ ...await releasePhase(route2, t, opts), route: route2.route });
|
|
19449
19859
|
emitReleaseSummary(emit, summaries);
|
|
19450
19860
|
if (!dryRun) state.releaseHeartbeat().touch();
|
|
19451
19861
|
try {
|
|
@@ -19533,8 +19943,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
19533
19943
|
}
|
|
19534
19944
|
if (host === "linux" && hasSystemd) {
|
|
19535
19945
|
const label = labelFor(CREW_HOME, "run");
|
|
19536
|
-
const staleTimer =
|
|
19537
|
-
if (
|
|
19946
|
+
const staleTimer = join43(homedir10(), ".config", "systemd", "user", `${label}.timer`);
|
|
19947
|
+
if (existsSync39(staleTimer)) {
|
|
19538
19948
|
process.stdout.write(
|
|
19539
19949
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
19540
19950
|
`
|
|
@@ -19561,7 +19971,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19561
19971
|
const tracker = new Tracker(c, cfg.ship);
|
|
19562
19972
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
19563
19973
|
for (const r of await resolvedRepos(c)) {
|
|
19564
|
-
if (!
|
|
19974
|
+
if (!existsSync39(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
19565
19975
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
19566
19976
|
readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
19567
19977
|
}
|
|
@@ -19579,7 +19989,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
19579
19989
|
}
|
|
19580
19990
|
async function doctorFix(o) {
|
|
19581
19991
|
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))));
|
|
19582
|
-
const unitExists = (p) =>
|
|
19992
|
+
const unitExists = (p) => existsSync39(p);
|
|
19583
19993
|
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
19584
19994
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
19585
19995
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -20142,7 +20552,7 @@ ${cu.tail.trimEnd()}
|
|
|
20142
20552
|
case "run": {
|
|
20143
20553
|
const skipInlineRelease = flag("no-release");
|
|
20144
20554
|
if (!dryRun) {
|
|
20145
|
-
for (const r of fleetWide ? cfg.routes : [
|
|
20555
|
+
for (const r of fleetWide ? cfg.routes : [route2]) {
|
|
20146
20556
|
await reconcileRouteShipName({ route: r, ship: cfg.ship, configFile: cfg.configFile, emit: emit.forRoute(r.route) });
|
|
20147
20557
|
}
|
|
20148
20558
|
}
|
|
@@ -20189,8 +20599,8 @@ ${cu.tail.trimEnd()}
|
|
|
20189
20599
|
`swept ${r.parked} parked, ${r.restored} restored${r.failed ? `, ${r.failed} failed` : ""}${r.contended ? `, ${r.contended} contended` : ""} (${e.route.route})`,
|
|
20190
20600
|
{ step: "sweep", data: r }
|
|
20191
20601
|
);
|
|
20192
|
-
} catch (
|
|
20193
|
-
remit.warn(`could not sweep ${e.route.route}: ${
|
|
20602
|
+
} catch (err2) {
|
|
20603
|
+
remit.warn(`could not sweep ${e.route.route}: ${err2.message}`, { step: "sweep" });
|
|
20194
20604
|
}
|
|
20195
20605
|
}
|
|
20196
20606
|
process.stdout.write(renderFleet(fleet, state));
|
|
@@ -20256,11 +20666,11 @@ ${cu.tail.trimEnd()}
|
|
|
20256
20666
|
if (w.role === "dev" || w.role === "design" || w.role === "qa") {
|
|
20257
20667
|
const seat = w.route.resolved?.seats[w.role];
|
|
20258
20668
|
if (seat) {
|
|
20259
|
-
const
|
|
20669
|
+
const candidates2 = rankedCandidates(w.role, w.decision.selectionInput);
|
|
20260
20670
|
const contract = w.decision.selectionInput.contract ?? DEFAULT_CONTRACT;
|
|
20261
20671
|
const result = await resolveTopCandidate(
|
|
20262
20672
|
fleetTracker,
|
|
20263
|
-
|
|
20673
|
+
candidates2,
|
|
20264
20674
|
seat,
|
|
20265
20675
|
claimStatuses(w.role, contract).approved,
|
|
20266
20676
|
claimStatuses(w.role, contract).building,
|
|
@@ -20382,9 +20792,9 @@ ${cu.tail.trimEnd()}
|
|
|
20382
20792
|
if (!skipInlineRelease) await releaseFleet();
|
|
20383
20793
|
break;
|
|
20384
20794
|
}
|
|
20385
|
-
if (!await anyRepoServable(
|
|
20795
|
+
if (!await anyRepoServable(route2)) {
|
|
20386
20796
|
emit.error(
|
|
20387
|
-
`refusing this route \u2014 this ship is ${cfg.ship.platform}, but ${await explainUnservable(
|
|
20797
|
+
`refusing this route \u2014 this ship is ${cfg.ship.platform}, but ${await explainUnservable(route2)}`
|
|
20388
20798
|
);
|
|
20389
20799
|
process.exit(1);
|
|
20390
20800
|
}
|
|
@@ -20392,10 +20802,10 @@ ${cu.tail.trimEnd()}
|
|
|
20392
20802
|
emit.emit("the crew is paused");
|
|
20393
20803
|
break;
|
|
20394
20804
|
}
|
|
20395
|
-
const decision = await decideCycle({ route, ship: cfg.ship, state, emit });
|
|
20805
|
+
const decision = await decideCycle({ route: route2, ship: cfg.ship, state, emit });
|
|
20396
20806
|
if (!dryRun) {
|
|
20397
|
-
state.attention(
|
|
20398
|
-
state.unroutable(
|
|
20807
|
+
state.attention(route2.route).persist(decision.attention);
|
|
20808
|
+
state.unroutable(route2.route).persist(decision.unroutable);
|
|
20399
20809
|
}
|
|
20400
20810
|
if (decision.sweep.length) {
|
|
20401
20811
|
if (dryRun) {
|
|
@@ -20406,8 +20816,8 @@ ${cu.tail.trimEnd()}
|
|
|
20406
20816
|
});
|
|
20407
20817
|
}
|
|
20408
20818
|
} else {
|
|
20409
|
-
const seat =
|
|
20410
|
-
const r = await applySweep(new Tracker(
|
|
20819
|
+
const seat = route2.resolved?.seats.qa ?? route2.resolved?.seats.dev;
|
|
20820
|
+
const r = await applySweep(new Tracker(route2, cfg.ship), decision.sweep, seat ?? "", emit);
|
|
20411
20821
|
emit.emit(
|
|
20412
20822
|
`swept ${r.parked} parked, ${r.restored} restored${r.failed ? `, ${r.failed} failed` : ""}${r.contended ? `, ${r.contended} contended` : ""}`,
|
|
20413
20823
|
{ step: "sweep", data: r }
|
|
@@ -20418,19 +20828,19 @@ ${cu.tail.trimEnd()}
|
|
|
20418
20828
|
if (dryRun) {
|
|
20419
20829
|
for (const s of decision.epicSync) emit.emit(`would move ${describeEpicStep(s)}`, { step: "sweep" });
|
|
20420
20830
|
} else {
|
|
20421
|
-
const tracker = new Tracker(
|
|
20831
|
+
const tracker = new Tracker(route2, cfg.ship);
|
|
20422
20832
|
await applyEpicSync(tracker, decision.epicSync, tracker.contract, emit);
|
|
20423
20833
|
}
|
|
20424
20834
|
}
|
|
20425
20835
|
if (!dryRun && decision.holdReleases?.length) {
|
|
20426
|
-
const tracker = new Tracker(
|
|
20836
|
+
const tracker = new Tracker(route2, cfg.ship);
|
|
20427
20837
|
await applyHoldReleases(tracker, decision.holdReleases, tracker.contract, emit);
|
|
20428
20838
|
}
|
|
20429
20839
|
const role = value("role") ?? decision.selection.selected;
|
|
20430
20840
|
if (command === "poll") break;
|
|
20431
20841
|
if (!dryRun) {
|
|
20432
20842
|
try {
|
|
20433
|
-
await new Tracker(
|
|
20843
|
+
await new Tracker(route2, cfg.ship).beatShip(cfg.ship.name);
|
|
20434
20844
|
} catch (e) {
|
|
20435
20845
|
emit.warn(`could not beat ship: ${e.message}`, { step: "poll" });
|
|
20436
20846
|
}
|
|
@@ -20445,7 +20855,7 @@ ${cu.tail.trimEnd()}
|
|
|
20445
20855
|
const explicitRole = value("role") !== void 0;
|
|
20446
20856
|
const ran = [];
|
|
20447
20857
|
let current = role;
|
|
20448
|
-
const tracker2 = new Tracker(
|
|
20858
|
+
const tracker2 = new Tracker(route2, cfg.ship);
|
|
20449
20859
|
while (current) {
|
|
20450
20860
|
if (!dryRun) {
|
|
20451
20861
|
const got = state.acquireRun(current, cfg.ship.maxConcurrentAgents, cfg.ship.roleConcurrency[current] ?? 1);
|
|
@@ -20462,20 +20872,20 @@ ${cu.tail.trimEnd()}
|
|
|
20462
20872
|
lock = got;
|
|
20463
20873
|
}
|
|
20464
20874
|
let ticketHint = decision.actionable.top?.issue_id;
|
|
20465
|
-
let ticketRepo = repoTargetFor(
|
|
20875
|
+
let ticketRepo = repoTargetFor(route2, decision.actionable.top?.repo_id)?.name;
|
|
20466
20876
|
let workingId = decision.actionable.top?.id ?? null;
|
|
20467
20877
|
if (!dryRun && (current === "dev" || current === "design" || current === "qa")) {
|
|
20468
|
-
const seat =
|
|
20878
|
+
const seat = route2.resolved?.seats[current];
|
|
20469
20879
|
if (seat) {
|
|
20470
|
-
const
|
|
20880
|
+
const candidates2 = rankedCandidates(current, decision.selectionInput);
|
|
20471
20881
|
const contract = decision.selectionInput.contract ?? DEFAULT_CONTRACT;
|
|
20472
20882
|
const result = await resolveTopCandidate(
|
|
20473
20883
|
tracker2,
|
|
20474
|
-
|
|
20884
|
+
candidates2,
|
|
20475
20885
|
seat,
|
|
20476
20886
|
claimStatuses(current, contract).approved,
|
|
20477
20887
|
claimStatuses(current, contract).building,
|
|
20478
|
-
(t) => dirForRepo(
|
|
20888
|
+
(t) => dirForRepo(route2, t.repo_id) !== null,
|
|
20479
20889
|
withHoldCheck(await tracker2.claimAffinity(), decision.selectionInput),
|
|
20480
20890
|
(t) => {
|
|
20481
20891
|
const got = state.acquireTicket(t.id);
|
|
@@ -20513,7 +20923,7 @@ ${cu.tail.trimEnd()}
|
|
|
20513
20923
|
decision.selectionInput.ships?.rows ?? [],
|
|
20514
20924
|
cfg.ship.name,
|
|
20515
20925
|
(t) => {
|
|
20516
|
-
const d = dirForRepo(
|
|
20926
|
+
const d = dirForRepo(route2, t.repo_id);
|
|
20517
20927
|
return d ? originBranchSha(d, t, resolve2) : null;
|
|
20518
20928
|
}
|
|
20519
20929
|
);
|
|
@@ -20531,11 +20941,11 @@ ${cu.tail.trimEnd()}
|
|
|
20531
20941
|
if (result.claimed) {
|
|
20532
20942
|
const idx = decision.tickets.findIndex((t) => t.id === result.ticket.id);
|
|
20533
20943
|
if (idx >= 0) decision.tickets[idx] = result.ticket;
|
|
20534
|
-
writeDigest({ route, ship: cfg.ship, state, emit }, decision, current, cfg.ship.stateDir);
|
|
20944
|
+
writeDigest({ route: route2, ship: cfg.ship, state, emit }, decision, current, cfg.ship.stateDir);
|
|
20535
20945
|
}
|
|
20536
20946
|
ticketHint = result.ticket.issue_id;
|
|
20537
20947
|
workingId = result.ticket.id;
|
|
20538
|
-
const target = repoTargetFor(
|
|
20948
|
+
const target = repoTargetFor(route2, result.ticket.repo_id);
|
|
20539
20949
|
ticketRepo = target?.name;
|
|
20540
20950
|
try {
|
|
20541
20951
|
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
@@ -20556,19 +20966,19 @@ ${cu.tail.trimEnd()}
|
|
|
20556
20966
|
}
|
|
20557
20967
|
const plan = planAgentRun({
|
|
20558
20968
|
role: current,
|
|
20559
|
-
route,
|
|
20969
|
+
route: route2,
|
|
20560
20970
|
ship: cfg.ship,
|
|
20561
20971
|
stateDir: cfg.ship.stateDir,
|
|
20562
|
-
roster: rosterFor(decision,
|
|
20563
|
-
environment: await environmentFor(
|
|
20564
|
-
apiKey: resolveApiKey(
|
|
20972
|
+
roster: rosterFor(decision, route2, current),
|
|
20973
|
+
environment: await environmentFor(route2, ticketHint),
|
|
20974
|
+
apiKey: resolveApiKey(route2),
|
|
20565
20975
|
cycle: emit.cycle,
|
|
20566
20976
|
ticket: ticketHint,
|
|
20567
20977
|
repo: ticketRepo,
|
|
20568
|
-
...personaPromptOptions(await fetchPersonaPrompt(
|
|
20569
|
-
skillOverrides: await fetchSkillOverrides(
|
|
20570
|
-
agentPersona: await fetchSeatPersona(
|
|
20571
|
-
resolvedAgentId: await resolveAgentId(
|
|
20978
|
+
...personaPromptOptions(await fetchPersonaPrompt(route2, current, { userAgent: cfg.ship.userAgent })),
|
|
20979
|
+
skillOverrides: await fetchSkillOverrides(route2, { userAgent: cfg.ship.userAgent }),
|
|
20980
|
+
agentPersona: await fetchSeatPersona(route2, current, { userAgent: cfg.ship.userAgent }),
|
|
20981
|
+
resolvedAgentId: await resolveAgentId(route2, current, { userAgent: cfg.ship.userAgent })
|
|
20572
20982
|
});
|
|
20573
20983
|
if (dryRun) {
|
|
20574
20984
|
process.stdout.write(`${describePlan(plan)}
|
|
@@ -20576,7 +20986,7 @@ ${cu.tail.trimEnd()}
|
|
|
20576
20986
|
if (workingId && (current === "dev" || current === "design")) {
|
|
20577
20987
|
try {
|
|
20578
20988
|
const t = await tracker2.ticket(workingId);
|
|
20579
|
-
const r = repoConfigFor(
|
|
20989
|
+
const r = repoConfigFor(route2, t);
|
|
20580
20990
|
const would = r && describeHandoffPush(r.target.dir, r.cfg, t, current);
|
|
20581
20991
|
if (would) process.stdout.write(`${would}
|
|
20582
20992
|
`);
|
|
@@ -20588,7 +20998,7 @@ ${cu.tail.trimEnd()}
|
|
|
20588
20998
|
const before = snapshot(await tracker2.openTickets());
|
|
20589
20999
|
emit.enter("agent", current);
|
|
20590
21000
|
emit.emit("starting agent run");
|
|
20591
|
-
const memberId =
|
|
21001
|
+
const memberId = route2.resolved?.seats[current];
|
|
20592
21002
|
if (memberId) {
|
|
20593
21003
|
try {
|
|
20594
21004
|
await tracker2.setCrewStatus(memberId, "working", workingId);
|
|
@@ -20597,20 +21007,20 @@ ${cu.tail.trimEnd()}
|
|
|
20597
21007
|
}
|
|
20598
21008
|
}
|
|
20599
21009
|
try {
|
|
20600
|
-
await tracker2.beatEngaged(cfg.ship.name,
|
|
21010
|
+
await tracker2.beatEngaged(cfg.ship.name, route2.route, workingId);
|
|
20601
21011
|
} catch (e) {
|
|
20602
21012
|
emit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
|
|
20603
21013
|
}
|
|
20604
21014
|
try {
|
|
20605
21015
|
const runStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
20606
21016
|
await spawnAgent(plan, emit);
|
|
20607
|
-
await guardRunHandoff(
|
|
21017
|
+
await guardRunHandoff(route2, cfg.ship, tracker2, current, workingId, emit, runStart);
|
|
20608
21018
|
} finally {
|
|
20609
21019
|
dropLock();
|
|
20610
21020
|
await settleRunEnd({
|
|
20611
21021
|
tracker: tracker2,
|
|
20612
21022
|
shipName: cfg.ship.name,
|
|
20613
|
-
route,
|
|
21023
|
+
route: route2,
|
|
20614
21024
|
role: current,
|
|
20615
21025
|
memberId,
|
|
20616
21026
|
ownTicketRecordId: workingId,
|
|
@@ -20693,21 +21103,43 @@ ${cu.tail.trimEnd()}
|
|
|
20693
21103
|
process.stderr.write("crew: daemon does not support --dry-run \u2014 it only ever performs real cycles.\n");
|
|
20694
21104
|
process.exit(2);
|
|
20695
21105
|
}
|
|
20696
|
-
if (!await anyRepoServable(
|
|
21106
|
+
if (!await anyRepoServable(route2)) {
|
|
20697
21107
|
emit.error(
|
|
20698
|
-
`refusing this route \u2014 this ship is ${cfg.ship.platform}, but ${await explainUnservable(
|
|
21108
|
+
`refusing this route \u2014 this ship is ${cfg.ship.platform}, but ${await explainUnservable(route2)}`
|
|
20699
21109
|
);
|
|
20700
21110
|
process.exit(1);
|
|
20701
21111
|
}
|
|
20702
21112
|
requireArmed("run the daemon loop");
|
|
20703
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
21113
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join43(CREW_HOME, "src"), join43(CREW_HOME, "dist"), join43(CREW_HOME, "bin")];
|
|
20704
21114
|
const configPaths = daemonConfigPaths(cfg);
|
|
20705
21115
|
watchPaths.push(...configPaths);
|
|
20706
21116
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
20707
21117
|
const spawnBreaker = new SpawnBreaker();
|
|
21118
|
+
const controlRoutes = cfg.routes.filter((r) => r.resolved?.workspaceId);
|
|
21119
|
+
const controlServer = await startControlServer({
|
|
21120
|
+
stateDir: cfg.ship.stateDir,
|
|
21121
|
+
state,
|
|
21122
|
+
auth: new ControlAuth({
|
|
21123
|
+
baseUrl: route2.baseUrl || DEFAULT_BASE_URL,
|
|
21124
|
+
workspaceIds: [...new Set(controlRoutes.map((r) => r.resolved.workspaceId))],
|
|
21125
|
+
shipKeys: () => cfg.routes.flatMap((r) => {
|
|
21126
|
+
try {
|
|
21127
|
+
return [resolveApiKey(r)];
|
|
21128
|
+
} catch {
|
|
21129
|
+
return [];
|
|
21130
|
+
}
|
|
21131
|
+
}),
|
|
21132
|
+
userAgent: cfg.ship.userAgent
|
|
21133
|
+
}),
|
|
21134
|
+
status: () => crewJson(["status", route2.route, "--json"]),
|
|
21135
|
+
doctor: () => crewJson(["doctor", route2.route, "--json", "--offline"]),
|
|
21136
|
+
scheduler: schedulerHandler(planDaemonControl(CREW_HOME, hostPlatform(), detectSystemd())),
|
|
21137
|
+
log: (m) => emit.warn(m)
|
|
21138
|
+
});
|
|
21139
|
+
if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
|
|
20708
21140
|
const stopReason = await runDaemonLoop({
|
|
20709
21141
|
runPass: () => runOnePass({
|
|
20710
|
-
route,
|
|
21142
|
+
route: route2,
|
|
20711
21143
|
ship: cfg.ship,
|
|
20712
21144
|
state,
|
|
20713
21145
|
maxConcurrentAgents: cfg.ship.maxConcurrentAgents,
|
|
@@ -20715,7 +21147,7 @@ ${cu.tail.trimEnd()}
|
|
|
20715
21147
|
spawnBreaker,
|
|
20716
21148
|
configFile: cfg.configFile,
|
|
20717
21149
|
newEmitter: () => new Emitter({
|
|
20718
|
-
route:
|
|
21150
|
+
route: route2.route,
|
|
20719
21151
|
eventFile: eventFileFor(cfg.ship.stateDir),
|
|
20720
21152
|
rotateBytes: cfg.ship.eventsRotateBytes,
|
|
20721
21153
|
logFile: cfg.ship.logFile,
|
|
@@ -20723,13 +21155,13 @@ ${cu.tail.trimEnd()}
|
|
|
20723
21155
|
`)
|
|
20724
21156
|
})
|
|
20725
21157
|
}),
|
|
20726
|
-
onTransient: ({ err, consecutive, backoffMs }) => emit.warn(
|
|
20727
|
-
`tracker unavailable (${describeCause(
|
|
21158
|
+
onTransient: ({ err: err2, consecutive, backoffMs }) => emit.warn(
|
|
21159
|
+
`tracker unavailable (${describeCause(err2)}); attempt ${consecutive}, retrying in ${Math.round(backoffMs / 1e3)}s`
|
|
20728
21160
|
),
|
|
20729
|
-
onOutage: ({ err, downMs }) => {
|
|
20730
|
-
const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(
|
|
21161
|
+
onOutage: ({ err: err2, downMs }) => {
|
|
21162
|
+
const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(err2)}`;
|
|
20731
21163
|
emit.error(msg);
|
|
20732
|
-
void raiseShipAttention({ state, emit, route, ship: cfg.ship }, {
|
|
21164
|
+
void raiseShipAttention({ state, emit, route: route2, ship: cfg.ship }, {
|
|
20733
21165
|
kind: "tracker_unreachable",
|
|
20734
21166
|
key: TRACKER_OUTAGE_KEY,
|
|
20735
21167
|
message: msg,
|
|
@@ -20740,8 +21172,8 @@ ${cu.tail.trimEnd()}
|
|
|
20740
21172
|
clearShipAttention(state, TRACKER_OUTAGE_KEY);
|
|
20741
21173
|
emit.emit(`tracker is back after ${Math.max(1, Math.round(downMs / 1e3))}s`);
|
|
20742
21174
|
},
|
|
20743
|
-
onPassError: (
|
|
20744
|
-
`daemon pass failed, continuing: ${
|
|
21175
|
+
onPassError: (err2) => emit.error(
|
|
21176
|
+
`daemon pass failed, continuing: ${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}`
|
|
20745
21177
|
),
|
|
20746
21178
|
checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
|
|
20747
21179
|
onPass: (result) => {
|
|
@@ -20749,6 +21181,7 @@ ${cu.tail.trimEnd()}
|
|
|
20749
21181
|
for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
|
|
20750
21182
|
}
|
|
20751
21183
|
});
|
|
21184
|
+
await controlServer?.close();
|
|
20752
21185
|
if (stopReason === "stale") {
|
|
20753
21186
|
emit.emit(
|
|
20754
21187
|
"crew daemon: installed tree, crew.yaml or a reload request changed on disk \u2014 exiting cleanly so the OS-level restart relaunches with the update"
|
|
@@ -20777,7 +21210,7 @@ ${cu.tail.trimEnd()}
|
|
|
20777
21210
|
role: value("role"),
|
|
20778
21211
|
ticket: value("ticket"),
|
|
20779
21212
|
level: value("level"),
|
|
20780
|
-
route: positional[1] ?
|
|
21213
|
+
route: positional[1] ? route2.route : void 0
|
|
20781
21214
|
}
|
|
20782
21215
|
});
|
|
20783
21216
|
process.on("SIGINT", () => {
|
|
@@ -20929,7 +21362,7 @@ ${cu.tail.trimEnd()}
|
|
|
20929
21362
|
`);
|
|
20930
21363
|
if (!dryRun) {
|
|
20931
21364
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
20932
|
-
const raw =
|
|
21365
|
+
const raw = existsSync39(resolvedPath) ? JSON.parse(readFileSync34(resolvedPath, "utf8")) : {};
|
|
20933
21366
|
raw.agentSkills = result.agentSkills;
|
|
20934
21367
|
mkdirSync25(dirOf2(resolvedPath), { recursive: true });
|
|
20935
21368
|
writeFileSync26(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -21009,7 +21442,7 @@ ${cu.tail.trimEnd()}
|
|
|
21009
21442
|
const found = cfg.routes.find((r) => r.route === routeArg);
|
|
21010
21443
|
if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
|
|
21011
21444
|
const named2 = value("route") ? void 0 : positional[3];
|
|
21012
|
-
const dir = named2 ? repoDirOf(configText,
|
|
21445
|
+
const dir = named2 ? repoDirOf(configText, dirname22(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
|
|
21013
21446
|
const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
|
|
21014
21447
|
const o = target ? found.repoOverrides[target.name] : void 0;
|
|
21015
21448
|
runReposCheck(dir, {
|
|
@@ -21032,11 +21465,11 @@ ${cu.tail.trimEnd()}
|
|
|
21032
21465
|
if (sub === "hooks") {
|
|
21033
21466
|
try {
|
|
21034
21467
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
21035
|
-
const dir = repoDirOf(configText,
|
|
21036
|
-
if (!
|
|
21037
|
-
const files = { exists: (r) =>
|
|
21468
|
+
const dir = repoDirOf(configText, dirname22(cfg.configFile), a.route, a.name);
|
|
21469
|
+
if (!existsSync39(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
21470
|
+
const files = { exists: (r) => existsSync39(resolvePath(dir, r)), list: (r) => {
|
|
21038
21471
|
try {
|
|
21039
|
-
return
|
|
21472
|
+
return readdirSync18(resolvePath(dir, r));
|
|
21040
21473
|
} catch {
|
|
21041
21474
|
return [];
|
|
21042
21475
|
}
|
|
@@ -21152,7 +21585,7 @@ ${cu.tail.trimEnd()}
|
|
|
21152
21585
|
if (sub === "list" && !routeName) usage2();
|
|
21153
21586
|
if (sub === "add") {
|
|
21154
21587
|
try {
|
|
21155
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
21588
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync39(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
|
|
21156
21589
|
routeName = a.route;
|
|
21157
21590
|
addPath = a.path;
|
|
21158
21591
|
addName = a.name;
|
|
@@ -21181,7 +21614,7 @@ ${cu.tail.trimEnd()}
|
|
|
21181
21614
|
}
|
|
21182
21615
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
21183
21616
|
let resolvedRemotes;
|
|
21184
|
-
if (
|
|
21617
|
+
if (existsSync39(resolvedFile)) {
|
|
21185
21618
|
try {
|
|
21186
21619
|
resolvedRemotes = JSON.parse(readFileSync34(resolvedFile, "utf8"));
|
|
21187
21620
|
} catch {
|
|
@@ -21189,8 +21622,8 @@ ${cu.tail.trimEnd()}
|
|
|
21189
21622
|
}
|
|
21190
21623
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
21191
21624
|
const text = configText;
|
|
21192
|
-
const base =
|
|
21193
|
-
const isCheckout = (d) =>
|
|
21625
|
+
const base = dirname22(cfg.configFile);
|
|
21626
|
+
const isCheckout = (d) => existsSync39(`${d}/.git`);
|
|
21194
21627
|
try {
|
|
21195
21628
|
if (sub === "list") {
|
|
21196
21629
|
if (flag("json")) {
|
|
@@ -21200,7 +21633,7 @@ ${cu.tail.trimEnd()}
|
|
|
21200
21633
|
base,
|
|
21201
21634
|
resolved: resolvedRemotes,
|
|
21202
21635
|
originOf,
|
|
21203
|
-
exists: (d) =>
|
|
21636
|
+
exists: (d) => existsSync39(d),
|
|
21204
21637
|
isCheckout,
|
|
21205
21638
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
21206
21639
|
try {
|
|
@@ -21221,7 +21654,7 @@ ${cu.tail.trimEnd()}
|
|
|
21221
21654
|
for (const r of repos) {
|
|
21222
21655
|
const origin2 = originOf(r.dir);
|
|
21223
21656
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
21224
|
-
const note = !
|
|
21657
|
+
const note = !existsSync39(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
21225
21658
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
21226
21659
|
`);
|
|
21227
21660
|
}
|
|
@@ -21233,9 +21666,9 @@ ${cu.tail.trimEnd()}
|
|
|
21233
21666
|
` : `${human}
|
|
21234
21667
|
`);
|
|
21235
21668
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
|
|
21236
|
-
if (
|
|
21669
|
+
if (existsSync39(plan2.target) && (existsSync39(`${plan2.target}/.git`) || readdirSync18(plan2.target).length > 0)) {
|
|
21237
21670
|
throw new ReposError(
|
|
21238
|
-
|
|
21671
|
+
existsSync39(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
|
|
21239
21672
|
);
|
|
21240
21673
|
}
|
|
21241
21674
|
if (dryRun) {
|
|
@@ -21260,7 +21693,7 @@ ${cu.tail.trimEnd()}
|
|
|
21260
21693
|
break;
|
|
21261
21694
|
}
|
|
21262
21695
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
|
|
21263
|
-
const top =
|
|
21696
|
+
const top = existsSync39(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
21264
21697
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
21265
21698
|
const origin = originOf(top);
|
|
21266
21699
|
const plan = planRepoAdd({
|
|
@@ -21312,7 +21745,7 @@ ${cu.tail.trimEnd()}
|
|
|
21312
21745
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
21313
21746
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
21314
21747
|
`);
|
|
21315
|
-
if (
|
|
21748
|
+
if (existsSync39(resolvedFile)) {
|
|
21316
21749
|
const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
|
|
21317
21750
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
21318
21751
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
@@ -21693,11 +22126,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21693
22126
|
if (found.shipRow) {
|
|
21694
22127
|
connectEvents.emit("ship_row", `Ships row "${found.shipRow.name}" ${found.shipRow.action}`, found.shipRow);
|
|
21695
22128
|
}
|
|
21696
|
-
const
|
|
21697
|
-
const resolvedPath = resolvedPathFor(cfg.ship.stateDir,
|
|
22129
|
+
const route3 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
|
|
22130
|
+
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, route3);
|
|
21698
22131
|
const capabilities = await checkCapabilities(authOpts, found);
|
|
21699
22132
|
if (flag("json")) {
|
|
21700
|
-
process.stdout.write(`${JSON.stringify({ event: "capabilities", route:
|
|
22133
|
+
process.stdout.write(`${JSON.stringify({ event: "capabilities", route: route3, items: capabilities })}
|
|
21701
22134
|
`);
|
|
21702
22135
|
} else {
|
|
21703
22136
|
process.stderr.write("capabilities for this key:\n");
|
|
@@ -21706,10 +22139,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21706
22139
|
}
|
|
21707
22140
|
const capabilityMisses = capabilities.filter((i) => i.status === "missing").map(missLine);
|
|
21708
22141
|
if (found.locksModelId && !dryRun) {
|
|
21709
|
-
const configured = cfg.routes.find((r) => r.route ===
|
|
22142
|
+
const configured = cfg.routes.find((r) => r.route === route3);
|
|
21710
22143
|
const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
|
|
21711
22144
|
try {
|
|
21712
|
-
const made = await provisionReleaseLocks(authOpts, found.locksModelId,
|
|
22145
|
+
const made = await provisionReleaseLocks(authOpts, found.locksModelId, route3, repoNames);
|
|
21713
22146
|
for (const sc of made) process.stderr.write(`created release lock row ${sc}
|
|
21714
22147
|
`);
|
|
21715
22148
|
} catch (e) {
|
|
@@ -21730,7 +22163,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21730
22163
|
}
|
|
21731
22164
|
}
|
|
21732
22165
|
let previous;
|
|
21733
|
-
const hadPreviousFile =
|
|
22166
|
+
const hadPreviousFile = existsSync39(resolvedPath);
|
|
21734
22167
|
if (hadPreviousFile) {
|
|
21735
22168
|
try {
|
|
21736
22169
|
previous = JSON.parse(readFileSync34(resolvedPath, "utf8"));
|
|
@@ -21785,7 +22218,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21785
22218
|
}
|
|
21786
22219
|
if (!operator && !hadPreviousFile && !operatorWouldBeCreated) {
|
|
21787
22220
|
process.stderr.write(
|
|
21788
|
-
`crew connect ${
|
|
22221
|
+
`crew connect ${route3}: cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it, none matching this key's own email, and there's no terminal to ask.
|
|
21789
22222
|
Re-run from a real terminal to pick one, or pass --key for a key whose own
|
|
21790
22223
|
identity email matches exactly one Crew row.
|
|
21791
22224
|
`
|
|
@@ -21862,7 +22295,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21862
22295
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
21863
22296
|
`);
|
|
21864
22297
|
} else {
|
|
21865
|
-
mkdirSync25(
|
|
22298
|
+
mkdirSync25(dirname22(resolvedPath), { recursive: true });
|
|
21866
22299
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
21867
22300
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
21868
22301
|
writeFileSync26(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
@@ -21906,12 +22339,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21906
22339
|
stopWithProblem(msg, "wizard");
|
|
21907
22340
|
}
|
|
21908
22341
|
if ((mintedFromDevice || firstRunFromFlags) && !keychainBacked) {
|
|
21909
|
-
apiKeyFile = apiKeyPathFor(cfg.ship.stateDir,
|
|
22342
|
+
apiKeyFile = apiKeyPathFor(cfg.ship.stateDir, route3);
|
|
21910
22343
|
if (dryRun) {
|
|
21911
22344
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
21912
22345
|
`);
|
|
21913
22346
|
} else {
|
|
21914
|
-
mkdirSync25(
|
|
22347
|
+
mkdirSync25(dirname22(apiKeyFile), { recursive: true });
|
|
21915
22348
|
writeFileSync26(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
21916
22349
|
`, { mode: 384 });
|
|
21917
22350
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
@@ -21924,7 +22357,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
21924
22357
|
const routeDir = value("dir");
|
|
21925
22358
|
const routeBlock = renderConnection(
|
|
21926
22359
|
found,
|
|
21927
|
-
|
|
22360
|
+
route3,
|
|
21928
22361
|
routeDir ?? (writeFirstRun ? null : "REPLACE \u2014 the local checkout this route works"),
|
|
21929
22362
|
{
|
|
21930
22363
|
area: value("area"),
|
|
@@ -22006,7 +22439,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22006
22439
|
}
|
|
22007
22440
|
if (wroteConfig) {
|
|
22008
22441
|
process.stderr.write(nextSteps({
|
|
22009
|
-
route:
|
|
22442
|
+
route: route3,
|
|
22010
22443
|
configPath: wizard.configPath,
|
|
22011
22444
|
platform: cfg.ship.platform,
|
|
22012
22445
|
needsRepos: !value("dir")
|
|
@@ -22016,14 +22449,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22016
22449
|
} else if (operator) {
|
|
22017
22450
|
process.stderr.write(
|
|
22018
22451
|
`
|
|
22019
|
-
Paste the block above under routes: in crew.yaml (if "${
|
|
22452
|
+
Paste the block above under routes: in crew.yaml (if "${route3}" isn't there already), then: crew doctor ${route3}
|
|
22020
22453
|
`
|
|
22021
22454
|
);
|
|
22022
22455
|
} else {
|
|
22023
22456
|
process.stderr.write(
|
|
22024
22457
|
`
|
|
22025
|
-
Paste the block above under routes: in crew.yaml (if "${
|
|
22026
|
-
already), set operator in ${resolvedPath}, then: crew doctor ${
|
|
22458
|
+
Paste the block above under routes: in crew.yaml (if "${route3}" isn't there
|
|
22459
|
+
already), set operator in ${resolvedPath}, then: crew doctor ${route3}
|
|
22027
22460
|
`
|
|
22028
22461
|
);
|
|
22029
22462
|
}
|
|
@@ -22032,12 +22465,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22032
22465
|
process.stderr.write(`
|
|
22033
22466
|
agents sync: skipped (${why})
|
|
22034
22467
|
`);
|
|
22035
|
-
connectEvents.emit("agents_synced", `agents sync skipped: ${why}`, { route:
|
|
22468
|
+
connectEvents.emit("agents_synced", `agents sync skipped: ${why}`, { route: route3, skipped: why });
|
|
22036
22469
|
};
|
|
22037
22470
|
let syncRoute;
|
|
22038
22471
|
try {
|
|
22039
22472
|
const reloaded = loadConfig(CREW_HOME, wizard?.configPath ?? cfg.configFile, { allowNoRoutes: true });
|
|
22040
|
-
syncRoute = reloaded.routes.find((r) => r.route ===
|
|
22473
|
+
syncRoute = reloaded.routes.find((r) => r.route === route3);
|
|
22041
22474
|
if (syncRoute) await hydrateApiKeys([syncRoute]);
|
|
22042
22475
|
} catch (e) {
|
|
22043
22476
|
skip(`config did not load: ${e.message}`);
|
|
@@ -22065,24 +22498,24 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22065
22498
|
process.stderr.write(missing ? " A workspace admin has to open the Crew app or run `crew agents sync` once before this ship's seats can run.\n" : " Seats are linked to the workspace's existing personas.\n");
|
|
22066
22499
|
}
|
|
22067
22500
|
connectEvents.emit("agents_synced", run2.readOnly ? "agents linked (read-only key)" : "agents synced", {
|
|
22068
|
-
route:
|
|
22501
|
+
route: route3,
|
|
22069
22502
|
admin: !run2.readOnly,
|
|
22070
22503
|
personas: run2.personaResult.outcomes,
|
|
22071
22504
|
skills: run2.skillResult?.outcomes ?? [],
|
|
22072
22505
|
diverged: run2.diverged
|
|
22073
22506
|
});
|
|
22074
|
-
for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route:
|
|
22507
|
+
for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route3, ...m });
|
|
22075
22508
|
if (run2.diverged) process.exitCode = 1;
|
|
22076
22509
|
} catch (e) {
|
|
22077
22510
|
if (e instanceof AgentsSyncError || e instanceof SkillSyncError) skip(e.message);
|
|
22078
22511
|
else throw e;
|
|
22079
22512
|
}
|
|
22080
22513
|
} else if (!syncRoute) {
|
|
22081
|
-
skip(`"${
|
|
22514
|
+
skip(`"${route3}" is not in crew.yaml yet; add the block above, then run \`crew agents sync\``);
|
|
22082
22515
|
}
|
|
22083
22516
|
}
|
|
22084
|
-
connectEvents.emit("done", `connected ${
|
|
22085
|
-
route:
|
|
22517
|
+
connectEvents.emit("done", `connected ${route3}`, {
|
|
22518
|
+
route: route3,
|
|
22086
22519
|
resolvedPath: dryRun ? void 0 : resolvedPath,
|
|
22087
22520
|
configPath: wroteConfig ? wizard.configPath : void 0,
|
|
22088
22521
|
wroteConfig,
|
|
@@ -22196,7 +22629,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22196
22629
|
break;
|
|
22197
22630
|
}
|
|
22198
22631
|
case "ports": {
|
|
22199
|
-
const where = await worktreeLocations(
|
|
22632
|
+
const where = await worktreeLocations(route2);
|
|
22200
22633
|
const rows = listeners().map((l) => ({ ...l, t: ticketForPort(l.port) })).filter((r) => r.t).sort((a, b) => a.port - b.port);
|
|
22201
22634
|
if (!rows.length) {
|
|
22202
22635
|
process.stdout.write("nothing of ours is listening\n");
|
|
@@ -22214,7 +22647,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22214
22647
|
case "reap": {
|
|
22215
22648
|
emit.enter("worktree");
|
|
22216
22649
|
let anything = false;
|
|
22217
|
-
const orphans = findOrphansIn(await worktreeLocations(
|
|
22650
|
+
const orphans = findOrphansIn(await worktreeLocations(route2));
|
|
22218
22651
|
for (const o of orphans) {
|
|
22219
22652
|
anything = true;
|
|
22220
22653
|
emit.emit(
|
|
@@ -22222,12 +22655,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22222
22655
|
);
|
|
22223
22656
|
}
|
|
22224
22657
|
if (!dryRun && orphans.length) await killGently(orphans.map((o) => o.pid));
|
|
22225
|
-
const tracker = new Tracker(
|
|
22658
|
+
const tracker = new Tracker(route2, cfg.ship);
|
|
22226
22659
|
const truncated = (limit) => emit.warn(`ticket lookup for the sweep hit its ${limit}-row limit; some tickets may be missed`, { step: "worktree" });
|
|
22227
|
-
for (const r of await resolvedRepos(
|
|
22228
|
-
const wtTickets = ticketsByRepo(
|
|
22660
|
+
for (const r of await resolvedRepos(route2)) {
|
|
22661
|
+
const wtTickets = ticketsByRepo(route2, await tracker.ticketsByNumbers(worktreeTicketNumbers(r), truncated)).byRepo.get(r.name) ?? [];
|
|
22229
22662
|
const brTickets = ticketsByRepo(
|
|
22230
|
-
|
|
22663
|
+
route2,
|
|
22231
22664
|
await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
|
|
22232
22665
|
).byRepo.get(r.name) ?? [];
|
|
22233
22666
|
const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
|
|
@@ -22282,26 +22715,26 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22282
22715
|
process.exit(2);
|
|
22283
22716
|
}
|
|
22284
22717
|
const num = Number(n);
|
|
22285
|
-
const repos = await resolvedRepos(
|
|
22286
|
-
const
|
|
22287
|
-
if (
|
|
22718
|
+
const repos = await resolvedRepos(route2);
|
|
22719
|
+
const candidates2 = repos.map((r) => ({ repo: r, found: worktreeForNumber(r.dir, n) })).filter((c) => c.found !== null);
|
|
22720
|
+
if (candidates2.length === 0) {
|
|
22288
22721
|
process.stderr.write(
|
|
22289
22722
|
`drop: no worktree for ISSUE-${n} in any of this route's repositories (looked in: ${repos.map((r) => r.dir).join(", ")})
|
|
22290
22723
|
`
|
|
22291
22724
|
);
|
|
22292
22725
|
process.exit(2);
|
|
22293
22726
|
}
|
|
22294
|
-
if (
|
|
22727
|
+
if (candidates2.length > 1) {
|
|
22295
22728
|
process.stderr.write(
|
|
22296
|
-
`drop: ISSUE-${n} has a worktree in more than one repository (${
|
|
22729
|
+
`drop: ISSUE-${n} has a worktree in more than one repository (${candidates2.map((c) => c.repo.name).join(", ")}) \u2014 remove them one at a time by path
|
|
22297
22730
|
`
|
|
22298
22731
|
);
|
|
22299
22732
|
process.exit(2);
|
|
22300
22733
|
}
|
|
22301
|
-
const { repo: target, found } =
|
|
22734
|
+
const { repo: target, found } = candidates2[0];
|
|
22302
22735
|
const wt = found.path;
|
|
22303
22736
|
try {
|
|
22304
|
-
const trD = new Tracker(
|
|
22737
|
+
const trD = new Tracker(route2, cfg.ship);
|
|
22305
22738
|
const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num));
|
|
22306
22739
|
if (tD && tD[trD.contract.columns.status] === trD.contract.statuses.draft) {
|
|
22307
22740
|
process.stderr.write(`drop: ${tD.issue_tag ?? tD.issue_id} is a draft ticket \u2014 its worktree belongs to a periscope; end it with \`crew periscope down ${num} --keep\` or \`--discard\`
|
|
@@ -22340,9 +22773,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22340
22773
|
}
|
|
22341
22774
|
case "rotate-passenger-url": {
|
|
22342
22775
|
emit.enter("passengers");
|
|
22343
|
-
if (!
|
|
22776
|
+
if (!route2.hostPassengers) {
|
|
22344
22777
|
process.stderr.write(
|
|
22345
|
-
`crew rotate-passenger-url: route "${
|
|
22778
|
+
`crew rotate-passenger-url: route "${route2.route}" does not have hostPassengers enabled
|
|
22346
22779
|
`
|
|
22347
22780
|
);
|
|
22348
22781
|
process.exit(2);
|
|
@@ -22351,16 +22784,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22351
22784
|
process.stderr.write("crew rotate-passenger-url: this ship has no relayHost configured \u2014 nothing to rotate\n");
|
|
22352
22785
|
process.exit(2);
|
|
22353
22786
|
}
|
|
22354
|
-
const workspaceId =
|
|
22787
|
+
const workspaceId = route2.resolved?.workspaceId;
|
|
22355
22788
|
if (!workspaceId) {
|
|
22356
|
-
process.stderr.write(`crew rotate-passenger-url: route "${
|
|
22789
|
+
process.stderr.write(`crew rotate-passenger-url: route "${route2.route}" has no resolved ids \u2014 run \`crew connect\`
|
|
22357
22790
|
`);
|
|
22358
22791
|
process.exit(2);
|
|
22359
22792
|
}
|
|
22360
22793
|
const plans = planContainers(cfg).filter((p) => p.workspaceId === workspaceId);
|
|
22361
22794
|
if (!plans.length) {
|
|
22362
22795
|
process.stderr.write(
|
|
22363
|
-
`crew rotate-passenger-url: no Host Passengers container is planned for "${
|
|
22796
|
+
`crew rotate-passenger-url: no Host Passengers container is planned for "${route2.route}" right now
|
|
22364
22797
|
`
|
|
22365
22798
|
);
|
|
22366
22799
|
process.exit(2);
|
|
@@ -22369,7 +22802,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22369
22802
|
const hasLiveTunnel = existing !== void 0 && isPidAlive(existing.pid);
|
|
22370
22803
|
if (dryRun) {
|
|
22371
22804
|
emit.emit(
|
|
22372
|
-
hasLiveTunnel ? `would kill tunnel pid ${existing.pid} for ${
|
|
22805
|
+
hasLiveTunnel ? `would kill tunnel pid ${existing.pid} for ${route2.route} and reconnect with a fresh public URL` : `would reconnect ${route2.route} with a fresh public URL (no live tunnel to kill)`
|
|
22373
22806
|
);
|
|
22374
22807
|
break;
|
|
22375
22808
|
}
|
|
@@ -22381,13 +22814,13 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22381
22814
|
}
|
|
22382
22815
|
}
|
|
22383
22816
|
const { privateKeyPath } = ensureShipSshKeypair(cfg.ship.stateDir);
|
|
22384
|
-
const rotateReconcile = endpointReconcileDeps([
|
|
22817
|
+
const rotateReconcile = endpointReconcileDeps([route2], cfg.ship.relayHost);
|
|
22385
22818
|
const rotatePending = [];
|
|
22386
22819
|
syncPassengerTunnels(plans, cfg.ship.relayHost, cfg.ship.relayPort, privateKeyPath, cfg.ship.stateDir, /* @__PURE__ */ new Map(), {
|
|
22387
|
-
apiKeys: passengerApiKeys([
|
|
22820
|
+
apiKeys: passengerApiKeys([route2]),
|
|
22388
22821
|
onStatus: (wsId, status2, info) => {
|
|
22389
22822
|
rotatePending.push(
|
|
22390
|
-
new Tracker(
|
|
22823
|
+
new Tracker(route2, cfg.ship).updateTunnelState(cfg.ship.name, { tunnel_status: status2 }).catch((e) => {
|
|
22391
22824
|
emit.warn(`could not write tunnel_status: ${e.message}`, { step: "passengers" });
|
|
22392
22825
|
})
|
|
22393
22826
|
);
|
|
@@ -22395,7 +22828,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22395
22828
|
}
|
|
22396
22829
|
});
|
|
22397
22830
|
await Promise.all(rotatePending);
|
|
22398
|
-
emit.emit(`rotated the passenger tunnel for ${
|
|
22831
|
+
emit.emit(`rotated the passenger tunnel for ${route2.route} \u2014 it reconnects under a fresh slug and the registered endpoint follows on the next passengers cycle`);
|
|
22399
22832
|
break;
|
|
22400
22833
|
}
|
|
22401
22834
|
case "worktree": {
|
|
@@ -22406,7 +22839,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22406
22839
|
process.stderr.write("worktree: need a ticket number (326, ISSUE-326, or TABL-326)\n");
|
|
22407
22840
|
process.exit(2);
|
|
22408
22841
|
}
|
|
22409
|
-
const candidatesWt = !named && cfg.routes.length > 1 ? cfg.routes.filter((r) => r.enabled) : [
|
|
22842
|
+
const candidatesWt = !named && cfg.routes.length > 1 ? cfg.routes.filter((r) => r.enabled) : [route2];
|
|
22410
22843
|
let ticketWt;
|
|
22411
22844
|
const lookedWt = [];
|
|
22412
22845
|
const hitsWt = /* @__PURE__ */ new Map();
|
|
@@ -22426,7 +22859,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22426
22859
|
`);
|
|
22427
22860
|
process.exit(2);
|
|
22428
22861
|
}
|
|
22429
|
-
const routeWt = candidatesWt.find((r) => r.route === pickedWt.route) ??
|
|
22862
|
+
const routeWt = candidatesWt.find((r) => r.route === pickedWt.route) ?? route2;
|
|
22430
22863
|
ticketWt = hitsWt.get(pickedWt.route);
|
|
22431
22864
|
if (!ticketWt) process.exit(2);
|
|
22432
22865
|
const repoWt = (await resolvedRepos(routeWt)).find((r) => r.id === ticketWt.repo_id) ?? (await resolvedRepos(routeWt))[0];
|
|
@@ -22646,7 +23079,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
22646
23079
|
const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
|
|
22647
23080
|
const resumed = await runPeriscopeResume({
|
|
22648
23081
|
find: async () => hit && { row: hit.row, tag: hit.tag },
|
|
22649
|
-
worktreeExists: (p) =>
|
|
23082
|
+
worktreeExists: (p) => existsSync39(p),
|
|
22650
23083
|
caps: periscopeCaps(cfg.ship),
|
|
22651
23084
|
runHook: async (name, cwd, env) => {
|
|
22652
23085
|
const script = hit?.repo.config.hooks[name];
|
|
@@ -22740,10 +23173,10 @@ ${where.join("\n")}`);
|
|
|
22740
23173
|
},
|
|
22741
23174
|
prepare: async (path) => {
|
|
22742
23175
|
for (const f of repoP.config.worktrees.copy) {
|
|
22743
|
-
const src =
|
|
22744
|
-
if (!
|
|
22745
|
-
mkdirSync25(dirOf2(
|
|
22746
|
-
copyFileSync2(src,
|
|
23176
|
+
const src = join43(repoP.dir, f);
|
|
23177
|
+
if (!existsSync39(src)) continue;
|
|
23178
|
+
mkdirSync25(dirOf2(join43(path, f)), { recursive: true });
|
|
23179
|
+
copyFileSync2(src, join43(path, f));
|
|
22747
23180
|
}
|
|
22748
23181
|
const setup = repoP.config.hooks.setup;
|
|
22749
23182
|
return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
|
|
@@ -22852,13 +23285,13 @@ ${where.join("\n")}`);
|
|
|
22852
23285
|
process.stderr.write("unassign: need a ticket number (326, ISSUE-326, or TABL-326)\n");
|
|
22853
23286
|
process.exit(2);
|
|
22854
23287
|
}
|
|
22855
|
-
const tracker3 = new Tracker(
|
|
23288
|
+
const tracker3 = new Tracker(route2, cfg.ship);
|
|
22856
23289
|
const c = tracker3.contract;
|
|
22857
23290
|
const key = `ISSUE-${n}`;
|
|
22858
23291
|
const open = await tracker3.openTickets();
|
|
22859
23292
|
const ticket = open.find((t) => t[c.columns.key] === key);
|
|
22860
23293
|
if (!ticket) {
|
|
22861
|
-
process.stderr.write(`unassign: no open ticket ${key} on ${
|
|
23294
|
+
process.stderr.write(`unassign: no open ticket ${key} on ${route2.route} (already resolved, or not this route's)
|
|
22862
23295
|
`);
|
|
22863
23296
|
process.exit(2);
|
|
22864
23297
|
}
|
|
@@ -22892,10 +23325,10 @@ ${where.join("\n")}`);
|
|
|
22892
23325
|
let baseChecked = 0;
|
|
22893
23326
|
let baseActed = 0;
|
|
22894
23327
|
let baseUnsafe = 0;
|
|
22895
|
-
for (const r of await resolvedRepos(
|
|
23328
|
+
for (const r of await resolvedRepos(route2)) {
|
|
22896
23329
|
const base = refreshBaseBranch(r.dir, r.config.branch.remote, r.config.branch.base, dryRun);
|
|
22897
23330
|
const att = dryRun ? null : baseAttention(r.name, base, r.config.branch.remote, r.config.branch.base);
|
|
22898
|
-
if (att && "raise" in att) await raiseShipAttention({ state, emit, route, ship: cfg.ship }, att.raise);
|
|
23331
|
+
if (att && "raise" in att) await raiseShipAttention({ state, emit, route: route2, ship: cfg.ship }, att.raise);
|
|
22899
23332
|
else if (att) clearShipAttention(state, att.clear);
|
|
22900
23333
|
if (base.action !== "not-applicable") {
|
|
22901
23334
|
baseChecked++;
|
|
@@ -23133,13 +23566,13 @@ ${where.join("\n")}`);
|
|
|
23133
23566
|
`);
|
|
23134
23567
|
return process.exit(2);
|
|
23135
23568
|
};
|
|
23136
|
-
if (!
|
|
23137
|
-
const tracker = new Tracker(
|
|
23569
|
+
if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
|
|
23570
|
+
const tracker = new Tracker(route2, cfg.ship);
|
|
23138
23571
|
const configText = readFileSync34(cfg.configFile, "utf8");
|
|
23139
|
-
const cfgBase =
|
|
23140
|
-
const configured = configuredRepos(configText,
|
|
23141
|
-
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir),
|
|
23142
|
-
const resolvedFile = resolvedPathFor(cfg.ship.stateDir,
|
|
23572
|
+
const cfgBase = dirname22(cfg.configFile);
|
|
23573
|
+
const configured = configuredRepos(configText, route2.route, cfgBase);
|
|
23574
|
+
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
|
|
23575
|
+
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
|
|
23143
23576
|
const rows = await tracker.repoRows();
|
|
23144
23577
|
const row = rows.find((r) => r.name === SAMPLE_REPO);
|
|
23145
23578
|
if (flag("remove")) {
|
|
@@ -23164,7 +23597,7 @@ ${where.join("\n")}`);
|
|
|
23164
23597
|
}
|
|
23165
23598
|
if (configured.some((r) => r.dir === dir)) {
|
|
23166
23599
|
try {
|
|
23167
|
-
writeFileSync26(cfg.configFile, planRepoRemove(configText, cfgBase,
|
|
23600
|
+
writeFileSync26(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
|
|
23168
23601
|
} catch (e) {
|
|
23169
23602
|
if (e instanceof ReposError) fail(e.message);
|
|
23170
23603
|
throw e;
|
|
@@ -23172,7 +23605,7 @@ ${where.join("\n")}`);
|
|
|
23172
23605
|
}
|
|
23173
23606
|
if (row) {
|
|
23174
23607
|
await tracker.deleteRepoRow(row.id);
|
|
23175
|
-
if (
|
|
23608
|
+
if (existsSync39(resolvedFile)) {
|
|
23176
23609
|
const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
|
|
23177
23610
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
23178
23611
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
@@ -23180,21 +23613,21 @@ ${where.join("\n")}`);
|
|
|
23180
23613
|
`);
|
|
23181
23614
|
}
|
|
23182
23615
|
}
|
|
23183
|
-
if (
|
|
23616
|
+
if (existsSync39(join43(dir, ".git")) && basename13(dir) === SAMPLE_REPO) rmSync18(dir, { recursive: true, force: true });
|
|
23184
23617
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
23185
23618
|
break;
|
|
23186
23619
|
}
|
|
23187
|
-
const projectId =
|
|
23188
|
-
const fresh = !
|
|
23189
|
-
if (
|
|
23620
|
+
const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
23621
|
+
const fresh = !existsSync39(join43(dir, ".git"));
|
|
23622
|
+
if (existsSync39(dir) && fresh && readdirSync18(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
23190
23623
|
if (dryRun) {
|
|
23191
23624
|
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`);
|
|
23192
23625
|
break;
|
|
23193
23626
|
}
|
|
23194
23627
|
if (fresh) {
|
|
23195
23628
|
mkdirSync25(dir, { recursive: true });
|
|
23196
|
-
writeFileSync26(
|
|
23197
|
-
writeFileSync26(
|
|
23629
|
+
writeFileSync26(join43(dir, "README.md"), SAMPLE_README);
|
|
23630
|
+
writeFileSync26(join43(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
23198
23631
|
const g = (...a) => {
|
|
23199
23632
|
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
23200
23633
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -23212,7 +23645,7 @@ ${where.join("\n")}`);
|
|
|
23212
23645
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
23213
23646
|
});
|
|
23214
23647
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
23215
|
-
if (
|
|
23648
|
+
if (existsSync39(resolvedFile)) {
|
|
23216
23649
|
const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
|
|
23217
23650
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
23218
23651
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
@@ -23222,7 +23655,7 @@ ${where.join("\n")}`);
|
|
|
23222
23655
|
}
|
|
23223
23656
|
if (!configured.some((r) => r.dir === dir)) {
|
|
23224
23657
|
try {
|
|
23225
|
-
writeFileSync26(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route:
|
|
23658
|
+
writeFileSync26(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
|
|
23226
23659
|
} catch (e) {
|
|
23227
23660
|
if (e instanceof ReposError) fail(e.message);
|
|
23228
23661
|
throw e;
|
|
@@ -23230,7 +23663,7 @@ ${where.join("\n")}`);
|
|
|
23230
23663
|
}
|
|
23231
23664
|
const ticket = await tracker.fileTicket(
|
|
23232
23665
|
planSampleTicket({ projectId, repoId, approvedStatus: tracker.contract.statuses.approved }),
|
|
23233
|
-
|
|
23666
|
+
route2.resolved.seats.dev ?? ""
|
|
23234
23667
|
);
|
|
23235
23668
|
const tag = ticket.issue_tag ?? ticket.issue_id;
|
|
23236
23669
|
say("filed", { ticket: tag, id: ticket.id, dir, repoId }, `filed ${tag} against ${dir}; follow it with \`crew watch\``);
|
|
@@ -23245,20 +23678,20 @@ ${where.join("\n")}`);
|
|
|
23245
23678
|
`);
|
|
23246
23679
|
process.exit(2);
|
|
23247
23680
|
}
|
|
23248
|
-
if (!
|
|
23249
|
-
process.stderr.write(`crew selftest: route ${
|
|
23681
|
+
if (!route2.resolved) {
|
|
23682
|
+
process.stderr.write(`crew selftest: route ${route2.route} is not connected \u2014 run \`crew connect\` first
|
|
23250
23683
|
`);
|
|
23251
23684
|
process.exit(2);
|
|
23252
23685
|
}
|
|
23253
|
-
const seatKey = await resolveSeatKey(
|
|
23686
|
+
const seatKey = await resolveSeatKey(route2, "dev");
|
|
23254
23687
|
const keychainDown = seatKey.kind === "ship" && /keychain/i.test(seatKey.reason);
|
|
23255
23688
|
const apiKey = keychainDown ? void 0 : seatKey.apiKey;
|
|
23256
|
-
const baseUrl =
|
|
23689
|
+
const baseUrl = route2.baseUrl || DEFAULT_BASE_URL;
|
|
23257
23690
|
let workspaceName = "";
|
|
23258
23691
|
let upfront;
|
|
23259
23692
|
if (apiKey) {
|
|
23260
23693
|
try {
|
|
23261
|
-
const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/workspaces/${
|
|
23694
|
+
const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/workspaces/${route2.resolved.workspaceId}`, {
|
|
23262
23695
|
headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": cfg.ship.userAgent }
|
|
23263
23696
|
});
|
|
23264
23697
|
if (res.status === 401 || res.status === 403) {
|
|
@@ -23284,7 +23717,7 @@ ${where.join("\n")}`);
|
|
|
23284
23717
|
ship: cfg.ship,
|
|
23285
23718
|
toolchain: tc,
|
|
23286
23719
|
baseUrl,
|
|
23287
|
-
workspaceId:
|
|
23720
|
+
workspaceId: route2.resolved.workspaceId,
|
|
23288
23721
|
apiKey,
|
|
23289
23722
|
// Only the seat's reason: runSelftest appends the remedy itself, as for every other class.
|
|
23290
23723
|
keyProblem: keychainDown && seatKey.kind === "ship" ? seatKey.reason : void 0,
|
|
@@ -23333,25 +23766,25 @@ ${where.join("\n")}`);
|
|
|
23333
23766
|
break;
|
|
23334
23767
|
}
|
|
23335
23768
|
case "status": {
|
|
23336
|
-
const fairness = state.fairness(
|
|
23769
|
+
const fairness = state.fairness(route2.route);
|
|
23337
23770
|
const waiting = fairness.waiting();
|
|
23338
|
-
const statusTracker = new Tracker(
|
|
23771
|
+
const statusTracker = new Tracker(route2, cfg.ship);
|
|
23339
23772
|
const shipRows = await statusTracker.shipRows();
|
|
23340
23773
|
const engagedTicketIds = shipRows.map((s) => s.engaged_ticket_id).filter((id) => !!id);
|
|
23341
23774
|
const engagedTickets = engagedTicketIds.length ? await statusTracker.ticketsByIds(engagedTicketIds) : [];
|
|
23342
23775
|
const issueLabelById = new Map(engagedTickets.map((t) => [t.id, t.issue_id]));
|
|
23343
|
-
const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract,
|
|
23776
|
+
const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route2.resolved?.operator ?? "")).catch(() => []);
|
|
23344
23777
|
const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
|
|
23345
|
-
const crewMinVersion = await readCrewFloor(
|
|
23778
|
+
const crewMinVersion = await readCrewFloor(route2, cfg.ship) ?? null;
|
|
23346
23779
|
const runs = liveRuns(cfg.ship.stateDir);
|
|
23347
23780
|
const periscopes = await periscopesForStatus(cfg.ship.stateDir);
|
|
23348
23781
|
if (flag("json")) {
|
|
23349
|
-
const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
|
|
23350
|
-
const repos = await resolvedRepos(
|
|
23351
|
-
const seatIds =
|
|
23782
|
+
const nameSync = await reconcileRouteShipName({ route: route2, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
|
|
23783
|
+
const repos = await resolvedRepos(route2);
|
|
23784
|
+
const seatIds = route2.resolved?.seats ?? {};
|
|
23352
23785
|
const crewRows = Object.keys(seatIds).length ? await statusTracker.crewRows() : [];
|
|
23353
23786
|
const crewRowById = new Map(crewRows.map((c) => [c.id, c]));
|
|
23354
|
-
const agentPersonas =
|
|
23787
|
+
const agentPersonas = route2.resolved?.agentPersonas ?? {};
|
|
23355
23788
|
const seats2 = ROLE_NAMES.filter((r) => r !== "pair").map((r) => {
|
|
23356
23789
|
const seatId = seatIds[r];
|
|
23357
23790
|
const crewRow = seatId ? crewRowById.get(seatId) : void 0;
|
|
@@ -23364,7 +23797,7 @@ ${where.join("\n")}`);
|
|
|
23364
23797
|
name: crewRow?.name ?? null,
|
|
23365
23798
|
personaName,
|
|
23366
23799
|
// Additive (CREW-2822): how many runs this seat has live right now.
|
|
23367
|
-
running: runs.filter((x) => x.role === r && x.route === routeSlug(
|
|
23800
|
+
running: runs.filter((x) => x.role === r && x.route === routeSlug(route2.route)).length
|
|
23368
23801
|
};
|
|
23369
23802
|
});
|
|
23370
23803
|
process.stdout.write(`${JSON.stringify({
|
|
@@ -23408,30 +23841,30 @@ ${where.join("\n")}`);
|
|
|
23408
23841
|
seats: seats2
|
|
23409
23842
|
},
|
|
23410
23843
|
// Additive (CREW-2822): every live run on this ship. `version` stays 1.
|
|
23411
|
-
runs: runs.map((x) => describeRun(x,
|
|
23844
|
+
runs: runs.map((x) => describeRun(x, route2, seatIds)),
|
|
23412
23845
|
// Additive (CREW-2857): every live periscope on this ship, from its local record. `version` stays 1.
|
|
23413
23846
|
periscopes,
|
|
23414
23847
|
watermark: state.watermark(),
|
|
23415
23848
|
route: {
|
|
23416
|
-
name:
|
|
23849
|
+
name: route2.route,
|
|
23417
23850
|
// Additive (CREW-2248): the running version and the workspace's floor. `version` stays 1.
|
|
23418
23851
|
crewVersion: sentVersion(cfg.ship),
|
|
23419
23852
|
crewMinVersion,
|
|
23420
23853
|
// Additive (CREW-1496): each project and its work mode. `version` stays 1.
|
|
23421
23854
|
projects: projects.map((p) => ({ id: p.id, name: p.name, workMode: p.mode })),
|
|
23422
|
-
dir:
|
|
23855
|
+
dir: route2.dir,
|
|
23423
23856
|
// Additive (CREW-2548): open tickets with no repository, as of the last cycle.
|
|
23424
|
-
unroutable: state.unroutable(
|
|
23425
|
-
enabled:
|
|
23426
|
-
area:
|
|
23857
|
+
unroutable: state.unroutable(route2.route).previous().length,
|
|
23858
|
+
enabled: route2.enabled,
|
|
23859
|
+
area: route2.area ?? null,
|
|
23427
23860
|
// Additive (CREW-1511): the toolchain this route's sessions run on.
|
|
23428
|
-
toolchain: resolveToolchain(cfg.ship,
|
|
23429
|
-
baseUrl:
|
|
23861
|
+
toolchain: resolveToolchain(cfg.ship, route2).name,
|
|
23862
|
+
baseUrl: route2.baseUrl,
|
|
23430
23863
|
repos: repos.map((r) => ({
|
|
23431
23864
|
name: r.name,
|
|
23432
23865
|
dir: r.dir,
|
|
23433
23866
|
// Additive (CREW-1529): the toolchain a ticket in this repo runs on.
|
|
23434
|
-
toolchain: resolveToolchain(cfg.ship,
|
|
23867
|
+
toolchain: resolveToolchain(cfg.ship, route2, r.name).name,
|
|
23435
23868
|
// Additive (CREW-1379): `unsafe` means a ship must not cut a
|
|
23436
23869
|
// worktree or release from this base. `version` stays 1.
|
|
23437
23870
|
baseBranch: baseHealth(r),
|
|
@@ -23453,7 +23886,7 @@ ${where.join("\n")}`);
|
|
|
23453
23886
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
23454
23887
|
// config-provenance display; this is the field that actually
|
|
23455
23888
|
// describes where a worktree lands by default.
|
|
23456
|
-
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${
|
|
23889
|
+
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename13(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
|
|
23457
23890
|
worktreeParent: resolvePath(r.dir, "..")
|
|
23458
23891
|
})),
|
|
23459
23892
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -23482,8 +23915,8 @@ ${owed.map((l) => ` ${l}
|
|
|
23482
23915
|
`);
|
|
23483
23916
|
process.stdout.write(
|
|
23484
23917
|
`ship: ${cfg.ship.name} (${cfg.ship.platform})
|
|
23485
|
-
route: ${
|
|
23486
|
-
enabled: ${
|
|
23918
|
+
route: ${route2.route} -> ${route2.dir}
|
|
23919
|
+
enabled: ${route2.enabled}
|
|
23487
23920
|
periscopes: ${periscopeSummary(cfg)}
|
|
23488
23921
|
` + periscopes.map((p) => `periscope: ${p.ticket} ${p.repo} ${p.url} ${p.state}${p.serving ? "" : " (not serving)"} expires ${p.expiresAt}
|
|
23489
23922
|
`).join("") + `crew: ${state.isPaused() ? "paused" : "active"}
|
|
@@ -23501,11 +23934,11 @@ watermark: ${state.watermark()}
|
|
|
23501
23934
|
process.stdout.write(`attention: ${i.kind} \u2014 ${i.message}
|
|
23502
23935
|
`);
|
|
23503
23936
|
}
|
|
23504
|
-
for (const r of await resolvedRepos(
|
|
23937
|
+
for (const r of await resolvedRepos(route2)) {
|
|
23505
23938
|
const bh = baseHealth(r);
|
|
23506
23939
|
if (bh.unsafe) process.stdout.write(`base: ${r.name} \u2014 UNSAFE, no worktree cuts or releases: ${bh.detail}
|
|
23507
23940
|
`);
|
|
23508
|
-
process.stdout.write(`releaser: ${r.name} \u2014 ${await releaserLine(statusTracker,
|
|
23941
|
+
process.stdout.write(`releaser: ${r.name} \u2014 ${await releaserLine(statusTracker, route2.route, r.name, r.config.release)}
|
|
23509
23942
|
`);
|
|
23510
23943
|
}
|
|
23511
23944
|
try {
|
|
@@ -23550,14 +23983,14 @@ watermark: ${state.watermark()}
|
|
|
23550
23983
|
{
|
|
23551
23984
|
const prBased = /* @__PURE__ */ new Set();
|
|
23552
23985
|
let mentionNode = false;
|
|
23553
|
-
for (const c of [
|
|
23986
|
+
for (const c of [route2]) {
|
|
23554
23987
|
const { settled: repos, errors } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
|
|
23555
23988
|
for (const e of errors) {
|
|
23556
23989
|
pushAuth.push({ route: c.route, repo: e.repo, result: { state: "failed", detail: `could not resolve this repo's configuration: ${e.detail}`, hint: "fix the repo's .crew.yaml" } });
|
|
23557
23990
|
}
|
|
23558
23991
|
for (const r of repos) {
|
|
23559
23992
|
const remote = r.config.branch.remote;
|
|
23560
|
-
if (
|
|
23993
|
+
if (existsSync39(r.dir)) {
|
|
23561
23994
|
pushAuth.push({
|
|
23562
23995
|
route: c.route,
|
|
23563
23996
|
repo: r.name,
|
|
@@ -23582,7 +24015,7 @@ watermark: ${state.watermark()}
|
|
|
23582
24015
|
if (flag("json")) {
|
|
23583
24016
|
const problems = [];
|
|
23584
24017
|
let passed = 0;
|
|
23585
|
-
for (const c of [
|
|
24018
|
+
for (const c of [route2]) {
|
|
23586
24019
|
const { settled: repos, errors } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
|
|
23587
24020
|
for (const e of errors) {
|
|
23588
24021
|
problems.push({ severity: "blocker", kind: "repo_config_invalid", route: c.route, repo: e.repo, message: `could not resolve this repo's configuration: ${e.detail}`, remedy: "fix the repo's .crew.yaml" });
|
|
@@ -23602,19 +24035,19 @@ watermark: ${state.watermark()}
|
|
|
23602
24035
|
}
|
|
23603
24036
|
}
|
|
23604
24037
|
if (!flag("offline")) {
|
|
23605
|
-
for (const p of await probeSeatReads(
|
|
24038
|
+
for (const p of await probeSeatReads(route2, { userAgent: cfg.ship.userAgent })) {
|
|
23606
24039
|
if (p.state === "forbidden") {
|
|
23607
|
-
problems.push({ severity: "blocker", kind: "seat_cannot_read_issues", route:
|
|
24040
|
+
problems.push({ severity: "blocker", kind: "seat_cannot_read_issues", route: route2.route, message: `${p.role} agent key refused on ${p.tables.filter((t) => t.state === "forbidden").map((t) => t.table).join(", ")} (403)`, remedy: describeSeatForbidden(p.role, p.tables).replace(/^.*?— /, "") });
|
|
23608
24041
|
} else if (p.state === "ok") passed++;
|
|
23609
24042
|
}
|
|
23610
24043
|
}
|
|
23611
24044
|
for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
|
|
23612
24045
|
const previewProblem2 = await previewHostProblem(cfg);
|
|
23613
24046
|
if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
|
|
23614
|
-
if (cfg.routes.some((x) => x.route ===
|
|
23615
|
-
const { settled } = await settleRepos(reposOf(
|
|
23616
|
-
const tablePresent = flag("offline") ? void 0 : (await new Tracker(
|
|
23617
|
-
problems.push(...routePeriscopeProblems({ route:
|
|
24047
|
+
if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
|
|
24048
|
+
const { settled } = await settleRepos(reposOf(route2), (t) => t.name, (t) => resolveOneRepo(route2, t));
|
|
24049
|
+
const tablePresent = flag("offline") ? void 0 : (await new Tracker(route2, cfg.ship).periscopesStatus().catch(() => ({ present: true }))).present;
|
|
24050
|
+
problems.push(...routePeriscopeProblems({ route: route2.route, hosts: true, repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview })), tablePresent }));
|
|
23618
24051
|
}
|
|
23619
24052
|
problems.push(...attentionProblems(openShipAttention(state), problems));
|
|
23620
24053
|
if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
|
|
@@ -23640,7 +24073,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
23640
24073
|
}
|
|
23641
24074
|
try {
|
|
23642
24075
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
23643
|
-
const evSize =
|
|
24076
|
+
const evSize = existsSync39(evFile) ? statSync13(evFile).size : 0;
|
|
23644
24077
|
process.stdout.write(
|
|
23645
24078
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
23646
24079
|
` : "events log: ok\n"
|
|
@@ -23667,15 +24100,15 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
23667
24100
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
|
23668
24101
|
if (changed2) {
|
|
23669
24102
|
process.stdout.write(
|
|
23670
|
-
`daemon config: WARN ${changed2} changed at ${new Date(
|
|
24103
|
+
`daemon config: WARN ${changed2} changed at ${new Date(statSync13(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
|
|
23671
24104
|
`
|
|
23672
24105
|
);
|
|
23673
24106
|
}
|
|
23674
24107
|
}
|
|
23675
|
-
const tracker = new Tracker(
|
|
24108
|
+
const tracker = new Tracker(route2, cfg.ship);
|
|
23676
24109
|
{
|
|
23677
24110
|
const have = sentVersion(cfg.ship);
|
|
23678
|
-
const floor = await readCrewFloor(
|
|
24111
|
+
const floor = await readCrewFloor(route2, cfg.ship);
|
|
23679
24112
|
if (have && floor) {
|
|
23680
24113
|
process.stdout.write(
|
|
23681
24114
|
`crew version: ${belowFloor(have, floor) ? "WARN " : ""}${have} (workspace requires ${floor})${belowFloor(have, floor) ? " \u2014 update crew (the Crew app, or `npm install -g @tablation/crew`)" : ""}
|
|
@@ -23692,9 +24125,9 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
23692
24125
|
` + contractProblems.map((p) => ` - ${p}
|
|
23693
24126
|
`).join("") : "contract: consistent\n"
|
|
23694
24127
|
);
|
|
23695
|
-
for (const r of await resolvedRepos(
|
|
24128
|
+
for (const r of await resolvedRepos(route2)) {
|
|
23696
24129
|
const { config } = r;
|
|
23697
|
-
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${
|
|
24130
|
+
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename13(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
|
|
23698
24131
|
process.stdout.write(
|
|
23699
24132
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
23700
24133
|
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
|
@@ -23725,7 +24158,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
23725
24158
|
if (warn) process.stdout.write(` WARNING ${warn}
|
|
23726
24159
|
`);
|
|
23727
24160
|
}
|
|
23728
|
-
const scope = releaseLockScope(
|
|
24161
|
+
const scope = releaseLockScope(route2.route, r.name);
|
|
23729
24162
|
const lock2 = await tracker.lockState(scope).catch(() => void 0);
|
|
23730
24163
|
if (lock2?.holder && staleOwnLockClaim(lock2.holder, cfg.ship.name)) {
|
|
23731
24164
|
process.stdout.write(` ${formatStaleOwnLock(scope, lock2.holder)}`);
|
|
@@ -23742,10 +24175,10 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
23742
24175
|
`
|
|
23743
24176
|
);
|
|
23744
24177
|
const periscopes = await tracker.periscopesStatus();
|
|
23745
|
-
if (cfg.routes.some((x) => x.route ===
|
|
23746
|
-
const { settled } = await settleRepos(reposOf(
|
|
24178
|
+
if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
|
|
24179
|
+
const { settled } = await settleRepos(reposOf(route2), (t) => t.name, (t) => resolveOneRepo(route2, t));
|
|
23747
24180
|
for (const f of routePeriscopeProblems({
|
|
23748
|
-
route:
|
|
24181
|
+
route: route2.route,
|
|
23749
24182
|
hosts: true,
|
|
23750
24183
|
tablePresent: periscopes.present,
|
|
23751
24184
|
repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview }))
|
|
@@ -23760,26 +24193,26 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
23760
24193
|
`
|
|
23761
24194
|
);
|
|
23762
24195
|
process.stdout.write(
|
|
23763
|
-
|
|
24196
|
+
route2.resolved?.releaseSeat ? "release seat: this ship has its own Release Crew row\n" : "release seat: none \u2014 release comments are credited to the dev seat; run `crew connect` once the Crew table has this ship's row\n"
|
|
23764
24197
|
);
|
|
23765
|
-
const keyRole =
|
|
23766
|
-
const roleRoute =
|
|
24198
|
+
const keyRole = route2.resolved?.role ?? await fetchKeyRole(route2, cfg.ship.userAgent);
|
|
24199
|
+
const roleRoute = route2.resolved && keyRole ? { ...route2, resolved: { ...route2.resolved, role: keyRole } } : route2;
|
|
23767
24200
|
process.stdout.write(`api key role: ${keyRole ?? "unknown"}
|
|
23768
24201
|
`);
|
|
23769
|
-
for (const m of
|
|
24202
|
+
for (const m of route2.resolved?.capabilityMisses ?? []) process.stdout.write(`capability: MISSING ${m} (re-run \`crew connect\` once granted)
|
|
23770
24203
|
`);
|
|
23771
24204
|
const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
|
|
23772
24205
|
process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}
|
|
23773
24206
|
`);
|
|
23774
24207
|
for (const role of SEAT_KEY_ROLES) {
|
|
23775
|
-
if (!
|
|
24208
|
+
if (!route2.resolved?.seats[role]) continue;
|
|
23776
24209
|
const id = await resolveSeatKey(roleRoute, role);
|
|
23777
24210
|
process.stdout.write(
|
|
23778
24211
|
`seat identity: ${role} writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
|
|
23779
24212
|
`
|
|
23780
24213
|
);
|
|
23781
24214
|
}
|
|
23782
|
-
if (
|
|
24215
|
+
if (route2.resolved?.releaseSeat) {
|
|
23783
24216
|
const id = await resolveReleaseKey(roleRoute);
|
|
23784
24217
|
process.stdout.write(
|
|
23785
24218
|
`seat identity: release writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
|
|
@@ -23829,7 +24262,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
23829
24262
|
}
|
|
23830
24263
|
}
|
|
23831
24264
|
{
|
|
23832
|
-
const op =
|
|
24265
|
+
const op = route2.resolved?.operator;
|
|
23833
24266
|
const row = op ? crewRows.find((c) => c.id === op) : void 0;
|
|
23834
24267
|
if (!op) {
|
|
23835
24268
|
process.stdout.write("operator identity: no operator on this route \u2014 run `crew connect`\n");
|
|
@@ -23844,7 +24277,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
23844
24277
|
);
|
|
23845
24278
|
process.exitCode = 1;
|
|
23846
24279
|
} else {
|
|
23847
|
-
const resolves = memberByIdentity(buildRoster(configuredMembers(
|
|
24280
|
+
const resolves = memberByIdentity(buildRoster(configuredMembers(route2), crewRows), row.user_id);
|
|
23848
24281
|
process.stdout.write(
|
|
23849
24282
|
resolves ? `operator identity: comments from ${row.name ?? op} resolve to ${crewLabel(resolves)}
|
|
23850
24283
|
` : `operator identity: ERROR user_id ${row.user_id} does not resolve through the roster
|
|
@@ -23855,7 +24288,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
23855
24288
|
}
|
|
23856
24289
|
if (ships.length > 0) {
|
|
23857
24290
|
try {
|
|
23858
|
-
const humanHolds = new Set(holdIds(buildRoster(configuredMembers(
|
|
24291
|
+
const humanHolds = new Set(holdIds(buildRoster(configuredMembers(route2), crewRows)));
|
|
23859
24292
|
const warnings = holdWarnings(await tracker.openTickets(), ships, tracker.contract, humanHolds);
|
|
23860
24293
|
process.stdout.write(
|
|
23861
24294
|
warnings.length === 0 ? "ship holds: every in-flight ticket is held by a live ship\n" : warnings.map((w) => `ship holds: WARNING ${w}
|
|
@@ -23910,7 +24343,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
23910
24343
|
`);
|
|
23911
24344
|
}
|
|
23912
24345
|
}
|
|
23913
|
-
for (const [repoName, ov] of Object.entries(
|
|
24346
|
+
for (const [repoName, ov] of Object.entries(route2.repoOverrides)) {
|
|
23914
24347
|
if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
|
|
23915
24348
|
`);
|
|
23916
24349
|
}
|
|
@@ -23924,15 +24357,15 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
23924
24357
|
`
|
|
23925
24358
|
);
|
|
23926
24359
|
}
|
|
23927
|
-
if (
|
|
23928
|
-
const mcpUrl = mcpUrlFor(
|
|
24360
|
+
if (route2.apiKey) {
|
|
24361
|
+
const mcpUrl = mcpUrlFor(route2.baseUrl);
|
|
23929
24362
|
const seenMcp = /* @__PURE__ */ new Set();
|
|
23930
24363
|
for (const tc of shipToolchains(cfg.ship)) {
|
|
23931
24364
|
const id = `${tc.harness}\0${tc.bin}`;
|
|
23932
24365
|
if (seenMcp.has(id)) continue;
|
|
23933
24366
|
seenMcp.add(id);
|
|
23934
24367
|
const adapter = adapterFor(tc.harness);
|
|
23935
|
-
const m = adapter.inspectMcp({ bin: tc.bin, url: mcpUrl, key:
|
|
24368
|
+
const m = adapter.inspectMcp({ bin: tc.bin, url: mcpUrl, key: route2.apiKey });
|
|
23936
24369
|
process.stdout.write(
|
|
23937
24370
|
m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
|
|
23938
24371
|
` : m.state === "no-binary" ? `mcp: ${tc.bin} not found \u2014 ${adapter.displayName} was not checked
|
|
@@ -23944,22 +24377,22 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
23944
24377
|
}
|
|
23945
24378
|
for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
|
|
23946
24379
|
const inst = pairInstallerFor(h);
|
|
23947
|
-
const v = inst.verifyPair({ url: mcpUrlFor(
|
|
24380
|
+
const v = inst.verifyPair({ url: mcpUrlFor(route2.baseUrl) });
|
|
23948
24381
|
process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
|
|
23949
24382
|
` : ` NOT installed \u2014 ${v.detail}; run crew connect
|
|
23950
24383
|
`));
|
|
23951
|
-
const envWarn = inst.envWarning?.({ url: mcpUrlFor(
|
|
24384
|
+
const envWarn = inst.envWarning?.({ url: mcpUrlFor(route2.baseUrl) });
|
|
23952
24385
|
if (envWarn) process.stdout.write(`pair ${h} env:`.padEnd(19) + ` WARN \u2014 ${envWarn}
|
|
23953
24386
|
`);
|
|
23954
24387
|
}
|
|
23955
24388
|
const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
|
|
23956
|
-
if (
|
|
24389
|
+
if (route2.apiKey && route2.resolved) {
|
|
23957
24390
|
let flagged = 0;
|
|
23958
24391
|
for (const role of ROLE_NAMES) {
|
|
23959
|
-
const persona = await fetchSeatPersona(
|
|
24392
|
+
const persona = await fetchSeatPersona(route2, role, { userAgent: cfg.ship.userAgent });
|
|
23960
24393
|
if (persona?.vendor && !shipVendors.has(persona.vendor)) {
|
|
23961
24394
|
flagged++;
|
|
23962
|
-
process.stdout.write(`seat vendor: ${role} persona wants "${persona.vendor}" \u2014 no toolchain on this ship serves it (have: ${[...shipVendors].join(", ")}); it will run ${resolveSeat(cfg.ship,
|
|
24395
|
+
process.stdout.write(`seat vendor: ${role} persona wants "${persona.vendor}" \u2014 no toolchain on this ship serves it (have: ${[...shipVendors].join(", ")}); it will run ${resolveSeat(cfg.ship, route2, role).toolchain.name} with a warning
|
|
23963
24396
|
`);
|
|
23964
24397
|
}
|
|
23965
24398
|
}
|