@tablation/crew 0.40.1 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { resolve as resolve4, dirname as dirname12, join as
|
|
4
|
+
import { resolve as resolve4, dirname as dirname12, join as join23, basename as basename9 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { hostname as hostname2, homedir as
|
|
6
|
+
import { hostname as hostname2, homedir as homedir8 } from "os";
|
|
7
7
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
8
8
|
|
|
9
9
|
// src/argv.ts
|
|
@@ -40,11 +40,31 @@ function positionalArgs(argv2) {
|
|
|
40
40
|
}
|
|
41
41
|
return out;
|
|
42
42
|
}
|
|
43
|
+
var WORKTREE_ROLES = ["dev", "design", "qa"];
|
|
44
|
+
function splitWorktreeArgs(positional2, routeNames) {
|
|
45
|
+
const args = positional2.slice(1);
|
|
46
|
+
if (args.length > 1 && WORKTREE_ROLES.includes(args[args.length - 1] ?? "")) args.pop();
|
|
47
|
+
const [a, b] = [args[0], args[1]];
|
|
48
|
+
const isTicket = (s) => !!s && /^(?:[A-Za-z]+-)?\d+$/.test(s.trim());
|
|
49
|
+
if (b !== void 0) return { route: a, ticket: b };
|
|
50
|
+
if (a !== void 0 && isTicket(a) && !routeNames.includes(a)) return { ticket: a };
|
|
51
|
+
return { route: a, ticket: void 0 };
|
|
52
|
+
}
|
|
53
|
+
function pickWorktreeRoute(ticketNumber, candidates) {
|
|
54
|
+
for (const c of candidates) {
|
|
55
|
+
if (c.hit && (candidates.length === 1 || c.servedRepoIds.includes(c.hit.repoId ?? ""))) return { route: c.route };
|
|
56
|
+
}
|
|
57
|
+
const looked = candidates.map((c) => c.route).join(", ");
|
|
58
|
+
const found = candidates.filter((c) => c.hit).map((c) => `${c.route} (repo ${c.hit?.repoId ?? "none"})`);
|
|
59
|
+
return {
|
|
60
|
+
error: `no open ticket ISSUE-${ticketNumber} served by this ship \u2014 looked on ${looked}` + (found.length ? `; found it only on ${found.join(", ")}, which has no checkout for its repo` : "")
|
|
61
|
+
};
|
|
62
|
+
}
|
|
43
63
|
|
|
44
64
|
// src/config.ts
|
|
45
|
-
import { readFileSync as
|
|
46
|
-
import { resolve, dirname as dirname3, isAbsolute, join as
|
|
47
|
-
import { homedir as
|
|
65
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
66
|
+
import { resolve, dirname as dirname3, isAbsolute, join as join7, basename as basename3 } from "path";
|
|
67
|
+
import { homedir as homedir5, tmpdir } from "os";
|
|
48
68
|
import { parse as parse2 } from "yaml";
|
|
49
69
|
|
|
50
70
|
// src/events.ts
|
|
@@ -193,10 +213,10 @@ var Emitter = class _Emitter {
|
|
|
193
213
|
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
194
214
|
|
|
195
215
|
// src/pair-install.ts
|
|
196
|
-
import { existsSync as
|
|
216
|
+
import { existsSync as existsSync4, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
197
217
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
198
|
-
import { homedir as
|
|
199
|
-
import { join as
|
|
218
|
+
import { homedir as homedir4 } from "os";
|
|
219
|
+
import { join as join5 } from "path";
|
|
200
220
|
|
|
201
221
|
// src/stream.ts
|
|
202
222
|
function toolTarget(name, input) {
|
|
@@ -320,6 +340,74 @@ function removeClaudeMcp(o) {
|
|
|
320
340
|
return rm.status === 0 ? `mcp: removed the ${MCP_SERVER_NAME} MCP server from Claude Code (user scope)` : `mcp: \`claude mcp remove\` failed (${rm.stderr.trim() || `exit ${rm.status}`})`;
|
|
321
341
|
}
|
|
322
342
|
|
|
343
|
+
// src/launcher.ts
|
|
344
|
+
import { basename, join as join3 } from "path";
|
|
345
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
346
|
+
import { homedir as homedir2 } from "os";
|
|
347
|
+
function isOriLauncher(spec) {
|
|
348
|
+
return !!spec.launcher && basename(spec.launcher) === "ori";
|
|
349
|
+
}
|
|
350
|
+
function isOmlxLauncher(spec) {
|
|
351
|
+
return !!spec.launcher && basename(spec.launcher) === "omlx";
|
|
352
|
+
}
|
|
353
|
+
function defaultOmlxBin(home = homedir2()) {
|
|
354
|
+
return join3(home, ".omlx", "bin", "omlx");
|
|
355
|
+
}
|
|
356
|
+
function launcherExecutable(spec, home) {
|
|
357
|
+
if (!spec.launcher) return void 0;
|
|
358
|
+
if (spec.launcherBin) return spec.launcherBin;
|
|
359
|
+
return isOmlxLauncher(spec) ? defaultOmlxBin(home) : spec.launcher;
|
|
360
|
+
}
|
|
361
|
+
function wrapInvocation(spec, o) {
|
|
362
|
+
const exe = launcherExecutable(spec);
|
|
363
|
+
if (!exe) return { bin: spec.bin, args: o.args };
|
|
364
|
+
if (isOmlxLauncher(spec)) return { bin: exe, args: ["launch", spec.harness, "--model", o.model, "--", ...o.args] };
|
|
365
|
+
return { bin: exe, args: [spec.bin, ...o.args] };
|
|
366
|
+
}
|
|
367
|
+
function selectsCustomModelProvider(args) {
|
|
368
|
+
for (let i = 0; i < args.length; i++) {
|
|
369
|
+
const a = args[i];
|
|
370
|
+
const v = a === "-c" || a === "--config" ? args[i + 1] : a.startsWith("--config=") ? a.slice("--config=".length) : void 0;
|
|
371
|
+
const m = v && /^model_provider\s*=\s*"?([^"\s]+)"?$/.exec(v.trim());
|
|
372
|
+
if (m && m[1] !== "openai") return true;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
function parseOmlxSettings(text) {
|
|
377
|
+
let raw;
|
|
378
|
+
try {
|
|
379
|
+
raw = JSON.parse(text);
|
|
380
|
+
} catch {
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
384
|
+
const { server, model } = raw;
|
|
385
|
+
const port = Number(server?.port);
|
|
386
|
+
if (!Number.isInteger(port) || port <= 0) return void 0;
|
|
387
|
+
const host = typeof server?.host === "string" && server.host ? server.host : "127.0.0.1";
|
|
388
|
+
const dirs = Array.isArray(model?.model_dirs) ? model.model_dirs.filter((d) => typeof d === "string") : [];
|
|
389
|
+
return { host: host === "0.0.0.0" ? "127.0.0.1" : host, port, modelDirs: dirs };
|
|
390
|
+
}
|
|
391
|
+
function interpretOmlxProbe(status2) {
|
|
392
|
+
if (status2 === void 0) return { up: false, detail: "not reachable" };
|
|
393
|
+
if (status2 === 401) return { up: true, detail: "up (key required)" };
|
|
394
|
+
if (status2 >= 200 && status2 < 300) return { up: true, detail: "up (no key required)" };
|
|
395
|
+
return { up: false, detail: `answered HTTP ${status2}` };
|
|
396
|
+
}
|
|
397
|
+
function omlxModelPresent(model, dirs) {
|
|
398
|
+
return dirs.some((d) => {
|
|
399
|
+
try {
|
|
400
|
+
return readdirSync(d).includes(model);
|
|
401
|
+
} catch {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function readOmlxSettings(home = homedir2()) {
|
|
407
|
+
const p = join3(home, ".omlx", "settings.json");
|
|
408
|
+
return existsSync2(p) ? parseOmlxSettings(readFileSync3(p, "utf8")) : void 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
323
411
|
// src/capability.ts
|
|
324
412
|
function profileFor(role) {
|
|
325
413
|
if (role === "triage") return { level: "read", designTools: false };
|
|
@@ -400,6 +488,9 @@ var claudeAdapter = {
|
|
|
400
488
|
...allowedTools(o.role),
|
|
401
489
|
"--disallowedTools",
|
|
402
490
|
...DISALLOWED_TOOLS,
|
|
491
|
+
// oMLX denies LSP itself for prefix-cache stability, but only when the
|
|
492
|
+
// caller passes no --disallowedTools of its own — and crew always does.
|
|
493
|
+
...isOmlxLauncher(spec) ? ["LSP"] : [],
|
|
403
494
|
"--model",
|
|
404
495
|
o.model,
|
|
405
496
|
"--output-format",
|
|
@@ -421,7 +512,7 @@ var claudeAdapter = {
|
|
|
421
512
|
})] : [],
|
|
422
513
|
...spec.args
|
|
423
514
|
];
|
|
424
|
-
return spec
|
|
515
|
+
return wrapInvocation(spec, { model: o.model, args });
|
|
425
516
|
},
|
|
426
517
|
runEnv(spec) {
|
|
427
518
|
return {
|
|
@@ -446,12 +537,12 @@ var claudeAdapter = {
|
|
|
446
537
|
|
|
447
538
|
// src/adapters/codex.ts
|
|
448
539
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
449
|
-
import { homedir as
|
|
450
|
-
import { join as
|
|
540
|
+
import { homedir as homedir3 } from "os";
|
|
541
|
+
import { join as join4 } from "path";
|
|
451
542
|
import { rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
452
543
|
|
|
453
544
|
// src/pair-files.ts
|
|
454
|
-
import { existsSync as
|
|
545
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync, rmSync as rmSync2 } from "fs";
|
|
455
546
|
import { dirname as dirname2 } from "path";
|
|
456
547
|
var PAIR_BLOCK_START = "<!-- crew:pair start -->";
|
|
457
548
|
var PAIR_BLOCK_END = "<!-- crew:pair end -->";
|
|
@@ -492,7 +583,7 @@ function removePairBlock(text) {
|
|
|
492
583
|
return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
|
|
493
584
|
}
|
|
494
585
|
function readIfExists(path) {
|
|
495
|
-
return
|
|
586
|
+
return existsSync3(path) ? readFileSync4(path, "utf8") : void 0;
|
|
496
587
|
}
|
|
497
588
|
function writeFileMkdir(path, text) {
|
|
498
589
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
@@ -639,8 +730,8 @@ function codexTable(url) {
|
|
|
639
730
|
return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
|
|
640
731
|
}
|
|
641
732
|
var codexPaths = (home) => ({
|
|
642
|
-
instructions:
|
|
643
|
-
config:
|
|
733
|
+
instructions: join4(home, ".codex", "AGENTS.md"),
|
|
734
|
+
config: join4(home, ".codex", "config.toml")
|
|
644
735
|
});
|
|
645
736
|
function codexMcpState(config, url) {
|
|
646
737
|
const lines = config.split("\n");
|
|
@@ -656,7 +747,7 @@ var codexPair = {
|
|
|
656
747
|
harness: "codex",
|
|
657
748
|
displayName: "Codex",
|
|
658
749
|
installPair(o) {
|
|
659
|
-
const p = codexPaths(o.home ??
|
|
750
|
+
const p = codexPaths(o.home ?? homedir3());
|
|
660
751
|
const config = readIfExists(p.config) ?? "";
|
|
661
752
|
const st = codexMcpState(config, o.url);
|
|
662
753
|
if (st.state === "different" && !o.replace) {
|
|
@@ -673,7 +764,7 @@ var codexPair = {
|
|
|
673
764
|
return `pair(codex): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair block in ${p.instructions} and ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
|
|
674
765
|
},
|
|
675
766
|
verifyPair(o) {
|
|
676
|
-
const p = codexPaths(o.home ??
|
|
767
|
+
const p = codexPaths(o.home ?? homedir3());
|
|
677
768
|
const b = blockVerdict(p.instructions, "codex");
|
|
678
769
|
if (!b.ok) return b;
|
|
679
770
|
const st = codexMcpState(readIfExists(p.config) ?? "", o.url);
|
|
@@ -682,7 +773,7 @@ var codexPair = {
|
|
|
682
773
|
return { ok: true, detail: `${p.instructions}, ${p.config}` };
|
|
683
774
|
},
|
|
684
775
|
removePair(o) {
|
|
685
|
-
const p = codexPaths(o.home ??
|
|
776
|
+
const p = codexPaths(o.home ?? homedir3());
|
|
686
777
|
const hadBlock = removeBlockFile(p.instructions, o.dryRun);
|
|
687
778
|
const config = readIfExists(p.config);
|
|
688
779
|
const st = codexMcpState(config ?? "", o.url);
|
|
@@ -758,21 +849,21 @@ var codexAdapter = {
|
|
|
758
849
|
// Prompt on stdin.
|
|
759
850
|
"-"
|
|
760
851
|
];
|
|
761
|
-
return spec
|
|
852
|
+
return wrapInvocation(spec, { model: o.model, args });
|
|
762
853
|
},
|
|
763
854
|
runEnv: (spec) => ({ ...spec.env }),
|
|
764
|
-
provider: () => "openai",
|
|
855
|
+
provider: (spec) => selectsCustomModelProvider(spec.args) ? "local" : "openai",
|
|
765
856
|
unsetEnv: CODEX_BILLING_VARS_TO_UNSET,
|
|
766
857
|
mapStreamLine: mapCodexLine,
|
|
767
858
|
extractResult: extractCodexResult,
|
|
768
859
|
extractTurnTokens: extractCodexTurnTokens,
|
|
769
860
|
inspectMcp(o) {
|
|
770
861
|
if (binMissing(o.bin)) return { state: "no-binary" };
|
|
771
|
-
const st = codexMcpState(readIfExists(codexPaths(o.home ??
|
|
862
|
+
const st = codexMcpState(readIfExists(codexPaths(o.home ?? homedir3()).config) ?? "", o.url);
|
|
772
863
|
return st.state === "registered" ? { state: "registered", url: o.url } : st;
|
|
773
864
|
},
|
|
774
865
|
ensureMcp(o) {
|
|
775
|
-
const p = codexPaths(o.home ??
|
|
866
|
+
const p = codexPaths(o.home ?? homedir3());
|
|
776
867
|
const config = readIfExists(p.config) ?? "";
|
|
777
868
|
const st = codexMcpState(config, o.url);
|
|
778
869
|
if (st.state === "registered") return `mcp: ${SERVER} already registered with Codex \u2192 ${o.url}`;
|
|
@@ -787,8 +878,8 @@ var codexAdapter = {
|
|
|
787
878
|
|
|
788
879
|
// src/pair-install.ts
|
|
789
880
|
var geminiPaths = (home) => ({
|
|
790
|
-
instructions:
|
|
791
|
-
config:
|
|
881
|
+
instructions: join5(home, ".gemini", "GEMINI.md"),
|
|
882
|
+
config: join5(home, ".gemini", "settings.json")
|
|
792
883
|
});
|
|
793
884
|
function readGeminiSettings(path) {
|
|
794
885
|
const text = readIfExists(path);
|
|
@@ -809,14 +900,14 @@ function geminiMcpState(s, url) {
|
|
|
809
900
|
return { state: "registered" };
|
|
810
901
|
}
|
|
811
902
|
function inspectGeminiMcp(o) {
|
|
812
|
-
const p = geminiPaths(o.home ??
|
|
903
|
+
const p = geminiPaths(o.home ?? homedir4());
|
|
813
904
|
const settings = readGeminiSettings(p.config);
|
|
814
905
|
if (!settings) return { state: "different", detail: `${p.config} is not a JSON object` };
|
|
815
906
|
const st = geminiMcpState(settings, o.url);
|
|
816
907
|
return st.state === "registered" ? { state: "registered", url: o.url } : st;
|
|
817
908
|
}
|
|
818
909
|
function ensureGeminiMcp(o) {
|
|
819
|
-
const p = geminiPaths(o.home ??
|
|
910
|
+
const p = geminiPaths(o.home ?? homedir4());
|
|
820
911
|
const settings = readGeminiSettings(p.config);
|
|
821
912
|
if (!settings) return `gemini: ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
822
913
|
const st = geminiMcpState(settings, o.url);
|
|
@@ -834,7 +925,7 @@ var geminiPair = {
|
|
|
834
925
|
harness: "gemini",
|
|
835
926
|
displayName: "Gemini CLI",
|
|
836
927
|
installPair(o) {
|
|
837
|
-
const p = geminiPaths(o.home ??
|
|
928
|
+
const p = geminiPaths(o.home ?? homedir4());
|
|
838
929
|
const settings = readGeminiSettings(p.config);
|
|
839
930
|
if (!settings) return `pair(gemini): ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
840
931
|
const st = geminiMcpState(settings, o.url);
|
|
@@ -853,7 +944,7 @@ var geminiPair = {
|
|
|
853
944
|
return `pair(gemini): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair block in ${p.instructions} and ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
|
|
854
945
|
},
|
|
855
946
|
verifyPair(o) {
|
|
856
|
-
const p = geminiPaths(o.home ??
|
|
947
|
+
const p = geminiPaths(o.home ?? homedir4());
|
|
857
948
|
const b = blockVerdict(p.instructions, "gemini");
|
|
858
949
|
if (!b.ok) return b;
|
|
859
950
|
const settings = readGeminiSettings(p.config);
|
|
@@ -864,7 +955,7 @@ var geminiPair = {
|
|
|
864
955
|
return { ok: true, detail: `${p.instructions}, ${p.config}` };
|
|
865
956
|
},
|
|
866
957
|
removePair(o) {
|
|
867
|
-
const p = geminiPaths(o.home ??
|
|
958
|
+
const p = geminiPaths(o.home ?? homedir4());
|
|
868
959
|
const hadBlock = removeBlockFile(p.instructions, o.dryRun);
|
|
869
960
|
const settings = readGeminiSettings(p.config);
|
|
870
961
|
const st = settings ? geminiMcpState(settings, o.url) : { state: "missing" };
|
|
@@ -885,11 +976,11 @@ var geminiPair = {
|
|
|
885
976
|
};
|
|
886
977
|
var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
|
|
887
978
|
function vscodeUserDirs(o) {
|
|
888
|
-
const home = o.home ??
|
|
979
|
+
const home = o.home ?? homedir4();
|
|
889
980
|
const platform2 = o.platform ?? process.platform;
|
|
890
|
-
const root = platform2 === "darwin" ?
|
|
891
|
-
const all = VSCODE_FLAVOURS.map((f) =>
|
|
892
|
-
const present = all.filter((d) =>
|
|
981
|
+
const root = platform2 === "darwin" ? join5(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join5(home, "AppData", "Roaming") : join5(home, ".config");
|
|
982
|
+
const all = VSCODE_FLAVOURS.map((f) => join5(root, f, "User"));
|
|
983
|
+
const present = all.filter((d) => existsSync4(d));
|
|
893
984
|
return present.length > 0 ? present : [all[0]];
|
|
894
985
|
}
|
|
895
986
|
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
@@ -936,8 +1027,8 @@ var copilotPair = {
|
|
|
936
1027
|
installPair(o) {
|
|
937
1028
|
const lines = [];
|
|
938
1029
|
for (const dir of vscodeUserDirs(o)) {
|
|
939
|
-
const mcpPath =
|
|
940
|
-
const instrPath =
|
|
1030
|
+
const mcpPath = join5(dir, "mcp.json");
|
|
1031
|
+
const instrPath = join5(dir, "prompts", "crew-pair.instructions.md");
|
|
941
1032
|
const settings = readVscodeMcp(mcpPath);
|
|
942
1033
|
if (!settings) {
|
|
943
1034
|
lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
|
|
@@ -965,8 +1056,8 @@ var copilotPair = {
|
|
|
965
1056
|
},
|
|
966
1057
|
verifyPair(o) {
|
|
967
1058
|
const verdicts = vscodeUserDirs(o).map((dir) => {
|
|
968
|
-
const instrPath =
|
|
969
|
-
const mcpPath =
|
|
1059
|
+
const instrPath = join5(dir, "prompts", "crew-pair.instructions.md");
|
|
1060
|
+
const mcpPath = join5(dir, "mcp.json");
|
|
970
1061
|
const b = blockVerdict(instrPath, "copilot");
|
|
971
1062
|
if (!b.ok) return b;
|
|
972
1063
|
const settings = readVscodeMcp(mcpPath);
|
|
@@ -981,8 +1072,8 @@ var copilotPair = {
|
|
|
981
1072
|
removePair(o) {
|
|
982
1073
|
const lines = [];
|
|
983
1074
|
for (const dir of vscodeUserDirs(o)) {
|
|
984
|
-
const mcpPath =
|
|
985
|
-
const hadBlock = removeCopilotInstructions(
|
|
1075
|
+
const mcpPath = join5(dir, "mcp.json");
|
|
1076
|
+
const hadBlock = removeCopilotInstructions(join5(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
|
|
986
1077
|
const settings = readVscodeMcp(mcpPath);
|
|
987
1078
|
const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
|
|
988
1079
|
const hadMcp = st.state === "registered";
|
|
@@ -1040,9 +1131,6 @@ function harnessPromptNote(harness) {
|
|
|
1040
1131
|
`;
|
|
1041
1132
|
}
|
|
1042
1133
|
|
|
1043
|
-
// src/toolchain.ts
|
|
1044
|
-
import { basename } from "path";
|
|
1045
|
-
|
|
1046
1134
|
// src/adapters/gemini.ts
|
|
1047
1135
|
var BILLING_VARS_TO_UNSET2 = [
|
|
1048
1136
|
"GEMINI_API_KEY",
|
|
@@ -1136,7 +1224,7 @@ var geminiAdapter = {
|
|
|
1136
1224
|
...geminiGrantArgs(o.role),
|
|
1137
1225
|
...spec.args
|
|
1138
1226
|
];
|
|
1139
|
-
return spec
|
|
1227
|
+
return wrapInvocation(spec, { model: o.model, args });
|
|
1140
1228
|
},
|
|
1141
1229
|
runEnv: (spec) => ({ ...spec.env }),
|
|
1142
1230
|
provider: () => "google",
|
|
@@ -1192,13 +1280,13 @@ function resolveToolchain(ship, route, repo) {
|
|
|
1192
1280
|
}
|
|
1193
1281
|
function toolchainVendor(spec) {
|
|
1194
1282
|
if (isOriLauncher(spec)) return "openrouter";
|
|
1283
|
+
if (isOmlxLauncher(spec)) return "local";
|
|
1195
1284
|
return adapterFor(spec.harness).vendor;
|
|
1196
1285
|
}
|
|
1197
|
-
function isOriLauncher(spec) {
|
|
1198
|
-
return !!spec.launcher && basename(spec.launcher) === "ori";
|
|
1199
|
-
}
|
|
1200
1286
|
function toolchainProvider(spec) {
|
|
1201
|
-
|
|
1287
|
+
if (isOriLauncher(spec)) return "openrouter";
|
|
1288
|
+
if (isOmlxLauncher(spec)) return "local";
|
|
1289
|
+
return adapterFor(spec.harness).provider(spec);
|
|
1202
1290
|
}
|
|
1203
1291
|
function resolveSeat(ship, route, role, persona = {}, repo) {
|
|
1204
1292
|
const warnings = [];
|
|
@@ -1226,7 +1314,7 @@ function resolveSeat(ship, route, role, persona = {}, repo) {
|
|
|
1226
1314
|
let modelWhy = `toolchain ${chosen.name} model`;
|
|
1227
1315
|
let reasoning = chosen.tiers.standard?.reasoning;
|
|
1228
1316
|
const explicit = persona.model;
|
|
1229
|
-
const explicitOk = explicit !== void 0 && (vendor === "openrouter" || adapter.ownsModel(explicit));
|
|
1317
|
+
const explicitOk = explicit !== void 0 && (vendor === "openrouter" || vendor === "local" || adapter.ownsModel(explicit));
|
|
1230
1318
|
if (explicit !== void 0 && !explicitOk) {
|
|
1231
1319
|
warnings.push(`seat ${role}: persona model "${explicit}" does not belong to vendor "${vendor}" (toolchain ${chosen.name}); ignored`);
|
|
1232
1320
|
}
|
|
@@ -1284,8 +1372,8 @@ function explain(ship, required) {
|
|
|
1284
1372
|
}
|
|
1285
1373
|
|
|
1286
1374
|
// src/repo-config.ts
|
|
1287
|
-
import { readFileSync as
|
|
1288
|
-
import { join as
|
|
1375
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
1376
|
+
import { join as join6, basename as basename2 } from "path";
|
|
1289
1377
|
import { parse } from "yaml";
|
|
1290
1378
|
var SPEC_VERSION = 1;
|
|
1291
1379
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -1358,8 +1446,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
1358
1446
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
1359
1447
|
function findRepoConfig(dir) {
|
|
1360
1448
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
1361
|
-
const p =
|
|
1362
|
-
if (
|
|
1449
|
+
const p = join6(dir, name);
|
|
1450
|
+
if (existsSync5(p)) return p;
|
|
1363
1451
|
}
|
|
1364
1452
|
return null;
|
|
1365
1453
|
}
|
|
@@ -1589,7 +1677,7 @@ function parseRepoConfig(text, file) {
|
|
|
1589
1677
|
function loadRepoConfig(dir) {
|
|
1590
1678
|
const file = findRepoConfig(dir);
|
|
1591
1679
|
if (!file) return null;
|
|
1592
|
-
return parseRepoConfig(
|
|
1680
|
+
return parseRepoConfig(readFileSync5(file, "utf8"), file);
|
|
1593
1681
|
}
|
|
1594
1682
|
function hookLabel(cfg, hook2) {
|
|
1595
1683
|
return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
|
|
@@ -2002,16 +2090,16 @@ function mergeRouteRelease(route, override) {
|
|
|
2002
2090
|
};
|
|
2003
2091
|
}
|
|
2004
2092
|
function expand(p, base) {
|
|
2005
|
-
let out = p.startsWith("~") ? p.replace(/^~/,
|
|
2006
|
-
out = out.replace(/\$\{?HOME\}?/g,
|
|
2093
|
+
let out = p.startsWith("~") ? p.replace(/^~/, homedir5()) : p;
|
|
2094
|
+
out = out.replace(/\$\{?HOME\}?/g, homedir5());
|
|
2007
2095
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
2008
2096
|
}
|
|
2009
2097
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
2010
|
-
if (!promptSet) return
|
|
2098
|
+
if (!promptSet) return join7(crewHome, "prompts", "default");
|
|
2011
2099
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
2012
2100
|
return expand(promptSet, base);
|
|
2013
2101
|
}
|
|
2014
|
-
return
|
|
2102
|
+
return join7(crewHome, "prompts", promptSet);
|
|
2015
2103
|
}
|
|
2016
2104
|
var Missing = class {
|
|
2017
2105
|
paths = [];
|
|
@@ -2035,7 +2123,7 @@ var Missing = class {
|
|
|
2035
2123
|
};
|
|
2036
2124
|
function crewVersion(crewHome) {
|
|
2037
2125
|
try {
|
|
2038
|
-
const pkg = JSON.parse(
|
|
2126
|
+
const pkg = JSON.parse(readFileSync6(join7(crewHome, "package.json"), "utf8"));
|
|
2039
2127
|
return pkg.version ?? "0.0.0";
|
|
2040
2128
|
} catch {
|
|
2041
2129
|
return "0.0.0";
|
|
@@ -2045,26 +2133,26 @@ function configSearchPath(crewHome) {
|
|
|
2045
2133
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
2046
2134
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2047
2135
|
return [
|
|
2048
|
-
...xdg ? [
|
|
2049
|
-
|
|
2136
|
+
...xdg ? [join7(xdg, "crew", "crew.yaml")] : [],
|
|
2137
|
+
join7(homedir5(), ".config", "crew", "crew.yaml"),
|
|
2050
2138
|
resolve(crewHome, "crew.yaml")
|
|
2051
2139
|
];
|
|
2052
2140
|
}
|
|
2053
2141
|
function findConfigFile(crewHome) {
|
|
2054
2142
|
const candidates = configSearchPath(crewHome);
|
|
2055
|
-
return candidates.find((c) =>
|
|
2143
|
+
return candidates.find((c) => existsSync6(c)) ?? candidates[candidates.length - 1];
|
|
2056
2144
|
}
|
|
2057
|
-
var defaultConfigPath = () =>
|
|
2145
|
+
var defaultConfigPath = () => join7(process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config"), "crew", "crew.yaml");
|
|
2058
2146
|
function routeSlug(route) {
|
|
2059
2147
|
return route.replace(/\//g, "-");
|
|
2060
2148
|
}
|
|
2061
2149
|
function resolvedPathFor(stateDir, route) {
|
|
2062
2150
|
const [workspace, project] = splitRoute(route);
|
|
2063
|
-
return
|
|
2151
|
+
return join7(stateDir, "resolved", workspace, `${project}.json`);
|
|
2064
2152
|
}
|
|
2065
2153
|
function apiKeyPathFor(stateDir, route) {
|
|
2066
2154
|
const [workspace, project] = splitRoute(route);
|
|
2067
|
-
return
|
|
2155
|
+
return join7(stateDir, "keys", workspace, `${project}.env`);
|
|
2068
2156
|
}
|
|
2069
2157
|
function splitRoute(route) {
|
|
2070
2158
|
const parts = route.split("/");
|
|
@@ -2075,9 +2163,9 @@ function splitRoute(route) {
|
|
|
2075
2163
|
}
|
|
2076
2164
|
function readResolvedFile(stateDir, route) {
|
|
2077
2165
|
const path = resolvedPathFor(stateDir, route);
|
|
2078
|
-
if (!
|
|
2166
|
+
if (!existsSync6(path)) return void 0;
|
|
2079
2167
|
try {
|
|
2080
|
-
return JSON.parse(
|
|
2168
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
2081
2169
|
} catch (e) {
|
|
2082
2170
|
throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
|
|
2083
2171
|
}
|
|
@@ -2133,18 +2221,18 @@ function parseResolved(raw, m, where) {
|
|
|
2133
2221
|
}
|
|
2134
2222
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
2135
2223
|
const file = configFile ?? findConfigFile(crewHome);
|
|
2136
|
-
if (opts.text === void 0 && !
|
|
2224
|
+
if (opts.text === void 0 && !existsSync6(file)) {
|
|
2137
2225
|
throw new ConfigError(
|
|
2138
2226
|
`no config found. Looked in:
|
|
2139
2227
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
2140
2228
|
|
|
2141
2229
|
Create the first of those:
|
|
2142
|
-
mkdir -p ${dirname3(defaultConfigPath())} && cp ${
|
|
2230
|
+
mkdir -p ${dirname3(defaultConfigPath())} && cp ${join7(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
2143
2231
|
);
|
|
2144
2232
|
}
|
|
2145
2233
|
let raw;
|
|
2146
2234
|
try {
|
|
2147
|
-
raw = parse2(opts.text ??
|
|
2235
|
+
raw = parse2(opts.text ?? readFileSync6(file, "utf8"));
|
|
2148
2236
|
} catch (e) {
|
|
2149
2237
|
throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
|
|
2150
2238
|
}
|
|
@@ -2222,7 +2310,7 @@ Create the first of those:
|
|
|
2222
2310
|
useNvm: shipRaw.useNvm !== false,
|
|
2223
2311
|
nvmSh: shipRaw.nvmSh,
|
|
2224
2312
|
stateDir,
|
|
2225
|
-
logFile: expand(shipRaw.logFile ??
|
|
2313
|
+
logFile: expand(shipRaw.logFile ?? join7(tmpdir(), "crew.log"), base),
|
|
2226
2314
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
2227
2315
|
maxConcurrentAgents,
|
|
2228
2316
|
streamRetentionDays,
|
|
@@ -2442,8 +2530,8 @@ function repoIdForName(r, name) {
|
|
|
2442
2530
|
}
|
|
2443
2531
|
function resolveApiKey(r) {
|
|
2444
2532
|
if (r.apiKey) return r.apiKey;
|
|
2445
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
2446
|
-
for (const line of
|
|
2533
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync6(r.apiKeyFile)) {
|
|
2534
|
+
for (const line of readFileSync6(r.apiKeyFile, "utf8").split("\n")) {
|
|
2447
2535
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
2448
2536
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
2449
2537
|
}
|
|
@@ -2506,7 +2594,7 @@ function configuredMembers(rt) {
|
|
|
2506
2594
|
return out;
|
|
2507
2595
|
}
|
|
2508
2596
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
2509
|
-
return
|
|
2597
|
+
return join7(basePath, workspace, repoName);
|
|
2510
2598
|
}
|
|
2511
2599
|
function reposOf(r) {
|
|
2512
2600
|
const named = Object.entries(r.repos);
|
|
@@ -2530,7 +2618,7 @@ function passengerRepoTargets(r) {
|
|
|
2530
2618
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
2531
2619
|
}
|
|
2532
2620
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
2533
|
-
return
|
|
2621
|
+
return join7(basePath, workspace, ".passenger-checkouts", repoName);
|
|
2534
2622
|
}
|
|
2535
2623
|
function passengerCheckoutTargets(r) {
|
|
2536
2624
|
if (!r.hostPassengers) return [];
|
|
@@ -2662,8 +2750,8 @@ function rosterMarkdown(roster, meId) {
|
|
|
2662
2750
|
}
|
|
2663
2751
|
|
|
2664
2752
|
// src/state.ts
|
|
2665
|
-
import { readFileSync as
|
|
2666
|
-
import { join as
|
|
2753
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
|
|
2754
|
+
import { join as join8 } from "path";
|
|
2667
2755
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
2668
2756
|
var State = class _State {
|
|
2669
2757
|
dir;
|
|
@@ -2672,7 +2760,7 @@ var State = class _State {
|
|
|
2672
2760
|
mkdirSync3(dir, { recursive: true });
|
|
2673
2761
|
}
|
|
2674
2762
|
path(...parts) {
|
|
2675
|
-
return
|
|
2763
|
+
return join8(this.dir, ...parts);
|
|
2676
2764
|
}
|
|
2677
2765
|
/**
|
|
2678
2766
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -2683,7 +2771,7 @@ var State = class _State {
|
|
|
2683
2771
|
*/
|
|
2684
2772
|
watermark() {
|
|
2685
2773
|
try {
|
|
2686
|
-
const v =
|
|
2774
|
+
const v = readFileSync7(this.path(".poll-watermark"), "utf8").trim();
|
|
2687
2775
|
return v || EPOCH;
|
|
2688
2776
|
} catch {
|
|
2689
2777
|
return EPOCH;
|
|
@@ -2708,10 +2796,10 @@ var State = class _State {
|
|
|
2708
2796
|
}
|
|
2709
2797
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
2710
2798
|
isPaused() {
|
|
2711
|
-
return
|
|
2799
|
+
return existsSync7(this.path(".crew-paused"));
|
|
2712
2800
|
}
|
|
2713
2801
|
isRolePaused(role) {
|
|
2714
|
-
return
|
|
2802
|
+
return existsSync7(this.path(`.role-paused-${role}`));
|
|
2715
2803
|
}
|
|
2716
2804
|
pausedRoles(roles) {
|
|
2717
2805
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
@@ -2728,7 +2816,7 @@ var State = class _State {
|
|
|
2728
2816
|
* while agents keep working.
|
|
2729
2817
|
*/
|
|
2730
2818
|
isReleasePaused() {
|
|
2731
|
-
return
|
|
2819
|
+
return existsSync7(this.path(".release-paused"));
|
|
2732
2820
|
}
|
|
2733
2821
|
setReleasePaused(paused) {
|
|
2734
2822
|
if (paused) writeFileSync4(this.path(".release-paused"), "");
|
|
@@ -2769,7 +2857,7 @@ var State = class _State {
|
|
|
2769
2857
|
const name = _State.safe(rawName);
|
|
2770
2858
|
const file = this.path(`.${name}.lock`);
|
|
2771
2859
|
try {
|
|
2772
|
-
const existing = Number.parseInt(
|
|
2860
|
+
const existing = Number.parseInt(readFileSync7(file, "utf8").trim(), 10);
|
|
2773
2861
|
if (Number.isFinite(existing) && existing > 0) {
|
|
2774
2862
|
try {
|
|
2775
2863
|
process.kill(existing, 0);
|
|
@@ -2863,7 +2951,7 @@ var State = class _State {
|
|
|
2863
2951
|
const heldReported = `.release-held-${suffix}`;
|
|
2864
2952
|
const read = (f) => {
|
|
2865
2953
|
try {
|
|
2866
|
-
return
|
|
2954
|
+
return readFileSync7(this.path(f), "utf8").trim() || null;
|
|
2867
2955
|
} catch {
|
|
2868
2956
|
return null;
|
|
2869
2957
|
}
|
|
@@ -2913,7 +3001,7 @@ var State = class _State {
|
|
|
2913
3001
|
/** Whether the board was already told about this head's red gate. */
|
|
2914
3002
|
heldReported: () => {
|
|
2915
3003
|
try {
|
|
2916
|
-
return
|
|
3004
|
+
return readFileSync7(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
|
|
2917
3005
|
} catch {
|
|
2918
3006
|
return null;
|
|
2919
3007
|
}
|
|
@@ -2950,7 +3038,7 @@ var State = class _State {
|
|
|
2950
3038
|
const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
|
|
2951
3039
|
const read = (f) => {
|
|
2952
3040
|
try {
|
|
2953
|
-
return
|
|
3041
|
+
return readFileSync7(this.path(f), "utf8").trim() || null;
|
|
2954
3042
|
} catch {
|
|
2955
3043
|
return null;
|
|
2956
3044
|
}
|
|
@@ -3015,7 +3103,7 @@ var State = class _State {
|
|
|
3015
3103
|
return {
|
|
3016
3104
|
previous: () => {
|
|
3017
3105
|
try {
|
|
3018
|
-
const parsed = JSON.parse(
|
|
3106
|
+
const parsed = JSON.parse(readFileSync7(this.path(file), "utf8"));
|
|
3019
3107
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3020
3108
|
} catch {
|
|
3021
3109
|
return {};
|
|
@@ -3036,7 +3124,7 @@ var State = class _State {
|
|
|
3036
3124
|
return {
|
|
3037
3125
|
previous: () => {
|
|
3038
3126
|
try {
|
|
3039
|
-
const parsed = JSON.parse(
|
|
3127
|
+
const parsed = JSON.parse(readFileSync7(this.path(file), "utf8"));
|
|
3040
3128
|
return Array.isArray(parsed) ? parsed : [];
|
|
3041
3129
|
} catch {
|
|
3042
3130
|
return [];
|
|
@@ -3069,7 +3157,7 @@ var State = class _State {
|
|
|
3069
3157
|
}
|
|
3070
3158
|
readInt(file) {
|
|
3071
3159
|
try {
|
|
3072
|
-
return Number.parseInt(
|
|
3160
|
+
return Number.parseInt(readFileSync7(this.path(file), "utf8").trim(), 10) || 0;
|
|
3073
3161
|
} catch {
|
|
3074
3162
|
return 0;
|
|
3075
3163
|
}
|
|
@@ -3103,7 +3191,8 @@ var DEFAULT_CONTRACT = {
|
|
|
3103
3191
|
ship: "ship_id",
|
|
3104
3192
|
heldBy: "held_by_ship_id",
|
|
3105
3193
|
verifiedSha: "verified_sha",
|
|
3106
|
-
prRef: "pr_ref"
|
|
3194
|
+
prRef: "pr_ref",
|
|
3195
|
+
position: "position"
|
|
3107
3196
|
},
|
|
3108
3197
|
comments: {
|
|
3109
3198
|
parent: "ticket_id",
|
|
@@ -3373,9 +3462,9 @@ function describeRelease(o, route) {
|
|
|
3373
3462
|
}
|
|
3374
3463
|
|
|
3375
3464
|
// src/install.ts
|
|
3376
|
-
import { dirname as dirname4, join as
|
|
3377
|
-
import { homedir as
|
|
3378
|
-
import { accessSync, constants, existsSync as
|
|
3465
|
+
import { dirname as dirname4, join as join9, extname } from "path";
|
|
3466
|
+
import { homedir as homedir6 } from "os";
|
|
3467
|
+
import { accessSync, constants, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
|
|
3379
3468
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3380
3469
|
import { createHash } from "crypto";
|
|
3381
3470
|
|
|
@@ -3387,7 +3476,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
3387
3476
|
// src/install.ts
|
|
3388
3477
|
function crewInvocation(crewHome) {
|
|
3389
3478
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
3390
|
-
return [process.execPath,
|
|
3479
|
+
return [process.execPath, join9(crewHome, "bin", "crew")];
|
|
3391
3480
|
}
|
|
3392
3481
|
var INTERVAL_SECONDS = 120;
|
|
3393
3482
|
function labelFor(crewHome, job) {
|
|
@@ -3425,7 +3514,7 @@ function pathFor(ship) {
|
|
|
3425
3514
|
return parts.join(":");
|
|
3426
3515
|
}
|
|
3427
3516
|
function findOnPath(bin, searchPath) {
|
|
3428
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
3517
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join9(dir, bin));
|
|
3429
3518
|
for (const candidate of candidates) {
|
|
3430
3519
|
try {
|
|
3431
3520
|
accessSync(candidate, constants.X_OK);
|
|
@@ -3435,7 +3524,7 @@ function findOnPath(bin, searchPath) {
|
|
|
3435
3524
|
}
|
|
3436
3525
|
return void 0;
|
|
3437
3526
|
}
|
|
3438
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
3527
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join9(homedir6(), ".docker", "bin")].join(":");
|
|
3439
3528
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
3440
3529
|
const schedulerPath = pathFor(ship);
|
|
3441
3530
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -3447,7 +3536,7 @@ function xmlEscape(s) {
|
|
|
3447
3536
|
}
|
|
3448
3537
|
function planLaunchd(ship, crewHome, job) {
|
|
3449
3538
|
const label = labelFor(crewHome, job);
|
|
3450
|
-
const unitPath =
|
|
3539
|
+
const unitPath = join9(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3451
3540
|
const crewLog = ship.logFile;
|
|
3452
3541
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3453
3542
|
const persistent = job === "run";
|
|
@@ -3499,8 +3588,8 @@ ${scheduleKeys}
|
|
|
3499
3588
|
}
|
|
3500
3589
|
function planSystemd(ship, crewHome, job) {
|
|
3501
3590
|
const label = labelFor(crewHome, job);
|
|
3502
|
-
const unitDir =
|
|
3503
|
-
const servicePath =
|
|
3591
|
+
const unitDir = join9(homedir6(), ".config", "systemd", "user");
|
|
3592
|
+
const servicePath = join9(unitDir, `${label}.service`);
|
|
3504
3593
|
const crewLog = ship.logFile;
|
|
3505
3594
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3506
3595
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -3537,7 +3626,7 @@ WantedBy=default.target
|
|
|
3537
3626
|
schedulerLog
|
|
3538
3627
|
};
|
|
3539
3628
|
}
|
|
3540
|
-
const timerPath =
|
|
3629
|
+
const timerPath = join9(unitDir, `${label}.timer`);
|
|
3541
3630
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
3542
3631
|
[Unit]
|
|
3543
3632
|
Description=${description}
|
|
@@ -3604,7 +3693,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3604
3693
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
3605
3694
|
const label = labelFor(crewHome, job);
|
|
3606
3695
|
if (hostShip === "macos") {
|
|
3607
|
-
const unitPath =
|
|
3696
|
+
const unitPath = join9(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3608
3697
|
return {
|
|
3609
3698
|
mechanism: "launchd",
|
|
3610
3699
|
unitPaths: [unitPath],
|
|
@@ -3614,18 +3703,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3614
3703
|
};
|
|
3615
3704
|
}
|
|
3616
3705
|
if (hostShip === "linux" && hasSystemd) {
|
|
3617
|
-
const unitDir =
|
|
3706
|
+
const unitDir = join9(homedir6(), ".config", "systemd", "user");
|
|
3618
3707
|
if (job === "run") {
|
|
3619
3708
|
return {
|
|
3620
3709
|
mechanism: "systemd",
|
|
3621
|
-
unitPaths: [
|
|
3710
|
+
unitPaths: [join9(unitDir, `${label}.service`)],
|
|
3622
3711
|
crontabLine: null,
|
|
3623
3712
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
3624
3713
|
};
|
|
3625
3714
|
}
|
|
3626
3715
|
return {
|
|
3627
3716
|
mechanism: "systemd",
|
|
3628
|
-
unitPaths: [
|
|
3717
|
+
unitPaths: [join9(unitDir, `${label}.service`), join9(unitDir, `${label}.timer`)],
|
|
3629
3718
|
crontabLine: null,
|
|
3630
3719
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
3631
3720
|
};
|
|
@@ -3643,7 +3732,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3643
3732
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
3644
3733
|
const label = labelFor(crewHome, "run");
|
|
3645
3734
|
if (hostShip === "macos") {
|
|
3646
|
-
const unitPath =
|
|
3735
|
+
const unitPath = join9(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3647
3736
|
return {
|
|
3648
3737
|
mechanism: "launchd",
|
|
3649
3738
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -3738,7 +3827,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3738
3827
|
}
|
|
3739
3828
|
}
|
|
3740
3829
|
for (const path of plan.unitPaths) {
|
|
3741
|
-
if (!
|
|
3830
|
+
if (!existsSync8(path)) continue;
|
|
3742
3831
|
if (dryRun) {
|
|
3743
3832
|
log.emit(`would remove ${path}`);
|
|
3744
3833
|
continue;
|
|
@@ -3757,7 +3846,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3757
3846
|
}
|
|
3758
3847
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
3759
3848
|
function launchAgentsDir() {
|
|
3760
|
-
return
|
|
3849
|
+
return join9(homedir6(), "Library", "LaunchAgents");
|
|
3761
3850
|
}
|
|
3762
3851
|
function xmlUnescape(s) {
|
|
3763
3852
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -3774,25 +3863,25 @@ function loadedCrewLabels() {
|
|
|
3774
3863
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3775
3864
|
}
|
|
3776
3865
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
3777
|
-
const files =
|
|
3866
|
+
const files = existsSync8(dir) ? readdirSync2(dir) : [];
|
|
3778
3867
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
3779
|
-
const plistPath =
|
|
3868
|
+
const plistPath = join9(dir, f);
|
|
3780
3869
|
let program = null;
|
|
3781
3870
|
try {
|
|
3782
|
-
program = programFromPlist(
|
|
3871
|
+
program = programFromPlist(readFileSync8(plistPath, "utf8"));
|
|
3783
3872
|
} catch {
|
|
3784
3873
|
}
|
|
3785
3874
|
return {
|
|
3786
3875
|
label: f.slice(0, -".plist".length),
|
|
3787
3876
|
plistPath,
|
|
3788
3877
|
program,
|
|
3789
|
-
programExists: program !== null &&
|
|
3878
|
+
programExists: program !== null && existsSync8(program)
|
|
3790
3879
|
};
|
|
3791
3880
|
});
|
|
3792
3881
|
const seen = new Set(units.map((u) => u.label));
|
|
3793
3882
|
for (const label of [...loaded].sort()) {
|
|
3794
3883
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
3795
|
-
units.push({ label, plistPath:
|
|
3884
|
+
units.push({ label, plistPath: join9(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
3796
3885
|
}
|
|
3797
3886
|
return units;
|
|
3798
3887
|
}
|
|
@@ -3805,9 +3894,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3805
3894
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3806
3895
|
return;
|
|
3807
3896
|
}
|
|
3808
|
-
if (
|
|
3897
|
+
if (existsSync8(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3809
3898
|
spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3810
|
-
if (
|
|
3899
|
+
if (existsSync8(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3811
3900
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3812
3901
|
}
|
|
3813
3902
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -3890,7 +3979,7 @@ import { statfsSync } from "fs";
|
|
|
3890
3979
|
// src/git.ts
|
|
3891
3980
|
import { execFileSync } from "child_process";
|
|
3892
3981
|
import {
|
|
3893
|
-
existsSync as
|
|
3982
|
+
existsSync as existsSync9,
|
|
3894
3983
|
mkdirSync as mkdirSync5,
|
|
3895
3984
|
openSync as openSync2,
|
|
3896
3985
|
closeSync as closeSync2,
|
|
@@ -4262,7 +4351,7 @@ function cloneUrlFor(remote) {
|
|
|
4262
4351
|
return `git@github.com:${remote}.git`;
|
|
4263
4352
|
}
|
|
4264
4353
|
function ensureRepoCheckout(dir, remote) {
|
|
4265
|
-
if (
|
|
4354
|
+
if (existsSync9(`${dir}/.git`)) return false;
|
|
4266
4355
|
if (!remote) {
|
|
4267
4356
|
throw new GitError(
|
|
4268
4357
|
`${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`
|
|
@@ -5232,7 +5321,14 @@ function missingBlockerIds(tickets) {
|
|
|
5232
5321
|
function blockerInfoMap(tickets, extra = []) {
|
|
5233
5322
|
const info = {};
|
|
5234
5323
|
for (const t of [...tickets, ...extra]) {
|
|
5235
|
-
info[t.id] = {
|
|
5324
|
+
info[t.id] = {
|
|
5325
|
+
issue_id: t.issue_id,
|
|
5326
|
+
issue_tag: t.issue_tag,
|
|
5327
|
+
status: t.status,
|
|
5328
|
+
repo_id: t.repo_id,
|
|
5329
|
+
merged_at: t.merged_at,
|
|
5330
|
+
commit_sha: t.commit_sha
|
|
5331
|
+
};
|
|
5236
5332
|
}
|
|
5237
5333
|
return info;
|
|
5238
5334
|
}
|
|
@@ -5241,14 +5337,18 @@ function computeBlockedIds(tickets, info) {
|
|
|
5241
5337
|
for (const t of tickets) if (unresolvedBlockers(t, info).length > 0) blocked.add(t.id);
|
|
5242
5338
|
return blocked;
|
|
5243
5339
|
}
|
|
5244
|
-
function unresolvedBlockers(t, info) {
|
|
5340
|
+
function unresolvedBlockers(t, info, opts = {}) {
|
|
5341
|
+
const mergeGated = opts.mergeGated ?? true;
|
|
5245
5342
|
return (t.blocked_by ?? []).filter((id) => {
|
|
5246
|
-
const
|
|
5247
|
-
|
|
5343
|
+
const b = info[id];
|
|
5344
|
+
const status2 = b?.status;
|
|
5345
|
+
if (!b || status2 === void 0) return false;
|
|
5346
|
+
if (!RESOLVED_STATUSES.has(status2)) return true;
|
|
5347
|
+
return mergeGated && status2 === "verified" && !!t.repo_id && b.repo_id === t.repo_id && !b.merged_at && !b.commit_sha;
|
|
5248
5348
|
});
|
|
5249
5349
|
}
|
|
5250
|
-
function describeUnresolved(t, info) {
|
|
5251
|
-
return unresolvedBlockers(t, info).map((id) => `${info[id]?.issue_tag ?? info[id]?.issue_id ?? "?"} (${info[id]?.status ?? "unknown"})`).join(", ");
|
|
5350
|
+
function describeUnresolved(t, info, opts = {}) {
|
|
5351
|
+
return unresolvedBlockers(t, info, opts).map((id) => `${info[id]?.issue_tag ?? info[id]?.issue_id ?? "?"} (${info[id]?.status ?? "unknown"})`).join(", ");
|
|
5252
5352
|
}
|
|
5253
5353
|
function sweepDiagnostics(tickets, info) {
|
|
5254
5354
|
const dangling = [];
|
|
@@ -5317,7 +5417,7 @@ function filingErrors(tickets, opts = {}) {
|
|
|
5317
5417
|
function sweepComment(step) {
|
|
5318
5418
|
return step.action === "park" ? `Parked as **Blocked** by the crew \u2014 waiting on: ${step.blockers}.
|
|
5319
5419
|
|
|
5320
|
-
This is automatic and reversible: an approved ticket whose \`Blocked by\` entries are not all resolved is parked out of the queue, and restored on the first poll after the last one resolves. Nothing else about the ticket changes. A blocker counts as resolved at \`
|
|
5420
|
+
This is automatic and reversible: an approved ticket whose \`Blocked by\` entries are not all resolved is parked out of the queue, and restored on the first poll after the last one resolves. Nothing else about the ticket changes. A blocker counts as resolved at \`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\`, or at \`verified\` when it is in a different repository or already merged (\`merged_at\` set) \u2014 not at \`fixed\`, which is still an unmerged branch awaiting QA, and not at \`verified\` for a blocker in this ticket's own repository until the release has merged it, so nothing gets built stacked on an unmerged branch.` : `Restored to **Approved** by the crew \u2014 every blocker has resolved${step.blockers && step.blockers !== "\u2014" ? ` (${step.blockers})` : ""}.
|
|
5321
5421
|
|
|
5322
5422
|
This restores the status it was approved at before being parked; it is back in the queue and a role may pick it up. If a blocker resolved as \`closed_wont_fix\` or \`closed_duplicate\` this ticket may still not be buildable as written \u2014 that is a call for a human, not the crew.`;
|
|
5323
5423
|
}
|
|
@@ -5380,20 +5480,25 @@ function issueNumber(t) {
|
|
|
5380
5480
|
function epicRank(t) {
|
|
5381
5481
|
return t.epicInProgress ? 0 : 1;
|
|
5382
5482
|
}
|
|
5483
|
+
function positionRank(t) {
|
|
5484
|
+
const p = t.position;
|
|
5485
|
+
return typeof p === "number" && Number.isFinite(p) ? p : Number.POSITIVE_INFINITY;
|
|
5486
|
+
}
|
|
5383
5487
|
function rank(t, c = DEFAULT_CONTRACT) {
|
|
5384
|
-
return [effectivePriority(t, c), severityRank(t, c), epicRank(t), issueNumber(t)];
|
|
5488
|
+
return [effectivePriority(t, c), positionRank(t), severityRank(t, c), epicRank(t), issueNumber(t)];
|
|
5385
5489
|
}
|
|
5386
5490
|
function compareRank(a, b, c = DEFAULT_CONTRACT) {
|
|
5387
5491
|
const ra = rank(a, c);
|
|
5388
5492
|
const rb = rank(b, c);
|
|
5389
5493
|
for (let i = 0; i < ra.length; i++) {
|
|
5390
|
-
const
|
|
5391
|
-
|
|
5494
|
+
const x = ra[i];
|
|
5495
|
+
const y = rb[i];
|
|
5496
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
5392
5497
|
}
|
|
5393
5498
|
return 0;
|
|
5394
5499
|
}
|
|
5395
5500
|
function rankScalar(t, c = DEFAULT_CONTRACT) {
|
|
5396
|
-
const [p, s, e, n] = rank(t, c);
|
|
5501
|
+
const [p, , s, e, n] = rank(t, c);
|
|
5397
5502
|
return p * 1e9 + s * 1e7 + e * 1e6 + n;
|
|
5398
5503
|
}
|
|
5399
5504
|
var NOTHING_ACTIONABLE = 999999999999;
|
|
@@ -5600,7 +5705,7 @@ function claimStatuses(role, c) {
|
|
|
5600
5705
|
}
|
|
5601
5706
|
function rankedCandidates(role, i) {
|
|
5602
5707
|
const c = i.contract ?? DEFAULT_CONTRACT;
|
|
5603
|
-
return [...roleCandidates(role, i)].sort((a, b) =>
|
|
5708
|
+
return [...roleCandidates(role, i)].sort((a, b) => compareRank(a, b, c));
|
|
5604
5709
|
}
|
|
5605
5710
|
function roleTopRank(role, i) {
|
|
5606
5711
|
if (role !== "qa" && role !== "triage" && !i.seats[role]) return NOTHING_ACTIONABLE;
|
|
@@ -5613,7 +5718,7 @@ function actionableSummary(i, pending) {
|
|
|
5613
5718
|
const c = i.contract ?? DEFAULT_CONTRACT;
|
|
5614
5719
|
const all = pending.flatMap((role) => roleCandidates(role, i));
|
|
5615
5720
|
if (all.length === 0) return { count: 0 };
|
|
5616
|
-
const top = all.reduce((best, t) =>
|
|
5721
|
+
const top = all.reduce((best, t) => compareRank(t, best, c) < 0 ? t : best);
|
|
5617
5722
|
return { count: all.length, top };
|
|
5618
5723
|
}
|
|
5619
5724
|
var FIXED_RANK = { qa: -2, triage: -1 };
|
|
@@ -5839,6 +5944,7 @@ function blockers(t, i) {
|
|
|
5839
5944
|
function modeCell(t, i) {
|
|
5840
5945
|
return i.workModes && i.workModes.modes.size > 0 ? modeOf(t, i.workModes) : "\u2014";
|
|
5841
5946
|
}
|
|
5947
|
+
var effCell = (t) => `p${effectivePriority(t)}${positionRank(t) === Number.POSITIVE_INFINITY ? "" : ` #${positionRank(t)}`}`;
|
|
5842
5948
|
var sorted = (ts) => [...ts].sort(compareRank);
|
|
5843
5949
|
var stream = (segments) => `${segments.join("\n")}
|
|
5844
5950
|
`;
|
|
@@ -5857,7 +5963,7 @@ function buildingDigest(i) {
|
|
|
5857
5963
|
const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
|
|
5858
5964
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
5859
5965
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
5860
|
-
return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${modeCell(t, i)} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} |
|
|
5966
|
+
return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${modeCell(t, i)} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | ${effCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
5861
5967
|
};
|
|
5862
5968
|
const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
|
|
5863
5969
|
const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
|
|
@@ -5897,7 +6003,7 @@ ${sorted(held).map((t) => `| ${displayKey2(t)} | ${stopOf(t)} |`).join("\n")}
|
|
|
5897
6003
|
"\nWork the first one you are not required to skip. Tickets waiting on an\nunresolved dependency are already out of this table \u2014 see Blocked below.\n",
|
|
5898
6004
|
table(step2, i, header, row),
|
|
5899
6005
|
"\n### Blocked \u2014 waiting on a dependency, not yours to start\n",
|
|
5900
|
-
'\nListed rather than hidden so "nothing to do" stays distinguishable from\n"everything is parked". The loop parks and restores these itself, on every\npoll: an approved ticket whose `Blocked by` entries are unresolved moves to\n`blocked`, and back to `accepted` once the last one resolves. **Do not pick\none up, do not set or clear `blocked` by hand, and do not "unblock" one by\nediting its `Blocked by` field** \u2014 if a blocker looks wrong, say so in your\nrun summary. A blocker counts as resolved at `
|
|
6006
|
+
'\nListed rather than hidden so "nothing to do" stays distinguishable from\n"everything is parked". The loop parks and restores these itself, on every\npoll: an approved ticket whose `Blocked by` entries are unresolved moves to\n`blocked`, and back to `accepted` once the last one resolves. **Do not pick\none up, do not set or clear `blocked` by hand, and do not "unblock" one by\nediting its `Blocked by` field** \u2014 if a blocker looks wrong, say so in your\nrun summary. A blocker counts as resolved at `closed_deployed`,\n`closed_wont_fix` or `closed_duplicate`, or at `verified` when it is in another\nrepository or already merged; `fixed` is still an unmerged branch awaiting QA,\nand a `verified` blocker in the same repository as the blocked ticket is unmerged until the\nrelease merges it, so neither counts.\n',
|
|
5901
6007
|
blockedTable(i.tickets.filter(isBlocked)),
|
|
5902
6008
|
...heldTable ? [heldTable] : []
|
|
5903
6009
|
]);
|
|
@@ -5918,7 +6024,7 @@ function qaDigest(i) {
|
|
|
5918
6024
|
const n = newFromOthers(i, t.id);
|
|
5919
6025
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
5920
6026
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
5921
|
-
return `| ${displayKey2(t)} | ${repo} | ${modeCell(t, i)} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} |
|
|
6027
|
+
return `| ${displayKey2(t)} | ${repo} | ${modeCell(t, i)} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} | ${effCell(t)} | ${branchCell(t)} | ${worktreeCell(t)} | ${t.updated_at.slice(0, 16)}Z | ${lastComment(i, t.id)} | ${n > 0 ? `**${n} new**` : "\u2014"} |`;
|
|
5922
6028
|
};
|
|
5923
6029
|
const waiting = i.tickets.filter((t) => (t.status === "qa" || t.status === "fixed") && i.blocked.has(t.id));
|
|
5924
6030
|
const takeable = (status2) => i.tickets.filter((t) => t.status === status2 && !i.blocked.has(t.id));
|
|
@@ -5926,7 +6032,7 @@ function qaDigest(i) {
|
|
|
5926
6032
|
### Waiting on blockers \u2014 not yours to verify yet
|
|
5927
6033
|
|
|
5928
6034
|
A \`fixed\` or \`qa\` ticket whose \`Blocked by\` names something not yet resolved
|
|
5929
|
-
(\`
|
|
6035
|
+
(\`closed_deployed\`, \`closed_wont_fix\` or \`closed_duplicate\`, or \`verified\` once merged or in another repository) is held out of
|
|
5930
6036
|
verification: its change needs the blocker released first. It becomes yours on
|
|
5931
6037
|
the first poll after the last blocker resolves. No one needs to change the ticket.
|
|
5932
6038
|
|
|
@@ -6000,7 +6106,7 @@ function materializeBranchForTicket(dir, cfg, t, role) {
|
|
|
6000
6106
|
|
|
6001
6107
|
// src/poll.ts
|
|
6002
6108
|
import { writeFileSync as writeFileSync6 } from "fs";
|
|
6003
|
-
import { join as
|
|
6109
|
+
import { join as join10 } from "path";
|
|
6004
6110
|
|
|
6005
6111
|
// src/stalled.ts
|
|
6006
6112
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -6113,6 +6219,7 @@ async function decideCycle(o) {
|
|
|
6113
6219
|
data: { tickets: tickets.length, comments: comments.length }
|
|
6114
6220
|
});
|
|
6115
6221
|
const epicColumn = tracker.contract.columns.epic;
|
|
6222
|
+
const positionColumn = tracker.contract.columns.position;
|
|
6116
6223
|
const epicBuilding = tracker.contract.epics.statuses.building;
|
|
6117
6224
|
const epicStatusColumn = tracker.contract.epics.statusColumn;
|
|
6118
6225
|
const epicStatusById = new Map(epicRows.map((e) => [e.id, e[epicStatusColumn] ?? null]));
|
|
@@ -6132,6 +6239,9 @@ async function decideCycle(o) {
|
|
|
6132
6239
|
for (const t of tickets) {
|
|
6133
6240
|
const epicId = t[epicColumn];
|
|
6134
6241
|
t.epicInProgress = typeof epicId === "string" && epicStatusById.get(epicId) === epicBuilding;
|
|
6242
|
+
const pos = t[positionColumn];
|
|
6243
|
+
const n = typeof pos === "string" && pos.trim() !== "" ? Number(pos) : pos;
|
|
6244
|
+
t.position = typeof n === "number" && Number.isFinite(n) ? n : null;
|
|
6135
6245
|
}
|
|
6136
6246
|
const extra = await tracker.ticketsByIds(missingBlockerIds(tickets));
|
|
6137
6247
|
const info = blockerInfoMap(tickets, extra);
|
|
@@ -6272,7 +6382,7 @@ async function decideCycle(o) {
|
|
|
6272
6382
|
}
|
|
6273
6383
|
return decision;
|
|
6274
6384
|
}
|
|
6275
|
-
var digestPath = (stateDir, route, role) =>
|
|
6385
|
+
var digestPath = (stateDir, route, role) => join10(stateDir, `digest-${route}-${role}.md`);
|
|
6276
6386
|
function writeDigest(o, d, role, stateDir) {
|
|
6277
6387
|
const me = o.route.resolved?.seats[role];
|
|
6278
6388
|
if (!me) return false;
|
|
@@ -6642,9 +6752,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
6642
6752
|
|
|
6643
6753
|
// src/agent.ts
|
|
6644
6754
|
import { spawn as spawn2 } from "child_process";
|
|
6645
|
-
import { readFileSync as
|
|
6755
|
+
import { readFileSync as readFileSync11, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
|
|
6646
6756
|
import { finished } from "stream/promises";
|
|
6647
|
-
import { join as
|
|
6757
|
+
import { join as join13, dirname as dirname6 } from "path";
|
|
6648
6758
|
import { createHash as createHash2 } from "crypto";
|
|
6649
6759
|
|
|
6650
6760
|
// src/environment.ts
|
|
@@ -7033,14 +7143,14 @@ function expandSkillMarkers(text, lookup, opts = {}) {
|
|
|
7033
7143
|
}
|
|
7034
7144
|
|
|
7035
7145
|
// src/skills.ts
|
|
7036
|
-
import { readFileSync as
|
|
7037
|
-
import { join as
|
|
7146
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync11 } from "fs";
|
|
7147
|
+
import { join as join12 } from "path";
|
|
7038
7148
|
import { parse as parseYaml } from "yaml";
|
|
7039
7149
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
7040
7150
|
|
|
7041
7151
|
// src/agents.ts
|
|
7042
|
-
import { readFileSync as
|
|
7043
|
-
import { join as
|
|
7152
|
+
import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
|
|
7153
|
+
import { join as join11 } from "path";
|
|
7044
7154
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
7045
7155
|
var PERSONA_NAME = {
|
|
7046
7156
|
dev: "Developer",
|
|
@@ -7054,12 +7164,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
7054
7164
|
var AgentsSyncError = class extends Error {
|
|
7055
7165
|
};
|
|
7056
7166
|
function personaDefaultPrompt(promptsDir, role) {
|
|
7057
|
-
const briefPath =
|
|
7058
|
-
if (!
|
|
7059
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
7060
|
-
const commonPath =
|
|
7061
|
-
if (!
|
|
7062
|
-
return `${
|
|
7167
|
+
const briefPath = join11(promptsDir, "personas", `lane-${role}.md`);
|
|
7168
|
+
if (!existsSync10(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
7169
|
+
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync9(briefPath, "utf8");
|
|
7170
|
+
const commonPath = join11(promptsDir, "personas", "common.md");
|
|
7171
|
+
if (!existsSync10(commonPath)) return readFileSync9(briefPath, "utf8");
|
|
7172
|
+
return `${readFileSync9(commonPath, "utf8")}${readFileSync9(briefPath, "utf8")}`;
|
|
7063
7173
|
}
|
|
7064
7174
|
async function resolveAgentId(route, role, opts) {
|
|
7065
7175
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -7383,7 +7493,7 @@ function describeCrewLink(o) {
|
|
|
7383
7493
|
var SkillSyncError = class extends Error {
|
|
7384
7494
|
};
|
|
7385
7495
|
function parseSkillFile(path) {
|
|
7386
|
-
const text =
|
|
7496
|
+
const text = readFileSync10(path, "utf8");
|
|
7387
7497
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
7388
7498
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
7389
7499
|
const frontmatter = match[1];
|
|
@@ -7396,9 +7506,9 @@ function parseSkillFile(path) {
|
|
|
7396
7506
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
7397
7507
|
}
|
|
7398
7508
|
function listLocalSkills(promptsDir) {
|
|
7399
|
-
const dir =
|
|
7400
|
-
if (!
|
|
7401
|
-
return
|
|
7509
|
+
const dir = join12(promptsDir, "skills");
|
|
7510
|
+
if (!existsSync11(dir)) return [];
|
|
7511
|
+
return readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join12(dir, f)));
|
|
7402
7512
|
}
|
|
7403
7513
|
async function syncSkills(route, opts) {
|
|
7404
7514
|
if (!route.resolved) {
|
|
@@ -7590,23 +7700,23 @@ var AgentError = class extends Error {
|
|
|
7590
7700
|
var AgentSpawnError = class extends AgentError {
|
|
7591
7701
|
};
|
|
7592
7702
|
function planAgentRun(o) {
|
|
7593
|
-
const briefPath =
|
|
7594
|
-
if (!
|
|
7703
|
+
const briefPath = join13(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
7704
|
+
if (!existsSync12(briefPath)) {
|
|
7595
7705
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
7596
7706
|
}
|
|
7597
|
-
const commonPath =
|
|
7598
|
-
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) &&
|
|
7707
|
+
const commonPath = join13(o.route.promptsDir, "personas", "common.md");
|
|
7708
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync12(commonPath);
|
|
7599
7709
|
const now = o.now ?? (() => Date.now());
|
|
7600
7710
|
const routeLabel = routeSlug(o.route.route);
|
|
7601
|
-
const digestPath2 =
|
|
7711
|
+
const digestPath2 = join13(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
7602
7712
|
let digest;
|
|
7603
7713
|
let ageSeconds;
|
|
7604
|
-
if (
|
|
7714
|
+
if (existsSync12(digestPath2)) {
|
|
7605
7715
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
7606
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
7716
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync11(digestPath2, "utf8");
|
|
7607
7717
|
}
|
|
7608
|
-
const common = usesCommon ?
|
|
7609
|
-
const brief =
|
|
7718
|
+
const common = usesCommon ? readFileSync11(commonPath, "utf8") : "";
|
|
7719
|
+
const brief = readFileSync11(briefPath, "utf8");
|
|
7610
7720
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
7611
7721
|
const effectiveBrief = o.divergedPrompt ?? brief;
|
|
7612
7722
|
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
@@ -7628,7 +7738,7 @@ function planAgentRun(o) {
|
|
|
7628
7738
|
digest
|
|
7629
7739
|
});
|
|
7630
7740
|
const promptSha = createHash2("sha256").update(expanded.text).digest("hex");
|
|
7631
|
-
const streamsDir =
|
|
7741
|
+
const streamsDir = join13(o.stateDir, "streams");
|
|
7632
7742
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
7633
7743
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
7634
7744
|
const { toolchain, model } = seat;
|
|
@@ -7684,8 +7794,8 @@ function planAgentRun(o) {
|
|
|
7684
7794
|
digestAttached: digest !== void 0,
|
|
7685
7795
|
digestAgeSeconds: ageSeconds,
|
|
7686
7796
|
model,
|
|
7687
|
-
streamPath:
|
|
7688
|
-
eventsPath:
|
|
7797
|
+
streamPath: join13(streamsDir, `${base}.jsonl`),
|
|
7798
|
+
eventsPath: join13(streamsDir, `${base}.events.jsonl`),
|
|
7689
7799
|
ticket: o.ticket,
|
|
7690
7800
|
agentLog,
|
|
7691
7801
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
@@ -7695,7 +7805,7 @@ function planAgentRun(o) {
|
|
|
7695
7805
|
function describePlan(p) {
|
|
7696
7806
|
return [
|
|
7697
7807
|
`role: ${p.role}`,
|
|
7698
|
-
...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}`] : [],
|
|
7808
|
+
...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}${p.provider ? `; provider ${p.provider}` : ""}`] : [],
|
|
7699
7809
|
...(p.warnings ?? []).map((w) => `warning: ${w}`),
|
|
7700
7810
|
// Only for a non-default toolchain, so a plain Claude run's output is unchanged.
|
|
7701
7811
|
...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
|
|
@@ -7891,13 +8001,13 @@ function spawnAgent(plan, emit) {
|
|
|
7891
8001
|
}
|
|
7892
8002
|
|
|
7893
8003
|
// src/run-worktree.ts
|
|
7894
|
-
import { existsSync as
|
|
7895
|
-
import { dirname as dirname7, join as
|
|
8004
|
+
import { existsSync as existsSync14 } from "fs";
|
|
8005
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
7896
8006
|
|
|
7897
8007
|
// src/ports.ts
|
|
7898
8008
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
7899
|
-
import { existsSync as
|
|
7900
|
-
import { join as
|
|
8009
|
+
import { existsSync as existsSync13 } from "fs";
|
|
8010
|
+
import { join as join14 } from "path";
|
|
7901
8011
|
var run = (cmd, args) => {
|
|
7902
8012
|
try {
|
|
7903
8013
|
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -7942,7 +8052,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
7942
8052
|
}
|
|
7943
8053
|
function worktreeExists(parent, prefix, n) {
|
|
7944
8054
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
7945
|
-
if (
|
|
8055
|
+
if (existsSync13(join14(parent, name, ".git"))) return true;
|
|
7946
8056
|
}
|
|
7947
8057
|
return false;
|
|
7948
8058
|
}
|
|
@@ -8016,7 +8126,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
8016
8126
|
const existing = worktreeForNumber(dir, num);
|
|
8017
8127
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
8018
8128
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
8019
|
-
if (existing && !existing.prunable &&
|
|
8129
|
+
if (existing && !existing.prunable && existsSync14(existing.path)) {
|
|
8020
8130
|
const branch = existing.branch;
|
|
8021
8131
|
if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
|
|
8022
8132
|
if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
|
|
@@ -8030,7 +8140,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
8030
8140
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
8031
8141
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
8032
8142
|
}
|
|
8033
|
-
const path =
|
|
8143
|
+
const path = join15(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
|
|
8034
8144
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
8035
8145
|
return { action: "cut-remote", path, branch: located.branch };
|
|
8036
8146
|
}
|
|
@@ -8077,7 +8187,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
8077
8187
|
}
|
|
8078
8188
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
8079
8189
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
8080
|
-
if (!wt || !
|
|
8190
|
+
if (!wt || !existsSync14(wt.path)) return { kind: "none" };
|
|
8081
8191
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
8082
8192
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
8083
8193
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -8089,7 +8199,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
8089
8199
|
if (pids.length) await killGently(pids);
|
|
8090
8200
|
}
|
|
8091
8201
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
8092
|
-
return
|
|
8202
|
+
return existsSync14(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
8093
8203
|
}
|
|
8094
8204
|
|
|
8095
8205
|
// src/handoff-guard.ts
|
|
@@ -8238,13 +8348,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
8238
8348
|
}
|
|
8239
8349
|
|
|
8240
8350
|
// src/connect-wizard.ts
|
|
8241
|
-
import { delimiter, dirname as dirname8, join as
|
|
8242
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
8351
|
+
import { delimiter, dirname as dirname8, join as join16 } from "path";
|
|
8352
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
8243
8353
|
import { stringify } from "yaml";
|
|
8244
8354
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
8245
8355
|
function shouldRunWizard(opts) {
|
|
8246
8356
|
if (!opts.isTTY) return false;
|
|
8247
|
-
const exists = opts.configExists ??
|
|
8357
|
+
const exists = opts.configExists ?? existsSync15;
|
|
8248
8358
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
8249
8359
|
}
|
|
8250
8360
|
function firstRunConfigPath() {
|
|
@@ -8261,7 +8371,7 @@ function defaultIsExecutable(p) {
|
|
|
8261
8371
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
8262
8372
|
for (const dir of pathEnv.split(delimiter)) {
|
|
8263
8373
|
if (!dir) continue;
|
|
8264
|
-
const candidate =
|
|
8374
|
+
const candidate = join16(dir, name);
|
|
8265
8375
|
if (isExecutable(candidate)) return candidate;
|
|
8266
8376
|
}
|
|
8267
8377
|
return void 0;
|
|
@@ -8356,8 +8466,8 @@ Wrote ${o.configPath}.
|
|
|
8356
8466
|
}
|
|
8357
8467
|
|
|
8358
8468
|
// src/release-run.ts
|
|
8359
|
-
import { readFileSync as
|
|
8360
|
-
import { dirname as dirname9, join as
|
|
8469
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync16, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
|
|
8470
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
8361
8471
|
import { tmpdir as tmpdir2 } from "os";
|
|
8362
8472
|
|
|
8363
8473
|
// src/release.ts
|
|
@@ -8421,7 +8531,7 @@ function checkGuards(cwd, base = "main") {
|
|
|
8421
8531
|
}
|
|
8422
8532
|
function heldByBlockers(tickets, verifiedStatus, info) {
|
|
8423
8533
|
if (!info) return [];
|
|
8424
|
-
return tickets.filter((t) => t.status === verifiedStatus && unresolvedBlockers(t, info).length > 0).map((ticket) => ({ ticket, blockers: describeUnresolved(ticket, info) }));
|
|
8534
|
+
return tickets.filter((t) => t.status === verifiedStatus && unresolvedBlockers(t, info, { mergeGated: false }).length > 0).map((ticket) => ({ ticket, blockers: describeUnresolved(ticket, info, { mergeGated: false }) }));
|
|
8425
8535
|
}
|
|
8426
8536
|
var describeHeld = (held) => held.map((h) => `${h.ticket.issue_tag ?? h.ticket.issue_id} held: blocked by ${h.blockers}`);
|
|
8427
8537
|
var CHANGELOG_LINE = /^Changelog:[ \t]*(.*)$/gm;
|
|
@@ -9015,13 +9125,13 @@ async function bump(o, size) {
|
|
|
9015
9125
|
}
|
|
9016
9126
|
return printed;
|
|
9017
9127
|
}
|
|
9018
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
9128
|
+
const files = o.repo.release.versionFiles.map((f) => join17(o.cwd, f));
|
|
9019
9129
|
const first = files[0];
|
|
9020
|
-
if (!
|
|
9130
|
+
if (!existsSync16(first)) {
|
|
9021
9131
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
9022
9132
|
return null;
|
|
9023
9133
|
}
|
|
9024
|
-
const current = JSON.parse(
|
|
9134
|
+
const current = JSON.parse(readFileSync12(first, "utf8")).version;
|
|
9025
9135
|
if (!current) {
|
|
9026
9136
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
9027
9137
|
return null;
|
|
@@ -9034,7 +9144,7 @@ async function bump(o, size) {
|
|
|
9034
9144
|
return version;
|
|
9035
9145
|
}
|
|
9036
9146
|
for (const f of files) {
|
|
9037
|
-
const src =
|
|
9147
|
+
const src = readFileSync12(f, "utf8");
|
|
9038
9148
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
9039
9149
|
if (out === src) {
|
|
9040
9150
|
o.emit.error(`no version field in ${f}`);
|
|
@@ -9047,13 +9157,13 @@ async function bump(o, size) {
|
|
|
9047
9157
|
function writeChangelog(o, version, entries) {
|
|
9048
9158
|
const file = o.repo.release.changelog;
|
|
9049
9159
|
if (!file || entries.length === 0) return;
|
|
9050
|
-
const path =
|
|
9160
|
+
const path = join17(o.cwd, file);
|
|
9051
9161
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
9052
9162
|
if (o.dryRun) {
|
|
9053
9163
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
9054
9164
|
return;
|
|
9055
9165
|
}
|
|
9056
|
-
const existing =
|
|
9166
|
+
const existing = existsSync16(path) ? readFileSync12(path, "utf8") : "# Changelog\n";
|
|
9057
9167
|
writeFileSync9(path, insertChangelogSection(existing, section));
|
|
9058
9168
|
}
|
|
9059
9169
|
async function confirm(o, expected) {
|
|
@@ -9135,8 +9245,8 @@ async function confirmLastRelease(o, decision) {
|
|
|
9135
9245
|
function openReleaseCheckout(o) {
|
|
9136
9246
|
const { remote, base } = o.repo.branch;
|
|
9137
9247
|
const branch = `crew/release-${Date.now()}`;
|
|
9138
|
-
const root = mkdtempSync(
|
|
9139
|
-
const dir =
|
|
9248
|
+
const root = mkdtempSync(join17(tmpdir2(), "crew-release-"));
|
|
9249
|
+
const dir = join17(root, "checkout");
|
|
9140
9250
|
const close = () => {
|
|
9141
9251
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
9142
9252
|
rmSync6(root, { recursive: true, force: true });
|
|
@@ -9150,10 +9260,10 @@ function openReleaseCheckout(o) {
|
|
|
9150
9260
|
throw e;
|
|
9151
9261
|
}
|
|
9152
9262
|
for (const f of o.repo.worktrees.copy) {
|
|
9153
|
-
const src =
|
|
9154
|
-
if (!
|
|
9155
|
-
mkdirSync8(dirname9(
|
|
9156
|
-
copyFileSync(src,
|
|
9263
|
+
const src = join17(o.cwd, f);
|
|
9264
|
+
if (!existsSync16(src)) continue;
|
|
9265
|
+
mkdirSync8(dirname9(join17(dir, f)), { recursive: true });
|
|
9266
|
+
copyFileSync(src, join17(dir, f));
|
|
9157
9267
|
}
|
|
9158
9268
|
return { dir, branch, close };
|
|
9159
9269
|
}
|
|
@@ -9622,7 +9732,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
9622
9732
|
}
|
|
9623
9733
|
|
|
9624
9734
|
// src/watch.ts
|
|
9625
|
-
import { watch as watchFile, existsSync as
|
|
9735
|
+
import { watch as watchFile, existsSync as existsSync17, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
|
|
9626
9736
|
var E = "\x1B[";
|
|
9627
9737
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
9628
9738
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -9723,7 +9833,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
9723
9833
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
9724
9834
|
}
|
|
9725
9835
|
function readFrom(file, from) {
|
|
9726
|
-
if (!
|
|
9836
|
+
if (!existsSync17(file)) return { events: [], offset: from };
|
|
9727
9837
|
const size = statSync6(file).size;
|
|
9728
9838
|
if (size < from) return readFrom(file, 0);
|
|
9729
9839
|
if (size === from) return { events: [], offset: from };
|
|
@@ -10434,7 +10544,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
10434
10544
|
}
|
|
10435
10545
|
|
|
10436
10546
|
// src/repos-cmd.ts
|
|
10437
|
-
import { homedir as
|
|
10547
|
+
import { homedir as homedir7 } from "os";
|
|
10438
10548
|
import { basename as basename7, resolve as resolve3 } from "path";
|
|
10439
10549
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
10440
10550
|
var ReposError = class extends Error {
|
|
@@ -10481,7 +10591,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
10481
10591
|
return { route, path: "." };
|
|
10482
10592
|
}
|
|
10483
10593
|
function expandDir(p, base) {
|
|
10484
|
-
const home = p === "~" ?
|
|
10594
|
+
const home = p === "~" ? homedir7() : p.startsWith("~/") ? `${homedir7()}/${p.slice(2)}` : p;
|
|
10485
10595
|
return resolve3(base, home);
|
|
10486
10596
|
}
|
|
10487
10597
|
function routeMapOf(doc, route) {
|
|
@@ -10616,7 +10726,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
10616
10726
|
}
|
|
10617
10727
|
|
|
10618
10728
|
// src/worktree-sweep.ts
|
|
10619
|
-
import { existsSync as
|
|
10729
|
+
import { existsSync as existsSync18 } from "fs";
|
|
10620
10730
|
import { basename as basename8 } from "path";
|
|
10621
10731
|
function planWorktreeSweep(target, tickets, contract) {
|
|
10622
10732
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -10644,7 +10754,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
10644
10754
|
let removed = 0;
|
|
10645
10755
|
let keptBranches = 0;
|
|
10646
10756
|
for (const a of actions) {
|
|
10647
|
-
if (!dryRun &&
|
|
10757
|
+
if (!dryRun && existsSync18(a.path)) {
|
|
10648
10758
|
let dirty = false;
|
|
10649
10759
|
try {
|
|
10650
10760
|
dirty = status(a.path).length > 0;
|
|
@@ -10658,7 +10768,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
10658
10768
|
continue;
|
|
10659
10769
|
}
|
|
10660
10770
|
}
|
|
10661
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
10771
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync18(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
10662
10772
|
log.warn(
|
|
10663
10773
|
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
10664
10774
|
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
@@ -10739,14 +10849,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
10739
10849
|
}
|
|
10740
10850
|
|
|
10741
10851
|
// src/stream-sweep.ts
|
|
10742
|
-
import { existsSync as
|
|
10743
|
-
import { join as
|
|
10852
|
+
import { existsSync as existsSync19, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync7 } from "fs";
|
|
10853
|
+
import { join as join18 } from "path";
|
|
10744
10854
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
10745
10855
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
10746
|
-
const dir =
|
|
10747
|
-
if (!
|
|
10856
|
+
const dir = join18(stateDir, "streams");
|
|
10857
|
+
if (!existsSync19(dir)) return [];
|
|
10748
10858
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
10749
|
-
return
|
|
10859
|
+
return readdirSync4(dir).map((name) => join18(dir, name)).filter((path) => {
|
|
10750
10860
|
try {
|
|
10751
10861
|
return statSync7(path).mtimeMs < cutoff;
|
|
10752
10862
|
} catch {
|
|
@@ -10772,9 +10882,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
10772
10882
|
return removed;
|
|
10773
10883
|
}
|
|
10774
10884
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
10775
|
-
if (!
|
|
10885
|
+
if (!existsSync19(stateDir)) return [];
|
|
10776
10886
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
10777
|
-
return
|
|
10887
|
+
return readdirSync4(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join18(stateDir, name)).filter((path) => {
|
|
10778
10888
|
try {
|
|
10779
10889
|
return statSync7(path).mtimeMs < cutoff;
|
|
10780
10890
|
} catch {
|
|
@@ -10784,23 +10894,23 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
10784
10894
|
}
|
|
10785
10895
|
|
|
10786
10896
|
// src/cli.ts
|
|
10787
|
-
import { readFileSync as
|
|
10897
|
+
import { readFileSync as readFileSync17, existsSync as existsSync24, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
10788
10898
|
import { createInterface } from "readline/promises";
|
|
10789
10899
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
10790
10900
|
|
|
10791
10901
|
// src/ssh-keys.ts
|
|
10792
10902
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
10793
|
-
import { existsSync as
|
|
10794
|
-
import { dirname as dirname11, join as
|
|
10903
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync13, chmodSync } from "fs";
|
|
10904
|
+
import { dirname as dirname11, join as join19 } from "path";
|
|
10795
10905
|
var SshKeyError = class extends Error {
|
|
10796
10906
|
};
|
|
10797
10907
|
function shipSshKeyPathFor(stateDir) {
|
|
10798
|
-
return
|
|
10908
|
+
return join19(stateDir, "ssh", "id_ed25519");
|
|
10799
10909
|
}
|
|
10800
10910
|
function ensureShipSshKeypair(stateDir) {
|
|
10801
10911
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
10802
10912
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
10803
|
-
if (!
|
|
10913
|
+
if (!existsSync20(privateKeyPath)) {
|
|
10804
10914
|
mkdirSync9(dirname11(privateKeyPath), { recursive: true });
|
|
10805
10915
|
try {
|
|
10806
10916
|
execFileSync3("ssh-keygen", [
|
|
@@ -10821,10 +10931,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
10821
10931
|
}
|
|
10822
10932
|
chmodSync(privateKeyPath, 384);
|
|
10823
10933
|
}
|
|
10824
|
-
if (!
|
|
10934
|
+
if (!existsSync20(publicKeyPath)) {
|
|
10825
10935
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
10826
10936
|
}
|
|
10827
|
-
const publicKey =
|
|
10937
|
+
const publicKey = readFileSync13(publicKeyPath, "utf8").trim();
|
|
10828
10938
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
10829
10939
|
}
|
|
10830
10940
|
function sshKeygenAvailable() {
|
|
@@ -11038,9 +11148,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
11038
11148
|
|
|
11039
11149
|
// src/tunnel.ts
|
|
11040
11150
|
import { spawn as spawn3 } from "child_process";
|
|
11041
|
-
import { existsSync as
|
|
11151
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync5 } from "fs";
|
|
11042
11152
|
import { randomInt } from "crypto";
|
|
11043
|
-
import { join as
|
|
11153
|
+
import { join as join20 } from "path";
|
|
11044
11154
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
11045
11155
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
11046
11156
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -11107,20 +11217,20 @@ function startTunnel(opts) {
|
|
|
11107
11217
|
};
|
|
11108
11218
|
}
|
|
11109
11219
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
11110
|
-
return
|
|
11220
|
+
return join20(stateDir, "passengers", `${workspaceId}.json`);
|
|
11111
11221
|
}
|
|
11112
11222
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
11113
11223
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
11114
|
-
if (!
|
|
11224
|
+
if (!existsSync21(path)) return void 0;
|
|
11115
11225
|
try {
|
|
11116
|
-
return JSON.parse(
|
|
11226
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
11117
11227
|
} catch {
|
|
11118
11228
|
return void 0;
|
|
11119
11229
|
}
|
|
11120
11230
|
}
|
|
11121
11231
|
function writePersistedTunnel(stateDir, state) {
|
|
11122
11232
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
11123
|
-
mkdirSync10(
|
|
11233
|
+
mkdirSync10(join20(stateDir, "passengers"), { recursive: true });
|
|
11124
11234
|
writeFileSync10(path, `${JSON.stringify(state, null, 2)}
|
|
11125
11235
|
`);
|
|
11126
11236
|
}
|
|
@@ -11218,9 +11328,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
11218
11328
|
}
|
|
11219
11329
|
}
|
|
11220
11330
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
11221
|
-
const dir =
|
|
11222
|
-
if (!
|
|
11223
|
-
return
|
|
11331
|
+
const dir = join20(stateDir, "passengers");
|
|
11332
|
+
if (!existsSync21(dir)) return [];
|
|
11333
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
11224
11334
|
}
|
|
11225
11335
|
|
|
11226
11336
|
// src/mcp-endpoint-registry.ts
|
|
@@ -11340,8 +11450,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
11340
11450
|
import { createServer as createHttpServer } from "http";
|
|
11341
11451
|
import { spawn as spawn4 } from "child_process";
|
|
11342
11452
|
import { randomBytes } from "crypto";
|
|
11343
|
-
import { existsSync as
|
|
11344
|
-
import { join as
|
|
11453
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync6 } from "fs";
|
|
11454
|
+
import { join as join21 } from "path";
|
|
11345
11455
|
|
|
11346
11456
|
// src/secret-compare.ts
|
|
11347
11457
|
import { timingSafeEqual } from "crypto";
|
|
@@ -11430,20 +11540,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
11430
11540
|
return child;
|
|
11431
11541
|
};
|
|
11432
11542
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
11433
|
-
return
|
|
11543
|
+
return join21(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
11434
11544
|
}
|
|
11435
11545
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
11436
11546
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
11437
|
-
if (!
|
|
11547
|
+
if (!existsSync22(path)) return void 0;
|
|
11438
11548
|
try {
|
|
11439
|
-
return JSON.parse(
|
|
11549
|
+
return JSON.parse(readFileSync15(path, "utf8"));
|
|
11440
11550
|
} catch {
|
|
11441
11551
|
return void 0;
|
|
11442
11552
|
}
|
|
11443
11553
|
}
|
|
11444
11554
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
11445
11555
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
11446
|
-
mkdirSync11(
|
|
11556
|
+
mkdirSync11(join21(stateDir, "passengers"), { recursive: true });
|
|
11447
11557
|
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
11448
11558
|
`);
|
|
11449
11559
|
}
|
|
@@ -11464,10 +11574,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
11464
11574
|
}
|
|
11465
11575
|
}
|
|
11466
11576
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
11467
|
-
const dir =
|
|
11468
|
-
if (!
|
|
11577
|
+
const dir = join21(stateDir, "passengers");
|
|
11578
|
+
if (!existsSync22(dir)) return [];
|
|
11469
11579
|
const suffix = "-sync.json";
|
|
11470
|
-
return
|
|
11580
|
+
return readdirSync6(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
11471
11581
|
}
|
|
11472
11582
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
11473
11583
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -11522,8 +11632,8 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
11522
11632
|
}
|
|
11523
11633
|
|
|
11524
11634
|
// src/daemon.ts
|
|
11525
|
-
import { existsSync as
|
|
11526
|
-
import { join as
|
|
11635
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync16, readdirSync as readdirSync7, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
|
|
11636
|
+
import { join as join22 } from "path";
|
|
11527
11637
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
11528
11638
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
11529
11639
|
var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
|
|
@@ -11561,8 +11671,8 @@ function latestMtime(path) {
|
|
|
11561
11671
|
if (stat.isFile()) return stat.mtimeMs;
|
|
11562
11672
|
if (!stat.isDirectory()) return 0;
|
|
11563
11673
|
let latest = stat.mtimeMs;
|
|
11564
|
-
for (const entry of
|
|
11565
|
-
latest = Math.max(latest, latestMtime(
|
|
11674
|
+
for (const entry of readdirSync7(path, { withFileTypes: true })) {
|
|
11675
|
+
latest = Math.max(latest, latestMtime(join22(path, entry.name)));
|
|
11566
11676
|
}
|
|
11567
11677
|
return latest;
|
|
11568
11678
|
}
|
|
@@ -11571,13 +11681,13 @@ function computeTreeSignature(paths) {
|
|
|
11571
11681
|
}
|
|
11572
11682
|
function makeStaleChecker(paths, opts = {}) {
|
|
11573
11683
|
const initial = computeTreeSignature(paths);
|
|
11574
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
11684
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync23(opts.reloadSentinel);
|
|
11575
11685
|
}
|
|
11576
11686
|
function reloadSentinelFor(stateDir) {
|
|
11577
|
-
return
|
|
11687
|
+
return join22(stateDir, "reload-requested");
|
|
11578
11688
|
}
|
|
11579
11689
|
function daemonStampFor(stateDir) {
|
|
11580
|
-
return
|
|
11690
|
+
return join22(stateDir, "daemon.started");
|
|
11581
11691
|
}
|
|
11582
11692
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
11583
11693
|
mkdirSync12(stateDir, { recursive: true });
|
|
@@ -11587,7 +11697,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
11587
11697
|
}
|
|
11588
11698
|
function readDaemonStarted(stateDir) {
|
|
11589
11699
|
try {
|
|
11590
|
-
const v = JSON.parse(
|
|
11700
|
+
const v = JSON.parse(readFileSync16(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
11591
11701
|
return typeof v === "number" ? v : null;
|
|
11592
11702
|
} catch {
|
|
11593
11703
|
return null;
|
|
@@ -12177,8 +12287,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
12177
12287
|
const FLEET_CAPABLE = /* @__PURE__ */ new Set(["poll", "run", "passengers"]);
|
|
12178
12288
|
const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
|
|
12179
12289
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
12180
|
-
const
|
|
12181
|
-
const
|
|
12290
|
+
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
12291
|
+
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
12292
|
+
const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "projects" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
|
|
12182
12293
|
let route;
|
|
12183
12294
|
try {
|
|
12184
12295
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -12384,8 +12495,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
12384
12495
|
}
|
|
12385
12496
|
if (host === "linux" && hasSystemd) {
|
|
12386
12497
|
const label = labelFor(CREW_HOME, "run");
|
|
12387
|
-
const staleTimer =
|
|
12388
|
-
if (
|
|
12498
|
+
const staleTimer = join23(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
12499
|
+
if (existsSync24(staleTimer)) {
|
|
12389
12500
|
process.stdout.write(
|
|
12390
12501
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
12391
12502
|
`
|
|
@@ -12411,7 +12522,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12411
12522
|
const tracker = new Tracker(c, cfg.ship);
|
|
12412
12523
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
12413
12524
|
for (const r of await resolvedRepos(c)) {
|
|
12414
|
-
if (!
|
|
12525
|
+
if (!existsSync24(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
12415
12526
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
12416
12527
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
12417
12528
|
}
|
|
@@ -12429,7 +12540,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12429
12540
|
}
|
|
12430
12541
|
async function doctorFix(o) {
|
|
12431
12542
|
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))));
|
|
12432
|
-
const unitExists = (p) =>
|
|
12543
|
+
const unitExists = (p) => existsSync24(p);
|
|
12433
12544
|
const crontab = o.host === "linux" ? spawnSync5("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
12434
12545
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
12435
12546
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -12446,7 +12557,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12446
12557
|
for (const a of plan.actions) {
|
|
12447
12558
|
if (a.kind === "enable") {
|
|
12448
12559
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
12449
|
-
writeFileSync13(cfg.configFile, planEnable(
|
|
12560
|
+
writeFileSync13(cfg.configFile, planEnable(readFileSync17(cfg.configFile, "utf8"), a.route));
|
|
12450
12561
|
enabledSound.add(a.route);
|
|
12451
12562
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
12452
12563
|
} else {
|
|
@@ -13353,7 +13464,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13353
13464
|
process.exit(1);
|
|
13354
13465
|
}
|
|
13355
13466
|
requireArmed("run the daemon loop");
|
|
13356
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
13467
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join23(CREW_HOME, "src"), join23(CREW_HOME, "dist"), join23(CREW_HOME, "bin")];
|
|
13357
13468
|
const configPaths = daemonConfigPaths(cfg);
|
|
13358
13469
|
watchPaths.push(...configPaths);
|
|
13359
13470
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -13512,7 +13623,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13512
13623
|
`);
|
|
13513
13624
|
if (!dryRun) {
|
|
13514
13625
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13515
|
-
const raw =
|
|
13626
|
+
const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
|
|
13516
13627
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
13517
13628
|
raw.agentSkills = skillResult.agentSkills;
|
|
13518
13629
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
@@ -13563,7 +13674,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13563
13674
|
`);
|
|
13564
13675
|
if (!dryRun) {
|
|
13565
13676
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13566
|
-
const raw =
|
|
13677
|
+
const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
|
|
13567
13678
|
raw.agentSkills = result.agentSkills;
|
|
13568
13679
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
13569
13680
|
writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -13579,14 +13690,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13579
13690
|
process.exit(2);
|
|
13580
13691
|
};
|
|
13581
13692
|
if (sub !== "add" && sub !== "list") usage2();
|
|
13582
|
-
const configText =
|
|
13693
|
+
const configText = readFileSync17(cfg.configFile, "utf8");
|
|
13583
13694
|
let routeName = positional[2];
|
|
13584
13695
|
let addPath = ".";
|
|
13585
13696
|
let addName;
|
|
13586
13697
|
if (sub === "list" && !routeName) usage2();
|
|
13587
13698
|
if (sub === "add") {
|
|
13588
13699
|
try {
|
|
13589
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
13700
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync24(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
13590
13701
|
routeName = a.route;
|
|
13591
13702
|
addPath = a.path;
|
|
13592
13703
|
addName = a.name;
|
|
@@ -13601,9 +13712,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13601
13712
|
}
|
|
13602
13713
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
13603
13714
|
let resolvedRemotes;
|
|
13604
|
-
if (
|
|
13715
|
+
if (existsSync24(resolvedFile)) {
|
|
13605
13716
|
try {
|
|
13606
|
-
resolvedRemotes = JSON.parse(
|
|
13717
|
+
resolvedRemotes = JSON.parse(readFileSync17(resolvedFile, "utf8"));
|
|
13607
13718
|
} catch {
|
|
13608
13719
|
}
|
|
13609
13720
|
}
|
|
@@ -13618,14 +13729,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13618
13729
|
for (const r of repos) {
|
|
13619
13730
|
const origin2 = originOf(r.dir);
|
|
13620
13731
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
13621
|
-
const note = !
|
|
13732
|
+
const note = !existsSync24(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
13622
13733
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
13623
13734
|
`);
|
|
13624
13735
|
}
|
|
13625
13736
|
break;
|
|
13626
13737
|
}
|
|
13627
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
13628
|
-
const top =
|
|
13738
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
13739
|
+
const top = existsSync24(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
13629
13740
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
13630
13741
|
const origin = originOf(top);
|
|
13631
13742
|
const plan = planRepoAdd({
|
|
@@ -13674,8 +13785,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13674
13785
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
13675
13786
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
13676
13787
|
`);
|
|
13677
|
-
if (
|
|
13678
|
-
const raw = JSON.parse(
|
|
13788
|
+
if (existsSync24(resolvedFile)) {
|
|
13789
|
+
const raw = JSON.parse(readFileSync17(resolvedFile, "utf8"));
|
|
13679
13790
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
13680
13791
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
13681
13792
|
writeFileSync13(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -13921,10 +14032,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13921
14032
|
operator = await pickOperator(found.holds);
|
|
13922
14033
|
}
|
|
13923
14034
|
let previous;
|
|
13924
|
-
const hadPreviousFile =
|
|
14035
|
+
const hadPreviousFile = existsSync24(resolvedPath);
|
|
13925
14036
|
if (hadPreviousFile) {
|
|
13926
14037
|
try {
|
|
13927
|
-
previous = JSON.parse(
|
|
14038
|
+
previous = JSON.parse(readFileSync17(resolvedPath, "utf8"));
|
|
13928
14039
|
} catch {
|
|
13929
14040
|
}
|
|
13930
14041
|
}
|
|
@@ -14012,7 +14123,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14012
14123
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
14013
14124
|
const wizard = firstRun;
|
|
14014
14125
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
14015
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
14126
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join23(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
14016
14127
|
const routeBlock = renderConnection(
|
|
14017
14128
|
found,
|
|
14018
14129
|
route2,
|
|
@@ -14353,21 +14464,36 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14353
14464
|
}
|
|
14354
14465
|
case "worktree": {
|
|
14355
14466
|
emit.enter("worktree");
|
|
14356
|
-
const rawWt =
|
|
14467
|
+
const rawWt = worktreeArgs?.ticket;
|
|
14357
14468
|
const nWt = rawWt ? parseTicketNumber(rawWt) : null;
|
|
14358
14469
|
if (!nWt) {
|
|
14359
14470
|
process.stderr.write("worktree: need a ticket number (326, ISSUE-326, or TABL-326)\n");
|
|
14360
14471
|
process.exit(2);
|
|
14361
14472
|
}
|
|
14362
|
-
const
|
|
14363
|
-
|
|
14364
|
-
const
|
|
14365
|
-
|
|
14366
|
-
|
|
14473
|
+
const candidatesWt = !named && cfg.routes.length > 1 ? cfg.routes.filter((r) => r.enabled) : [route];
|
|
14474
|
+
let ticketWt;
|
|
14475
|
+
const lookedWt = [];
|
|
14476
|
+
const hitsWt = /* @__PURE__ */ new Map();
|
|
14477
|
+
for (const cand of candidatesWt) {
|
|
14478
|
+
const tr = new Tracker(cand, cfg.ship);
|
|
14479
|
+
const hit = (await tr.openTickets()).find((t) => t[tr.contract.columns.key] === `ISSUE-${nWt}`);
|
|
14480
|
+
if (hit) hitsWt.set(cand.route, hit);
|
|
14481
|
+
lookedWt.push({
|
|
14482
|
+
route: cand.route,
|
|
14483
|
+
hit: hit ? { repoId: hit.repo_id ?? null } : void 0,
|
|
14484
|
+
servedRepoIds: hit ? (await resolvedRepos(cand)).flatMap((r) => r.id ? [r.id] : []) : []
|
|
14485
|
+
});
|
|
14486
|
+
}
|
|
14487
|
+
const pickedWt = pickWorktreeRoute(Number(nWt), lookedWt);
|
|
14488
|
+
if ("error" in pickedWt) {
|
|
14489
|
+
process.stderr.write(`worktree: ${pickedWt.error}
|
|
14367
14490
|
`);
|
|
14368
14491
|
process.exit(2);
|
|
14369
14492
|
}
|
|
14370
|
-
const
|
|
14493
|
+
const routeWt = candidatesWt.find((r) => r.route === pickedWt.route) ?? route;
|
|
14494
|
+
ticketWt = hitsWt.get(pickedWt.route);
|
|
14495
|
+
if (!ticketWt) process.exit(2);
|
|
14496
|
+
const repoWt = (await resolvedRepos(routeWt)).find((r) => r.id === ticketWt.repo_id) ?? (await resolvedRepos(routeWt))[0];
|
|
14371
14497
|
if (!repoWt) {
|
|
14372
14498
|
process.stderr.write("worktree: this route has no repository\n");
|
|
14373
14499
|
process.exit(2);
|
|
@@ -14577,7 +14703,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14577
14703
|
}
|
|
14578
14704
|
case "log": {
|
|
14579
14705
|
try {
|
|
14580
|
-
const text =
|
|
14706
|
+
const text = readFileSync17(cfg.ship.logFile, "utf8").split("\n");
|
|
14581
14707
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
14582
14708
|
`);
|
|
14583
14709
|
} catch {
|
|
@@ -14787,7 +14913,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
14787
14913
|
);
|
|
14788
14914
|
try {
|
|
14789
14915
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
14790
|
-
const evSize =
|
|
14916
|
+
const evSize = existsSync24(evFile) ? statSync9(evFile).size : 0;
|
|
14791
14917
|
process.stdout.write(
|
|
14792
14918
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
14793
14919
|
` : "events log: ok\n"
|
|
@@ -14921,15 +15047,39 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
14921
15047
|
let allBinsFound = true;
|
|
14922
15048
|
for (const tc of shipToolchains(cfg.ship)) {
|
|
14923
15049
|
const adapter = adapterFor(tc.harness);
|
|
14924
|
-
const
|
|
15050
|
+
const runBin = launcherExecutable(tc) ?? tc.bin;
|
|
15051
|
+
const binPath = findOnPath(runBin, schedulerPath);
|
|
14925
15052
|
if (!binPath) allBinsFound = false;
|
|
14926
15053
|
const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
|
|
14927
15054
|
process.stdout.write(
|
|
14928
|
-
`${label} ${
|
|
15055
|
+
`${label} ${runBin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
|
|
14929
15056
|
`
|
|
14930
15057
|
);
|
|
14931
15058
|
for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
|
|
14932
15059
|
`);
|
|
15060
|
+
if (isOmlxLauncher(tc)) {
|
|
15061
|
+
const s = readOmlxSettings();
|
|
15062
|
+
if (!s) {
|
|
15063
|
+
process.stdout.write(` omlx: ~/.omlx/settings.json not found or has no server.port \u2014 cannot check the server
|
|
15064
|
+
`);
|
|
15065
|
+
} else {
|
|
15066
|
+
let status2;
|
|
15067
|
+
try {
|
|
15068
|
+
status2 = (await fetch(`http://${s.host}:${s.port}/v1/models`, { signal: AbortSignal.timeout(3e3) })).status;
|
|
15069
|
+
} catch {
|
|
15070
|
+
status2 = void 0;
|
|
15071
|
+
}
|
|
15072
|
+
const probe = interpretOmlxProbe(status2);
|
|
15073
|
+
process.stdout.write(` omlx server: ${s.host}:${s.port} \u2014 ${probe.detail}${probe.up ? "" : " (start oMLX)"}
|
|
15074
|
+
`);
|
|
15075
|
+
process.stdout.write(
|
|
15076
|
+
` omlx model: ${tc.model} \u2014 ${omlxModelPresent(tc.model, s.modelDirs) ? "present" : `NOT FOUND in ${s.modelDirs.join(", ") || "any model dir"}`}
|
|
15077
|
+
`
|
|
15078
|
+
);
|
|
15079
|
+
}
|
|
15080
|
+
process.stdout.write(` local model: tool-call reliability and prefill speed vary by model
|
|
15081
|
+
`);
|
|
15082
|
+
}
|
|
14933
15083
|
if (isOriLauncher(tc)) {
|
|
14934
15084
|
process.stdout.write(` ori: headless auth under the scheduler is unverified \u2014 run \`ori login\` as the scheduler user, then confirm a run records provider openrouter (see docs/toolchains.md)
|
|
14935
15085
|
`);
|