@tablation/crew 0.50.0 → 0.50.2
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 +211 -112
- 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";
|
|
@@ -240,7 +271,7 @@ function mapStreamLine(line) {
|
|
|
240
271
|
if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
|
|
241
272
|
else if (block.type === "text") out.push({ kind: "text", text: block.text });
|
|
242
273
|
else if (block.type === "tool_use") {
|
|
243
|
-
out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
|
|
274
|
+
out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input), input: block.input });
|
|
244
275
|
}
|
|
245
276
|
}
|
|
246
277
|
return out;
|
|
@@ -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) {
|
|
@@ -2263,7 +2294,7 @@ function parseResolved(raw, m, where) {
|
|
|
2263
2294
|
}
|
|
2264
2295
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
2265
2296
|
const file = configFile ?? findConfigFile(crewHome);
|
|
2266
|
-
if (opts.text === void 0 && !
|
|
2297
|
+
if (opts.text === void 0 && !existsSync7(file)) {
|
|
2267
2298
|
throw new ConfigError(
|
|
2268
2299
|
`no config found. Looked in:
|
|
2269
2300
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
@@ -2573,7 +2604,7 @@ function repoIdForName(r, name) {
|
|
|
2573
2604
|
}
|
|
2574
2605
|
function resolveApiKey(r) {
|
|
2575
2606
|
if (r.apiKey) return r.apiKey;
|
|
2576
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
2607
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
|
|
2577
2608
|
for (const line of readFileSync6(r.apiKeyFile, "utf8").split("\n")) {
|
|
2578
2609
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
2579
2610
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
@@ -2793,7 +2824,7 @@ function rosterMarkdown(roster, meId) {
|
|
|
2793
2824
|
}
|
|
2794
2825
|
|
|
2795
2826
|
// src/state.ts
|
|
2796
|
-
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as
|
|
2827
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
|
|
2797
2828
|
import { join as join8 } from "path";
|
|
2798
2829
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
2799
2830
|
var State = class _State {
|
|
@@ -2839,10 +2870,10 @@ var State = class _State {
|
|
|
2839
2870
|
}
|
|
2840
2871
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
2841
2872
|
isPaused() {
|
|
2842
|
-
return
|
|
2873
|
+
return existsSync8(this.path(".crew-paused"));
|
|
2843
2874
|
}
|
|
2844
2875
|
isRolePaused(role) {
|
|
2845
|
-
return
|
|
2876
|
+
return existsSync8(this.path(`.role-paused-${role}`));
|
|
2846
2877
|
}
|
|
2847
2878
|
pausedRoles(roles) {
|
|
2848
2879
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
@@ -2859,7 +2890,7 @@ var State = class _State {
|
|
|
2859
2890
|
* while agents keep working.
|
|
2860
2891
|
*/
|
|
2861
2892
|
isReleasePaused() {
|
|
2862
|
-
return
|
|
2893
|
+
return existsSync8(this.path(".release-paused"));
|
|
2863
2894
|
}
|
|
2864
2895
|
setReleasePaused(paused) {
|
|
2865
2896
|
if (paused) writeFileSync4(this.path(".release-paused"), "");
|
|
@@ -3507,7 +3538,7 @@ function describeRelease(o, route) {
|
|
|
3507
3538
|
// src/install.ts
|
|
3508
3539
|
import { dirname as dirname4, join as join9, extname } from "path";
|
|
3509
3540
|
import { homedir as homedir6 } from "os";
|
|
3510
|
-
import { accessSync, constants, existsSync as
|
|
3541
|
+
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
3542
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3512
3543
|
import { createHash } from "crypto";
|
|
3513
3544
|
|
|
@@ -3882,7 +3913,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3882
3913
|
}
|
|
3883
3914
|
}
|
|
3884
3915
|
for (const path of plan.unitPaths) {
|
|
3885
|
-
if (!
|
|
3916
|
+
if (!existsSync9(path)) continue;
|
|
3886
3917
|
if (dryRun) {
|
|
3887
3918
|
log.emit(`would remove ${path}`);
|
|
3888
3919
|
continue;
|
|
@@ -3918,7 +3949,7 @@ function loadedCrewLabels() {
|
|
|
3918
3949
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3919
3950
|
}
|
|
3920
3951
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
3921
|
-
const files =
|
|
3952
|
+
const files = existsSync9(dir) ? readdirSync2(dir) : [];
|
|
3922
3953
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
3923
3954
|
const plistPath = join9(dir, f);
|
|
3924
3955
|
let program = null;
|
|
@@ -3930,7 +3961,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
|
3930
3961
|
label: f.slice(0, -".plist".length),
|
|
3931
3962
|
plistPath,
|
|
3932
3963
|
program,
|
|
3933
|
-
programExists: program !== null &&
|
|
3964
|
+
programExists: program !== null && existsSync9(program)
|
|
3934
3965
|
};
|
|
3935
3966
|
});
|
|
3936
3967
|
const seen = new Set(units.map((u) => u.label));
|
|
@@ -3949,9 +3980,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3949
3980
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3950
3981
|
return;
|
|
3951
3982
|
}
|
|
3952
|
-
if (
|
|
3983
|
+
if (existsSync9(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3953
3984
|
spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3954
|
-
if (
|
|
3985
|
+
if (existsSync9(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3955
3986
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3956
3987
|
}
|
|
3957
3988
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -4034,7 +4065,7 @@ import { statfsSync } from "fs";
|
|
|
4034
4065
|
// src/git.ts
|
|
4035
4066
|
import { execFileSync } from "child_process";
|
|
4036
4067
|
import {
|
|
4037
|
-
existsSync as
|
|
4068
|
+
existsSync as existsSync10,
|
|
4038
4069
|
mkdirSync as mkdirSync5,
|
|
4039
4070
|
openSync as openSync2,
|
|
4040
4071
|
closeSync as closeSync2,
|
|
@@ -4434,7 +4465,7 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
4434
4465
|
try {
|
|
4435
4466
|
const st = statSync4(root);
|
|
4436
4467
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
4437
|
-
if (
|
|
4468
|
+
if (existsSync10(join10(root, "checkout"))) continue;
|
|
4438
4469
|
rmSync6(root, { recursive: true, force: true });
|
|
4439
4470
|
removed.push(name);
|
|
4440
4471
|
} catch {
|
|
@@ -4477,7 +4508,7 @@ function originUrlOf(dir, remoteName) {
|
|
|
4477
4508
|
}
|
|
4478
4509
|
}
|
|
4479
4510
|
function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
4480
|
-
if (
|
|
4511
|
+
if (existsSync10(`${dir}/.git`)) return false;
|
|
4481
4512
|
if (!remote) {
|
|
4482
4513
|
throw new GitError(
|
|
4483
4514
|
`${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 +4571,10 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
4540
4571
|
function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
|
|
4541
4572
|
const release = acquireCheckoutLock(dir);
|
|
4542
4573
|
try {
|
|
4543
|
-
if (!
|
|
4574
|
+
if (!existsSync10(`${dir}/.git`) && githubRemote) {
|
|
4544
4575
|
const url = cloneUrlFor(githubRemote, clone);
|
|
4545
4576
|
const marker = cloneFailureMarker(dir);
|
|
4546
|
-
if (
|
|
4577
|
+
if (existsSync10(marker) && readFileSync9(marker, "utf8").split("\n")[0] === url) {
|
|
4547
4578
|
return { action: "clone-failed", detail: readFileSync9(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
|
|
4548
4579
|
}
|
|
4549
4580
|
try {
|
|
@@ -7126,7 +7157,7 @@ var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id ===
|
|
|
7126
7157
|
|
|
7127
7158
|
// src/agent.ts
|
|
7128
7159
|
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
7129
|
-
import { readFileSync as readFileSync12, existsSync as
|
|
7160
|
+
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
7161
|
import { StringDecoder } from "string_decoder";
|
|
7131
7162
|
import { finished } from "stream/promises";
|
|
7132
7163
|
import { join as join14, dirname as dirname6 } from "path";
|
|
@@ -7544,7 +7575,7 @@ function expandSkillMarkers(text, lookup, opts = {}) {
|
|
|
7544
7575
|
}
|
|
7545
7576
|
|
|
7546
7577
|
// src/skills.ts
|
|
7547
|
-
import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as
|
|
7578
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync12 } from "fs";
|
|
7548
7579
|
import { join as join13 } from "path";
|
|
7549
7580
|
import { parse as parseYaml } from "yaml";
|
|
7550
7581
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError8, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
@@ -7561,7 +7592,7 @@ ${additions}`;
|
|
|
7561
7592
|
}
|
|
7562
7593
|
|
|
7563
7594
|
// src/agents.ts
|
|
7564
|
-
import { readFileSync as readFileSync10, existsSync as
|
|
7595
|
+
import { readFileSync as readFileSync10, existsSync as existsSync11 } from "fs";
|
|
7565
7596
|
import { join as join12 } from "path";
|
|
7566
7597
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError7, TablationApiError } from "@tablation/client";
|
|
7567
7598
|
var PERSONA_NAME = {
|
|
@@ -7577,10 +7608,10 @@ var AgentsSyncError = class extends Error {
|
|
|
7577
7608
|
};
|
|
7578
7609
|
function personaDefaultPrompt(promptsDir, role) {
|
|
7579
7610
|
const briefPath = join12(promptsDir, "personas", `lane-${role}.md`);
|
|
7580
|
-
if (!
|
|
7611
|
+
if (!existsSync11(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
7581
7612
|
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync10(briefPath, "utf8");
|
|
7582
7613
|
const commonPath = join12(promptsDir, "personas", "common.md");
|
|
7583
|
-
if (!
|
|
7614
|
+
if (!existsSync11(commonPath)) return readFileSync10(briefPath, "utf8");
|
|
7584
7615
|
return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
|
|
7585
7616
|
}
|
|
7586
7617
|
async function resolveAgentId(route, role, opts) {
|
|
@@ -7969,7 +8000,7 @@ function parseSkillFile(path) {
|
|
|
7969
8000
|
}
|
|
7970
8001
|
function listLocalSkills(promptsDir) {
|
|
7971
8002
|
const dir = join13(promptsDir, "skills");
|
|
7972
|
-
if (!
|
|
8003
|
+
if (!existsSync12(dir)) return [];
|
|
7973
8004
|
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join13(dir, f)));
|
|
7974
8005
|
}
|
|
7975
8006
|
async function syncSkills(route, opts) {
|
|
@@ -8161,6 +8192,15 @@ function ticketMentions(text) {
|
|
|
8161
8192
|
var TicketAttributionTracker = class {
|
|
8162
8193
|
#tokensByTicket = /* @__PURE__ */ new Map();
|
|
8163
8194
|
#current;
|
|
8195
|
+
/** The ticket key most recently named in the transcript, or undefined before any. */
|
|
8196
|
+
get current() {
|
|
8197
|
+
return this.#current;
|
|
8198
|
+
}
|
|
8199
|
+
/** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
|
|
8200
|
+
note(text) {
|
|
8201
|
+
const mentions = ticketMentions(text);
|
|
8202
|
+
if (mentions.length > 0) this.#current = mentions[mentions.length - 1];
|
|
8203
|
+
}
|
|
8164
8204
|
/** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
|
|
8165
8205
|
add(text, tokens) {
|
|
8166
8206
|
const mentions = ticketMentions(text);
|
|
@@ -8186,6 +8226,11 @@ var TicketAttributionTracker = class {
|
|
|
8186
8226
|
return best;
|
|
8187
8227
|
}
|
|
8188
8228
|
};
|
|
8229
|
+
function isClaimWrite(tool, input) {
|
|
8230
|
+
if (!tool || !/records_controller_update$/.test(tool)) return false;
|
|
8231
|
+
const body = input?.body;
|
|
8232
|
+
return typeof body === "object" && body !== null && body.status === "in_progress";
|
|
8233
|
+
}
|
|
8189
8234
|
|
|
8190
8235
|
// src/agent.ts
|
|
8191
8236
|
var DIGEST_MAX_AGE_SECONDS = 600;
|
|
@@ -8208,17 +8253,17 @@ var AgentSpawnError = class extends AgentError {
|
|
|
8208
8253
|
};
|
|
8209
8254
|
function planAgentRun(o) {
|
|
8210
8255
|
const briefPath = join14(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
8211
|
-
if (!
|
|
8256
|
+
if (!existsSync13(briefPath)) {
|
|
8212
8257
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
8213
8258
|
}
|
|
8214
8259
|
const commonPath = join14(o.route.promptsDir, "personas", "common.md");
|
|
8215
|
-
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) &&
|
|
8260
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync13(commonPath);
|
|
8216
8261
|
const now = o.now ?? (() => Date.now());
|
|
8217
8262
|
const routeLabel = routeSlug(o.route.route);
|
|
8218
8263
|
const digestPath2 = join14(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
8219
8264
|
let digest;
|
|
8220
8265
|
let ageSeconds;
|
|
8221
|
-
if (
|
|
8266
|
+
if (existsSync13(digestPath2)) {
|
|
8222
8267
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
8223
8268
|
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync12(digestPath2, "utf8");
|
|
8224
8269
|
}
|
|
@@ -8415,6 +8460,13 @@ var RunObserver = class {
|
|
|
8415
8460
|
/** Set once the stream carries the harness's final result (turn count or cost). */
|
|
8416
8461
|
sawResult = false;
|
|
8417
8462
|
muted = false;
|
|
8463
|
+
/**
|
|
8464
|
+
* The label stamped on events. Starts as the poll hint and switches to the
|
|
8465
|
+
* ticket the run actually claims (CREW-1776); `id.ticket` stays the hint.
|
|
8466
|
+
*/
|
|
8467
|
+
liveTicket;
|
|
8468
|
+
/** Called after `liveTicket` changes, so the sidecar can be rewritten. */
|
|
8469
|
+
onClaim;
|
|
8418
8470
|
buf = "";
|
|
8419
8471
|
mapper;
|
|
8420
8472
|
id;
|
|
@@ -8426,6 +8478,7 @@ var RunObserver = class {
|
|
|
8426
8478
|
this.adapter = adapter;
|
|
8427
8479
|
this.eventsSink = eventsSink;
|
|
8428
8480
|
this.emitCycle = emitCycle;
|
|
8481
|
+
this.liveTicket = id.ticket ?? void 0;
|
|
8429
8482
|
this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
|
|
8430
8483
|
}
|
|
8431
8484
|
feed(text) {
|
|
@@ -8466,7 +8519,29 @@ var RunObserver = class {
|
|
|
8466
8519
|
consume(parsed, events) {
|
|
8467
8520
|
const plan = this.id;
|
|
8468
8521
|
let turnText = "";
|
|
8522
|
+
for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
|
|
8469
8523
|
for (const ev of events) {
|
|
8524
|
+
if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
|
|
8525
|
+
const claimed = this.attribution.current;
|
|
8526
|
+
if (claimed && claimed !== this.liveTicket) {
|
|
8527
|
+
if (!this.muted) {
|
|
8528
|
+
this.eventsSink?.write(`${JSON.stringify({
|
|
8529
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8530
|
+
cycle: this.emitCycle,
|
|
8531
|
+
route: plan.route,
|
|
8532
|
+
step: "agent",
|
|
8533
|
+
level: "info",
|
|
8534
|
+
message: `ticket: ${this.liveTicket ?? "none"} \u2192 ${claimed}`,
|
|
8535
|
+
role: plan.role,
|
|
8536
|
+
ticket: claimed,
|
|
8537
|
+
data: { kind: "ticket", from: this.liveTicket ?? null, to: claimed }
|
|
8538
|
+
})}
|
|
8539
|
+
`);
|
|
8540
|
+
}
|
|
8541
|
+
this.liveTicket = claimed;
|
|
8542
|
+
if (!this.muted) this.onClaim?.();
|
|
8543
|
+
}
|
|
8544
|
+
}
|
|
8470
8545
|
const e = {
|
|
8471
8546
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8472
8547
|
cycle: this.emitCycle,
|
|
@@ -8475,7 +8550,7 @@ var RunObserver = class {
|
|
|
8475
8550
|
level: "info",
|
|
8476
8551
|
message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
|
|
8477
8552
|
role: plan.role,
|
|
8478
|
-
...
|
|
8553
|
+
...this.liveTicket ? { ticket: this.liveTicket } : {},
|
|
8479
8554
|
data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
|
|
8480
8555
|
};
|
|
8481
8556
|
if (!this.muted) this.eventsSink?.write(`${JSON.stringify(e)}
|
|
@@ -8510,25 +8585,39 @@ var RunObserver = class {
|
|
|
8510
8585
|
if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
|
|
8511
8586
|
}
|
|
8512
8587
|
};
|
|
8513
|
-
|
|
8514
|
-
const attributedTicket = obs.attribution.winner(id.ticket ?? void 0);
|
|
8515
|
-
const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
|
|
8516
|
-
const { result } = obs;
|
|
8588
|
+
function writeRunMeta(id, startedAt, liveTicket, extra) {
|
|
8517
8589
|
try {
|
|
8518
8590
|
writeFileSync8(`${id.streamPath}.meta.json`, JSON.stringify({
|
|
8519
8591
|
route: id.route,
|
|
8520
8592
|
role: id.role,
|
|
8521
|
-
|
|
8522
|
-
|
|
8593
|
+
hint: id.ticket,
|
|
8594
|
+
ticket: liveTicket ?? id.ticket,
|
|
8523
8595
|
startedAt,
|
|
8524
|
-
exitCode: end.code,
|
|
8525
8596
|
model: id.model,
|
|
8597
|
+
toolchain: id.toolchain,
|
|
8598
|
+
harness: id.harness,
|
|
8599
|
+
provider: id.provider,
|
|
8600
|
+
exitCode: null,
|
|
8601
|
+
running: true,
|
|
8602
|
+
...extra
|
|
8603
|
+
}));
|
|
8604
|
+
} catch {
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8607
|
+
async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
8608
|
+
const attributedTicket = obs.attribution.winner(obs.liveTicket);
|
|
8609
|
+
const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
|
|
8610
|
+
const { result } = obs;
|
|
8611
|
+
try {
|
|
8612
|
+
writeRunMeta(id, startedAt, obs.liveTicket, {
|
|
8613
|
+
attributedTicket: attributedTicket ?? null,
|
|
8614
|
+
running: void 0,
|
|
8615
|
+
exitCode: end.code,
|
|
8526
8616
|
...end.interrupted ? { interrupted: end.interrupted } : {},
|
|
8527
|
-
...identity,
|
|
8528
8617
|
sessionId: result?.sessionId ?? null,
|
|
8529
8618
|
numTurns: result?.numTurns ?? null,
|
|
8530
8619
|
totalCostUsd: result?.costUsd ?? null
|
|
8531
|
-
})
|
|
8620
|
+
});
|
|
8532
8621
|
} catch {
|
|
8533
8622
|
}
|
|
8534
8623
|
const ms2 = Date.now() - Date.parse(startedAt);
|
|
@@ -8616,6 +8705,9 @@ function spawnAgent(plan, emit) {
|
|
|
8616
8705
|
}
|
|
8617
8706
|
}
|
|
8618
8707
|
const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
|
|
8708
|
+
const meta = { pid: child.pid ?? null };
|
|
8709
|
+
obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
8710
|
+
if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
|
|
8619
8711
|
const tail = new FileTail(plan.streamPath);
|
|
8620
8712
|
const timer = setInterval(() => obs.feed(tail.poll()), TAIL_POLL_MS);
|
|
8621
8713
|
let spawnFailed = false;
|
|
@@ -8674,12 +8766,12 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
|
|
|
8674
8766
|
}
|
|
8675
8767
|
|
|
8676
8768
|
// src/run-worktree.ts
|
|
8677
|
-
import { existsSync as
|
|
8769
|
+
import { existsSync as existsSync15 } from "fs";
|
|
8678
8770
|
import { dirname as dirname7, join as join16 } from "path";
|
|
8679
8771
|
|
|
8680
8772
|
// src/ports.ts
|
|
8681
8773
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
8682
|
-
import { existsSync as
|
|
8774
|
+
import { existsSync as existsSync14 } from "fs";
|
|
8683
8775
|
import { join as join15 } from "path";
|
|
8684
8776
|
var run = (cmd, args) => {
|
|
8685
8777
|
try {
|
|
@@ -8725,7 +8817,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
8725
8817
|
}
|
|
8726
8818
|
function worktreeExists(parent, prefix, n) {
|
|
8727
8819
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
8728
|
-
if (
|
|
8820
|
+
if (existsSync14(join15(parent, name, ".git"))) return true;
|
|
8729
8821
|
}
|
|
8730
8822
|
return false;
|
|
8731
8823
|
}
|
|
@@ -8801,7 +8893,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
8801
8893
|
const existing = worktreeForNumber(dir, num);
|
|
8802
8894
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
8803
8895
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
8804
|
-
if (existing && !existing.prunable &&
|
|
8896
|
+
if (existing && !existing.prunable && existsSync15(existing.path)) {
|
|
8805
8897
|
const branch = existing.branch;
|
|
8806
8898
|
if (status(existing.path).length > 0) {
|
|
8807
8899
|
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
@@ -8866,7 +8958,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
8866
8958
|
}
|
|
8867
8959
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
8868
8960
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
8869
|
-
if (!wt || !
|
|
8961
|
+
if (!wt || !existsSync15(wt.path)) return { kind: "none" };
|
|
8870
8962
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
8871
8963
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
8872
8964
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -8878,7 +8970,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
8878
8970
|
if (pids.length) await killGently(pids);
|
|
8879
8971
|
}
|
|
8880
8972
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
8881
|
-
return
|
|
8973
|
+
return existsSync15(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
8882
8974
|
}
|
|
8883
8975
|
|
|
8884
8976
|
// src/handoff-guard.ts
|
|
@@ -9114,12 +9206,12 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
9114
9206
|
|
|
9115
9207
|
// src/connect-wizard.ts
|
|
9116
9208
|
import { delimiter, dirname as dirname8, join as join17 } from "path";
|
|
9117
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
9209
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
9118
9210
|
import { stringify } from "yaml";
|
|
9119
9211
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
9120
9212
|
function shouldRunWizard(opts) {
|
|
9121
9213
|
if (!opts.isTTY) return false;
|
|
9122
|
-
const exists = opts.configExists ??
|
|
9214
|
+
const exists = opts.configExists ?? existsSync16;
|
|
9123
9215
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
9124
9216
|
}
|
|
9125
9217
|
function firstRunConfigPath() {
|
|
@@ -9231,7 +9323,7 @@ Wrote ${o.configPath}.
|
|
|
9231
9323
|
}
|
|
9232
9324
|
|
|
9233
9325
|
// src/release-run.ts
|
|
9234
|
-
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as
|
|
9326
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync17, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync7 } from "fs";
|
|
9235
9327
|
import { dirname as dirname9, join as join18 } from "path";
|
|
9236
9328
|
import { tmpdir as tmpdir2 } from "os";
|
|
9237
9329
|
|
|
@@ -9922,7 +10014,7 @@ async function bump(o, size) {
|
|
|
9922
10014
|
}
|
|
9923
10015
|
const files = o.repo.release.versionFiles.map((f) => join18(o.cwd, f));
|
|
9924
10016
|
const first = files[0];
|
|
9925
|
-
if (!
|
|
10017
|
+
if (!existsSync17(first)) {
|
|
9926
10018
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
9927
10019
|
return null;
|
|
9928
10020
|
}
|
|
@@ -9958,7 +10050,7 @@ function writeChangelog(o, version, entries) {
|
|
|
9958
10050
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
9959
10051
|
return;
|
|
9960
10052
|
}
|
|
9961
|
-
const existing =
|
|
10053
|
+
const existing = existsSync17(path) ? readFileSync13(path, "utf8") : "# Changelog\n";
|
|
9962
10054
|
writeFileSync10(path, insertChangelogSection(existing, section));
|
|
9963
10055
|
}
|
|
9964
10056
|
async function confirm(o, expected) {
|
|
@@ -10064,7 +10156,7 @@ function openReleaseCheckout(o) {
|
|
|
10064
10156
|
}
|
|
10065
10157
|
for (const f of o.repo.worktrees.copy) {
|
|
10066
10158
|
const src = join18(o.cwd, f);
|
|
10067
|
-
if (!
|
|
10159
|
+
if (!existsSync17(src)) continue;
|
|
10068
10160
|
mkdirSync8(dirname9(join18(dir, f)), { recursive: true });
|
|
10069
10161
|
copyFileSync(src, join18(dir, f));
|
|
10070
10162
|
}
|
|
@@ -10649,7 +10741,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
10649
10741
|
}
|
|
10650
10742
|
|
|
10651
10743
|
// src/watch.ts
|
|
10652
|
-
import { watch as watchFile, existsSync as
|
|
10744
|
+
import { watch as watchFile, existsSync as existsSync18, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
10653
10745
|
var E = "\x1B[";
|
|
10654
10746
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
10655
10747
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -10750,7 +10842,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
10750
10842
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
10751
10843
|
}
|
|
10752
10844
|
function readFrom(file, from) {
|
|
10753
|
-
if (!
|
|
10845
|
+
if (!existsSync18(file)) return { events: [], offset: from };
|
|
10754
10846
|
const size = statSync6(file).size;
|
|
10755
10847
|
if (size < from) return readFrom(file, 0);
|
|
10756
10848
|
if (size === from) return { events: [], offset: from };
|
|
@@ -11769,7 +11861,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
11769
11861
|
}
|
|
11770
11862
|
|
|
11771
11863
|
// src/worktree-sweep.ts
|
|
11772
|
-
import { existsSync as
|
|
11864
|
+
import { existsSync as existsSync19 } from "fs";
|
|
11773
11865
|
import { basename as basename8 } from "path";
|
|
11774
11866
|
function planWorktreeSweep(target, tickets, contract) {
|
|
11775
11867
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -11805,7 +11897,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
11805
11897
|
let removed = 0;
|
|
11806
11898
|
let keptBranches = 0;
|
|
11807
11899
|
for (const a of actions) {
|
|
11808
|
-
if (!dryRun &&
|
|
11900
|
+
if (!dryRun && existsSync19(a.path)) {
|
|
11809
11901
|
let dirty = false;
|
|
11810
11902
|
try {
|
|
11811
11903
|
dirty = status(a.path).length > 0;
|
|
@@ -11819,7 +11911,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
11819
11911
|
continue;
|
|
11820
11912
|
}
|
|
11821
11913
|
}
|
|
11822
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
11914
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync19(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
11823
11915
|
log.warn(
|
|
11824
11916
|
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
11825
11917
|
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
@@ -11951,12 +12043,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
11951
12043
|
}
|
|
11952
12044
|
|
|
11953
12045
|
// src/stream-sweep.ts
|
|
11954
|
-
import { existsSync as
|
|
12046
|
+
import { existsSync as existsSync20, readdirSync as readdirSync5, rmSync as rmSync8, statSync as statSync7 } from "fs";
|
|
11955
12047
|
import { join as join19 } from "path";
|
|
11956
12048
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
11957
12049
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
11958
12050
|
const dir = join19(stateDir, "streams");
|
|
11959
|
-
if (!
|
|
12051
|
+
if (!existsSync20(dir)) return [];
|
|
11960
12052
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
11961
12053
|
return readdirSync5(dir).map((name) => join19(dir, name)).filter((path) => {
|
|
11962
12054
|
try {
|
|
@@ -11984,7 +12076,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
11984
12076
|
return removed;
|
|
11985
12077
|
}
|
|
11986
12078
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
11987
|
-
if (!
|
|
12079
|
+
if (!existsSync20(stateDir)) return [];
|
|
11988
12080
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
11989
12081
|
return readdirSync5(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join19(stateDir, name)).filter((path) => {
|
|
11990
12082
|
try {
|
|
@@ -11996,13 +12088,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
11996
12088
|
}
|
|
11997
12089
|
|
|
11998
12090
|
// src/cli.ts
|
|
11999
|
-
import { readFileSync as
|
|
12091
|
+
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";
|
|
12000
12092
|
import { createInterface } from "readline/promises";
|
|
12001
12093
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
12002
12094
|
|
|
12003
12095
|
// src/ssh-keys.ts
|
|
12004
12096
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
12005
|
-
import { existsSync as
|
|
12097
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
|
|
12006
12098
|
import { dirname as dirname11, join as join20 } from "path";
|
|
12007
12099
|
var SshKeyError = class extends Error {
|
|
12008
12100
|
};
|
|
@@ -12012,7 +12104,7 @@ function shipSshKeyPathFor(stateDir) {
|
|
|
12012
12104
|
function ensureShipSshKeypair(stateDir) {
|
|
12013
12105
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
12014
12106
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
12015
|
-
if (!
|
|
12107
|
+
if (!existsSync21(privateKeyPath)) {
|
|
12016
12108
|
mkdirSync9(dirname11(privateKeyPath), { recursive: true });
|
|
12017
12109
|
try {
|
|
12018
12110
|
execFileSync3("ssh-keygen", [
|
|
@@ -12033,7 +12125,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
12033
12125
|
}
|
|
12034
12126
|
chmodSync(privateKeyPath, 384);
|
|
12035
12127
|
}
|
|
12036
|
-
if (!
|
|
12128
|
+
if (!existsSync21(publicKeyPath)) {
|
|
12037
12129
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
12038
12130
|
}
|
|
12039
12131
|
const publicKey = readFileSync14(publicKeyPath, "utf8").trim();
|
|
@@ -12252,7 +12344,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
12252
12344
|
|
|
12253
12345
|
// src/tunnel.ts
|
|
12254
12346
|
import { spawn as spawn3 } from "child_process";
|
|
12255
|
-
import { existsSync as
|
|
12347
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync6 } from "fs";
|
|
12256
12348
|
import { randomInt } from "crypto";
|
|
12257
12349
|
import { join as join21 } from "path";
|
|
12258
12350
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -12325,7 +12417,7 @@ function tunnelStatePath(stateDir, workspaceId) {
|
|
|
12325
12417
|
}
|
|
12326
12418
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
12327
12419
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
12328
|
-
if (!
|
|
12420
|
+
if (!existsSync22(path)) return void 0;
|
|
12329
12421
|
try {
|
|
12330
12422
|
return JSON.parse(readFileSync15(path, "utf8"));
|
|
12331
12423
|
} catch {
|
|
@@ -12433,7 +12525,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
12433
12525
|
}
|
|
12434
12526
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
12435
12527
|
const dir = join21(stateDir, "passengers");
|
|
12436
|
-
if (!
|
|
12528
|
+
if (!existsSync22(dir)) return [];
|
|
12437
12529
|
return readdirSync6(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
12438
12530
|
}
|
|
12439
12531
|
|
|
@@ -12554,7 +12646,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
12554
12646
|
import { createServer as createHttpServer } from "http";
|
|
12555
12647
|
import { spawn as spawn4 } from "child_process";
|
|
12556
12648
|
import { randomBytes } from "crypto";
|
|
12557
|
-
import { existsSync as
|
|
12649
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync7 } from "fs";
|
|
12558
12650
|
import { join as join22 } from "path";
|
|
12559
12651
|
|
|
12560
12652
|
// src/secret-compare.ts
|
|
@@ -12648,7 +12740,7 @@ function syncDaemonStatePath(stateDir, workspaceId) {
|
|
|
12648
12740
|
}
|
|
12649
12741
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
12650
12742
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
12651
|
-
if (!
|
|
12743
|
+
if (!existsSync23(path)) return void 0;
|
|
12652
12744
|
try {
|
|
12653
12745
|
return JSON.parse(readFileSync16(path, "utf8"));
|
|
12654
12746
|
} catch {
|
|
@@ -12679,7 +12771,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
12679
12771
|
}
|
|
12680
12772
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
12681
12773
|
const dir = join22(stateDir, "passengers");
|
|
12682
|
-
if (!
|
|
12774
|
+
if (!existsSync23(dir)) return [];
|
|
12683
12775
|
const suffix = "-sync.json";
|
|
12684
12776
|
return readdirSync7(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
12685
12777
|
}
|
|
@@ -12736,11 +12828,11 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
12736
12828
|
}
|
|
12737
12829
|
|
|
12738
12830
|
// src/daemon.ts
|
|
12739
|
-
import { existsSync as
|
|
12831
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
12740
12832
|
import { join as join24 } from "path";
|
|
12741
12833
|
|
|
12742
12834
|
// src/interrupted.ts
|
|
12743
|
-
import { existsSync as
|
|
12835
|
+
import { existsSync as existsSync24, readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
|
|
12744
12836
|
import { join as join23 } from "path";
|
|
12745
12837
|
var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
|
|
12746
12838
|
function interruptedComment(b) {
|
|
@@ -12757,15 +12849,24 @@ var pidAlive = (pid) => {
|
|
|
12757
12849
|
return e.code === "EPERM";
|
|
12758
12850
|
}
|
|
12759
12851
|
};
|
|
12852
|
+
function runFinished(streamPath) {
|
|
12853
|
+
const path = `${streamPath}.meta.json`;
|
|
12854
|
+
if (!existsSync24(path)) return false;
|
|
12855
|
+
try {
|
|
12856
|
+
return JSON.parse(readFileSync17(path, "utf8")).running !== true;
|
|
12857
|
+
} catch {
|
|
12858
|
+
return true;
|
|
12859
|
+
}
|
|
12860
|
+
}
|
|
12760
12861
|
function orphanedStreams(o) {
|
|
12761
12862
|
const dir = join23(o.stateDir, "streams");
|
|
12762
|
-
if (!
|
|
12863
|
+
if (!existsSync24(dir)) return [];
|
|
12763
12864
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
12764
12865
|
const out = [];
|
|
12765
12866
|
for (const name of readdirSync8(dir)) {
|
|
12766
12867
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
12767
12868
|
const streamPath = join23(dir, name);
|
|
12768
|
-
if (
|
|
12869
|
+
if (runFinished(streamPath)) continue;
|
|
12769
12870
|
const sidecar = readRunSidecar(streamPath);
|
|
12770
12871
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
12771
12872
|
if (ownerAlive(sidecar.ownerPid)) continue;
|
|
@@ -12873,7 +12974,7 @@ function computeTreeSignature(paths) {
|
|
|
12873
12974
|
}
|
|
12874
12975
|
function makeStaleChecker(paths, opts = {}) {
|
|
12875
12976
|
const initial = computeTreeSignature(paths);
|
|
12876
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
12977
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync25(opts.reloadSentinel);
|
|
12877
12978
|
}
|
|
12878
12979
|
function reloadSentinelFor(stateDir) {
|
|
12879
12980
|
return join24(stateDir, "reload-requested");
|
|
@@ -12889,7 +12990,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
12889
12990
|
}
|
|
12890
12991
|
function readDaemonStarted(stateDir) {
|
|
12891
12992
|
try {
|
|
12892
|
-
const v = JSON.parse(
|
|
12993
|
+
const v = JSON.parse(readFileSync18(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
12893
12994
|
return typeof v === "number" ? v : null;
|
|
12894
12995
|
} catch {
|
|
12895
12996
|
return null;
|
|
@@ -13759,7 +13860,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13759
13860
|
if (host === "linux" && hasSystemd) {
|
|
13760
13861
|
const label = labelFor(CREW_HOME, "run");
|
|
13761
13862
|
const staleTimer = join25(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
13762
|
-
if (
|
|
13863
|
+
if (existsSync26(staleTimer)) {
|
|
13763
13864
|
process.stdout.write(
|
|
13764
13865
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
13765
13866
|
`
|
|
@@ -13785,7 +13886,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13785
13886
|
const tracker = new Tracker(c, cfg.ship);
|
|
13786
13887
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
13787
13888
|
for (const r of await resolvedRepos(c)) {
|
|
13788
|
-
if (!
|
|
13889
|
+
if (!existsSync26(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
13789
13890
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
13790
13891
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
13791
13892
|
}
|
|
@@ -13803,7 +13904,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13803
13904
|
}
|
|
13804
13905
|
async function doctorFix(o) {
|
|
13805
13906
|
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))));
|
|
13806
|
-
const unitExists = (p) =>
|
|
13907
|
+
const unitExists = (p) => existsSync26(p);
|
|
13807
13908
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
13808
13909
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
13809
13910
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -13820,7 +13921,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
13820
13921
|
for (const a of plan.actions) {
|
|
13821
13922
|
if (a.kind === "enable") {
|
|
13822
13923
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
13823
|
-
writeFileSync14(cfg.configFile, planEnable(
|
|
13924
|
+
writeFileSync14(cfg.configFile, planEnable(readFileSync19(cfg.configFile, "utf8"), a.route));
|
|
13824
13925
|
enabledSound.add(a.route);
|
|
13825
13926
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
13826
13927
|
} else {
|
|
@@ -14958,7 +15059,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
14958
15059
|
`);
|
|
14959
15060
|
if (!dryRun) {
|
|
14960
15061
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
14961
|
-
const raw =
|
|
15062
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
|
|
14962
15063
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
14963
15064
|
raw.agentSkills = skillResult.agentSkills;
|
|
14964
15065
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
@@ -15018,7 +15119,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15018
15119
|
`);
|
|
15019
15120
|
if (!dryRun) {
|
|
15020
15121
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
15021
|
-
const raw =
|
|
15122
|
+
const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
|
|
15022
15123
|
raw.agentSkills = result.agentSkills;
|
|
15023
15124
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
15024
15125
|
writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15034,14 +15135,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15034
15135
|
process.exit(2);
|
|
15035
15136
|
};
|
|
15036
15137
|
if (sub !== "add" && sub !== "list") usage2();
|
|
15037
|
-
const configText =
|
|
15138
|
+
const configText = readFileSync19(cfg.configFile, "utf8");
|
|
15038
15139
|
let routeName = positional[2];
|
|
15039
15140
|
let addPath = ".";
|
|
15040
15141
|
let addName;
|
|
15041
15142
|
if (sub === "list" && !routeName) usage2();
|
|
15042
15143
|
if (sub === "add") {
|
|
15043
15144
|
try {
|
|
15044
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
15145
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync26(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
15045
15146
|
routeName = a.route;
|
|
15046
15147
|
addPath = a.path;
|
|
15047
15148
|
addName = a.name;
|
|
@@ -15056,9 +15157,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15056
15157
|
}
|
|
15057
15158
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
15058
15159
|
let resolvedRemotes;
|
|
15059
|
-
if (
|
|
15160
|
+
if (existsSync26(resolvedFile)) {
|
|
15060
15161
|
try {
|
|
15061
|
-
resolvedRemotes = JSON.parse(
|
|
15162
|
+
resolvedRemotes = JSON.parse(readFileSync19(resolvedFile, "utf8"));
|
|
15062
15163
|
} catch {
|
|
15063
15164
|
}
|
|
15064
15165
|
}
|
|
@@ -15073,14 +15174,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15073
15174
|
for (const r of repos) {
|
|
15074
15175
|
const origin2 = originOf(r.dir);
|
|
15075
15176
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
15076
|
-
const note = !
|
|
15177
|
+
const note = !existsSync26(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
15077
15178
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
15078
15179
|
`);
|
|
15079
15180
|
}
|
|
15080
15181
|
break;
|
|
15081
15182
|
}
|
|
15082
15183
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
15083
|
-
const top =
|
|
15184
|
+
const top = existsSync26(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
15084
15185
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
15085
15186
|
const origin = originOf(top);
|
|
15086
15187
|
const plan = planRepoAdd({
|
|
@@ -15129,8 +15230,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
15129
15230
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
15130
15231
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
15131
15232
|
`);
|
|
15132
|
-
if (
|
|
15133
|
-
const raw = JSON.parse(
|
|
15233
|
+
if (existsSync26(resolvedFile)) {
|
|
15234
|
+
const raw = JSON.parse(readFileSync19(resolvedFile, "utf8"));
|
|
15134
15235
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
15135
15236
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
15136
15237
|
writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -15376,10 +15477,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
15376
15477
|
operator = await pickOperator(found.holds);
|
|
15377
15478
|
}
|
|
15378
15479
|
let previous;
|
|
15379
|
-
const hadPreviousFile =
|
|
15480
|
+
const hadPreviousFile = existsSync26(resolvedPath);
|
|
15380
15481
|
if (hadPreviousFile) {
|
|
15381
15482
|
try {
|
|
15382
|
-
previous = JSON.parse(
|
|
15483
|
+
previous = JSON.parse(readFileSync19(resolvedPath, "utf8"));
|
|
15383
15484
|
} catch {
|
|
15384
15485
|
}
|
|
15385
15486
|
}
|
|
@@ -16071,7 +16172,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
16071
16172
|
}
|
|
16072
16173
|
case "log": {
|
|
16073
16174
|
try {
|
|
16074
|
-
const text =
|
|
16175
|
+
const text = readFileSync19(cfg.ship.logFile, "utf8").split("\n");
|
|
16075
16176
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
16076
16177
|
`);
|
|
16077
16178
|
} catch {
|
|
@@ -16281,7 +16382,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16281
16382
|
);
|
|
16282
16383
|
try {
|
|
16283
16384
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
16284
|
-
const evSize =
|
|
16385
|
+
const evSize = existsSync26(evFile) ? statSync9(evFile).size : 0;
|
|
16285
16386
|
process.stdout.write(
|
|
16286
16387
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
16287
16388
|
` : "events log: ok\n"
|
|
@@ -16292,7 +16393,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16292
16393
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
16293
16394
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
16294
16395
|
try {
|
|
16295
|
-
return
|
|
16396
|
+
return readFileSync19(p, "utf8");
|
|
16296
16397
|
} catch {
|
|
16297
16398
|
return null;
|
|
16298
16399
|
}
|
|
@@ -16339,12 +16440,10 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
16339
16440
|
process.stdout.write(` - ${p2}
|
|
16340
16441
|
`);
|
|
16341
16442
|
}
|
|
16342
|
-
const
|
|
16343
|
-
|
|
16344
|
-
|
|
16345
|
-
|
|
16346
|
-
if (bh.unsafe) process.exitCode = 1;
|
|
16347
|
-
}
|
|
16443
|
+
const dh = doctorBaseReport(r, config.branch.base, baseHealth);
|
|
16444
|
+
process.stdout.write(dh.text);
|
|
16445
|
+
if (dh.failed) process.exitCode = 1;
|
|
16446
|
+
if (dh.skipRest) continue;
|
|
16348
16447
|
if (config.release.ship) {
|
|
16349
16448
|
process.stdout.write(` release.ship ${config.release.ship}${config.release.ship === cfg.ship.name ? " (this ship)" : ""}
|
|
16350
16449
|
`);
|
package/package.json
CHANGED