@tablation/crew 0.41.0 → 0.43.1
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 +454 -263
- package/package.json +1 -1
- package/prompts/default/personas/lane-qa.md +6 -3
- package/prompts/default/skills/crew-lane-agent.md +21 -8
- package/prompts/default/skills/ticket-attachments.md +14 -8
- package/prompts/dev-qa/personas/lane-qa.md +6 -3
- package/prompts/dev-qa/skills/crew-lane-agent.md +21 -8
- package/prompts/dev-qa/skills/ticket-attachments.md +14 -8
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
|
}
|
|
@@ -3096,6 +3184,7 @@ var DEFAULT_CONTRACT = {
|
|
|
3096
3184
|
blockedBy: "blocked_by",
|
|
3097
3185
|
updatedAt: "updated_at",
|
|
3098
3186
|
needsDesign: "needs_design",
|
|
3187
|
+
attachments: "attachments",
|
|
3099
3188
|
workMode: "work_mode",
|
|
3100
3189
|
reportType: "report_type",
|
|
3101
3190
|
epic: "epic_id",
|
|
@@ -3103,7 +3192,8 @@ var DEFAULT_CONTRACT = {
|
|
|
3103
3192
|
ship: "ship_id",
|
|
3104
3193
|
heldBy: "held_by_ship_id",
|
|
3105
3194
|
verifiedSha: "verified_sha",
|
|
3106
|
-
prRef: "pr_ref"
|
|
3195
|
+
prRef: "pr_ref",
|
|
3196
|
+
position: "position"
|
|
3107
3197
|
},
|
|
3108
3198
|
comments: {
|
|
3109
3199
|
parent: "ticket_id",
|
|
@@ -3373,9 +3463,9 @@ function describeRelease(o, route) {
|
|
|
3373
3463
|
}
|
|
3374
3464
|
|
|
3375
3465
|
// src/install.ts
|
|
3376
|
-
import { dirname as dirname4, join as
|
|
3377
|
-
import { homedir as
|
|
3378
|
-
import { accessSync, constants, existsSync as
|
|
3466
|
+
import { dirname as dirname4, join as join9, extname } from "path";
|
|
3467
|
+
import { homedir as homedir6 } from "os";
|
|
3468
|
+
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
3469
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3380
3470
|
import { createHash } from "crypto";
|
|
3381
3471
|
|
|
@@ -3387,7 +3477,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
3387
3477
|
// src/install.ts
|
|
3388
3478
|
function crewInvocation(crewHome) {
|
|
3389
3479
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
3390
|
-
return [process.execPath,
|
|
3480
|
+
return [process.execPath, join9(crewHome, "bin", "crew")];
|
|
3391
3481
|
}
|
|
3392
3482
|
var INTERVAL_SECONDS = 120;
|
|
3393
3483
|
function labelFor(crewHome, job) {
|
|
@@ -3425,7 +3515,7 @@ function pathFor(ship) {
|
|
|
3425
3515
|
return parts.join(":");
|
|
3426
3516
|
}
|
|
3427
3517
|
function findOnPath(bin, searchPath) {
|
|
3428
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
3518
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join9(dir, bin));
|
|
3429
3519
|
for (const candidate of candidates) {
|
|
3430
3520
|
try {
|
|
3431
3521
|
accessSync(candidate, constants.X_OK);
|
|
@@ -3435,7 +3525,7 @@ function findOnPath(bin, searchPath) {
|
|
|
3435
3525
|
}
|
|
3436
3526
|
return void 0;
|
|
3437
3527
|
}
|
|
3438
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
3528
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join9(homedir6(), ".docker", "bin")].join(":");
|
|
3439
3529
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
3440
3530
|
const schedulerPath = pathFor(ship);
|
|
3441
3531
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -3447,7 +3537,7 @@ function xmlEscape(s) {
|
|
|
3447
3537
|
}
|
|
3448
3538
|
function planLaunchd(ship, crewHome, job) {
|
|
3449
3539
|
const label = labelFor(crewHome, job);
|
|
3450
|
-
const unitPath =
|
|
3540
|
+
const unitPath = join9(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3451
3541
|
const crewLog = ship.logFile;
|
|
3452
3542
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3453
3543
|
const persistent = job === "run";
|
|
@@ -3499,8 +3589,8 @@ ${scheduleKeys}
|
|
|
3499
3589
|
}
|
|
3500
3590
|
function planSystemd(ship, crewHome, job) {
|
|
3501
3591
|
const label = labelFor(crewHome, job);
|
|
3502
|
-
const unitDir =
|
|
3503
|
-
const servicePath =
|
|
3592
|
+
const unitDir = join9(homedir6(), ".config", "systemd", "user");
|
|
3593
|
+
const servicePath = join9(unitDir, `${label}.service`);
|
|
3504
3594
|
const crewLog = ship.logFile;
|
|
3505
3595
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3506
3596
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -3537,7 +3627,7 @@ WantedBy=default.target
|
|
|
3537
3627
|
schedulerLog
|
|
3538
3628
|
};
|
|
3539
3629
|
}
|
|
3540
|
-
const timerPath =
|
|
3630
|
+
const timerPath = join9(unitDir, `${label}.timer`);
|
|
3541
3631
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
3542
3632
|
[Unit]
|
|
3543
3633
|
Description=${description}
|
|
@@ -3604,7 +3694,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3604
3694
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
3605
3695
|
const label = labelFor(crewHome, job);
|
|
3606
3696
|
if (hostShip === "macos") {
|
|
3607
|
-
const unitPath =
|
|
3697
|
+
const unitPath = join9(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3608
3698
|
return {
|
|
3609
3699
|
mechanism: "launchd",
|
|
3610
3700
|
unitPaths: [unitPath],
|
|
@@ -3614,18 +3704,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3614
3704
|
};
|
|
3615
3705
|
}
|
|
3616
3706
|
if (hostShip === "linux" && hasSystemd) {
|
|
3617
|
-
const unitDir =
|
|
3707
|
+
const unitDir = join9(homedir6(), ".config", "systemd", "user");
|
|
3618
3708
|
if (job === "run") {
|
|
3619
3709
|
return {
|
|
3620
3710
|
mechanism: "systemd",
|
|
3621
|
-
unitPaths: [
|
|
3711
|
+
unitPaths: [join9(unitDir, `${label}.service`)],
|
|
3622
3712
|
crontabLine: null,
|
|
3623
3713
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
3624
3714
|
};
|
|
3625
3715
|
}
|
|
3626
3716
|
return {
|
|
3627
3717
|
mechanism: "systemd",
|
|
3628
|
-
unitPaths: [
|
|
3718
|
+
unitPaths: [join9(unitDir, `${label}.service`), join9(unitDir, `${label}.timer`)],
|
|
3629
3719
|
crontabLine: null,
|
|
3630
3720
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
3631
3721
|
};
|
|
@@ -3643,7 +3733,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3643
3733
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
3644
3734
|
const label = labelFor(crewHome, "run");
|
|
3645
3735
|
if (hostShip === "macos") {
|
|
3646
|
-
const unitPath =
|
|
3736
|
+
const unitPath = join9(homedir6(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3647
3737
|
return {
|
|
3648
3738
|
mechanism: "launchd",
|
|
3649
3739
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -3738,7 +3828,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3738
3828
|
}
|
|
3739
3829
|
}
|
|
3740
3830
|
for (const path of plan.unitPaths) {
|
|
3741
|
-
if (!
|
|
3831
|
+
if (!existsSync8(path)) continue;
|
|
3742
3832
|
if (dryRun) {
|
|
3743
3833
|
log.emit(`would remove ${path}`);
|
|
3744
3834
|
continue;
|
|
@@ -3757,7 +3847,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3757
3847
|
}
|
|
3758
3848
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
3759
3849
|
function launchAgentsDir() {
|
|
3760
|
-
return
|
|
3850
|
+
return join9(homedir6(), "Library", "LaunchAgents");
|
|
3761
3851
|
}
|
|
3762
3852
|
function xmlUnescape(s) {
|
|
3763
3853
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -3774,25 +3864,25 @@ function loadedCrewLabels() {
|
|
|
3774
3864
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3775
3865
|
}
|
|
3776
3866
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
3777
|
-
const files =
|
|
3867
|
+
const files = existsSync8(dir) ? readdirSync2(dir) : [];
|
|
3778
3868
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
3779
|
-
const plistPath =
|
|
3869
|
+
const plistPath = join9(dir, f);
|
|
3780
3870
|
let program = null;
|
|
3781
3871
|
try {
|
|
3782
|
-
program = programFromPlist(
|
|
3872
|
+
program = programFromPlist(readFileSync8(plistPath, "utf8"));
|
|
3783
3873
|
} catch {
|
|
3784
3874
|
}
|
|
3785
3875
|
return {
|
|
3786
3876
|
label: f.slice(0, -".plist".length),
|
|
3787
3877
|
plistPath,
|
|
3788
3878
|
program,
|
|
3789
|
-
programExists: program !== null &&
|
|
3879
|
+
programExists: program !== null && existsSync8(program)
|
|
3790
3880
|
};
|
|
3791
3881
|
});
|
|
3792
3882
|
const seen = new Set(units.map((u) => u.label));
|
|
3793
3883
|
for (const label of [...loaded].sort()) {
|
|
3794
3884
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
3795
|
-
units.push({ label, plistPath:
|
|
3885
|
+
units.push({ label, plistPath: join9(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
3796
3886
|
}
|
|
3797
3887
|
return units;
|
|
3798
3888
|
}
|
|
@@ -3805,9 +3895,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3805
3895
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3806
3896
|
return;
|
|
3807
3897
|
}
|
|
3808
|
-
if (
|
|
3898
|
+
if (existsSync8(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3809
3899
|
spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3810
|
-
if (
|
|
3900
|
+
if (existsSync8(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3811
3901
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3812
3902
|
}
|
|
3813
3903
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -3890,7 +3980,7 @@ import { statfsSync } from "fs";
|
|
|
3890
3980
|
// src/git.ts
|
|
3891
3981
|
import { execFileSync } from "child_process";
|
|
3892
3982
|
import {
|
|
3893
|
-
existsSync as
|
|
3983
|
+
existsSync as existsSync9,
|
|
3894
3984
|
mkdirSync as mkdirSync5,
|
|
3895
3985
|
openSync as openSync2,
|
|
3896
3986
|
closeSync as closeSync2,
|
|
@@ -4262,7 +4352,7 @@ function cloneUrlFor(remote) {
|
|
|
4262
4352
|
return `git@github.com:${remote}.git`;
|
|
4263
4353
|
}
|
|
4264
4354
|
function ensureRepoCheckout(dir, remote) {
|
|
4265
|
-
if (
|
|
4355
|
+
if (existsSync9(`${dir}/.git`)) return false;
|
|
4266
4356
|
if (!remote) {
|
|
4267
4357
|
throw new GitError(
|
|
4268
4358
|
`${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`
|
|
@@ -4697,6 +4787,7 @@ var Tracker = class {
|
|
|
4697
4787
|
* so concurrent first writes share one round of field-metadata reads.
|
|
4698
4788
|
*/
|
|
4699
4789
|
authorship;
|
|
4790
|
+
attachments;
|
|
4700
4791
|
stateDir;
|
|
4701
4792
|
constructor(route, ship) {
|
|
4702
4793
|
this.route = route;
|
|
@@ -5149,6 +5240,28 @@ var Tracker = class {
|
|
|
5149
5240
|
shipId: shipRow?.id ?? null
|
|
5150
5241
|
};
|
|
5151
5242
|
}
|
|
5243
|
+
/**
|
|
5244
|
+
* The attachments probe (CREW-1558): which table the Issues `attachments`
|
|
5245
|
+
* column references, read once per process like the authorship probe. On
|
|
5246
|
+
* TABL-1557 two runs reported a ticket's screenshot as a 404 because, with
|
|
5247
|
+
* no table id in hand, they guessed `files/<attachment id>` paths; naming
|
|
5248
|
+
* the table in the Environment section is what makes the lookup mechanical.
|
|
5249
|
+
*/
|
|
5250
|
+
attachmentsProbe() {
|
|
5251
|
+
this.attachments ??= this.probeAttachments();
|
|
5252
|
+
return this.attachments;
|
|
5253
|
+
}
|
|
5254
|
+
async probeAttachments() {
|
|
5255
|
+
try {
|
|
5256
|
+
const model = await this.client.dataModels.get(this.models.issues);
|
|
5257
|
+
const field = model.fields.find((f) => f.columnName === this.contract.columns.attachments);
|
|
5258
|
+
const fieldType = field?.fieldType;
|
|
5259
|
+
const target = fieldType?.targetModelId;
|
|
5260
|
+
return { mediaLibraryModelId: typeof target === "string" && target ? target : null };
|
|
5261
|
+
} catch {
|
|
5262
|
+
return { mediaLibraryModelId: null };
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5152
5265
|
async columnNames(modelId) {
|
|
5153
5266
|
try {
|
|
5154
5267
|
const model = await this.client.dataModels.get(modelId);
|
|
@@ -5391,20 +5504,25 @@ function issueNumber(t) {
|
|
|
5391
5504
|
function epicRank(t) {
|
|
5392
5505
|
return t.epicInProgress ? 0 : 1;
|
|
5393
5506
|
}
|
|
5507
|
+
function positionRank(t) {
|
|
5508
|
+
const p = t.position;
|
|
5509
|
+
return typeof p === "number" && Number.isFinite(p) ? p : Number.POSITIVE_INFINITY;
|
|
5510
|
+
}
|
|
5394
5511
|
function rank(t, c = DEFAULT_CONTRACT) {
|
|
5395
|
-
return [effectivePriority(t, c), severityRank(t, c), epicRank(t), issueNumber(t)];
|
|
5512
|
+
return [effectivePriority(t, c), positionRank(t), severityRank(t, c), epicRank(t), issueNumber(t)];
|
|
5396
5513
|
}
|
|
5397
5514
|
function compareRank(a, b, c = DEFAULT_CONTRACT) {
|
|
5398
5515
|
const ra = rank(a, c);
|
|
5399
5516
|
const rb = rank(b, c);
|
|
5400
5517
|
for (let i = 0; i < ra.length; i++) {
|
|
5401
|
-
const
|
|
5402
|
-
|
|
5518
|
+
const x = ra[i];
|
|
5519
|
+
const y = rb[i];
|
|
5520
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
5403
5521
|
}
|
|
5404
5522
|
return 0;
|
|
5405
5523
|
}
|
|
5406
5524
|
function rankScalar(t, c = DEFAULT_CONTRACT) {
|
|
5407
|
-
const [p, s, e, n] = rank(t, c);
|
|
5525
|
+
const [p, , s, e, n] = rank(t, c);
|
|
5408
5526
|
return p * 1e9 + s * 1e7 + e * 1e6 + n;
|
|
5409
5527
|
}
|
|
5410
5528
|
var NOTHING_ACTIONABLE = 999999999999;
|
|
@@ -5611,7 +5729,7 @@ function claimStatuses(role, c) {
|
|
|
5611
5729
|
}
|
|
5612
5730
|
function rankedCandidates(role, i) {
|
|
5613
5731
|
const c = i.contract ?? DEFAULT_CONTRACT;
|
|
5614
|
-
return [...roleCandidates(role, i)].sort((a, b) =>
|
|
5732
|
+
return [...roleCandidates(role, i)].sort((a, b) => compareRank(a, b, c));
|
|
5615
5733
|
}
|
|
5616
5734
|
function roleTopRank(role, i) {
|
|
5617
5735
|
if (role !== "qa" && role !== "triage" && !i.seats[role]) return NOTHING_ACTIONABLE;
|
|
@@ -5624,7 +5742,7 @@ function actionableSummary(i, pending) {
|
|
|
5624
5742
|
const c = i.contract ?? DEFAULT_CONTRACT;
|
|
5625
5743
|
const all = pending.flatMap((role) => roleCandidates(role, i));
|
|
5626
5744
|
if (all.length === 0) return { count: 0 };
|
|
5627
|
-
const top = all.reduce((best, t) =>
|
|
5745
|
+
const top = all.reduce((best, t) => compareRank(t, best, c) < 0 ? t : best);
|
|
5628
5746
|
return { count: all.length, top };
|
|
5629
5747
|
}
|
|
5630
5748
|
var FIXED_RANK = { qa: -2, triage: -1 };
|
|
@@ -5850,6 +5968,7 @@ function blockers(t, i) {
|
|
|
5850
5968
|
function modeCell(t, i) {
|
|
5851
5969
|
return i.workModes && i.workModes.modes.size > 0 ? modeOf(t, i.workModes) : "\u2014";
|
|
5852
5970
|
}
|
|
5971
|
+
var effCell = (t) => `p${effectivePriority(t)}${positionRank(t) === Number.POSITIVE_INFINITY ? "" : ` #${positionRank(t)}`}`;
|
|
5853
5972
|
var sorted = (ts) => [...ts].sort(compareRank);
|
|
5854
5973
|
var stream = (segments) => `${segments.join("\n")}
|
|
5855
5974
|
`;
|
|
@@ -5868,7 +5987,7 @@ function buildingDigest(i) {
|
|
|
5868
5987
|
const worktree = i.worktreeFor ? i.worktreeFor(t) : "";
|
|
5869
5988
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
5870
5989
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
5871
|
-
return `| ${displayKey2(t)} | ${repo} | ${branch} | ${worktree} | ${modeCell(t, i)} | ${t.status} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} |
|
|
5990
|
+
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"} |`;
|
|
5872
5991
|
};
|
|
5873
5992
|
const blockedRow = (t) => `| ${displayKey2(t)} | ${t.status} | ${who(t, i)} | p${effectivePriority(t)} | ${blockers(t, i)} |`;
|
|
5874
5993
|
const blockedTable = (ts) => ts.length === 0 ? "\n_None._\n" : `
|
|
@@ -5929,7 +6048,7 @@ function qaDigest(i) {
|
|
|
5929
6048
|
const n = newFromOthers(i, t.id);
|
|
5930
6049
|
const dir = i.dirFor ? i.dirFor(t) : null;
|
|
5931
6050
|
const repo = i.dirFor ? dir ?? "**NO CHECKOUT**" : "";
|
|
5932
|
-
return `| ${displayKey2(t)} | ${repo} | ${modeCell(t, i)} | ${t.status} | ${builtBy(t)} | ${who(t, i)} | ${filedBy(t, i)} | ${t.severity ?? "\u2014"} | ${t.priority ?? "\u2014"} |
|
|
6051
|
+
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"} |`;
|
|
5933
6052
|
};
|
|
5934
6053
|
const waiting = i.tickets.filter((t) => (t.status === "qa" || t.status === "fixed") && i.blocked.has(t.id));
|
|
5935
6054
|
const takeable = (status2) => i.tickets.filter((t) => t.status === status2 && !i.blocked.has(t.id));
|
|
@@ -6011,7 +6130,7 @@ function materializeBranchForTicket(dir, cfg, t, role) {
|
|
|
6011
6130
|
|
|
6012
6131
|
// src/poll.ts
|
|
6013
6132
|
import { writeFileSync as writeFileSync6 } from "fs";
|
|
6014
|
-
import { join as
|
|
6133
|
+
import { join as join10 } from "path";
|
|
6015
6134
|
|
|
6016
6135
|
// src/stalled.ts
|
|
6017
6136
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -6124,6 +6243,7 @@ async function decideCycle(o) {
|
|
|
6124
6243
|
data: { tickets: tickets.length, comments: comments.length }
|
|
6125
6244
|
});
|
|
6126
6245
|
const epicColumn = tracker.contract.columns.epic;
|
|
6246
|
+
const positionColumn = tracker.contract.columns.position;
|
|
6127
6247
|
const epicBuilding = tracker.contract.epics.statuses.building;
|
|
6128
6248
|
const epicStatusColumn = tracker.contract.epics.statusColumn;
|
|
6129
6249
|
const epicStatusById = new Map(epicRows.map((e) => [e.id, e[epicStatusColumn] ?? null]));
|
|
@@ -6143,6 +6263,9 @@ async function decideCycle(o) {
|
|
|
6143
6263
|
for (const t of tickets) {
|
|
6144
6264
|
const epicId = t[epicColumn];
|
|
6145
6265
|
t.epicInProgress = typeof epicId === "string" && epicStatusById.get(epicId) === epicBuilding;
|
|
6266
|
+
const pos = t[positionColumn];
|
|
6267
|
+
const n = typeof pos === "string" && pos.trim() !== "" ? Number(pos) : pos;
|
|
6268
|
+
t.position = typeof n === "number" && Number.isFinite(n) ? n : null;
|
|
6146
6269
|
}
|
|
6147
6270
|
const extra = await tracker.ticketsByIds(missingBlockerIds(tickets));
|
|
6148
6271
|
const info = blockerInfoMap(tickets, extra);
|
|
@@ -6283,7 +6406,7 @@ async function decideCycle(o) {
|
|
|
6283
6406
|
}
|
|
6284
6407
|
return decision;
|
|
6285
6408
|
}
|
|
6286
|
-
var digestPath = (stateDir, route, role) =>
|
|
6409
|
+
var digestPath = (stateDir, route, role) => join10(stateDir, `digest-${route}-${role}.md`);
|
|
6287
6410
|
function writeDigest(o, d, role, stateDir) {
|
|
6288
6411
|
const me = o.route.resolved?.seats[role];
|
|
6289
6412
|
if (!me) return false;
|
|
@@ -6653,9 +6776,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
6653
6776
|
|
|
6654
6777
|
// src/agent.ts
|
|
6655
6778
|
import { spawn as spawn2 } from "child_process";
|
|
6656
|
-
import { readFileSync as
|
|
6779
|
+
import { readFileSync as readFileSync11, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
|
|
6657
6780
|
import { finished } from "stream/promises";
|
|
6658
|
-
import { join as
|
|
6781
|
+
import { join as join13, dirname as dirname6 } from "path";
|
|
6659
6782
|
import { createHash as createHash2 } from "crypto";
|
|
6660
6783
|
|
|
6661
6784
|
// src/environment.ts
|
|
@@ -6775,6 +6898,7 @@ function renderEnvironment(i) {
|
|
|
6775
6898
|
const only = repos.length === 1 ? repos[0] : void 0;
|
|
6776
6899
|
const models = route.resolved?.models;
|
|
6777
6900
|
const epicsModelId = route.resolved?.epicsModelId;
|
|
6901
|
+
const mediaLibraryModelId = i.attachments?.mediaLibraryModelId ?? null;
|
|
6778
6902
|
const key = "ISSUE-000";
|
|
6779
6903
|
const lines = [
|
|
6780
6904
|
"# Your environment",
|
|
@@ -6799,7 +6923,8 @@ function renderEnvironment(i) {
|
|
|
6799
6923
|
`- Issues: \`${models.issues}\``,
|
|
6800
6924
|
`- Comments: \`${models.comments}\``,
|
|
6801
6925
|
`- Crew: \`${models.crew}\``,
|
|
6802
|
-
...epicsModelId ? [`- Epics: \`${epicsModelId}\``] : []
|
|
6926
|
+
...epicsModelId ? [`- Epics: \`${epicsModelId}\``] : [],
|
|
6927
|
+
...mediaLibraryModelId ? [`- Media Library: \`${mediaLibraryModelId}\` (what a ticket's \`${contract.columns.attachments}\` ids point into)`] : []
|
|
6803
6928
|
);
|
|
6804
6929
|
}
|
|
6805
6930
|
const workspaceId = route.resolved?.workspaceId;
|
|
@@ -6826,6 +6951,30 @@ function renderEnvironment(i) {
|
|
|
6826
6951
|
"status and response body in your comment \u2014 do not write that uploads are",
|
|
6827
6952
|
"impossible."
|
|
6828
6953
|
);
|
|
6954
|
+
if (mediaLibraryModelId) {
|
|
6955
|
+
lines.push(
|
|
6956
|
+
"",
|
|
6957
|
+
"## Opening a ticket's attachments",
|
|
6958
|
+
"",
|
|
6959
|
+
`A ticket's or a comment's \`${contract.columns.attachments}\` value is a list of Media Library`,
|
|
6960
|
+
"**record ids** \u2014 not file ids and not URLs. `files/<id>`, `files/<id>/download` and",
|
|
6961
|
+
"`media-library/<id>/...` do not exist for such an id and return 404. For each id,",
|
|
6962
|
+
"read the record, then fetch the file by its storage key:",
|
|
6963
|
+
"",
|
|
6964
|
+
"```sh",
|
|
6965
|
+
`curl -s -H "Authorization: Bearer $${API_KEY_VAR}" -H "User-Agent: ${i.userAgent}" \\`,
|
|
6966
|
+
` "${base}/api/workspaces/${workspaceId}/data-models/${mediaLibraryModelId}/records/<attachment id>"`,
|
|
6967
|
+
'# \u2192 JSON with `storage_key` ("<uuid>/<filename>") and `mime_type`',
|
|
6968
|
+
`curl -s -o <scratch dir>/<filename> -H "Authorization: Bearer $${API_KEY_VAR}" -H "User-Agent: ${i.userAgent}" \\`,
|
|
6969
|
+
` "${base}/api/workspaces/${workspaceId}/files/<storage_key>"`,
|
|
6970
|
+
"```",
|
|
6971
|
+
"",
|
|
6972
|
+
"The MCP forms are `records_controller_find_one` (`dataModelId` = the Media Library",
|
|
6973
|
+
"id above) and `files_controller_download` (`uuid` and `filename` = the two halves",
|
|
6974
|
+
"of the storage key). Save the file outside the repository and open it; where it",
|
|
6975
|
+
"disagrees with the description, the picture is the reproduction."
|
|
6976
|
+
);
|
|
6977
|
+
}
|
|
6829
6978
|
}
|
|
6830
6979
|
if (i.shipName) {
|
|
6831
6980
|
lines.push(
|
|
@@ -7044,14 +7193,14 @@ function expandSkillMarkers(text, lookup, opts = {}) {
|
|
|
7044
7193
|
}
|
|
7045
7194
|
|
|
7046
7195
|
// src/skills.ts
|
|
7047
|
-
import { readFileSync as
|
|
7048
|
-
import { join as
|
|
7196
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync3, existsSync as existsSync11 } from "fs";
|
|
7197
|
+
import { join as join12 } from "path";
|
|
7049
7198
|
import { parse as parseYaml } from "yaml";
|
|
7050
7199
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
7051
7200
|
|
|
7052
7201
|
// src/agents.ts
|
|
7053
|
-
import { readFileSync as
|
|
7054
|
-
import { join as
|
|
7202
|
+
import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
|
|
7203
|
+
import { join as join11 } from "path";
|
|
7055
7204
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
7056
7205
|
var PERSONA_NAME = {
|
|
7057
7206
|
dev: "Developer",
|
|
@@ -7065,12 +7214,12 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
7065
7214
|
var AgentsSyncError = class extends Error {
|
|
7066
7215
|
};
|
|
7067
7216
|
function personaDefaultPrompt(promptsDir, role) {
|
|
7068
|
-
const briefPath =
|
|
7069
|
-
if (!
|
|
7070
|
-
if (SELF_CONTAINED_ROLES.has(role)) return
|
|
7071
|
-
const commonPath =
|
|
7072
|
-
if (!
|
|
7073
|
-
return `${
|
|
7217
|
+
const briefPath = join11(promptsDir, "personas", `lane-${role}.md`);
|
|
7218
|
+
if (!existsSync10(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
7219
|
+
if (SELF_CONTAINED_ROLES.has(role)) return readFileSync9(briefPath, "utf8");
|
|
7220
|
+
const commonPath = join11(promptsDir, "personas", "common.md");
|
|
7221
|
+
if (!existsSync10(commonPath)) return readFileSync9(briefPath, "utf8");
|
|
7222
|
+
return `${readFileSync9(commonPath, "utf8")}${readFileSync9(briefPath, "utf8")}`;
|
|
7074
7223
|
}
|
|
7075
7224
|
async function resolveAgentId(route, role, opts) {
|
|
7076
7225
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -7394,7 +7543,7 @@ function describeCrewLink(o) {
|
|
|
7394
7543
|
var SkillSyncError = class extends Error {
|
|
7395
7544
|
};
|
|
7396
7545
|
function parseSkillFile(path) {
|
|
7397
|
-
const text =
|
|
7546
|
+
const text = readFileSync10(path, "utf8");
|
|
7398
7547
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
7399
7548
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
7400
7549
|
const frontmatter = match[1];
|
|
@@ -7407,9 +7556,9 @@ function parseSkillFile(path) {
|
|
|
7407
7556
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
7408
7557
|
}
|
|
7409
7558
|
function listLocalSkills(promptsDir) {
|
|
7410
|
-
const dir =
|
|
7411
|
-
if (!
|
|
7412
|
-
return
|
|
7559
|
+
const dir = join12(promptsDir, "skills");
|
|
7560
|
+
if (!existsSync11(dir)) return [];
|
|
7561
|
+
return readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join12(dir, f)));
|
|
7413
7562
|
}
|
|
7414
7563
|
async function syncSkills(route, opts) {
|
|
7415
7564
|
if (!route.resolved) {
|
|
@@ -7601,23 +7750,23 @@ var AgentError = class extends Error {
|
|
|
7601
7750
|
var AgentSpawnError = class extends AgentError {
|
|
7602
7751
|
};
|
|
7603
7752
|
function planAgentRun(o) {
|
|
7604
|
-
const briefPath =
|
|
7605
|
-
if (!
|
|
7753
|
+
const briefPath = join13(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
7754
|
+
if (!existsSync12(briefPath)) {
|
|
7606
7755
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
7607
7756
|
}
|
|
7608
|
-
const commonPath =
|
|
7609
|
-
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) &&
|
|
7757
|
+
const commonPath = join13(o.route.promptsDir, "personas", "common.md");
|
|
7758
|
+
const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync12(commonPath);
|
|
7610
7759
|
const now = o.now ?? (() => Date.now());
|
|
7611
7760
|
const routeLabel = routeSlug(o.route.route);
|
|
7612
|
-
const digestPath2 =
|
|
7761
|
+
const digestPath2 = join13(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
7613
7762
|
let digest;
|
|
7614
7763
|
let ageSeconds;
|
|
7615
|
-
if (
|
|
7764
|
+
if (existsSync12(digestPath2)) {
|
|
7616
7765
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
7617
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
7766
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync11(digestPath2, "utf8");
|
|
7618
7767
|
}
|
|
7619
|
-
const common = usesCommon ?
|
|
7620
|
-
const brief =
|
|
7768
|
+
const common = usesCommon ? readFileSync11(commonPath, "utf8") : "";
|
|
7769
|
+
const brief = readFileSync11(briefPath, "utf8");
|
|
7621
7770
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
7622
7771
|
const effectiveBrief = o.divergedPrompt ?? brief;
|
|
7623
7772
|
const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
|
|
@@ -7639,7 +7788,7 @@ function planAgentRun(o) {
|
|
|
7639
7788
|
digest
|
|
7640
7789
|
});
|
|
7641
7790
|
const promptSha = createHash2("sha256").update(expanded.text).digest("hex");
|
|
7642
|
-
const streamsDir =
|
|
7791
|
+
const streamsDir = join13(o.stateDir, "streams");
|
|
7643
7792
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
7644
7793
|
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
7645
7794
|
const { toolchain, model } = seat;
|
|
@@ -7695,8 +7844,8 @@ function planAgentRun(o) {
|
|
|
7695
7844
|
digestAttached: digest !== void 0,
|
|
7696
7845
|
digestAgeSeconds: ageSeconds,
|
|
7697
7846
|
model,
|
|
7698
|
-
streamPath:
|
|
7699
|
-
eventsPath:
|
|
7847
|
+
streamPath: join13(streamsDir, `${base}.jsonl`),
|
|
7848
|
+
eventsPath: join13(streamsDir, `${base}.events.jsonl`),
|
|
7700
7849
|
ticket: o.ticket,
|
|
7701
7850
|
agentLog,
|
|
7702
7851
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
@@ -7706,7 +7855,7 @@ function planAgentRun(o) {
|
|
|
7706
7855
|
function describePlan(p) {
|
|
7707
7856
|
return [
|
|
7708
7857
|
`role: ${p.role}`,
|
|
7709
|
-
...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}`] : [],
|
|
7858
|
+
...p.resolution ? [`resolved: toolchain ${p.toolchain} \u2190 ${p.resolution.toolchain}; model ${p.model} \u2190 ${p.resolution.model}${p.provider ? `; provider ${p.provider}` : ""}`] : [],
|
|
7710
7859
|
...(p.warnings ?? []).map((w) => `warning: ${w}`),
|
|
7711
7860
|
// Only for a non-default toolchain, so a plain Claude run's output is unchanged.
|
|
7712
7861
|
...p.toolchain && p.toolchain !== "claude" ? [`toolchain: ${p.toolchain} (${p.harness ?? "claude"})`] : [],
|
|
@@ -7902,13 +8051,13 @@ function spawnAgent(plan, emit) {
|
|
|
7902
8051
|
}
|
|
7903
8052
|
|
|
7904
8053
|
// src/run-worktree.ts
|
|
7905
|
-
import { existsSync as
|
|
7906
|
-
import { dirname as dirname7, join as
|
|
8054
|
+
import { existsSync as existsSync14 } from "fs";
|
|
8055
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
7907
8056
|
|
|
7908
8057
|
// src/ports.ts
|
|
7909
8058
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
7910
|
-
import { existsSync as
|
|
7911
|
-
import { join as
|
|
8059
|
+
import { existsSync as existsSync13 } from "fs";
|
|
8060
|
+
import { join as join14 } from "path";
|
|
7912
8061
|
var run = (cmd, args) => {
|
|
7913
8062
|
try {
|
|
7914
8063
|
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -7953,7 +8102,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
7953
8102
|
}
|
|
7954
8103
|
function worktreeExists(parent, prefix, n) {
|
|
7955
8104
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
7956
|
-
if (
|
|
8105
|
+
if (existsSync13(join14(parent, name, ".git"))) return true;
|
|
7957
8106
|
}
|
|
7958
8107
|
return false;
|
|
7959
8108
|
}
|
|
@@ -8027,7 +8176,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
8027
8176
|
const existing = worktreeForNumber(dir, num);
|
|
8028
8177
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
8029
8178
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
8030
|
-
if (existing && !existing.prunable &&
|
|
8179
|
+
if (existing && !existing.prunable && existsSync14(existing.path)) {
|
|
8031
8180
|
const branch = existing.branch;
|
|
8032
8181
|
if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
|
|
8033
8182
|
if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
|
|
@@ -8041,7 +8190,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
8041
8190
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
8042
8191
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
8043
8192
|
}
|
|
8044
|
-
const path =
|
|
8193
|
+
const path = join15(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
|
|
8045
8194
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
8046
8195
|
return { action: "cut-remote", path, branch: located.branch };
|
|
8047
8196
|
}
|
|
@@ -8088,7 +8237,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
8088
8237
|
}
|
|
8089
8238
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
8090
8239
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
8091
|
-
if (!wt || !
|
|
8240
|
+
if (!wt || !existsSync14(wt.path)) return { kind: "none" };
|
|
8092
8241
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
8093
8242
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
8094
8243
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -8100,7 +8249,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
8100
8249
|
if (pids.length) await killGently(pids);
|
|
8101
8250
|
}
|
|
8102
8251
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
8103
|
-
return
|
|
8252
|
+
return existsSync14(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
8104
8253
|
}
|
|
8105
8254
|
|
|
8106
8255
|
// src/handoff-guard.ts
|
|
@@ -8249,13 +8398,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
8249
8398
|
}
|
|
8250
8399
|
|
|
8251
8400
|
// src/connect-wizard.ts
|
|
8252
|
-
import { delimiter, dirname as dirname8, join as
|
|
8253
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
8401
|
+
import { delimiter, dirname as dirname8, join as join16 } from "path";
|
|
8402
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
8254
8403
|
import { stringify } from "yaml";
|
|
8255
8404
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
8256
8405
|
function shouldRunWizard(opts) {
|
|
8257
8406
|
if (!opts.isTTY) return false;
|
|
8258
|
-
const exists = opts.configExists ??
|
|
8407
|
+
const exists = opts.configExists ?? existsSync15;
|
|
8259
8408
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
8260
8409
|
}
|
|
8261
8410
|
function firstRunConfigPath() {
|
|
@@ -8272,7 +8421,7 @@ function defaultIsExecutable(p) {
|
|
|
8272
8421
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
8273
8422
|
for (const dir of pathEnv.split(delimiter)) {
|
|
8274
8423
|
if (!dir) continue;
|
|
8275
|
-
const candidate =
|
|
8424
|
+
const candidate = join16(dir, name);
|
|
8276
8425
|
if (isExecutable(candidate)) return candidate;
|
|
8277
8426
|
}
|
|
8278
8427
|
return void 0;
|
|
@@ -8367,8 +8516,8 @@ Wrote ${o.configPath}.
|
|
|
8367
8516
|
}
|
|
8368
8517
|
|
|
8369
8518
|
// src/release-run.ts
|
|
8370
|
-
import { readFileSync as
|
|
8371
|
-
import { dirname as dirname9, join as
|
|
8519
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync16, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
|
|
8520
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
8372
8521
|
import { tmpdir as tmpdir2 } from "os";
|
|
8373
8522
|
|
|
8374
8523
|
// src/release.ts
|
|
@@ -9026,13 +9175,13 @@ async function bump(o, size) {
|
|
|
9026
9175
|
}
|
|
9027
9176
|
return printed;
|
|
9028
9177
|
}
|
|
9029
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
9178
|
+
const files = o.repo.release.versionFiles.map((f) => join17(o.cwd, f));
|
|
9030
9179
|
const first = files[0];
|
|
9031
|
-
if (!
|
|
9180
|
+
if (!existsSync16(first)) {
|
|
9032
9181
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
9033
9182
|
return null;
|
|
9034
9183
|
}
|
|
9035
|
-
const current = JSON.parse(
|
|
9184
|
+
const current = JSON.parse(readFileSync12(first, "utf8")).version;
|
|
9036
9185
|
if (!current) {
|
|
9037
9186
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
9038
9187
|
return null;
|
|
@@ -9045,7 +9194,7 @@ async function bump(o, size) {
|
|
|
9045
9194
|
return version;
|
|
9046
9195
|
}
|
|
9047
9196
|
for (const f of files) {
|
|
9048
|
-
const src =
|
|
9197
|
+
const src = readFileSync12(f, "utf8");
|
|
9049
9198
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
9050
9199
|
if (out === src) {
|
|
9051
9200
|
o.emit.error(`no version field in ${f}`);
|
|
@@ -9058,13 +9207,13 @@ async function bump(o, size) {
|
|
|
9058
9207
|
function writeChangelog(o, version, entries) {
|
|
9059
9208
|
const file = o.repo.release.changelog;
|
|
9060
9209
|
if (!file || entries.length === 0) return;
|
|
9061
|
-
const path =
|
|
9210
|
+
const path = join17(o.cwd, file);
|
|
9062
9211
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
9063
9212
|
if (o.dryRun) {
|
|
9064
9213
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
9065
9214
|
return;
|
|
9066
9215
|
}
|
|
9067
|
-
const existing =
|
|
9216
|
+
const existing = existsSync16(path) ? readFileSync12(path, "utf8") : "# Changelog\n";
|
|
9068
9217
|
writeFileSync9(path, insertChangelogSection(existing, section));
|
|
9069
9218
|
}
|
|
9070
9219
|
async function confirm(o, expected) {
|
|
@@ -9146,8 +9295,8 @@ async function confirmLastRelease(o, decision) {
|
|
|
9146
9295
|
function openReleaseCheckout(o) {
|
|
9147
9296
|
const { remote, base } = o.repo.branch;
|
|
9148
9297
|
const branch = `crew/release-${Date.now()}`;
|
|
9149
|
-
const root = mkdtempSync(
|
|
9150
|
-
const dir =
|
|
9298
|
+
const root = mkdtempSync(join17(tmpdir2(), "crew-release-"));
|
|
9299
|
+
const dir = join17(root, "checkout");
|
|
9151
9300
|
const close = () => {
|
|
9152
9301
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
9153
9302
|
rmSync6(root, { recursive: true, force: true });
|
|
@@ -9161,10 +9310,10 @@ function openReleaseCheckout(o) {
|
|
|
9161
9310
|
throw e;
|
|
9162
9311
|
}
|
|
9163
9312
|
for (const f of o.repo.worktrees.copy) {
|
|
9164
|
-
const src =
|
|
9165
|
-
if (!
|
|
9166
|
-
mkdirSync8(dirname9(
|
|
9167
|
-
copyFileSync(src,
|
|
9313
|
+
const src = join17(o.cwd, f);
|
|
9314
|
+
if (!existsSync16(src)) continue;
|
|
9315
|
+
mkdirSync8(dirname9(join17(dir, f)), { recursive: true });
|
|
9316
|
+
copyFileSync(src, join17(dir, f));
|
|
9168
9317
|
}
|
|
9169
9318
|
return { dir, branch, close };
|
|
9170
9319
|
}
|
|
@@ -9633,7 +9782,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
9633
9782
|
}
|
|
9634
9783
|
|
|
9635
9784
|
// src/watch.ts
|
|
9636
|
-
import { watch as watchFile, existsSync as
|
|
9785
|
+
import { watch as watchFile, existsSync as existsSync17, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
|
|
9637
9786
|
var E = "\x1B[";
|
|
9638
9787
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
9639
9788
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -9734,7 +9883,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
9734
9883
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
9735
9884
|
}
|
|
9736
9885
|
function readFrom(file, from) {
|
|
9737
|
-
if (!
|
|
9886
|
+
if (!existsSync17(file)) return { events: [], offset: from };
|
|
9738
9887
|
const size = statSync6(file).size;
|
|
9739
9888
|
if (size < from) return readFrom(file, 0);
|
|
9740
9889
|
if (size === from) return { events: [], offset: from };
|
|
@@ -10445,7 +10594,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
10445
10594
|
}
|
|
10446
10595
|
|
|
10447
10596
|
// src/repos-cmd.ts
|
|
10448
|
-
import { homedir as
|
|
10597
|
+
import { homedir as homedir7 } from "os";
|
|
10449
10598
|
import { basename as basename7, resolve as resolve3 } from "path";
|
|
10450
10599
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
10451
10600
|
var ReposError = class extends Error {
|
|
@@ -10492,7 +10641,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
10492
10641
|
return { route, path: "." };
|
|
10493
10642
|
}
|
|
10494
10643
|
function expandDir(p, base) {
|
|
10495
|
-
const home = p === "~" ?
|
|
10644
|
+
const home = p === "~" ? homedir7() : p.startsWith("~/") ? `${homedir7()}/${p.slice(2)}` : p;
|
|
10496
10645
|
return resolve3(base, home);
|
|
10497
10646
|
}
|
|
10498
10647
|
function routeMapOf(doc, route) {
|
|
@@ -10627,7 +10776,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
10627
10776
|
}
|
|
10628
10777
|
|
|
10629
10778
|
// src/worktree-sweep.ts
|
|
10630
|
-
import { existsSync as
|
|
10779
|
+
import { existsSync as existsSync18 } from "fs";
|
|
10631
10780
|
import { basename as basename8 } from "path";
|
|
10632
10781
|
function planWorktreeSweep(target, tickets, contract) {
|
|
10633
10782
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -10655,7 +10804,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
10655
10804
|
let removed = 0;
|
|
10656
10805
|
let keptBranches = 0;
|
|
10657
10806
|
for (const a of actions) {
|
|
10658
|
-
if (!dryRun &&
|
|
10807
|
+
if (!dryRun && existsSync18(a.path)) {
|
|
10659
10808
|
let dirty = false;
|
|
10660
10809
|
try {
|
|
10661
10810
|
dirty = status(a.path).length > 0;
|
|
@@ -10669,7 +10818,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
10669
10818
|
continue;
|
|
10670
10819
|
}
|
|
10671
10820
|
}
|
|
10672
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
10821
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync18(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
10673
10822
|
log.warn(
|
|
10674
10823
|
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
10675
10824
|
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
@@ -10750,14 +10899,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
10750
10899
|
}
|
|
10751
10900
|
|
|
10752
10901
|
// src/stream-sweep.ts
|
|
10753
|
-
import { existsSync as
|
|
10754
|
-
import { join as
|
|
10902
|
+
import { existsSync as existsSync19, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync7 } from "fs";
|
|
10903
|
+
import { join as join18 } from "path";
|
|
10755
10904
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
10756
10905
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
10757
|
-
const dir =
|
|
10758
|
-
if (!
|
|
10906
|
+
const dir = join18(stateDir, "streams");
|
|
10907
|
+
if (!existsSync19(dir)) return [];
|
|
10759
10908
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
10760
|
-
return
|
|
10909
|
+
return readdirSync4(dir).map((name) => join18(dir, name)).filter((path) => {
|
|
10761
10910
|
try {
|
|
10762
10911
|
return statSync7(path).mtimeMs < cutoff;
|
|
10763
10912
|
} catch {
|
|
@@ -10783,9 +10932,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
10783
10932
|
return removed;
|
|
10784
10933
|
}
|
|
10785
10934
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
10786
|
-
if (!
|
|
10935
|
+
if (!existsSync19(stateDir)) return [];
|
|
10787
10936
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
10788
|
-
return
|
|
10937
|
+
return readdirSync4(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join18(stateDir, name)).filter((path) => {
|
|
10789
10938
|
try {
|
|
10790
10939
|
return statSync7(path).mtimeMs < cutoff;
|
|
10791
10940
|
} catch {
|
|
@@ -10795,23 +10944,23 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
10795
10944
|
}
|
|
10796
10945
|
|
|
10797
10946
|
// src/cli.ts
|
|
10798
|
-
import { readFileSync as
|
|
10947
|
+
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";
|
|
10799
10948
|
import { createInterface } from "readline/promises";
|
|
10800
10949
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
10801
10950
|
|
|
10802
10951
|
// src/ssh-keys.ts
|
|
10803
10952
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
10804
|
-
import { existsSync as
|
|
10805
|
-
import { dirname as dirname11, join as
|
|
10953
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync13, chmodSync } from "fs";
|
|
10954
|
+
import { dirname as dirname11, join as join19 } from "path";
|
|
10806
10955
|
var SshKeyError = class extends Error {
|
|
10807
10956
|
};
|
|
10808
10957
|
function shipSshKeyPathFor(stateDir) {
|
|
10809
|
-
return
|
|
10958
|
+
return join19(stateDir, "ssh", "id_ed25519");
|
|
10810
10959
|
}
|
|
10811
10960
|
function ensureShipSshKeypair(stateDir) {
|
|
10812
10961
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
10813
10962
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
10814
|
-
if (!
|
|
10963
|
+
if (!existsSync20(privateKeyPath)) {
|
|
10815
10964
|
mkdirSync9(dirname11(privateKeyPath), { recursive: true });
|
|
10816
10965
|
try {
|
|
10817
10966
|
execFileSync3("ssh-keygen", [
|
|
@@ -10832,10 +10981,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
10832
10981
|
}
|
|
10833
10982
|
chmodSync(privateKeyPath, 384);
|
|
10834
10983
|
}
|
|
10835
|
-
if (!
|
|
10984
|
+
if (!existsSync20(publicKeyPath)) {
|
|
10836
10985
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
10837
10986
|
}
|
|
10838
|
-
const publicKey =
|
|
10987
|
+
const publicKey = readFileSync13(publicKeyPath, "utf8").trim();
|
|
10839
10988
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
10840
10989
|
}
|
|
10841
10990
|
function sshKeygenAvailable() {
|
|
@@ -11049,9 +11198,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
11049
11198
|
|
|
11050
11199
|
// src/tunnel.ts
|
|
11051
11200
|
import { spawn as spawn3 } from "child_process";
|
|
11052
|
-
import { existsSync as
|
|
11201
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync5 } from "fs";
|
|
11053
11202
|
import { randomInt } from "crypto";
|
|
11054
|
-
import { join as
|
|
11203
|
+
import { join as join20 } from "path";
|
|
11055
11204
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
11056
11205
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
11057
11206
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -11118,20 +11267,20 @@ function startTunnel(opts) {
|
|
|
11118
11267
|
};
|
|
11119
11268
|
}
|
|
11120
11269
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
11121
|
-
return
|
|
11270
|
+
return join20(stateDir, "passengers", `${workspaceId}.json`);
|
|
11122
11271
|
}
|
|
11123
11272
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
11124
11273
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
11125
|
-
if (!
|
|
11274
|
+
if (!existsSync21(path)) return void 0;
|
|
11126
11275
|
try {
|
|
11127
|
-
return JSON.parse(
|
|
11276
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
11128
11277
|
} catch {
|
|
11129
11278
|
return void 0;
|
|
11130
11279
|
}
|
|
11131
11280
|
}
|
|
11132
11281
|
function writePersistedTunnel(stateDir, state) {
|
|
11133
11282
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
11134
|
-
mkdirSync10(
|
|
11283
|
+
mkdirSync10(join20(stateDir, "passengers"), { recursive: true });
|
|
11135
11284
|
writeFileSync10(path, `${JSON.stringify(state, null, 2)}
|
|
11136
11285
|
`);
|
|
11137
11286
|
}
|
|
@@ -11229,9 +11378,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
11229
11378
|
}
|
|
11230
11379
|
}
|
|
11231
11380
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
11232
|
-
const dir =
|
|
11233
|
-
if (!
|
|
11234
|
-
return
|
|
11381
|
+
const dir = join20(stateDir, "passengers");
|
|
11382
|
+
if (!existsSync21(dir)) return [];
|
|
11383
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
11235
11384
|
}
|
|
11236
11385
|
|
|
11237
11386
|
// src/mcp-endpoint-registry.ts
|
|
@@ -11351,8 +11500,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
11351
11500
|
import { createServer as createHttpServer } from "http";
|
|
11352
11501
|
import { spawn as spawn4 } from "child_process";
|
|
11353
11502
|
import { randomBytes } from "crypto";
|
|
11354
|
-
import { existsSync as
|
|
11355
|
-
import { join as
|
|
11503
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync6 } from "fs";
|
|
11504
|
+
import { join as join21 } from "path";
|
|
11356
11505
|
|
|
11357
11506
|
// src/secret-compare.ts
|
|
11358
11507
|
import { timingSafeEqual } from "crypto";
|
|
@@ -11441,20 +11590,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
11441
11590
|
return child;
|
|
11442
11591
|
};
|
|
11443
11592
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
11444
|
-
return
|
|
11593
|
+
return join21(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
11445
11594
|
}
|
|
11446
11595
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
11447
11596
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
11448
|
-
if (!
|
|
11597
|
+
if (!existsSync22(path)) return void 0;
|
|
11449
11598
|
try {
|
|
11450
|
-
return JSON.parse(
|
|
11599
|
+
return JSON.parse(readFileSync15(path, "utf8"));
|
|
11451
11600
|
} catch {
|
|
11452
11601
|
return void 0;
|
|
11453
11602
|
}
|
|
11454
11603
|
}
|
|
11455
11604
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
11456
11605
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
11457
|
-
mkdirSync11(
|
|
11606
|
+
mkdirSync11(join21(stateDir, "passengers"), { recursive: true });
|
|
11458
11607
|
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
11459
11608
|
`);
|
|
11460
11609
|
}
|
|
@@ -11475,10 +11624,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
11475
11624
|
}
|
|
11476
11625
|
}
|
|
11477
11626
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
11478
|
-
const dir =
|
|
11479
|
-
if (!
|
|
11627
|
+
const dir = join21(stateDir, "passengers");
|
|
11628
|
+
if (!existsSync22(dir)) return [];
|
|
11480
11629
|
const suffix = "-sync.json";
|
|
11481
|
-
return
|
|
11630
|
+
return readdirSync6(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
11482
11631
|
}
|
|
11483
11632
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
11484
11633
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -11533,8 +11682,8 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
11533
11682
|
}
|
|
11534
11683
|
|
|
11535
11684
|
// src/daemon.ts
|
|
11536
|
-
import { existsSync as
|
|
11537
|
-
import { join as
|
|
11685
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync16, readdirSync as readdirSync7, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
|
|
11686
|
+
import { join as join22 } from "path";
|
|
11538
11687
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
11539
11688
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
11540
11689
|
var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
|
|
@@ -11572,8 +11721,8 @@ function latestMtime(path) {
|
|
|
11572
11721
|
if (stat.isFile()) return stat.mtimeMs;
|
|
11573
11722
|
if (!stat.isDirectory()) return 0;
|
|
11574
11723
|
let latest = stat.mtimeMs;
|
|
11575
|
-
for (const entry of
|
|
11576
|
-
latest = Math.max(latest, latestMtime(
|
|
11724
|
+
for (const entry of readdirSync7(path, { withFileTypes: true })) {
|
|
11725
|
+
latest = Math.max(latest, latestMtime(join22(path, entry.name)));
|
|
11577
11726
|
}
|
|
11578
11727
|
return latest;
|
|
11579
11728
|
}
|
|
@@ -11582,13 +11731,13 @@ function computeTreeSignature(paths) {
|
|
|
11582
11731
|
}
|
|
11583
11732
|
function makeStaleChecker(paths, opts = {}) {
|
|
11584
11733
|
const initial = computeTreeSignature(paths);
|
|
11585
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
11734
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync23(opts.reloadSentinel);
|
|
11586
11735
|
}
|
|
11587
11736
|
function reloadSentinelFor(stateDir) {
|
|
11588
|
-
return
|
|
11737
|
+
return join22(stateDir, "reload-requested");
|
|
11589
11738
|
}
|
|
11590
11739
|
function daemonStampFor(stateDir) {
|
|
11591
|
-
return
|
|
11740
|
+
return join22(stateDir, "daemon.started");
|
|
11592
11741
|
}
|
|
11593
11742
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
11594
11743
|
mkdirSync12(stateDir, { recursive: true });
|
|
@@ -11598,7 +11747,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
11598
11747
|
}
|
|
11599
11748
|
function readDaemonStarted(stateDir) {
|
|
11600
11749
|
try {
|
|
11601
|
-
const v = JSON.parse(
|
|
11750
|
+
const v = JSON.parse(readFileSync16(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
11602
11751
|
return typeof v === "number" ? v : null;
|
|
11603
11752
|
} catch {
|
|
11604
11753
|
return null;
|
|
@@ -11888,7 +12037,8 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
11888
12037
|
};
|
|
11889
12038
|
});
|
|
11890
12039
|
const authorship = await tracker.authorshipProbe().catch(() => null);
|
|
11891
|
-
|
|
12040
|
+
const attachments = await tracker.attachmentsProbe().catch(() => null);
|
|
12041
|
+
return renderEnvironment({ route, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name });
|
|
11892
12042
|
}
|
|
11893
12043
|
|
|
11894
12044
|
// src/cli.ts
|
|
@@ -12188,8 +12338,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
12188
12338
|
const FLEET_CAPABLE = /* @__PURE__ */ new Set(["poll", "run", "passengers"]);
|
|
12189
12339
|
const releaseFleetWide = ["merge", "deploy", "release"].includes(command) && flag("fleet");
|
|
12190
12340
|
const DAEMON_SUBCOMMANDS = /* @__PURE__ */ new Set(["start", "stop", "status", "restart"]);
|
|
12191
|
-
const
|
|
12192
|
-
const
|
|
12341
|
+
const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
|
|
12342
|
+
const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
|
|
12343
|
+
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;
|
|
12193
12344
|
let route;
|
|
12194
12345
|
try {
|
|
12195
12346
|
route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
|
|
@@ -12395,8 +12546,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
12395
12546
|
}
|
|
12396
12547
|
if (host === "linux" && hasSystemd) {
|
|
12397
12548
|
const label = labelFor(CREW_HOME, "run");
|
|
12398
|
-
const staleTimer =
|
|
12399
|
-
if (
|
|
12549
|
+
const staleTimer = join23(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
12550
|
+
if (existsSync24(staleTimer)) {
|
|
12400
12551
|
process.stdout.write(
|
|
12401
12552
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
12402
12553
|
`
|
|
@@ -12422,7 +12573,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12422
12573
|
const tracker = new Tracker(c, cfg.ship);
|
|
12423
12574
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
12424
12575
|
for (const r of await resolvedRepos(c)) {
|
|
12425
|
-
if (!
|
|
12576
|
+
if (!existsSync24(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
12426
12577
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
12427
12578
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
12428
12579
|
}
|
|
@@ -12440,7 +12591,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12440
12591
|
}
|
|
12441
12592
|
async function doctorFix(o) {
|
|
12442
12593
|
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))));
|
|
12443
|
-
const unitExists = (p) =>
|
|
12594
|
+
const unitExists = (p) => existsSync24(p);
|
|
12444
12595
|
const crontab = o.host === "linux" ? spawnSync5("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
12445
12596
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
12446
12597
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -12457,7 +12608,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12457
12608
|
for (const a of plan.actions) {
|
|
12458
12609
|
if (a.kind === "enable") {
|
|
12459
12610
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
12460
|
-
writeFileSync13(cfg.configFile, planEnable(
|
|
12611
|
+
writeFileSync13(cfg.configFile, planEnable(readFileSync17(cfg.configFile, "utf8"), a.route));
|
|
12461
12612
|
enabledSound.add(a.route);
|
|
12462
12613
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
12463
12614
|
} else {
|
|
@@ -12520,6 +12671,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12520
12671
|
contract,
|
|
12521
12672
|
sourceTicket: ticket,
|
|
12522
12673
|
authorship: await tracker.authorshipProbe().catch(() => null),
|
|
12674
|
+
attachments: await tracker.attachmentsProbe().catch(() => null),
|
|
12523
12675
|
shipName: cfg.ship.name
|
|
12524
12676
|
});
|
|
12525
12677
|
}
|
|
@@ -13364,7 +13516,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13364
13516
|
process.exit(1);
|
|
13365
13517
|
}
|
|
13366
13518
|
requireArmed("run the daemon loop");
|
|
13367
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
13519
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join23(CREW_HOME, "src"), join23(CREW_HOME, "dist"), join23(CREW_HOME, "bin")];
|
|
13368
13520
|
const configPaths = daemonConfigPaths(cfg);
|
|
13369
13521
|
watchPaths.push(...configPaths);
|
|
13370
13522
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -13523,7 +13675,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13523
13675
|
`);
|
|
13524
13676
|
if (!dryRun) {
|
|
13525
13677
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13526
|
-
const raw =
|
|
13678
|
+
const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
|
|
13527
13679
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
13528
13680
|
raw.agentSkills = skillResult.agentSkills;
|
|
13529
13681
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
@@ -13574,7 +13726,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13574
13726
|
`);
|
|
13575
13727
|
if (!dryRun) {
|
|
13576
13728
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13577
|
-
const raw =
|
|
13729
|
+
const raw = existsSync24(resolvedPath) ? JSON.parse(readFileSync17(resolvedPath, "utf8")) : {};
|
|
13578
13730
|
raw.agentSkills = result.agentSkills;
|
|
13579
13731
|
mkdirSync13(dirOf(resolvedPath), { recursive: true });
|
|
13580
13732
|
writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -13590,14 +13742,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13590
13742
|
process.exit(2);
|
|
13591
13743
|
};
|
|
13592
13744
|
if (sub !== "add" && sub !== "list") usage2();
|
|
13593
|
-
const configText =
|
|
13745
|
+
const configText = readFileSync17(cfg.configFile, "utf8");
|
|
13594
13746
|
let routeName = positional[2];
|
|
13595
13747
|
let addPath = ".";
|
|
13596
13748
|
let addName;
|
|
13597
13749
|
if (sub === "list" && !routeName) usage2();
|
|
13598
13750
|
if (sub === "add") {
|
|
13599
13751
|
try {
|
|
13600
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
13752
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync24(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
13601
13753
|
routeName = a.route;
|
|
13602
13754
|
addPath = a.path;
|
|
13603
13755
|
addName = a.name;
|
|
@@ -13612,9 +13764,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13612
13764
|
}
|
|
13613
13765
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
13614
13766
|
let resolvedRemotes;
|
|
13615
|
-
if (
|
|
13767
|
+
if (existsSync24(resolvedFile)) {
|
|
13616
13768
|
try {
|
|
13617
|
-
resolvedRemotes = JSON.parse(
|
|
13769
|
+
resolvedRemotes = JSON.parse(readFileSync17(resolvedFile, "utf8"));
|
|
13618
13770
|
} catch {
|
|
13619
13771
|
}
|
|
13620
13772
|
}
|
|
@@ -13629,14 +13781,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13629
13781
|
for (const r of repos) {
|
|
13630
13782
|
const origin2 = originOf(r.dir);
|
|
13631
13783
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
13632
|
-
const note = !
|
|
13784
|
+
const note = !existsSync24(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
13633
13785
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
13634
13786
|
`);
|
|
13635
13787
|
}
|
|
13636
13788
|
break;
|
|
13637
13789
|
}
|
|
13638
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
13639
|
-
const top =
|
|
13790
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
13791
|
+
const top = existsSync24(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
13640
13792
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
13641
13793
|
const origin = originOf(top);
|
|
13642
13794
|
const plan = planRepoAdd({
|
|
@@ -13685,8 +13837,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13685
13837
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
13686
13838
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
13687
13839
|
`);
|
|
13688
|
-
if (
|
|
13689
|
-
const raw = JSON.parse(
|
|
13840
|
+
if (existsSync24(resolvedFile)) {
|
|
13841
|
+
const raw = JSON.parse(readFileSync17(resolvedFile, "utf8"));
|
|
13690
13842
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
13691
13843
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
13692
13844
|
writeFileSync13(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -13932,10 +14084,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13932
14084
|
operator = await pickOperator(found.holds);
|
|
13933
14085
|
}
|
|
13934
14086
|
let previous;
|
|
13935
|
-
const hadPreviousFile =
|
|
14087
|
+
const hadPreviousFile = existsSync24(resolvedPath);
|
|
13936
14088
|
if (hadPreviousFile) {
|
|
13937
14089
|
try {
|
|
13938
|
-
previous = JSON.parse(
|
|
14090
|
+
previous = JSON.parse(readFileSync17(resolvedPath, "utf8"));
|
|
13939
14091
|
} catch {
|
|
13940
14092
|
}
|
|
13941
14093
|
}
|
|
@@ -14023,7 +14175,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14023
14175
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
14024
14176
|
const wizard = firstRun;
|
|
14025
14177
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
14026
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
14178
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join23(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
14027
14179
|
const routeBlock = renderConnection(
|
|
14028
14180
|
found,
|
|
14029
14181
|
route2,
|
|
@@ -14364,21 +14516,36 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14364
14516
|
}
|
|
14365
14517
|
case "worktree": {
|
|
14366
14518
|
emit.enter("worktree");
|
|
14367
|
-
const rawWt =
|
|
14519
|
+
const rawWt = worktreeArgs?.ticket;
|
|
14368
14520
|
const nWt = rawWt ? parseTicketNumber(rawWt) : null;
|
|
14369
14521
|
if (!nWt) {
|
|
14370
14522
|
process.stderr.write("worktree: need a ticket number (326, ISSUE-326, or TABL-326)\n");
|
|
14371
14523
|
process.exit(2);
|
|
14372
14524
|
}
|
|
14373
|
-
const
|
|
14374
|
-
|
|
14375
|
-
const
|
|
14376
|
-
|
|
14377
|
-
|
|
14525
|
+
const candidatesWt = !named && cfg.routes.length > 1 ? cfg.routes.filter((r) => r.enabled) : [route];
|
|
14526
|
+
let ticketWt;
|
|
14527
|
+
const lookedWt = [];
|
|
14528
|
+
const hitsWt = /* @__PURE__ */ new Map();
|
|
14529
|
+
for (const cand of candidatesWt) {
|
|
14530
|
+
const tr = new Tracker(cand, cfg.ship);
|
|
14531
|
+
const hit = (await tr.openTickets()).find((t) => t[tr.contract.columns.key] === `ISSUE-${nWt}`);
|
|
14532
|
+
if (hit) hitsWt.set(cand.route, hit);
|
|
14533
|
+
lookedWt.push({
|
|
14534
|
+
route: cand.route,
|
|
14535
|
+
hit: hit ? { repoId: hit.repo_id ?? null } : void 0,
|
|
14536
|
+
servedRepoIds: hit ? (await resolvedRepos(cand)).flatMap((r) => r.id ? [r.id] : []) : []
|
|
14537
|
+
});
|
|
14538
|
+
}
|
|
14539
|
+
const pickedWt = pickWorktreeRoute(Number(nWt), lookedWt);
|
|
14540
|
+
if ("error" in pickedWt) {
|
|
14541
|
+
process.stderr.write(`worktree: ${pickedWt.error}
|
|
14378
14542
|
`);
|
|
14379
14543
|
process.exit(2);
|
|
14380
14544
|
}
|
|
14381
|
-
const
|
|
14545
|
+
const routeWt = candidatesWt.find((r) => r.route === pickedWt.route) ?? route;
|
|
14546
|
+
ticketWt = hitsWt.get(pickedWt.route);
|
|
14547
|
+
if (!ticketWt) process.exit(2);
|
|
14548
|
+
const repoWt = (await resolvedRepos(routeWt)).find((r) => r.id === ticketWt.repo_id) ?? (await resolvedRepos(routeWt))[0];
|
|
14382
14549
|
if (!repoWt) {
|
|
14383
14550
|
process.stderr.write("worktree: this route has no repository\n");
|
|
14384
14551
|
process.exit(2);
|
|
@@ -14588,7 +14755,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14588
14755
|
}
|
|
14589
14756
|
case "log": {
|
|
14590
14757
|
try {
|
|
14591
|
-
const text =
|
|
14758
|
+
const text = readFileSync17(cfg.ship.logFile, "utf8").split("\n");
|
|
14592
14759
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
14593
14760
|
`);
|
|
14594
14761
|
} catch {
|
|
@@ -14798,7 +14965,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
14798
14965
|
);
|
|
14799
14966
|
try {
|
|
14800
14967
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
14801
|
-
const evSize =
|
|
14968
|
+
const evSize = existsSync24(evFile) ? statSync9(evFile).size : 0;
|
|
14802
14969
|
process.stdout.write(
|
|
14803
14970
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
14804
14971
|
` : "events log: ok\n"
|
|
@@ -14932,15 +15099,39 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
14932
15099
|
let allBinsFound = true;
|
|
14933
15100
|
for (const tc of shipToolchains(cfg.ship)) {
|
|
14934
15101
|
const adapter = adapterFor(tc.harness);
|
|
14935
|
-
const
|
|
15102
|
+
const runBin = launcherExecutable(tc) ?? tc.bin;
|
|
15103
|
+
const binPath = findOnPath(runBin, schedulerPath);
|
|
14936
15104
|
if (!binPath) allBinsFound = false;
|
|
14937
15105
|
const label = tc.name === defaultToolchain ? "agent binary: " : `toolchain ${tc.name}:`.padEnd(18);
|
|
14938
15106
|
process.stdout.write(
|
|
14939
|
-
`${label} ${
|
|
15107
|
+
`${label} ${runBin} \u2014 ${binPath ? `OK (${binPath})` : "NOT FOUND on the scheduler PATH"}${tc.name === defaultToolchain ? "" : ` [${adapter.displayName}]`}
|
|
14940
15108
|
`
|
|
14941
15109
|
);
|
|
14942
15110
|
for (const limit of adapter.limitations) process.stdout.write(` cannot enforce: ${limit}
|
|
14943
15111
|
`);
|
|
15112
|
+
if (isOmlxLauncher(tc)) {
|
|
15113
|
+
const s = readOmlxSettings();
|
|
15114
|
+
if (!s) {
|
|
15115
|
+
process.stdout.write(` omlx: ~/.omlx/settings.json not found or has no server.port \u2014 cannot check the server
|
|
15116
|
+
`);
|
|
15117
|
+
} else {
|
|
15118
|
+
let status2;
|
|
15119
|
+
try {
|
|
15120
|
+
status2 = (await fetch(`http://${s.host}:${s.port}/v1/models`, { signal: AbortSignal.timeout(3e3) })).status;
|
|
15121
|
+
} catch {
|
|
15122
|
+
status2 = void 0;
|
|
15123
|
+
}
|
|
15124
|
+
const probe = interpretOmlxProbe(status2);
|
|
15125
|
+
process.stdout.write(` omlx server: ${s.host}:${s.port} \u2014 ${probe.detail}${probe.up ? "" : " (start oMLX)"}
|
|
15126
|
+
`);
|
|
15127
|
+
process.stdout.write(
|
|
15128
|
+
` omlx model: ${tc.model} \u2014 ${omlxModelPresent(tc.model, s.modelDirs) ? "present" : `NOT FOUND in ${s.modelDirs.join(", ") || "any model dir"}`}
|
|
15129
|
+
`
|
|
15130
|
+
);
|
|
15131
|
+
}
|
|
15132
|
+
process.stdout.write(` local model: tool-call reliability and prefill speed vary by model
|
|
15133
|
+
`);
|
|
15134
|
+
}
|
|
14944
15135
|
if (isOriLauncher(tc)) {
|
|
14945
15136
|
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)
|
|
14946
15137
|
`);
|