@tablation/crew 0.50.1 → 0.50.3
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 +259 -127
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/doctor-repo.ts
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
function doctorBaseReport(r, base, health) {
|
|
6
|
+
const pad = " ";
|
|
7
|
+
if (!existsSync(r.dir)) {
|
|
8
|
+
return {
|
|
9
|
+
text: `${pad}- ${r.dir} is missing on disk \u2014 no .crew.yaml read, settings above are defaults
|
|
10
|
+
`,
|
|
11
|
+
failed: true,
|
|
12
|
+
skipRest: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const bh = health(r);
|
|
17
|
+
if (bh.action === "not-applicable") return { text: "", failed: false, skipRest: false };
|
|
18
|
+
return {
|
|
19
|
+
text: `${pad}base ${base}: ${bh.unsafe ? "ERROR \u2014 " : ""}${bh.detail}
|
|
20
|
+
`,
|
|
21
|
+
failed: bh.unsafe,
|
|
22
|
+
skipRest: false
|
|
23
|
+
};
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return {
|
|
26
|
+
text: `${pad}base ${base}: could not check \u2014 ${e instanceof Error ? e.message : String(e)}
|
|
27
|
+
`,
|
|
28
|
+
failed: true,
|
|
29
|
+
skipRest: false
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
3
34
|
// src/cli.ts
|
|
4
35
|
import { resolve as resolve4, dirname as dirname12, join as join25, basename as basename9 } from "path";
|
|
5
36
|
import { fileURLToPath } from "url";
|
|
@@ -62,7 +93,7 @@ function pickWorktreeRoute(ticketNumber, candidates) {
|
|
|
62
93
|
}
|
|
63
94
|
|
|
64
95
|
// src/config.ts
|
|
65
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
96
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
66
97
|
import { resolve, dirname as dirname3, isAbsolute, join as join7, basename as basename3 } from "path";
|
|
67
98
|
import { homedir as homedir5, tmpdir } from "os";
|
|
68
99
|
import { parse as parse2 } from "yaml";
|
|
@@ -71,7 +102,7 @@ import { parse as parse2 } from "yaml";
|
|
|
71
102
|
import {
|
|
72
103
|
appendFileSync,
|
|
73
104
|
mkdirSync,
|
|
74
|
-
existsSync,
|
|
105
|
+
existsSync as existsSync2,
|
|
75
106
|
statSync,
|
|
76
107
|
renameSync,
|
|
77
108
|
openSync,
|
|
@@ -90,7 +121,7 @@ var ROTATED_EVENTS_RE = /^events\.\d{8}T\d{6}Z(?:-\d+)?\.jsonl$/;
|
|
|
90
121
|
var ROTATE_LOCK_STALE_MS = 6e4;
|
|
91
122
|
function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
|
|
92
123
|
try {
|
|
93
|
-
if (!
|
|
124
|
+
if (!existsSync2(file) || statSync(file).size <= maxBytes) return void 0;
|
|
94
125
|
} catch {
|
|
95
126
|
return void 0;
|
|
96
127
|
}
|
|
@@ -111,7 +142,7 @@ function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
|
|
|
111
142
|
if (statSync(file).size <= maxBytes) return void 0;
|
|
112
143
|
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
113
144
|
let dest = join(dirname(file), `events.${stamp}.jsonl`);
|
|
114
|
-
for (let n = 1;
|
|
145
|
+
for (let n = 1; existsSync2(dest); n++) dest = join(dirname(file), `events.${stamp}-${n}.jsonl`);
|
|
115
146
|
renameSync(file, dest);
|
|
116
147
|
return dest;
|
|
117
148
|
} finally {
|
|
@@ -213,7 +244,7 @@ var Emitter = class _Emitter {
|
|
|
213
244
|
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
214
245
|
|
|
215
246
|
// src/pair-install.ts
|
|
216
|
-
import { existsSync as
|
|
247
|
+
import { existsSync as existsSync5, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
217
248
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
218
249
|
import { homedir as homedir4 } from "os";
|
|
219
250
|
import { join as join5 } from "path";
|
|
@@ -342,7 +373,7 @@ function removeClaudeMcp(o) {
|
|
|
342
373
|
|
|
343
374
|
// src/launcher.ts
|
|
344
375
|
import { basename, join as join3 } from "path";
|
|
345
|
-
import { existsSync as
|
|
376
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
346
377
|
import { homedir as homedir2 } from "os";
|
|
347
378
|
function isOriLauncher(spec) {
|
|
348
379
|
return !!spec.launcher && basename(spec.launcher) === "ori";
|
|
@@ -405,7 +436,7 @@ function omlxModelPresent(model, dirs) {
|
|
|
405
436
|
}
|
|
406
437
|
function readOmlxSettings(home = homedir2()) {
|
|
407
438
|
const p = join3(home, ".omlx", "settings.json");
|
|
408
|
-
return
|
|
439
|
+
return existsSync3(p) ? parseOmlxSettings(readFileSync3(p, "utf8")) : void 0;
|
|
409
440
|
}
|
|
410
441
|
|
|
411
442
|
// src/capability.ts
|
|
@@ -542,7 +573,7 @@ import { join as join4 } from "path";
|
|
|
542
573
|
import { rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
543
574
|
|
|
544
575
|
// src/pair-files.ts
|
|
545
|
-
import { existsSync as
|
|
576
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync, rmSync as rmSync2 } from "fs";
|
|
546
577
|
import { dirname as dirname2 } from "path";
|
|
547
578
|
var PAIR_BLOCK_START = "<!-- crew:pair start -->";
|
|
548
579
|
var PAIR_BLOCK_END = "<!-- crew:pair end -->";
|
|
@@ -583,7 +614,7 @@ function removePairBlock(text) {
|
|
|
583
614
|
return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
584
615
|
}
|
|
585
616
|
function readIfExists(path) {
|
|
586
|
-
return
|
|
617
|
+
return existsSync4(path) ? readFileSync4(path, "utf8") : void 0;
|
|
587
618
|
}
|
|
588
619
|
function writeFileMkdir(path, text) {
|
|
589
620
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
@@ -980,7 +1011,7 @@ function vscodeUserDirs(o) {
|
|
|
980
1011
|
const platform2 = o.platform ?? process.platform;
|
|
981
1012
|
const root = platform2 === "darwin" ? join5(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join5(home, "AppData", "Roaming") : join5(home, ".config");
|
|
982
1013
|
const all = VSCODE_FLAVOURS.map((f) => join5(root, f, "User"));
|
|
983
|
-
const present = all.filter((d) =>
|
|
1014
|
+
const present = all.filter((d) => existsSync5(d));
|
|
984
1015
|
return present.length > 0 ? present : [all[0]];
|
|
985
1016
|
}
|
|
986
1017
|
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
@@ -1372,7 +1403,7 @@ function explain(ship, required) {
|
|
|
1372
1403
|
}
|
|
1373
1404
|
|
|
1374
1405
|
// src/repo-config.ts
|
|
1375
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
1406
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
1376
1407
|
import { join as join6, basename as basename2 } from "path";
|
|
1377
1408
|
import { parse } from "yaml";
|
|
1378
1409
|
var SPEC_VERSION = 1;
|
|
@@ -1456,7 +1487,7 @@ var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
|
1456
1487
|
function findRepoConfig(dir) {
|
|
1457
1488
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
1458
1489
|
const p = join6(dir, name);
|
|
1459
|
-
if (
|
|
1490
|
+
if (existsSync6(p)) return p;
|
|
1460
1491
|
}
|
|
1461
1492
|
return null;
|
|
1462
1493
|
}
|
|
@@ -2181,7 +2212,7 @@ function configSearchPath(crewHome) {
|
|
|
2181
2212
|
}
|
|
2182
2213
|
function findConfigFile(crewHome) {
|
|
2183
2214
|
const candidates = configSearchPath(crewHome);
|
|
2184
|
-
return candidates.find((c) =>
|
|
2215
|
+
return candidates.find((c) => existsSync7(c)) ?? candidates[candidates.length - 1];
|
|
2185
2216
|
}
|
|
2186
2217
|
var defaultConfigPath = () => join7(process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config"), "crew", "crew.yaml");
|
|
2187
2218
|
function routeSlug(route) {
|
|
@@ -2204,7 +2235,7 @@ function splitRoute(route) {
|
|
|
2204
2235
|
}
|
|
2205
2236
|
function readResolvedFile(stateDir, route) {
|
|
2206
2237
|
const path = resolvedPathFor(stateDir, route);
|
|
2207
|
-
if (!
|
|
2238
|
+
if (!existsSync7(path)) return void 0;
|
|
2208
2239
|
try {
|
|
2209
2240
|
return JSON.parse(readFileSync6(path, "utf8"));
|
|
2210
2241
|
} catch (e) {
|
|
@@ -2257,13 +2288,14 @@ function parseResolved(raw, m, where) {
|
|
|
2257
2288
|
contract: raw.contract ?? void 0,
|
|
2258
2289
|
...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
|
|
2259
2290
|
...typeof raw.releaseAgentId === "string" && raw.releaseAgentId ? { releaseAgentId: raw.releaseAgentId } : {},
|
|
2291
|
+
...typeof raw.role === "string" && raw.role ? { role: raw.role } : {},
|
|
2260
2292
|
...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
|
|
2261
2293
|
...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
|
|
2262
2294
|
};
|
|
2263
2295
|
}
|
|
2264
2296
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
2265
2297
|
const file = configFile ?? findConfigFile(crewHome);
|
|
2266
|
-
if (opts.text === void 0 && !
|
|
2298
|
+
if (opts.text === void 0 && !existsSync7(file)) {
|
|
2267
2299
|
throw new ConfigError(
|
|
2268
2300
|
`no config found. Looked in:
|
|
2269
2301
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
@@ -2573,7 +2605,7 @@ function repoIdForName(r, name) {
|
|
|
2573
2605
|
}
|
|
2574
2606
|
function resolveApiKey(r) {
|
|
2575
2607
|
if (r.apiKey) return r.apiKey;
|
|
2576
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
2608
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
|
|
2577
2609
|
for (const line of readFileSync6(r.apiKeyFile, "utf8").split("\n")) {
|
|
2578
2610
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
2579
2611
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
@@ -2793,7 +2825,7 @@ function rosterMarkdown(roster, meId) {
|
|
|
2793
2825
|
}
|
|
2794
2826
|
|
|
2795
2827
|
// src/state.ts
|
|
2796
|
-
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as
|
|
2828
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
|
|
2797
2829
|
import { join as join8 } from "path";
|
|
2798
2830
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
2799
2831
|
var State = class _State {
|
|
@@ -2839,10 +2871,10 @@ var State = class _State {
|
|
|
2839
2871
|
}
|
|
2840
2872
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
2841
2873
|
isPaused() {
|
|
2842
|
-
return
|
|
2874
|
+
return existsSync8(this.path(".crew-paused"));
|
|
2843
2875
|
}
|
|
2844
2876
|
isRolePaused(role) {
|
|
2845
|
-
return
|
|
2877
|
+
return existsSync8(this.path(`.role-paused-${role}`));
|
|
2846
2878
|
}
|
|
2847
2879
|
pausedRoles(roles) {
|
|
2848
2880
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
@@ -2859,7 +2891,7 @@ var State = class _State {
|
|
|
2859
2891
|
* while agents keep working.
|
|
2860
2892
|
*/
|
|
2861
2893
|
isReleasePaused() {
|
|
2862
|
-
return
|
|
2894
|
+
return existsSync8(this.path(".release-paused"));
|
|
2863
2895
|
}
|
|
2864
2896
|
setReleasePaused(paused) {
|
|
2865
2897
|
if (paused) writeFileSync4(this.path(".release-paused"), "");
|
|
@@ -3507,7 +3539,7 @@ function describeRelease(o, route) {
|
|
|
3507
3539
|
// src/install.ts
|
|
3508
3540
|
import { dirname as dirname4, join as join9, extname } from "path";
|
|
3509
3541
|
import { homedir as homedir6 } from "os";
|
|
3510
|
-
import { accessSync, constants, existsSync as
|
|
3542
|
+
import { accessSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
|
|
3511
3543
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3512
3544
|
import { createHash } from "crypto";
|
|
3513
3545
|
|
|
@@ -3882,7 +3914,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3882
3914
|
}
|
|
3883
3915
|
}
|
|
3884
3916
|
for (const path of plan.unitPaths) {
|
|
3885
|
-
if (!
|
|
3917
|
+
if (!existsSync9(path)) continue;
|
|
3886
3918
|
if (dryRun) {
|
|
3887
3919
|
log.emit(`would remove ${path}`);
|
|
3888
3920
|
continue;
|
|
@@ -3918,7 +3950,7 @@ function loadedCrewLabels() {
|
|
|
3918
3950
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3919
3951
|
}
|
|
3920
3952
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
3921
|
-
const files =
|
|
3953
|
+
const files = existsSync9(dir) ? readdirSync2(dir) : [];
|
|
3922
3954
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
3923
3955
|
const plistPath = join9(dir, f);
|
|
3924
3956
|
let program = null;
|
|
@@ -3930,7 +3962,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
|
3930
3962
|
label: f.slice(0, -".plist".length),
|
|
3931
3963
|
plistPath,
|
|
3932
3964
|
program,
|
|
3933
|
-
programExists: program !== null &&
|
|
3965
|
+
programExists: program !== null && existsSync9(program)
|
|
3934
3966
|
};
|
|
3935
3967
|
});
|
|
3936
3968
|
const seen = new Set(units.map((u) => u.label));
|
|
@@ -3949,9 +3981,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3949
3981
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3950
3982
|
return;
|
|
3951
3983
|
}
|
|
3952
|
-
if (
|
|
3984
|
+
if (existsSync9(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3953
3985
|
spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3954
|
-
if (
|
|
3986
|
+
if (existsSync9(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3955
3987
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3956
3988
|
}
|
|
3957
3989
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -4034,7 +4066,7 @@ import { statfsSync } from "fs";
|
|
|
4034
4066
|
// src/git.ts
|
|
4035
4067
|
import { execFileSync } from "child_process";
|
|
4036
4068
|
import {
|
|
4037
|
-
existsSync as
|
|
4069
|
+
existsSync as existsSync10,
|
|
4038
4070
|
mkdirSync as mkdirSync5,
|
|
4039
4071
|
openSync as openSync2,
|
|
4040
4072
|
closeSync as closeSync2,
|
|
@@ -4434,7 +4466,7 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
4434
4466
|
try {
|
|
4435
4467
|
const st = statSync4(root);
|
|
4436
4468
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
4437
|
-
if (
|
|
4469
|
+
if (existsSync10(join10(root, "checkout"))) continue;
|
|
4438
4470
|
rmSync6(root, { recursive: true, force: true });
|
|
4439
4471
|
removed.push(name);
|
|
4440
4472
|
} catch {
|
|
@@ -4477,7 +4509,7 @@ function originUrlOf(dir, remoteName) {
|
|
|
4477
4509
|
}
|
|
4478
4510
|
}
|
|
4479
4511
|
function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
4480
|
-
if (
|
|
4512
|
+
if (existsSync10(`${dir}/.git`)) return false;
|
|
4481
4513
|
if (!remote) {
|
|
4482
4514
|
throw new GitError(
|
|
4483
4515
|
`${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
|
|
@@ -4540,10 +4572,10 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
4540
4572
|
function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
|
|
4541
4573
|
const release = acquireCheckoutLock(dir);
|
|
4542
4574
|
try {
|
|
4543
|
-
if (!
|
|
4575
|
+
if (!existsSync10(`${dir}/.git`) && githubRemote) {
|
|
4544
4576
|
const url = cloneUrlFor(githubRemote, clone);
|
|
4545
4577
|
const marker = cloneFailureMarker(dir);
|
|
4546
|
-
if (
|
|
4578
|
+
if (existsSync10(marker) && readFileSync9(marker, "utf8").split("\n")[0] === url) {
|
|
4547
4579
|
return { action: "clone-failed", detail: readFileSync9(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
|
|
4548
4580
|
}
|
|
4549
4581
|
try {
|
|
@@ -7126,7 +7158,7 @@ var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id ===
|
|
|
7126
7158
|
|
|
7127
7159
|
// src/agent.ts
|
|
7128
7160
|
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
7129
|
-
import { readFileSync as readFileSync12, existsSync as
|
|
7161
|
+
import { readFileSync as readFileSync12, existsSync as existsSync13, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
|
|
7130
7162
|
import { StringDecoder } from "string_decoder";
|
|
7131
7163
|
import { finished } from "stream/promises";
|
|
7132
7164
|
import { join as join14, dirname as dirname6 } from "path";
|
|
@@ -7544,7 +7576,7 @@ function expandSkillMarkers(text, lookup, opts = {}) {
|
|
|
7544
7576
|
}
|
|
7545
7577
|
|
|
7546
7578
|
// src/skills.ts
|
|
7547
|
-
import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as
|
|
7579
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync12 } from "fs";
|
|
7548
7580
|
import { join as join13 } from "path";
|
|
7549
7581
|
import { parse as parseYaml } from "yaml";
|
|
7550
7582
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError8, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
@@ -7561,7 +7593,7 @@ ${additions}`;
|
|
|
7561
7593
|
}
|
|
7562
7594
|
|
|
7563
7595
|
// src/agents.ts
|
|
7564
|
-
import { readFileSync as readFileSync10, existsSync as
|
|
7596
|
+
import { readFileSync as readFileSync10, existsSync as existsSync11 } from "fs";
|
|
7565
7597
|
import { join as join12 } from "path";
|
|
7566
7598
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError7, TablationApiError } from "@tablation/client";
|
|
7567
7599
|
var PERSONA_NAME = {
|
|
@@ -7577,10 +7609,10 @@ var AgentsSyncError = class extends Error {
|
|
|
7577
7609
|
};
|
|
7578
7610
|
function personaDefaultPrompt(promptsDir, role) {
|
|
7579
7611
|
const briefPath = join12(promptsDir, "personas", `lane-${role}.md`);
|
|
7580
|
-
if (!
|
|
7612
|
+
if (!existsSync11(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
7581
7613
|
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync10(briefPath, "utf8");
|
|
7582
7614
|
const commonPath = join12(promptsDir, "personas", "common.md");
|
|
7583
|
-
if (!
|
|
7615
|
+
if (!existsSync11(commonPath)) return readFileSync10(briefPath, "utf8");
|
|
7584
7616
|
return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
|
|
7585
7617
|
}
|
|
7586
7618
|
async function resolveAgentId(route, role, opts) {
|
|
@@ -7693,6 +7725,25 @@ async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
|
|
|
7693
7725
|
return void 0;
|
|
7694
7726
|
}
|
|
7695
7727
|
}
|
|
7728
|
+
function isAdminRole(role) {
|
|
7729
|
+
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
7730
|
+
}
|
|
7731
|
+
async function fetchKeyRole(route, userAgent) {
|
|
7732
|
+
if (!route.resolved) return void 0;
|
|
7733
|
+
try {
|
|
7734
|
+
const res = await fetch(`${route.baseUrl.replace(/\/+$/, "")}/api/auth/me?workspaceId=${route.resolved.workspaceId}`, {
|
|
7735
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
|
|
7736
|
+
});
|
|
7737
|
+
if (!res.ok) return void 0;
|
|
7738
|
+
const me = await res.json();
|
|
7739
|
+
return typeof me.role === "string" ? me.role : void 0;
|
|
7740
|
+
} catch {
|
|
7741
|
+
return void 0;
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7744
|
+
function isForbidden(e) {
|
|
7745
|
+
return e instanceof TablationApiError && e.status === 403;
|
|
7746
|
+
}
|
|
7696
7747
|
function promptSourceFields(model, prompt) {
|
|
7697
7748
|
return model.fields?.some((f) => f.columnName === "prompt_baseline") ? { prompt_source: "crew", prompt_baseline: prompt } : {};
|
|
7698
7749
|
}
|
|
@@ -7736,6 +7787,16 @@ async function syncPersonas(route, opts) {
|
|
|
7736
7787
|
const name = PERSONA_NAME[role];
|
|
7737
7788
|
const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
|
|
7738
7789
|
const row = rows.find((r) => r.name === name);
|
|
7790
|
+
if (opts.readOnly) {
|
|
7791
|
+
if (!row) {
|
|
7792
|
+
outcomes.push({ role, action: "missing", agentId: "" });
|
|
7793
|
+
continue;
|
|
7794
|
+
}
|
|
7795
|
+
const prior = cache[role]?.agentId === row.id ? cache[role] : void 0;
|
|
7796
|
+
agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: prior?.lastSyncedUpdatedAt ?? row.updated_at, ...prior?.historyId ? { historyId: prior.historyId } : {} };
|
|
7797
|
+
outcomes.push({ role, action: "adopted", agentId: row.id });
|
|
7798
|
+
continue;
|
|
7799
|
+
}
|
|
7739
7800
|
if (!row) {
|
|
7740
7801
|
if (opts.dryRun) {
|
|
7741
7802
|
outcomes.push({ role, action: "created", agentId: "(dry run)" });
|
|
@@ -7837,35 +7898,70 @@ async function syncPersonas(route, opts) {
|
|
|
7837
7898
|
throw e;
|
|
7838
7899
|
}
|
|
7839
7900
|
}
|
|
7840
|
-
const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent);
|
|
7841
|
-
const
|
|
7842
|
-
return {
|
|
7901
|
+
const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent, opts.readOnly === true);
|
|
7902
|
+
const release = opts.dryRun ? { agentId: route.resolved.releaseAgentId } : await linkReleaseSeat(route, client2, agentsModel, opts.userAgent, opts.readOnly === true);
|
|
7903
|
+
return {
|
|
7904
|
+
outcomes,
|
|
7905
|
+
agentPersonas,
|
|
7906
|
+
crewLinks,
|
|
7907
|
+
...release.agentId ? { releaseAgentId: release.agentId } : {},
|
|
7908
|
+
...release.note ? { releaseNote: release.note } : {},
|
|
7909
|
+
...opts.readOnly ? { readOnly: true } : {}
|
|
7910
|
+
};
|
|
7843
7911
|
}
|
|
7844
|
-
async function linkReleaseSeat(route, client2, agentsModel, userAgent) {
|
|
7912
|
+
async function linkReleaseSeat(route, client2, agentsModel, userAgent, readOnly) {
|
|
7845
7913
|
const seatId = route.resolved.releaseSeat;
|
|
7846
|
-
if (!seatId) return
|
|
7914
|
+
if (!seatId) return {};
|
|
7847
7915
|
const rows = await client2.records.list(agentsModel.id, { limit: 200 });
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7916
|
+
let agentId = rows.find((r) => r.name === RELEASE_AGENT_NAME)?.id;
|
|
7917
|
+
if (!agentId) {
|
|
7918
|
+
if (readOnly) return { note: `Release: no "${RELEASE_AGENT_NAME}" Agents row in this workspace \u2014 writes as the ship until a workspace admin runs \`crew agents sync\`` };
|
|
7919
|
+
try {
|
|
7920
|
+
agentId = (await client2.records.create(agentsModel.id, {
|
|
7921
|
+
name: RELEASE_AGENT_NAME,
|
|
7922
|
+
prompt: RELEASE_AGENT_PROMPT,
|
|
7923
|
+
...promptSourceFields(agentsModel, RELEASE_AGENT_PROMPT)
|
|
7924
|
+
})).id;
|
|
7925
|
+
} catch (e) {
|
|
7926
|
+
if (isForbidden(e)) return { note: `Release: could not create the "${RELEASE_AGENT_NAME}" Agents row (${NO_WRITE_NOTE}) \u2014 writes as the ship` };
|
|
7927
|
+
throw e;
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7853
7930
|
const crewModelId = route.resolved.models.crew;
|
|
7854
7931
|
const crewModel = await client2.dataModels.get(crewModelId);
|
|
7855
|
-
|
|
7932
|
+
let columnName;
|
|
7933
|
+
try {
|
|
7934
|
+
columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModel.id, userAgent);
|
|
7935
|
+
} catch (e) {
|
|
7936
|
+
if (e instanceof CrewAgentFieldMissing) return { note: `Release: ${CREW_FIELD_MISSING_NOTE}` };
|
|
7937
|
+
throw e;
|
|
7938
|
+
}
|
|
7856
7939
|
const seatRow = await client2.records.get(crewModelId, seatId);
|
|
7857
7940
|
if (seatRow[CREW_AGENT_COLUMN] !== agentId) await client2.records.update(crewModelId, seatId, { [columnName]: agentId });
|
|
7858
|
-
return agentId;
|
|
7941
|
+
return { agentId };
|
|
7859
7942
|
}
|
|
7860
|
-
|
|
7943
|
+
var NO_WRITE_NOTE = "this key has no write access to the Agents table";
|
|
7944
|
+
var CREW_FIELD_MISSING_NOTE = "Crew table has no Agent field yet \u2014 a workspace admin's `crew agents sync` adds it";
|
|
7945
|
+
var CrewAgentFieldMissing = class extends Error {
|
|
7946
|
+
};
|
|
7947
|
+
async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent, readOnly) {
|
|
7861
7948
|
const seats2 = route.resolved.seats;
|
|
7862
7949
|
const rolesWithSeats = ROLE_NAMES.filter((role) => seats2[role] && agentPersonas[role]);
|
|
7863
7950
|
if (rolesWithSeats.length === 0) {
|
|
7951
|
+
if (readOnly) return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agents_row" : "no_seat" }));
|
|
7864
7952
|
return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "persona_diverged" : "no_seat" }));
|
|
7865
7953
|
}
|
|
7866
7954
|
const crewModelId = route.resolved.models.crew;
|
|
7867
7955
|
const crewModel = await client2.dataModels.get(crewModelId);
|
|
7868
|
-
|
|
7956
|
+
let columnName;
|
|
7957
|
+
try {
|
|
7958
|
+
columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent);
|
|
7959
|
+
} catch (e) {
|
|
7960
|
+
if (e instanceof CrewAgentFieldMissing) {
|
|
7961
|
+
return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agent_field" : "no_seat" }));
|
|
7962
|
+
}
|
|
7963
|
+
throw e;
|
|
7964
|
+
}
|
|
7869
7965
|
const outcomes = [];
|
|
7870
7966
|
for (const role of ROLE_NAMES) {
|
|
7871
7967
|
const seatId = seats2[role];
|
|
@@ -7875,7 +7971,7 @@ async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userA
|
|
|
7875
7971
|
continue;
|
|
7876
7972
|
}
|
|
7877
7973
|
if (!agentId) {
|
|
7878
|
-
outcomes.push({ role, action: "persona_diverged" });
|
|
7974
|
+
outcomes.push({ role, action: readOnly ? "no_agents_row" : "persona_diverged" });
|
|
7879
7975
|
continue;
|
|
7880
7976
|
}
|
|
7881
7977
|
const seatRow = await client2.records.get(crewModelId, seatId);
|
|
@@ -7916,6 +8012,7 @@ async function ensureCrewAgentField(route, client2, crewModel, agentsModelId, us
|
|
|
7916
8012
|
description: "The Agents-table row backing this seat's persona \u2014 set by `crew agents sync` (ISSUE-416), never edited by hand."
|
|
7917
8013
|
})
|
|
7918
8014
|
});
|
|
8015
|
+
if (createRes.status === 403) throw new CrewAgentFieldMissing();
|
|
7919
8016
|
if (!createRes.ok) throw new AgentsSyncError(`creating the Crew table's Agent field failed: ${createRes.status} ${createRes.statusText}`);
|
|
7920
8017
|
const created = await createRes.json();
|
|
7921
8018
|
return created.columnName;
|
|
@@ -7933,6 +8030,10 @@ function describeSyncOutcome(o) {
|
|
|
7933
8030
|
return `${label}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.agentId} in the app before syncing again`;
|
|
7934
8031
|
case "overridden":
|
|
7935
8032
|
return `${label}: override in place \u2014 prompt not synced (clear "Customize prompt" on the Agents row to resume)`;
|
|
8033
|
+
case "adopted":
|
|
8034
|
+
return `${label}: using the workspace's existing Agents row (${o.agentId})`;
|
|
8035
|
+
case "missing":
|
|
8036
|
+
return `${label}: no Agents row in this workspace \u2014 writes as the ship until a workspace admin runs \`crew agents sync\``;
|
|
7936
8037
|
case "forced":
|
|
7937
8038
|
return `${label}: --force overwrote local edits (previous prompt was: ${JSON.stringify(o.previousPrompt ?? "")})`;
|
|
7938
8039
|
}
|
|
@@ -7946,6 +8047,10 @@ function describeCrewLink(o) {
|
|
|
7946
8047
|
return `${label}: Crew row already references its Agents row`;
|
|
7947
8048
|
case "no_seat":
|
|
7948
8049
|
return `${label}: no seat configured for this role \u2014 skipped linking`;
|
|
8050
|
+
case "no_agent_field":
|
|
8051
|
+
return `${label}: ${CREW_FIELD_MISSING_NOTE}`;
|
|
8052
|
+
case "no_agents_row":
|
|
8053
|
+
return `${label}: no Agents row to link yet \u2014 a workspace admin's \`crew agents sync\` creates it; run it here afterwards to link this seat`;
|
|
7949
8054
|
case "persona_diverged":
|
|
7950
8055
|
return `${label}: persona has local edits and was not synced this pass \u2014 skipped linking until it's resolved`;
|
|
7951
8056
|
}
|
|
@@ -7969,7 +8074,7 @@ function parseSkillFile(path) {
|
|
|
7969
8074
|
}
|
|
7970
8075
|
function listLocalSkills(promptsDir) {
|
|
7971
8076
|
const dir = join13(promptsDir, "skills");
|
|
7972
|
-
if (!
|
|
8077
|
+
if (!existsSync12(dir)) return [];
|
|
7973
8078
|
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join13(dir, f)));
|
|
7974
8079
|
}
|
|
7975
8080
|
async function syncSkills(route, opts) {
|
|
@@ -8222,17 +8327,17 @@ var AgentSpawnError = class extends AgentError {
|
|
|
8222
8327
|
};
|
|
8223
8328
|
function planAgentRun(o) {
|
|
8224
8329
|
const briefPath = join14(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
8225
|
-
if (!
|
|
8330
|
+
if (!existsSync13(briefPath)) {
|
|
8226
8331
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
8227
8332
|
}
|
|
8228
8333
|
const commonPath = join14(o.route.promptsDir, "personas", "common.md");
|
|
8229
|
-
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) &&
|
|
8334
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync13(commonPath);
|
|
8230
8335
|
const now = o.now ?? (() => Date.now());
|
|
8231
8336
|
const routeLabel = routeSlug(o.route.route);
|
|
8232
8337
|
const digestPath2 = join14(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
8233
8338
|
let digest;
|
|
8234
8339
|
let ageSeconds;
|
|
8235
|
-
if (
|
|
8340
|
+
if (existsSync13(digestPath2)) {
|
|
8236
8341
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
8237
8342
|
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync12(digestPath2, "utf8");
|
|
8238
8343
|
}
|
|
@@ -8735,12 +8840,12 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
8735
8840
|
}
|
|
8736
8841
|
|
|
8737
8842
|
// src/run-worktree.ts
|
|
8738
|
-
import { existsSync as
|
|
8843
|
+
import { existsSync as existsSync15 } from "fs";
|
|
8739
8844
|
import { dirname as dirname7, join as join16 } from "path";
|
|
8740
8845
|
|
|
8741
8846
|
// src/ports.ts
|
|
8742
8847
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
8743
|
-
import { existsSync as
|
|
8848
|
+
import { existsSync as existsSync14 } from "fs";
|
|
8744
8849
|
import { join as join15 } from "path";
|
|
8745
8850
|
var run = (cmd, args) => {
|
|
8746
8851
|
try {
|
|
@@ -8786,7 +8891,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
8786
8891
|
}
|
|
8787
8892
|
function worktreeExists(parent, prefix, n) {
|
|
8788
8893
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
8789
|
-
if (
|
|
8894
|
+
if (existsSync14(join15(parent, name, ".git"))) return true;
|
|
8790
8895
|
}
|
|
8791
8896
|
return false;
|
|
8792
8897
|
}
|
|
@@ -8862,7 +8967,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
8862
8967
|
const existing = worktreeForNumber(dir, num);
|
|
8863
8968
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
8864
8969
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
8865
|
-
if (existing && !existing.prunable &&
|
|
8970
|
+
if (existing && !existing.prunable && existsSync15(existing.path)) {
|
|
8866
8971
|
const branch = existing.branch;
|
|
8867
8972
|
if (status(existing.path).length > 0) {
|
|
8868
8973
|
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
@@ -8927,7 +9032,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
8927
9032
|
}
|
|
8928
9033
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
8929
9034
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
8930
|
-
if (!wt || !
|
|
9035
|
+
if (!wt || !existsSync15(wt.path)) return { kind: "none" };
|
|
8931
9036
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
8932
9037
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
8933
9038
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -8939,7 +9044,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
8939
9044
|
if (pids.length) await killGently(pids);
|
|
8940
9045
|
}
|
|
8941
9046
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
8942
|
-
return
|
|
9047
|
+
return existsSync15(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
8943
9048
|
}
|
|
8944
9049
|
|
|
8945
9050
|
// src/handoff-guard.ts
|
|
@@ -9175,12 +9280,12 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
9175
9280
|
|
|
9176
9281
|
// src/connect-wizard.ts
|
|
9177
9282
|
import { delimiter, dirname as dirname8, join as join17 } from "path";
|
|
9178
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
9283
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
9179
9284
|
import { stringify } from "yaml";
|
|
9180
9285
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
9181
9286
|
function shouldRunWizard(opts) {
|
|
9182
9287
|
if (!opts.isTTY) return false;
|
|
9183
|
-
const exists = opts.configExists ??
|
|
9288
|
+
const exists = opts.configExists ?? existsSync16;
|
|
9184
9289
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
9185
9290
|
}
|
|
9186
9291
|
function firstRunConfigPath() {
|
|
@@ -9292,7 +9397,7 @@ Wrote ${o.configPath}.
|
|
|
9292
9397
|
}
|
|
9293
9398
|
|
|
9294
9399
|
// src/release-run.ts
|
|
9295
|
-
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as
|
|
9400
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync17, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync7 } from "fs";
|
|
9296
9401
|
import { dirname as dirname9, join as join18 } from "path";
|
|
9297
9402
|
import { tmpdir as tmpdir2 } from "os";
|
|
9298
9403
|
|
|
@@ -9983,7 +10088,7 @@ async function bump(o, size) {
|
|
|
9983
10088
|
}
|
|
9984
10089
|
const files = o.repo.release.versionFiles.map((f) => join18(o.cwd, f));
|
|
9985
10090
|
const first = files[0];
|
|
9986
|
-
if (!
|
|
10091
|
+
if (!existsSync17(first)) {
|
|
9987
10092
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
9988
10093
|
return null;
|
|
9989
10094
|
}
|
|
@@ -10019,7 +10124,7 @@ function writeChangelog(o, version, entries) {
|
|
|
10019
10124
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
10020
10125
|
return;
|
|
10021
10126
|
}
|
|
10022
|
-
const existing =
|
|
10127
|
+
const existing = existsSync17(path) ? readFileSync13(path, "utf8") : "# Changelog\n";
|
|
10023
10128
|
writeFileSync10(path, insertChangelogSection(existing, section));
|
|
10024
10129
|
}
|
|
10025
10130
|
async function confirm(o, expected) {
|
|
@@ -10125,7 +10230,7 @@ function openReleaseCheckout(o) {
|
|
|
10125
10230
|
}
|
|
10126
10231
|
for (const f of o.repo.worktrees.copy) {
|
|
10127
10232
|
const src = join18(o.cwd, f);
|
|
10128
|
-
if (!
|
|
10233
|
+
if (!existsSync17(src)) continue;
|
|
10129
10234
|
mkdirSync8(dirname9(join18(dir, f)), { recursive: true });
|
|
10130
10235
|
copyFileSync(src, join18(dir, f));
|
|
10131
10236
|
}
|
|
@@ -10710,7 +10815,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
10710
10815
|
}
|
|
10711
10816
|
|
|
10712
10817
|
// src/watch.ts
|
|
10713
|
-
import { watch as watchFile, existsSync as
|
|
10818
|
+
import { watch as watchFile, existsSync as existsSync18, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
10714
10819
|
var E = "\x1B[";
|
|
10715
10820
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
10716
10821
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -10811,7 +10916,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
10811
10916
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
10812
10917
|
}
|
|
10813
10918
|
function readFrom(file, from) {
|
|
10814
|
-
if (!
|
|
10919
|
+
if (!existsSync18(file)) return { events: [], offset: from };
|
|
10815
10920
|
const size = statSync6(file).size;
|
|
10816
10921
|
if (size < from) return readFrom(file, 0);
|
|
10817
10922
|
if (size === from) return { events: [], offset: from };
|
|
@@ -11443,6 +11548,12 @@ import {
|
|
|
11443
11548
|
// src/seat-keys.ts
|
|
11444
11549
|
import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
|
|
11445
11550
|
var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
|
|
11551
|
+
function keyIsAdmin(route) {
|
|
11552
|
+
const role = route.resolved?.role;
|
|
11553
|
+
return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
|
|
11554
|
+
}
|
|
11555
|
+
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";
|
|
11556
|
+
var MEMBER_NO_KEY = "no agent key for this seat \u2014 minting needs a workspace admin key";
|
|
11446
11557
|
function seatKeySlug(workspaceSlug, agentId) {
|
|
11447
11558
|
return `${workspaceSlug}#agent:${agentId}`;
|
|
11448
11559
|
}
|
|
@@ -11456,11 +11567,11 @@ function seatAgentId(route, role) {
|
|
|
11456
11567
|
async function resolveReleaseKey(route, getStore = getSessionStore2) {
|
|
11457
11568
|
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
|
|
11458
11569
|
const agentId = route.resolved?.releaseAgentId;
|
|
11459
|
-
if (!agentId) return ship("release seat has no linked Agents row (run `crew agents sync`)");
|
|
11570
|
+
if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
|
|
11460
11571
|
try {
|
|
11461
11572
|
const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
|
|
11462
11573
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
11463
|
-
return ship("no agent key stored for the release seat (run `crew agents sync`)");
|
|
11574
|
+
return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
11464
11575
|
} catch {
|
|
11465
11576
|
return ship("OS keychain unavailable");
|
|
11466
11577
|
}
|
|
@@ -11469,11 +11580,11 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
|
|
|
11469
11580
|
const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
|
|
11470
11581
|
if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
|
|
11471
11582
|
const agentId = seatAgentId(route, role);
|
|
11472
|
-
if (!agentId) return ship("seat has no linked Agents row (run `crew agents sync`)");
|
|
11583
|
+
if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
|
|
11473
11584
|
try {
|
|
11474
11585
|
const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
|
|
11475
11586
|
if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
|
|
11476
|
-
return ship("no agent key stored for this seat (run `crew agents sync`)");
|
|
11587
|
+
return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
|
|
11477
11588
|
} catch {
|
|
11478
11589
|
return ship("OS keychain unavailable");
|
|
11479
11590
|
}
|
|
@@ -11830,7 +11941,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
11830
11941
|
}
|
|
11831
11942
|
|
|
11832
11943
|
// src/worktree-sweep.ts
|
|
11833
|
-
import { existsSync as
|
|
11944
|
+
import { existsSync as existsSync19 } from "fs";
|
|
11834
11945
|
import { basename as basename8 } from "path";
|
|
11835
11946
|
function planWorktreeSweep(target, tickets, contract) {
|
|
11836
11947
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -11866,7 +11977,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
11866
11977
|
let removed = 0;
|
|
11867
11978
|
let keptBranches = 0;
|
|
11868
11979
|
for (const a of actions) {
|
|
11869
|
-
if (!dryRun &&
|
|
11980
|
+
if (!dryRun && existsSync19(a.path)) {
|
|
11870
11981
|
let dirty = false;
|
|
11871
11982
|
try {
|
|
11872
11983
|
dirty = status(a.path).length > 0;
|
|
@@ -11880,7 +11991,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
11880
11991
|
continue;
|
|
11881
11992
|
}
|
|
11882
11993
|
}
|
|
11883
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
11994
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync19(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
11884
11995
|
log.warn(
|
|
11885
11996
|
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
11886
11997
|
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
@@ -12012,12 +12123,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
12012
12123
|
}
|
|
12013
12124
|
|
|
12014
12125
|
// src/stream-sweep.ts
|
|
12015
|
-
import { existsSync as
|
|
12126
|
+
import { existsSync as existsSync20, readdirSync as readdirSync5, rmSync as rmSync8, statSync as statSync7 } from "fs";
|
|
12016
12127
|
import { join as join19 } from "path";
|
|
12017
12128
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
12018
12129
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12019
12130
|
const dir = join19(stateDir, "streams");
|
|
12020
|
-
if (!
|
|
12131
|
+
if (!existsSync20(dir)) return [];
|
|
12021
12132
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12022
12133
|
return readdirSync5(dir).map((name) => join19(dir, name)).filter((path) => {
|
|
12023
12134
|
try {
|
|
@@ -12045,7 +12156,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
12045
12156
|
return removed;
|
|
12046
12157
|
}
|
|
12047
12158
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
12048
|
-
if (!
|
|
12159
|
+
if (!existsSync20(stateDir)) return [];
|
|
12049
12160
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
12050
12161
|
return readdirSync5(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join19(stateDir, name)).filter((path) => {
|
|
12051
12162
|
try {
|
|
@@ -12057,13 +12168,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
12057
12168
|
}
|
|
12058
12169
|
|
|
12059
12170
|
// src/cli.ts
|
|
12060
|
-
import { readFileSync as readFileSync19, existsSync as
|
|
12171
|
+
import { readFileSync as readFileSync19, existsSync as existsSync26, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
12061
12172
|
import { createInterface } from "readline/promises";
|
|
12062
12173
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
12063
12174
|
|
|
12064
12175
|
// src/ssh-keys.ts
|
|
12065
12176
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
12066
|
-
import { existsSync as
|
|
12177
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
|
|
12067
12178
|
import { dirname as dirname11, join as join20 } from "path";
|
|
12068
12179
|
var SshKeyError = class extends Error {
|
|
12069
12180
|
};
|
|
@@ -12073,7 +12184,7 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
12073
12184
|
function ensureShipSshKeypair(stateDir) {
|
|
12074
12185
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
12075
12186
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
12076
|
-
if (!
|
|
12187
|
+
if (!existsSync21(privateKeyPath)) {
|
|
12077
12188
|
mkdirSync9(dirname11(privateKeyPath), { recursive: true });
|
|
12078
12189
|
try {
|
|
12079
12190
|
execFileSync3("ssh-keygen", [
|
|
@@ -12094,7 +12205,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
12094
12205
|
}
|
|
12095
12206
|
chmodSync(privateKeyPath, 384);
|
|
12096
12207
|
}
|
|
12097
|
-
if (!
|
|
12208
|
+
if (!existsSync21(publicKeyPath)) {
|
|
12098
12209
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
12099
12210
|
}
|
|
12100
12211
|
const publicKey = readFileSync14(publicKeyPath, "utf8").trim();
|
|
@@ -12313,7 +12424,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
12313
12424
|
|
|
12314
12425
|
// src/tunnel.ts
|
|
12315
12426
|
import { spawn as spawn3 } from "child_process";
|
|
12316
|
-
import { existsSync as
|
|
12427
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync6 } from "fs";
|
|
12317
12428
|
import { randomInt } from "crypto";
|
|
12318
12429
|
import { join as join21 } from "path";
|
|
12319
12430
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -12386,7 +12497,7 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
12386
12497
|
}
|
|
12387
12498
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
12388
12499
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
12389
|
-
if (!
|
|
12500
|
+
if (!existsSync22(path)) return void 0;
|
|
12390
12501
|
try {
|
|
12391
12502
|
return JSON.parse(readFileSync15(path, "utf8"));
|
|
12392
12503
|
} catch {
|
|
@@ -12494,7 +12605,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12494
12605
|
}
|
|
12495
12606
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
12496
12607
|
const dir = join21(stateDir, "passengers");
|
|
12497
|
-
if (!
|
|
12608
|
+
if (!existsSync22(dir)) return [];
|
|
12498
12609
|
return readdirSync6(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
12499
12610
|
}
|
|
12500
12611
|
|
|
@@ -12615,7 +12726,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
12615
12726
|
import { createServer as createHttpServer } from "http";
|
|
12616
12727
|
import { spawn as spawn4 } from "child_process";
|
|
12617
12728
|
import { randomBytes } from "crypto";
|
|
12618
|
-
import { existsSync as
|
|
12729
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync7 } from "fs";
|
|
12619
12730
|
import { join as join22 } from "path";
|
|
12620
12731
|
|
|
12621
12732
|
// src/secret-compare.ts
|
|
@@ -12709,7 +12820,7 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
12709
12820
|
}
|
|
12710
12821
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
12711
12822
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
12712
|
-
if (!
|
|
12823
|
+
if (!existsSync23(path)) return void 0;
|
|
12713
12824
|
try {
|
|
12714
12825
|
return JSON.parse(readFileSync16(path, "utf8"));
|
|
12715
12826
|
} catch {
|
|
@@ -12740,7 +12851,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
12740
12851
|
}
|
|
12741
12852
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
12742
12853
|
const dir = join22(stateDir, "passengers");
|
|
12743
|
-
if (!
|
|
12854
|
+
if (!existsSync23(dir)) return [];
|
|
12744
12855
|
const suffix = "-sync.json";
|
|
12745
12856
|
return readdirSync7(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
12746
12857
|
}
|
|
@@ -12797,11 +12908,11 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
12797
12908
|
}
|
|
12798
12909
|
|
|
12799
12910
|
// src/daemon.ts
|
|
12800
|
-
import { existsSync as
|
|
12911
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
12801
12912
|
import { join as join24 } from "path";
|
|
12802
12913
|
|
|
12803
12914
|
// src/interrupted.ts
|
|
12804
|
-
import { existsSync as
|
|
12915
|
+
import { existsSync as existsSync24, readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
|
|
12805
12916
|
import { join as join23 } from "path";
|
|
12806
12917
|
var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
|
|
12807
12918
|
function interruptedComment(b) {
|
|
@@ -12820,7 +12931,7 @@ var pidAlive = (pid) => {
|
|
|
12820
12931
|
};
|
|
12821
12932
|
function runFinished(streamPath) {
|
|
12822
12933
|
const path = `${streamPath}.meta.json`;
|
|
12823
|
-
if (!
|
|
12934
|
+
if (!existsSync24(path)) return false;
|
|
12824
12935
|
try {
|
|
12825
12936
|
return JSON.parse(readFileSync17(path, "utf8")).running !== true;
|
|
12826
12937
|
} catch {
|
|
@@ -12829,7 +12940,7 @@ function runFinished(streamPath) {
|
|
|
12829
12940
|
}
|
|
12830
12941
|
function orphanedStreams(o) {
|
|
12831
12942
|
const dir = join23(o.stateDir, "streams");
|
|
12832
|
-
if (!
|
|
12943
|
+
if (!existsSync24(dir)) return [];
|
|
12833
12944
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
12834
12945
|
const out = [];
|
|
12835
12946
|
for (const name of readdirSync8(dir)) {
|
|
@@ -12943,7 +13054,7 @@ function computeTreeSignature(paths) {
|
|
|
12943
13054
|
}
|
|
12944
13055
|
function makeStaleChecker(paths, opts = {}) {
|
|
12945
13056
|
const initial = computeTreeSignature(paths);
|
|
12946
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
13057
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync25(opts.reloadSentinel);
|
|
12947
13058
|
}
|
|
12948
13059
|
function reloadSentinelFor(stateDir) {
|
|
12949
13060
|
return join24(stateDir, "reload-requested");
|
|
@@ -13829,7 +13940,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13829
13940
|
if (host === "linux" && hasSystemd) {
|
|
13830
13941
|
const label = labelFor(CREW_HOME, "run");
|
|
13831
13942
|
const staleTimer = join25(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
13832
|
-
if (
|
|
13943
|
+
if (existsSync26(staleTimer)) {
|
|
13833
13944
|
process.stdout.write(
|
|
13834
13945
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
13835
13946
|
`
|
|
@@ -13855,7 +13966,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13855
13966
|
const tracker = new Tracker(c, cfg.ship);
|
|
13856
13967
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
13857
13968
|
for (const r of await resolvedRepos(c)) {
|
|
13858
|
-
if (!
|
|
13969
|
+
if (!existsSync26(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
13859
13970
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
13860
13971
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
13861
13972
|
}
|
|
@@ -13873,7 +13984,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13873
13984
|
}
|
|
13874
13985
|
async function doctorFix(o) {
|
|
13875
13986
|
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))));
|
|
13876
|
-
const unitExists = (p) =>
|
|
13987
|
+
const unitExists = (p) => existsSync26(p);
|
|
13877
13988
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
13878
13989
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
13879
13990
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -14993,9 +15104,18 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14993
15104
|
`);
|
|
14994
15105
|
process.exit(2);
|
|
14995
15106
|
}
|
|
15107
|
+
const keyRole = target.resolved.role ?? await fetchKeyRole(target, cfg.ship.userAgent);
|
|
15108
|
+
let readOnly = !isAdminRole(keyRole);
|
|
14996
15109
|
let personaResult;
|
|
15110
|
+
const runPersonas = (ro) => syncPersonas(target, { userAgent: cfg.ship.userAgent, dryRun, force, readOnly: ro });
|
|
14997
15111
|
try {
|
|
14998
|
-
|
|
15112
|
+
try {
|
|
15113
|
+
personaResult = await runPersonas(readOnly);
|
|
15114
|
+
} catch (e) {
|
|
15115
|
+
if (readOnly || !isForbidden(e)) throw e;
|
|
15116
|
+
readOnly = true;
|
|
15117
|
+
personaResult = await runPersonas(true);
|
|
15118
|
+
}
|
|
14999
15119
|
} catch (e) {
|
|
15000
15120
|
if (e instanceof AgentsSyncError) {
|
|
15001
15121
|
process.stderr.write(`crew agents sync: ${e.message}
|
|
@@ -15009,28 +15129,32 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15009
15129
|
for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
|
|
15010
15130
|
`);
|
|
15011
15131
|
for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
|
|
15132
|
+
`);
|
|
15133
|
+
if (personaResult.releaseNote) process.stdout.write(`${personaResult.releaseNote}
|
|
15012
15134
|
`);
|
|
15013
15135
|
const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
|
|
15014
15136
|
for (const w of skillMarkerWarnings(target.promptsDir, briefs)) process.stderr.write(`crew agents sync: warning: ${w}
|
|
15015
15137
|
`);
|
|
15016
15138
|
let skillResult;
|
|
15017
|
-
|
|
15018
|
-
|
|
15019
|
-
|
|
15020
|
-
|
|
15021
|
-
|
|
15139
|
+
if (!readOnly) {
|
|
15140
|
+
try {
|
|
15141
|
+
skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun, force });
|
|
15142
|
+
} catch (e) {
|
|
15143
|
+
if (isForbidden(e)) readOnly = true;
|
|
15144
|
+
else if (e instanceof SkillSyncError) {
|
|
15145
|
+
process.stderr.write(`crew agents sync: ${e.message}
|
|
15022
15146
|
`);
|
|
15023
|
-
|
|
15147
|
+
process.exit(2);
|
|
15148
|
+
} else throw e;
|
|
15024
15149
|
}
|
|
15025
|
-
throw e;
|
|
15026
15150
|
}
|
|
15027
|
-
for (const o of skillResult
|
|
15151
|
+
for (const o of skillResult?.outcomes ?? []) process.stdout.write(`${describeSkillSyncOutcome(o)}
|
|
15028
15152
|
`);
|
|
15029
15153
|
if (!dryRun) {
|
|
15030
15154
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15031
|
-
const raw =
|
|
15155
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
|
|
15032
15156
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
15033
|
-
raw.agentSkills = skillResult.agentSkills;
|
|
15157
|
+
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
15034
15158
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
15035
15159
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
15036
15160
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15040,11 +15164,16 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15040
15164
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
15041
15165
|
if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
|
|
15042
15166
|
}
|
|
15043
|
-
|
|
15044
|
-
process.stdout.write(
|
|
15167
|
+
if (readOnly) {
|
|
15168
|
+
process.stdout.write("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)\n");
|
|
15169
|
+
process.stdout.write("seat keys: not minted (needs a workspace admin key)\n");
|
|
15170
|
+
} else {
|
|
15171
|
+
for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
|
|
15172
|
+
process.stdout.write(`${describeMintOutcome(o)}
|
|
15045
15173
|
`);
|
|
15174
|
+
}
|
|
15046
15175
|
}
|
|
15047
|
-
if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult
|
|
15176
|
+
if (personaResult.outcomes.some((o) => o.action === "diverged") || (skillResult?.outcomes.some((o) => o.action === "diverged") ?? false)) {
|
|
15048
15177
|
process.exitCode = 1;
|
|
15049
15178
|
}
|
|
15050
15179
|
break;
|
|
@@ -15088,7 +15217,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15088
15217
|
`);
|
|
15089
15218
|
if (!dryRun) {
|
|
15090
15219
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15091
|
-
const raw =
|
|
15220
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
|
|
15092
15221
|
raw.agentSkills = result.agentSkills;
|
|
15093
15222
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
15094
15223
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15111,7 +15240,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15111
15240
|
if (sub === "list" && !routeName) usage2();
|
|
15112
15241
|
if (sub === "add") {
|
|
15113
15242
|
try {
|
|
15114
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
15243
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync26(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
15115
15244
|
routeName = a.route;
|
|
15116
15245
|
addPath = a.path;
|
|
15117
15246
|
addName = a.name;
|
|
@@ -15126,7 +15255,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15126
15255
|
}
|
|
15127
15256
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
15128
15257
|
let resolvedRemotes;
|
|
15129
|
-
if (
|
|
15258
|
+
if (existsSync26(resolvedFile)) {
|
|
15130
15259
|
try {
|
|
15131
15260
|
resolvedRemotes = JSON.parse(readFileSync19(resolvedFile, "utf8"));
|
|
15132
15261
|
} catch {
|
|
@@ -15143,14 +15272,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15143
15272
|
for (const r of repos) {
|
|
15144
15273
|
const origin2 = originOf(r.dir);
|
|
15145
15274
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
15146
|
-
const note = !
|
|
15275
|
+
const note = !existsSync26(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
15147
15276
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
15148
15277
|
`);
|
|
15149
15278
|
}
|
|
15150
15279
|
break;
|
|
15151
15280
|
}
|
|
15152
15281
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
15153
|
-
const top =
|
|
15282
|
+
const top = existsSync26(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
15154
15283
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
15155
15284
|
const origin = originOf(top);
|
|
15156
15285
|
const plan = planRepoAdd({
|
|
@@ -15199,7 +15328,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15199
15328
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
15200
15329
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
15201
15330
|
`);
|
|
15202
|
-
if (
|
|
15331
|
+
if (existsSync26(resolvedFile)) {
|
|
15203
15332
|
const raw = JSON.parse(readFileSync19(resolvedFile, "utf8"));
|
|
15204
15333
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
15205
15334
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
@@ -15446,7 +15575,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15446
15575
|
operator = await pickOperator(found.holds);
|
|
15447
15576
|
}
|
|
15448
15577
|
let previous;
|
|
15449
|
-
const hadPreviousFile =
|
|
15578
|
+
const hadPreviousFile = existsSync26(resolvedPath);
|
|
15450
15579
|
if (hadPreviousFile) {
|
|
15451
15580
|
try {
|
|
15452
15581
|
previous = JSON.parse(readFileSync19(resolvedPath, "utf8"));
|
|
@@ -15493,6 +15622,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15493
15622
|
seats: found.seats,
|
|
15494
15623
|
holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
|
|
15495
15624
|
...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
|
|
15625
|
+
...found.role ?? previous?.role ? { role: found.role ?? previous?.role } : {},
|
|
15496
15626
|
...operator ? { operator } : {},
|
|
15497
15627
|
...contract ? { contract } : {},
|
|
15498
15628
|
...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
|
|
@@ -16351,7 +16481,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16351
16481
|
);
|
|
16352
16482
|
try {
|
|
16353
16483
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
16354
|
-
const evSize =
|
|
16484
|
+
const evSize = existsSync26(evFile) ? statSync9(evFile).size : 0;
|
|
16355
16485
|
process.stdout.write(
|
|
16356
16486
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
16357
16487
|
` : "events log: ok\n"
|
|
@@ -16409,12 +16539,10 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16409
16539
|
process.stdout.write(` - ${p2}
|
|
16410
16540
|
`);
|
|
16411
16541
|
}
|
|
16412
|
-
const
|
|
16413
|
-
|
|
16414
|
-
|
|
16415
|
-
|
|
16416
|
-
if (bh.unsafe) process.exitCode = 1;
|
|
16417
|
-
}
|
|
16542
|
+
const dh = doctorBaseReport(r, config.branch.base, baseHealth);
|
|
16543
|
+
process.stdout.write(dh.text);
|
|
16544
|
+
if (dh.failed) process.exitCode = 1;
|
|
16545
|
+
if (dh.skipRest) continue;
|
|
16418
16546
|
if (config.release.ship) {
|
|
16419
16547
|
process.stdout.write(` release.ship ${config.release.ship}${config.release.ship === cfg.ship.name ? " (this ship)" : ""}
|
|
16420
16548
|
`);
|
|
@@ -16432,16 +16560,20 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16432
16560
|
process.stdout.write(
|
|
16433
16561
|
route.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"
|
|
16434
16562
|
);
|
|
16563
|
+
const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
|
|
16564
|
+
const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
|
|
16565
|
+
process.stdout.write(`api key role: ${keyRole ?? "unknown"}
|
|
16566
|
+
`);
|
|
16435
16567
|
for (const role of SEAT_KEY_ROLES) {
|
|
16436
16568
|
if (!route.resolved?.seats[role]) continue;
|
|
16437
|
-
const id = await resolveSeatKey(
|
|
16569
|
+
const id = await resolveSeatKey(roleRoute, role);
|
|
16438
16570
|
process.stdout.write(
|
|
16439
16571
|
`seat identity: ${role} writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
|
|
16440
16572
|
`
|
|
16441
16573
|
);
|
|
16442
16574
|
}
|
|
16443
16575
|
if (route.resolved?.releaseSeat) {
|
|
16444
|
-
const id = await resolveReleaseKey(
|
|
16576
|
+
const id = await resolveReleaseKey(roleRoute);
|
|
16445
16577
|
process.stdout.write(
|
|
16446
16578
|
`seat identity: release writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
|
|
16447
16579
|
`
|
package/package.json
CHANGED