@tablation/crew 0.34.1 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/crew.example.yaml +4 -2
- package/dist/cli.js +446 -264
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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 join22, basename as basename9 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { hostname as hostname2, homedir as
|
|
7
|
-
import { spawnSync as
|
|
6
|
+
import { hostname as hostname2, homedir as homedir7 } from "os";
|
|
7
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
11
|
-
import { resolve, dirname as dirname3, isAbsolute, join as
|
|
12
|
-
import { homedir as
|
|
10
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
11
|
+
import { resolve, dirname as dirname3, isAbsolute, join as join6, basename as basename3 } from "path";
|
|
12
|
+
import { homedir as homedir4, tmpdir } from "os";
|
|
13
13
|
import { parse as parse2 } from "yaml";
|
|
14
14
|
|
|
15
15
|
// src/events.ts
|
|
@@ -158,10 +158,10 @@ var Emitter = class _Emitter {
|
|
|
158
158
|
var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
|
|
159
159
|
|
|
160
160
|
// src/pair-install.ts
|
|
161
|
-
import { existsSync as
|
|
162
|
-
import { spawnSync as
|
|
163
|
-
import { homedir as
|
|
164
|
-
import {
|
|
161
|
+
import { existsSync as existsSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
162
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
163
|
+
import { homedir as homedir3 } from "os";
|
|
164
|
+
import { join as join4 } from "path";
|
|
165
165
|
|
|
166
166
|
// src/stream.ts
|
|
167
167
|
function toolTarget(name, input) {
|
|
@@ -409,7 +409,15 @@ var claudeAdapter = {
|
|
|
409
409
|
ensureMcp: ensureClaudeMcp
|
|
410
410
|
};
|
|
411
411
|
|
|
412
|
-
// src/
|
|
412
|
+
// src/adapters/codex.ts
|
|
413
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
414
|
+
import { homedir as homedir2 } from "os";
|
|
415
|
+
import { join as join3 } from "path";
|
|
416
|
+
import { rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
417
|
+
|
|
418
|
+
// src/pair-files.ts
|
|
419
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync, rmSync as rmSync2 } from "fs";
|
|
420
|
+
import { dirname as dirname2 } from "path";
|
|
413
421
|
var PAIR_BLOCK_START = "<!-- crew:pair start -->";
|
|
414
422
|
var PAIR_BLOCK_END = "<!-- crew:pair end -->";
|
|
415
423
|
var PAIR_KEY_VAR = "CREW_API_KEY";
|
|
@@ -497,9 +505,6 @@ function tomlTableRange(lines, name) {
|
|
|
497
505
|
return [start, end];
|
|
498
506
|
}
|
|
499
507
|
var tomlString = (v) => JSON.stringify(v);
|
|
500
|
-
function codexTable(url) {
|
|
501
|
-
return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
|
|
502
|
-
}
|
|
503
508
|
function upsertTomlTable(text, name, table2) {
|
|
504
509
|
const lines = text.split("\n");
|
|
505
510
|
const r = tomlTableRange(lines, name);
|
|
@@ -535,6 +540,69 @@ function tomlValue(lines, range, key) {
|
|
|
535
540
|
}
|
|
536
541
|
return void 0;
|
|
537
542
|
}
|
|
543
|
+
|
|
544
|
+
// src/adapters/codex.ts
|
|
545
|
+
var CODEX_BILLING_VARS_TO_UNSET = [
|
|
546
|
+
"OPENAI_API_KEY",
|
|
547
|
+
"OPENAI_BASE_URL",
|
|
548
|
+
"OPENAI_API_BASE",
|
|
549
|
+
"OPENAI_ORG_ID",
|
|
550
|
+
"OPENAI_PROJECT",
|
|
551
|
+
"CODEX_API_KEY"
|
|
552
|
+
];
|
|
553
|
+
function codexSandboxFor(p) {
|
|
554
|
+
return p.level === "read" ? "read-only" : "danger-full-access";
|
|
555
|
+
}
|
|
556
|
+
var REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
|
|
557
|
+
function commandTarget(command2) {
|
|
558
|
+
if (!command2) return void 0;
|
|
559
|
+
const wrapped = /^(?:\S*\/)?(?:ba|z|da)?sh\s+-\w*c\s+(['"]?)([\s\S]*)\1$/.exec(command2.trim());
|
|
560
|
+
const inner = (wrapped ? wrapped[2] : command2).trim();
|
|
561
|
+
return inner.split(/\s+/)[0] || void 0;
|
|
562
|
+
}
|
|
563
|
+
function mapCodexLine(line) {
|
|
564
|
+
const l = line;
|
|
565
|
+
if (!l || l.type !== "item.completed" || !l.item) return [];
|
|
566
|
+
const it = l.item;
|
|
567
|
+
switch (it.type) {
|
|
568
|
+
case "reasoning":
|
|
569
|
+
return it.text ? [{ kind: "thought", text: it.text }] : [];
|
|
570
|
+
case "agent_message":
|
|
571
|
+
return it.text ? [{ kind: "text", text: it.text }] : [];
|
|
572
|
+
case "command_execution":
|
|
573
|
+
return [{ kind: "tool", tool: "command", target: commandTarget(it.command) }];
|
|
574
|
+
case "file_change":
|
|
575
|
+
return [{ kind: "tool", tool: "file_change", target: it.changes?.[0]?.path }];
|
|
576
|
+
case "mcp_tool_call":
|
|
577
|
+
return [{ kind: "tool", tool: `mcp__${it.server ?? "mcp"}__${it.tool ?? "tool"}` }];
|
|
578
|
+
case "web_search":
|
|
579
|
+
return [{ kind: "tool", tool: "web_search", target: it.query }];
|
|
580
|
+
default:
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function extractCodexResult(line) {
|
|
585
|
+
const l = line;
|
|
586
|
+
if (!l) return void 0;
|
|
587
|
+
if (l.type === "thread.started" && l.thread_id) return { sessionId: l.thread_id };
|
|
588
|
+
if (l.type === "turn.completed" && l.usage) {
|
|
589
|
+
return {
|
|
590
|
+
inputTokens: l.usage.input_tokens,
|
|
591
|
+
outputTokens: l.usage.output_tokens,
|
|
592
|
+
cacheReadTokens: l.usage.cached_input_tokens,
|
|
593
|
+
cacheWriteTokens: l.usage.cache_write_input_tokens
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
function extractCodexTurnTokens(line) {
|
|
599
|
+
const l = line;
|
|
600
|
+
if (!l || l.type !== "turn.completed" || !l.usage) return void 0;
|
|
601
|
+
return (l.usage.input_tokens ?? 0) + (l.usage.output_tokens ?? 0);
|
|
602
|
+
}
|
|
603
|
+
function codexTable(url) {
|
|
604
|
+
return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
|
|
605
|
+
}
|
|
538
606
|
var codexPaths = (home) => ({
|
|
539
607
|
instructions: join3(home, ".codex", "AGENTS.md"),
|
|
540
608
|
config: join3(home, ".codex", "config.toml")
|
|
@@ -587,16 +655,105 @@ var codexPair = {
|
|
|
587
655
|
const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
|
|
588
656
|
if (hadMcp && !o.dryRun) {
|
|
589
657
|
const after = removeTomlTable(config, `mcp_servers.${SERVER}`);
|
|
590
|
-
if (after === "")
|
|
591
|
-
else
|
|
658
|
+
if (after === "") rmSync3(p.config);
|
|
659
|
+
else writeFileSync2(p.config, after);
|
|
592
660
|
}
|
|
593
661
|
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
594
662
|
return hadBlock || hadMcp ? `pair(codex): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(codex): nothing to remove${kept}`;
|
|
595
663
|
}
|
|
596
664
|
};
|
|
665
|
+
function binMissing(bin) {
|
|
666
|
+
const r = spawnSync2(bin, ["--version"], { encoding: "utf8", timeout: 3e4 });
|
|
667
|
+
return r.error?.code === "ENOENT";
|
|
668
|
+
}
|
|
669
|
+
var codexAdapter = {
|
|
670
|
+
harness: "codex",
|
|
671
|
+
displayName: "Codex",
|
|
672
|
+
vendor: "openai",
|
|
673
|
+
ownsModel: (model) => /^(gpt-|o\d|codex-)/.test(model),
|
|
674
|
+
// The tier's reasoning is Codex's effort level; anything else is ignored.
|
|
675
|
+
applyReasoning: (spec, reasoning) => REASONING_EFFORTS.includes(reasoning) ? { ...spec, reasoningEffort: reasoning } : spec,
|
|
676
|
+
defaultBin: "codex",
|
|
677
|
+
// What Codex CLI itself starts with today (the `gpt-5-codex` family the first
|
|
678
|
+
// cut named now answers 404 "does not exist or you do not have access" from
|
|
679
|
+
// the Responses API for a 2026 API key, which the recording for CREW-1520
|
|
680
|
+
// hit first). An operator pins something else per toolchain entry or tier.
|
|
681
|
+
defaultModel: "gpt-5.5",
|
|
682
|
+
defaultTiers: {
|
|
683
|
+
light: { model: "gpt-5.4-mini", reasoning: "low" },
|
|
684
|
+
standard: { model: "gpt-5.5" },
|
|
685
|
+
deep: { model: "gpt-5.5", reasoning: "high" }
|
|
686
|
+
},
|
|
687
|
+
limitations: [
|
|
688
|
+
"no per-tool allow or deny list, and a push (even a forced one) is not blocked",
|
|
689
|
+
"the write (QA) and edit (dev, design) profiles run with no filesystem sandbox (danger-full-access): Codex's workspace-write keeps every .git directory read-only, so a sandboxed seat could not cut a worktree or commit, and a headless run cannot approve the escalation",
|
|
690
|
+
"the read profile (triage) runs in the read-only sandbox, which has no network, so it can reach the tracker only through the MCP tools, not curl",
|
|
691
|
+
"the design canvas and skills are unavailable",
|
|
692
|
+
"no cost is reported (tokens only)"
|
|
693
|
+
],
|
|
694
|
+
invocation(spec, o) {
|
|
695
|
+
const profile = profileFor(o.role);
|
|
696
|
+
const mode = codexSandboxFor(profile);
|
|
697
|
+
const cfg = (k, v) => ["-c", `${k}=${tomlString(v)}`];
|
|
698
|
+
const args = [
|
|
699
|
+
"exec",
|
|
700
|
+
"--json",
|
|
701
|
+
"--skip-git-repo-check",
|
|
702
|
+
"--sandbox",
|
|
703
|
+
mode,
|
|
704
|
+
"--model",
|
|
705
|
+
o.model,
|
|
706
|
+
// Headless: nobody can answer an approval prompt.
|
|
707
|
+
"-c",
|
|
708
|
+
'approval_policy="never"',
|
|
709
|
+
...spec.reasoningEffort ? cfg("model_reasoning_effort", spec.reasoningEffort) : [],
|
|
710
|
+
// The tracker MCP for this run only, as -c overrides: the config file is
|
|
711
|
+
// untouched and the key is named by variable, never written (§7).
|
|
712
|
+
// `default_tools_approval_mode="approve"` is load-bearing: Codex asks
|
|
713
|
+
// before an MCP tool call by default, and under `approval_policy="never"`
|
|
714
|
+
// that question is auto-rejected ("MCP tool call requires approval, but
|
|
715
|
+
// approval policy is never" — seen on the first real recording), which
|
|
716
|
+
// would cut a headless seat off from the tracker entirely.
|
|
717
|
+
...o.mcp ? [
|
|
718
|
+
...cfg(`mcp_servers.${SERVER}.url`, o.mcp.url),
|
|
719
|
+
...cfg(`mcp_servers.${SERVER}.bearer_token_env_var`, o.mcp.keyVar),
|
|
720
|
+
...cfg(`mcp_servers.${SERVER}.default_tools_approval_mode`, "approve")
|
|
721
|
+
] : [],
|
|
722
|
+
...spec.args,
|
|
723
|
+
// Prompt on stdin.
|
|
724
|
+
"-"
|
|
725
|
+
];
|
|
726
|
+
return spec.launcher ? { bin: spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
|
|
727
|
+
},
|
|
728
|
+
runEnv: (spec) => ({ ...spec.env }),
|
|
729
|
+
provider: () => "openai",
|
|
730
|
+
unsetEnv: CODEX_BILLING_VARS_TO_UNSET,
|
|
731
|
+
mapStreamLine: mapCodexLine,
|
|
732
|
+
extractResult: extractCodexResult,
|
|
733
|
+
extractTurnTokens: extractCodexTurnTokens,
|
|
734
|
+
inspectMcp(o) {
|
|
735
|
+
if (binMissing(o.bin)) return { state: "no-binary" };
|
|
736
|
+
const st = codexMcpState(readIfExists(codexPaths(o.home ?? homedir2()).config) ?? "", o.url);
|
|
737
|
+
return st.state === "registered" ? { state: "registered", url: o.url } : st;
|
|
738
|
+
},
|
|
739
|
+
ensureMcp(o) {
|
|
740
|
+
const p = codexPaths(o.home ?? homedir2());
|
|
741
|
+
const config = readIfExists(p.config) ?? "";
|
|
742
|
+
const st = codexMcpState(config, o.url);
|
|
743
|
+
if (st.state === "registered") return `mcp: ${SERVER} already registered with Codex \u2192 ${o.url}`;
|
|
744
|
+
if (st.state === "different" && !o.replace) {
|
|
745
|
+
return `mcp: left Codex's "${SERVER}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
746
|
+
}
|
|
747
|
+
if (o.dryRun) return `(dry run) would register the ${SERVER} MCP server with Codex \u2192 ${o.url}`;
|
|
748
|
+
writeFileMkdir(p.config, upsertTomlTable(config, `mcp_servers.${SERVER}`, codexTable(o.url)));
|
|
749
|
+
return `mcp: registered the ${SERVER} MCP server with Codex \u2192 ${o.url} (${p.config}; key read from $${PAIR_KEY_VAR})`;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/pair-install.ts
|
|
597
754
|
var geminiPaths = (home) => ({
|
|
598
|
-
instructions:
|
|
599
|
-
config:
|
|
755
|
+
instructions: join4(home, ".gemini", "GEMINI.md"),
|
|
756
|
+
config: join4(home, ".gemini", "settings.json")
|
|
600
757
|
});
|
|
601
758
|
function readGeminiSettings(path) {
|
|
602
759
|
const text = readIfExists(path);
|
|
@@ -617,14 +774,14 @@ function geminiMcpState(s, url) {
|
|
|
617
774
|
return { state: "registered" };
|
|
618
775
|
}
|
|
619
776
|
function inspectGeminiMcp(o) {
|
|
620
|
-
const p = geminiPaths(o.home ??
|
|
777
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
621
778
|
const settings = readGeminiSettings(p.config);
|
|
622
779
|
if (!settings) return { state: "different", detail: `${p.config} is not a JSON object` };
|
|
623
780
|
const st = geminiMcpState(settings, o.url);
|
|
624
781
|
return st.state === "registered" ? { state: "registered", url: o.url } : st;
|
|
625
782
|
}
|
|
626
783
|
function ensureGeminiMcp(o) {
|
|
627
|
-
const p = geminiPaths(o.home ??
|
|
784
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
628
785
|
const settings = readGeminiSettings(p.config);
|
|
629
786
|
if (!settings) return `gemini: ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
630
787
|
const st = geminiMcpState(settings, o.url);
|
|
@@ -642,7 +799,7 @@ var geminiPair = {
|
|
|
642
799
|
harness: "gemini",
|
|
643
800
|
displayName: "Gemini CLI",
|
|
644
801
|
installPair(o) {
|
|
645
|
-
const p = geminiPaths(o.home ??
|
|
802
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
646
803
|
const settings = readGeminiSettings(p.config);
|
|
647
804
|
if (!settings) return `pair(gemini): ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
|
|
648
805
|
const st = geminiMcpState(settings, o.url);
|
|
@@ -661,7 +818,7 @@ var geminiPair = {
|
|
|
661
818
|
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})`;
|
|
662
819
|
},
|
|
663
820
|
verifyPair(o) {
|
|
664
|
-
const p = geminiPaths(o.home ??
|
|
821
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
665
822
|
const b = blockVerdict(p.instructions, "gemini");
|
|
666
823
|
if (!b.ok) return b;
|
|
667
824
|
const settings = readGeminiSettings(p.config);
|
|
@@ -672,7 +829,7 @@ var geminiPair = {
|
|
|
672
829
|
return { ok: true, detail: `${p.instructions}, ${p.config}` };
|
|
673
830
|
},
|
|
674
831
|
removePair(o) {
|
|
675
|
-
const p = geminiPaths(o.home ??
|
|
832
|
+
const p = geminiPaths(o.home ?? homedir3());
|
|
676
833
|
const hadBlock = removeBlockFile(p.instructions, o.dryRun);
|
|
677
834
|
const settings = readGeminiSettings(p.config);
|
|
678
835
|
const st = settings ? geminiMcpState(settings, o.url) : { state: "missing" };
|
|
@@ -683,8 +840,8 @@ var geminiPair = {
|
|
|
683
840
|
const next = { ...settings };
|
|
684
841
|
if (Object.keys(rest).length > 0) next.mcpServers = rest;
|
|
685
842
|
else delete next.mcpServers;
|
|
686
|
-
if (Object.keys(next).length === 0)
|
|
687
|
-
else
|
|
843
|
+
if (Object.keys(next).length === 0) rmSync4(p.config);
|
|
844
|
+
else writeFileSync3(p.config, `${JSON.stringify(next, null, 2)}
|
|
688
845
|
`);
|
|
689
846
|
}
|
|
690
847
|
const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
|
|
@@ -693,11 +850,11 @@ var geminiPair = {
|
|
|
693
850
|
};
|
|
694
851
|
var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
|
|
695
852
|
function vscodeUserDirs(o) {
|
|
696
|
-
const home = o.home ??
|
|
853
|
+
const home = o.home ?? homedir3();
|
|
697
854
|
const platform2 = o.platform ?? process.platform;
|
|
698
|
-
const root = platform2 === "darwin" ?
|
|
699
|
-
const all = VSCODE_FLAVOURS.map((f) =>
|
|
700
|
-
const present = all.filter((d) =>
|
|
855
|
+
const root = platform2 === "darwin" ? join4(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join4(home, "AppData", "Roaming") : join4(home, ".config");
|
|
856
|
+
const all = VSCODE_FLAVOURS.map((f) => join4(root, f, "User"));
|
|
857
|
+
const present = all.filter((d) => existsSync3(d));
|
|
701
858
|
return present.length > 0 ? present : [all[0]];
|
|
702
859
|
}
|
|
703
860
|
var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
|
|
@@ -734,8 +891,8 @@ function removeCopilotInstructions(path, dryRun) {
|
|
|
734
891
|
if (before === void 0 || !hasPairBlock(before)) return false;
|
|
735
892
|
if (dryRun) return true;
|
|
736
893
|
const after = removePairBlock(before);
|
|
737
|
-
if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim())
|
|
738
|
-
else
|
|
894
|
+
if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync4(path);
|
|
895
|
+
else writeFileSync3(path, after);
|
|
739
896
|
return true;
|
|
740
897
|
}
|
|
741
898
|
var copilotPair = {
|
|
@@ -744,8 +901,8 @@ var copilotPair = {
|
|
|
744
901
|
installPair(o) {
|
|
745
902
|
const lines = [];
|
|
746
903
|
for (const dir of vscodeUserDirs(o)) {
|
|
747
|
-
const mcpPath =
|
|
748
|
-
const instrPath =
|
|
904
|
+
const mcpPath = join4(dir, "mcp.json");
|
|
905
|
+
const instrPath = join4(dir, "prompts", "crew-pair.instructions.md");
|
|
749
906
|
const settings = readVscodeMcp(mcpPath);
|
|
750
907
|
if (!settings) {
|
|
751
908
|
lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
|
|
@@ -773,8 +930,8 @@ var copilotPair = {
|
|
|
773
930
|
},
|
|
774
931
|
verifyPair(o) {
|
|
775
932
|
const verdicts = vscodeUserDirs(o).map((dir) => {
|
|
776
|
-
const instrPath =
|
|
777
|
-
const mcpPath =
|
|
933
|
+
const instrPath = join4(dir, "prompts", "crew-pair.instructions.md");
|
|
934
|
+
const mcpPath = join4(dir, "mcp.json");
|
|
778
935
|
const b = blockVerdict(instrPath, "copilot");
|
|
779
936
|
if (!b.ok) return b;
|
|
780
937
|
const settings = readVscodeMcp(mcpPath);
|
|
@@ -789,8 +946,8 @@ var copilotPair = {
|
|
|
789
946
|
removePair(o) {
|
|
790
947
|
const lines = [];
|
|
791
948
|
for (const dir of vscodeUserDirs(o)) {
|
|
792
|
-
const mcpPath =
|
|
793
|
-
const hadBlock = removeCopilotInstructions(
|
|
949
|
+
const mcpPath = join4(dir, "mcp.json");
|
|
950
|
+
const hadBlock = removeCopilotInstructions(join4(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
|
|
794
951
|
const settings = readVscodeMcp(mcpPath);
|
|
795
952
|
const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
|
|
796
953
|
const hadMcp = st.state === "registered";
|
|
@@ -800,8 +957,8 @@ var copilotPair = {
|
|
|
800
957
|
const next = { ...settings };
|
|
801
958
|
if (Object.keys(rest).length > 0) next.servers = rest;
|
|
802
959
|
else delete next.servers;
|
|
803
|
-
if (Object.keys(next).length === 0)
|
|
804
|
-
else
|
|
960
|
+
if (Object.keys(next).length === 0) rmSync4(mcpPath);
|
|
961
|
+
else writeFileSync3(mcpPath, `${JSON.stringify(next, null, 2)}
|
|
805
962
|
`);
|
|
806
963
|
}
|
|
807
964
|
const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
|
|
@@ -817,7 +974,7 @@ var copilotPair = {
|
|
|
817
974
|
envWarning(o) {
|
|
818
975
|
if ((o.platform ?? process.platform) !== "darwin") return void 0;
|
|
819
976
|
const get2 = o.loginEnv ?? ((name) => {
|
|
820
|
-
const r =
|
|
977
|
+
const r = spawnSync3("launchctl", ["getenv", name], { encoding: "utf8" });
|
|
821
978
|
return r.status === 0 ? r.stdout.trim() : void 0;
|
|
822
979
|
});
|
|
823
980
|
if (get2(PAIR_KEY_VAR)) return void 0;
|
|
@@ -963,6 +1120,7 @@ var geminiAdapter = {
|
|
|
963
1120
|
var TIERS = ["light", "standard", "deep"];
|
|
964
1121
|
var ADAPTERS = {
|
|
965
1122
|
[claudeAdapter.harness]: claudeAdapter,
|
|
1123
|
+
[codexAdapter.harness]: codexAdapter,
|
|
966
1124
|
[geminiAdapter.harness]: geminiAdapter
|
|
967
1125
|
};
|
|
968
1126
|
var HARNESS_NAMES = Object.keys(ADAPTERS);
|
|
@@ -991,8 +1149,8 @@ function shipToolchains(ship) {
|
|
|
991
1149
|
};
|
|
992
1150
|
return [legacy, ...explicit];
|
|
993
1151
|
}
|
|
994
|
-
function resolveToolchain(ship, route) {
|
|
995
|
-
const name = route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
|
|
1152
|
+
function resolveToolchain(ship, route, repo) {
|
|
1153
|
+
const name = (repo && route?.repoOverrides?.[repo]?.toolchain) ?? route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
|
|
996
1154
|
const found = shipToolchains(ship).find((t) => t.name === name);
|
|
997
1155
|
if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
|
|
998
1156
|
return found;
|
|
@@ -1007,11 +1165,12 @@ function isOriLauncher(spec) {
|
|
|
1007
1165
|
function toolchainProvider(spec) {
|
|
1008
1166
|
return isOriLauncher(spec) ? "openrouter" : adapterFor(spec.harness).provider(spec);
|
|
1009
1167
|
}
|
|
1010
|
-
function resolveSeat(ship, route, role, persona = {}) {
|
|
1168
|
+
function resolveSeat(ship, route, role, persona = {}, repo) {
|
|
1011
1169
|
const warnings = [];
|
|
1012
|
-
const base = resolveToolchain(ship, route);
|
|
1170
|
+
const base = resolveToolchain(ship, route, repo);
|
|
1013
1171
|
const baseVendor = toolchainVendor(base);
|
|
1014
|
-
const
|
|
1172
|
+
const repoToolchain = repo ? route?.repoOverrides?.[repo]?.toolchain : void 0;
|
|
1173
|
+
const baseWhy = repoToolchain ? `repo ${repo} toolchain ${base.name}` : route?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
|
|
1015
1174
|
let chosen = base;
|
|
1016
1175
|
let toolchainWhy = baseWhy;
|
|
1017
1176
|
if (persona.vendor && persona.vendor !== baseVendor) {
|
|
@@ -1090,8 +1249,8 @@ function explain(ship, required) {
|
|
|
1090
1249
|
}
|
|
1091
1250
|
|
|
1092
1251
|
// src/repo-config.ts
|
|
1093
|
-
import { readFileSync as readFileSync4, existsSync as
|
|
1094
|
-
import { join as
|
|
1252
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1253
|
+
import { join as join5, basename as basename2 } from "path";
|
|
1095
1254
|
import { parse } from "yaml";
|
|
1096
1255
|
var SPEC_VERSION = 1;
|
|
1097
1256
|
var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
|
|
@@ -1164,8 +1323,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
|
|
|
1164
1323
|
var PROVIDERS = ["github", "buildkite", "other", "none"];
|
|
1165
1324
|
function findRepoConfig(dir) {
|
|
1166
1325
|
for (const name of REPO_CONFIG_FILENAMES) {
|
|
1167
|
-
const p =
|
|
1168
|
-
if (
|
|
1326
|
+
const p = join5(dir, name);
|
|
1327
|
+
if (existsSync4(p)) return p;
|
|
1169
1328
|
}
|
|
1170
1329
|
return null;
|
|
1171
1330
|
}
|
|
@@ -1710,7 +1869,7 @@ function parseWorktreeName(v, where, file) {
|
|
|
1710
1869
|
if (problem) throw new ConfigError(`${file}: ${where}.worktreeName ("${String(v)}") ${problem}`);
|
|
1711
1870
|
return String(v);
|
|
1712
1871
|
}
|
|
1713
|
-
var REPO_ENTRY_KEYS = /* @__PURE__ */ new Set(["dir", "hooks", "labels", "release", "branch", "hostPassengers", "worktreeName"]);
|
|
1872
|
+
var REPO_ENTRY_KEYS = /* @__PURE__ */ new Set(["dir", "hooks", "labels", "release", "branch", "hostPassengers", "worktreeName", "toolchain"]);
|
|
1714
1873
|
var BRANCH_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["base", "name", "push", "remote"]);
|
|
1715
1874
|
var RELEASE_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
|
|
1716
1875
|
"mode",
|
|
@@ -1807,16 +1966,16 @@ function mergeRouteRelease(route, override) {
|
|
|
1807
1966
|
};
|
|
1808
1967
|
}
|
|
1809
1968
|
function expand(p, base) {
|
|
1810
|
-
let out = p.startsWith("~") ? p.replace(/^~/,
|
|
1811
|
-
out = out.replace(/\$\{?HOME\}?/g,
|
|
1969
|
+
let out = p.startsWith("~") ? p.replace(/^~/, homedir4()) : p;
|
|
1970
|
+
out = out.replace(/\$\{?HOME\}?/g, homedir4());
|
|
1812
1971
|
return isAbsolute(out) ? out : resolve(base, out);
|
|
1813
1972
|
}
|
|
1814
1973
|
function resolvePromptsDir(promptSet, crewHome, base) {
|
|
1815
|
-
if (!promptSet) return
|
|
1974
|
+
if (!promptSet) return join6(crewHome, "prompts", "default");
|
|
1816
1975
|
if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
|
|
1817
1976
|
return expand(promptSet, base);
|
|
1818
1977
|
}
|
|
1819
|
-
return
|
|
1978
|
+
return join6(crewHome, "prompts", promptSet);
|
|
1820
1979
|
}
|
|
1821
1980
|
var Missing = class {
|
|
1822
1981
|
paths = [];
|
|
@@ -1840,7 +1999,7 @@ var Missing = class {
|
|
|
1840
1999
|
};
|
|
1841
2000
|
function crewVersion(crewHome) {
|
|
1842
2001
|
try {
|
|
1843
|
-
const pkg = JSON.parse(readFileSync5(
|
|
2002
|
+
const pkg = JSON.parse(readFileSync5(join6(crewHome, "package.json"), "utf8"));
|
|
1844
2003
|
return pkg.version ?? "0.0.0";
|
|
1845
2004
|
} catch {
|
|
1846
2005
|
return "0.0.0";
|
|
@@ -1850,26 +2009,26 @@ function configSearchPath(crewHome) {
|
|
|
1850
2009
|
if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
|
|
1851
2010
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
1852
2011
|
return [
|
|
1853
|
-
...xdg ? [
|
|
1854
|
-
|
|
2012
|
+
...xdg ? [join6(xdg, "crew", "crew.yaml")] : [],
|
|
2013
|
+
join6(homedir4(), ".config", "crew", "crew.yaml"),
|
|
1855
2014
|
resolve(crewHome, "crew.yaml")
|
|
1856
2015
|
];
|
|
1857
2016
|
}
|
|
1858
2017
|
function findConfigFile(crewHome) {
|
|
1859
2018
|
const candidates = configSearchPath(crewHome);
|
|
1860
|
-
return candidates.find((c) =>
|
|
2019
|
+
return candidates.find((c) => existsSync5(c)) ?? candidates[candidates.length - 1];
|
|
1861
2020
|
}
|
|
1862
|
-
var defaultConfigPath = () =>
|
|
2021
|
+
var defaultConfigPath = () => join6(process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config"), "crew", "crew.yaml");
|
|
1863
2022
|
function routeSlug(route) {
|
|
1864
2023
|
return route.replace(/\//g, "-");
|
|
1865
2024
|
}
|
|
1866
2025
|
function resolvedPathFor(stateDir, route) {
|
|
1867
2026
|
const [workspace, project] = splitRoute(route);
|
|
1868
|
-
return
|
|
2027
|
+
return join6(stateDir, "resolved", workspace, `${project}.json`);
|
|
1869
2028
|
}
|
|
1870
2029
|
function apiKeyPathFor(stateDir, route) {
|
|
1871
2030
|
const [workspace, project] = splitRoute(route);
|
|
1872
|
-
return
|
|
2031
|
+
return join6(stateDir, "keys", workspace, `${project}.env`);
|
|
1873
2032
|
}
|
|
1874
2033
|
function splitRoute(route) {
|
|
1875
2034
|
const parts = route.split("/");
|
|
@@ -1880,7 +2039,7 @@ function splitRoute(route) {
|
|
|
1880
2039
|
}
|
|
1881
2040
|
function readResolvedFile(stateDir, route) {
|
|
1882
2041
|
const path = resolvedPathFor(stateDir, route);
|
|
1883
|
-
if (!
|
|
2042
|
+
if (!existsSync5(path)) return void 0;
|
|
1884
2043
|
try {
|
|
1885
2044
|
return JSON.parse(readFileSync5(path, "utf8"));
|
|
1886
2045
|
} catch (e) {
|
|
@@ -1938,13 +2097,13 @@ function parseResolved(raw, m, where) {
|
|
|
1938
2097
|
}
|
|
1939
2098
|
function loadConfig(crewHome, configFile, opts = {}) {
|
|
1940
2099
|
const file = configFile ?? findConfigFile(crewHome);
|
|
1941
|
-
if (opts.text === void 0 && !
|
|
2100
|
+
if (opts.text === void 0 && !existsSync5(file)) {
|
|
1942
2101
|
throw new ConfigError(
|
|
1943
2102
|
`no config found. Looked in:
|
|
1944
2103
|
` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
|
|
1945
2104
|
|
|
1946
2105
|
Create the first of those:
|
|
1947
|
-
mkdir -p ${dirname3(defaultConfigPath())} && cp ${
|
|
2106
|
+
mkdir -p ${dirname3(defaultConfigPath())} && cp ${join6(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
|
|
1948
2107
|
);
|
|
1949
2108
|
}
|
|
1950
2109
|
let raw;
|
|
@@ -2027,7 +2186,7 @@ Create the first of those:
|
|
|
2027
2186
|
useNvm: shipRaw.useNvm !== false,
|
|
2028
2187
|
nvmSh: shipRaw.nvmSh,
|
|
2029
2188
|
stateDir,
|
|
2030
|
-
logFile: expand(shipRaw.logFile ??
|
|
2189
|
+
logFile: expand(shipRaw.logFile ?? join6(tmpdir(), "crew.log"), base),
|
|
2031
2190
|
userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
|
|
2032
2191
|
maxConcurrentAgents,
|
|
2033
2192
|
streamRetentionDays,
|
|
@@ -2140,7 +2299,11 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
2140
2299
|
const e = entry;
|
|
2141
2300
|
rejectUnknownKeys(e, REPO_ENTRY_KEYS, `${where}.repos.${name}`, file);
|
|
2142
2301
|
repoDirs[name] = expand(String(missing.req(e.dir, `${where}.repos.${name}.dir`) || ""), base);
|
|
2143
|
-
|
|
2302
|
+
const repoToolchain = e.toolchain === void 0 ? void 0 : String(e.toolchain);
|
|
2303
|
+
if (repoToolchain !== void 0 && !toolchainNames.has(repoToolchain)) {
|
|
2304
|
+
missing.add(`${where}.repos.${name}.toolchain "${repoToolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
|
|
2305
|
+
}
|
|
2306
|
+
if (e.hooks || e.labels || e.release || e.branch || e.hostPassengers !== void 0 || e.worktreeName !== void 0 || repoToolchain !== void 0) {
|
|
2144
2307
|
repoOverrides[name] = {
|
|
2145
2308
|
hooks: e.hooks,
|
|
2146
2309
|
labels: e.labels,
|
|
@@ -2151,7 +2314,8 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
|
|
|
2151
2314
|
// compare this literally.
|
|
2152
2315
|
...e.branch ? { branch: parseBranchOverride(e.branch, `${where}.repos.${name}`, file) } : {},
|
|
2153
2316
|
...e.hostPassengers !== void 0 ? { hostPassengers: e.hostPassengers === true } : {},
|
|
2154
|
-
...e.worktreeName !== void 0 ? { worktreeName: parseWorktreeName(e.worktreeName, `${where}.repos.${name}`, file) } : {}
|
|
2317
|
+
...e.worktreeName !== void 0 ? { worktreeName: parseWorktreeName(e.worktreeName, `${where}.repos.${name}`, file) } : {},
|
|
2318
|
+
...repoToolchain !== void 0 ? { toolchain: repoToolchain } : {}
|
|
2155
2319
|
};
|
|
2156
2320
|
}
|
|
2157
2321
|
} else {
|
|
@@ -2242,7 +2406,7 @@ function repoIdForName(r, name) {
|
|
|
2242
2406
|
}
|
|
2243
2407
|
function resolveApiKey(r) {
|
|
2244
2408
|
if (r.apiKey) return r.apiKey;
|
|
2245
|
-
if (r.apiKeyFile && r.apiKeyVar &&
|
|
2409
|
+
if (r.apiKeyFile && r.apiKeyVar && existsSync5(r.apiKeyFile)) {
|
|
2246
2410
|
for (const line of readFileSync5(r.apiKeyFile, "utf8").split("\n")) {
|
|
2247
2411
|
const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
|
|
2248
2412
|
if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
|
|
@@ -2306,7 +2470,7 @@ function configuredMembers(rt) {
|
|
|
2306
2470
|
return out;
|
|
2307
2471
|
}
|
|
2308
2472
|
function defaultRepoDir(basePath, workspace, repoName) {
|
|
2309
|
-
return
|
|
2473
|
+
return join6(basePath, workspace, repoName);
|
|
2310
2474
|
}
|
|
2311
2475
|
function reposOf(r) {
|
|
2312
2476
|
const named = Object.entries(r.repos);
|
|
@@ -2330,7 +2494,7 @@ function passengerRepoTargets(r) {
|
|
|
2330
2494
|
return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
|
|
2331
2495
|
}
|
|
2332
2496
|
function passengerCheckoutDir(basePath, workspace, repoName) {
|
|
2333
|
-
return
|
|
2497
|
+
return join6(basePath, workspace, ".passenger-checkouts", repoName);
|
|
2334
2498
|
}
|
|
2335
2499
|
function passengerCheckoutTargets(r) {
|
|
2336
2500
|
if (!r.hostPassengers) return [];
|
|
@@ -2462,8 +2626,8 @@ function rosterMarkdown(roster, meId) {
|
|
|
2462
2626
|
}
|
|
2463
2627
|
|
|
2464
2628
|
// src/state.ts
|
|
2465
|
-
import { readFileSync as readFileSync6, writeFileSync as
|
|
2466
|
-
import { join as
|
|
2629
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
|
|
2630
|
+
import { join as join7 } from "path";
|
|
2467
2631
|
var EPOCH = "1970-01-01T00:00:00.000Z";
|
|
2468
2632
|
var State = class _State {
|
|
2469
2633
|
dir;
|
|
@@ -2472,7 +2636,7 @@ var State = class _State {
|
|
|
2472
2636
|
mkdirSync3(dir, { recursive: true });
|
|
2473
2637
|
}
|
|
2474
2638
|
path(...parts) {
|
|
2475
|
-
return
|
|
2639
|
+
return join7(this.dir, ...parts);
|
|
2476
2640
|
}
|
|
2477
2641
|
/**
|
|
2478
2642
|
* The timestamp every "new since last poll" check is measured against.
|
|
@@ -2490,7 +2654,7 @@ var State = class _State {
|
|
|
2490
2654
|
}
|
|
2491
2655
|
}
|
|
2492
2656
|
setWatermark(at) {
|
|
2493
|
-
|
|
2657
|
+
writeFileSync4(this.path(".poll-watermark"), `${at}
|
|
2494
2658
|
`);
|
|
2495
2659
|
}
|
|
2496
2660
|
/**
|
|
@@ -2508,19 +2672,19 @@ var State = class _State {
|
|
|
2508
2672
|
}
|
|
2509
2673
|
/** Whole-crew pause, and the per-role sentinels one level down. */
|
|
2510
2674
|
isPaused() {
|
|
2511
|
-
return
|
|
2675
|
+
return existsSync6(this.path(".crew-paused"));
|
|
2512
2676
|
}
|
|
2513
2677
|
isRolePaused(role) {
|
|
2514
|
-
return
|
|
2678
|
+
return existsSync6(this.path(`.role-paused-${role}`));
|
|
2515
2679
|
}
|
|
2516
2680
|
pausedRoles(roles) {
|
|
2517
2681
|
return new Set(roles.filter((r) => this.isRolePaused(r)));
|
|
2518
2682
|
}
|
|
2519
2683
|
pause(role) {
|
|
2520
|
-
|
|
2684
|
+
writeFileSync4(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
|
|
2521
2685
|
}
|
|
2522
2686
|
resume(role) {
|
|
2523
|
-
|
|
2687
|
+
rmSync5(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
|
|
2524
2688
|
}
|
|
2525
2689
|
/**
|
|
2526
2690
|
* The release phase (merge, deploy, release) has its own sentinel, one
|
|
@@ -2528,11 +2692,11 @@ var State = class _State {
|
|
|
2528
2692
|
* while agents keep working.
|
|
2529
2693
|
*/
|
|
2530
2694
|
isReleasePaused() {
|
|
2531
|
-
return
|
|
2695
|
+
return existsSync6(this.path(".release-paused"));
|
|
2532
2696
|
}
|
|
2533
2697
|
setReleasePaused(paused) {
|
|
2534
|
-
if (paused)
|
|
2535
|
-
else
|
|
2698
|
+
if (paused) writeFileSync4(this.path(".release-paused"), "");
|
|
2699
|
+
else rmSync5(this.path(".release-paused"), { force: true });
|
|
2536
2700
|
}
|
|
2537
2701
|
/**
|
|
2538
2702
|
* Why a release cycle must not run right now, or null when it may. Every
|
|
@@ -2579,13 +2743,13 @@ var State = class _State {
|
|
|
2579
2743
|
}
|
|
2580
2744
|
} catch {
|
|
2581
2745
|
}
|
|
2582
|
-
|
|
2746
|
+
writeFileSync4(file, `${process.pid}
|
|
2583
2747
|
`);
|
|
2584
2748
|
return {
|
|
2585
2749
|
ok: true,
|
|
2586
2750
|
release: () => {
|
|
2587
2751
|
try {
|
|
2588
|
-
|
|
2752
|
+
rmSync5(file, { force: true });
|
|
2589
2753
|
} catch {
|
|
2590
2754
|
}
|
|
2591
2755
|
}
|
|
@@ -2672,12 +2836,12 @@ var State = class _State {
|
|
|
2672
2836
|
blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
|
|
2673
2837
|
noteBlocked: () => {
|
|
2674
2838
|
const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
|
|
2675
|
-
|
|
2839
|
+
writeFileSync4(this.path(blocked), `${n}
|
|
2676
2840
|
`);
|
|
2677
2841
|
return n;
|
|
2678
2842
|
},
|
|
2679
2843
|
clearBlocked: () => {
|
|
2680
|
-
|
|
2844
|
+
rmSync5(this.path(blocked), { force: true });
|
|
2681
2845
|
},
|
|
2682
2846
|
/**
|
|
2683
2847
|
* The commit a deploy last failed on.
|
|
@@ -2689,11 +2853,11 @@ var State = class _State {
|
|
|
2689
2853
|
*/
|
|
2690
2854
|
deployFailedSha: () => read(failed),
|
|
2691
2855
|
noteDeployFailed: (sha) => {
|
|
2692
|
-
|
|
2856
|
+
writeFileSync4(this.path(failed), `${sha}
|
|
2693
2857
|
`);
|
|
2694
2858
|
},
|
|
2695
2859
|
clearDeployFailed: () => {
|
|
2696
|
-
|
|
2860
|
+
rmSync5(this.path(failed), { force: true });
|
|
2697
2861
|
},
|
|
2698
2862
|
/**
|
|
2699
2863
|
* Consecutive test-gate failures on the same head (CREW-1368). A failure
|
|
@@ -2703,12 +2867,12 @@ var State = class _State {
|
|
|
2703
2867
|
noteTestGateFailed: (sha) => {
|
|
2704
2868
|
const [prev, n] = (read(gateFailed) ?? "").split(" ");
|
|
2705
2869
|
const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
|
|
2706
|
-
|
|
2870
|
+
writeFileSync4(this.path(gateFailed), `${sha} ${next}
|
|
2707
2871
|
`);
|
|
2708
2872
|
return next;
|
|
2709
2873
|
},
|
|
2710
2874
|
clearTestGateFailed: () => {
|
|
2711
|
-
|
|
2875
|
+
rmSync5(this.path(gateFailed), { force: true });
|
|
2712
2876
|
},
|
|
2713
2877
|
/** Whether the board was already told about this head's red gate. */
|
|
2714
2878
|
heldReported: () => {
|
|
@@ -2719,15 +2883,15 @@ var State = class _State {
|
|
|
2719
2883
|
}
|
|
2720
2884
|
},
|
|
2721
2885
|
noteHeldReported: (signature) => {
|
|
2722
|
-
|
|
2886
|
+
writeFileSync4(this.path(heldReported), `${signature}
|
|
2723
2887
|
`);
|
|
2724
2888
|
},
|
|
2725
2889
|
clearHeldReported: () => {
|
|
2726
|
-
|
|
2890
|
+
rmSync5(this.path(heldReported), { force: true });
|
|
2727
2891
|
},
|
|
2728
2892
|
testGateReported: (sha) => read(gateReported) === sha,
|
|
2729
2893
|
noteTestGateReported: (sha) => {
|
|
2730
|
-
|
|
2894
|
+
writeFileSync4(this.path(gateReported), `${sha}
|
|
2731
2895
|
`);
|
|
2732
2896
|
}
|
|
2733
2897
|
};
|
|
@@ -2756,7 +2920,7 @@ var State = class _State {
|
|
|
2756
2920
|
}
|
|
2757
2921
|
};
|
|
2758
2922
|
const clear2 = () => {
|
|
2759
|
-
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile])
|
|
2923
|
+
for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync5(this.path(f), { force: true });
|
|
2760
2924
|
};
|
|
2761
2925
|
return {
|
|
2762
2926
|
/** Consecutive cycles this route had actionable work and did not win. */
|
|
@@ -2783,18 +2947,18 @@ var State = class _State {
|
|
|
2783
2947
|
clear2();
|
|
2784
2948
|
return;
|
|
2785
2949
|
}
|
|
2786
|
-
|
|
2950
|
+
writeFileSync4(this.path(streakFile), `${this.readInt(streakFile) + 1}
|
|
2787
2951
|
`);
|
|
2788
2952
|
const prior = read(ticketFile);
|
|
2789
2953
|
if (topTicket && topTicket === prior) {
|
|
2790
|
-
|
|
2954
|
+
writeFileSync4(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
|
|
2791
2955
|
`);
|
|
2792
2956
|
} else if (topTicket) {
|
|
2793
|
-
|
|
2957
|
+
writeFileSync4(this.path(ticketFile), `${topTicket}
|
|
2794
2958
|
`);
|
|
2795
|
-
|
|
2959
|
+
writeFileSync4(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2796
2960
|
`);
|
|
2797
|
-
|
|
2961
|
+
writeFileSync4(this.path(ticketStreakFile), "1\n");
|
|
2798
2962
|
}
|
|
2799
2963
|
}
|
|
2800
2964
|
};
|
|
@@ -2822,7 +2986,7 @@ var State = class _State {
|
|
|
2822
2986
|
}
|
|
2823
2987
|
},
|
|
2824
2988
|
persist: (next) => {
|
|
2825
|
-
|
|
2989
|
+
writeFileSync4(this.path(file), JSON.stringify(next));
|
|
2826
2990
|
}
|
|
2827
2991
|
};
|
|
2828
2992
|
}
|
|
@@ -2843,7 +3007,7 @@ var State = class _State {
|
|
|
2843
3007
|
}
|
|
2844
3008
|
},
|
|
2845
3009
|
persist: (next) => {
|
|
2846
|
-
|
|
3010
|
+
writeFileSync4(this.path(file), JSON.stringify(next));
|
|
2847
3011
|
}
|
|
2848
3012
|
};
|
|
2849
3013
|
}
|
|
@@ -2856,7 +3020,7 @@ var State = class _State {
|
|
|
2856
3020
|
const file = ".release-last-run";
|
|
2857
3021
|
return {
|
|
2858
3022
|
touch: () => {
|
|
2859
|
-
|
|
3023
|
+
writeFileSync4(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
|
|
2860
3024
|
},
|
|
2861
3025
|
at: () => {
|
|
2862
3026
|
try {
|
|
@@ -3173,10 +3337,10 @@ function describeRelease(o, route) {
|
|
|
3173
3337
|
}
|
|
3174
3338
|
|
|
3175
3339
|
// src/install.ts
|
|
3176
|
-
import { dirname as dirname4, join as
|
|
3177
|
-
import { homedir as
|
|
3178
|
-
import { accessSync, constants, existsSync as
|
|
3179
|
-
import { spawnSync as
|
|
3340
|
+
import { dirname as dirname4, join as join8, extname } from "path";
|
|
3341
|
+
import { homedir as homedir5 } from "os";
|
|
3342
|
+
import { accessSync, constants, existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
|
|
3343
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
3180
3344
|
import { createHash } from "crypto";
|
|
3181
3345
|
|
|
3182
3346
|
// src/runtime-info.ts
|
|
@@ -3187,7 +3351,7 @@ function isCompiledBinary(moduleUrl) {
|
|
|
3187
3351
|
// src/install.ts
|
|
3188
3352
|
function crewInvocation(crewHome) {
|
|
3189
3353
|
if (isCompiledBinary(import.meta.url)) return [process.execPath];
|
|
3190
|
-
return [process.execPath,
|
|
3354
|
+
return [process.execPath, join8(crewHome, "bin", "crew")];
|
|
3191
3355
|
}
|
|
3192
3356
|
var INTERVAL_SECONDS = 120;
|
|
3193
3357
|
function labelFor(crewHome, job) {
|
|
@@ -3225,7 +3389,7 @@ function pathFor(ship) {
|
|
|
3225
3389
|
return parts.join(":");
|
|
3226
3390
|
}
|
|
3227
3391
|
function findOnPath(bin, searchPath) {
|
|
3228
|
-
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) =>
|
|
3392
|
+
const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join8(dir, bin));
|
|
3229
3393
|
for (const candidate of candidates) {
|
|
3230
3394
|
try {
|
|
3231
3395
|
accessSync(candidate, constants.X_OK);
|
|
@@ -3235,7 +3399,7 @@ function findOnPath(bin, searchPath) {
|
|
|
3235
3399
|
}
|
|
3236
3400
|
return void 0;
|
|
3237
3401
|
}
|
|
3238
|
-
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin",
|
|
3402
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join8(homedir5(), ".docker", "bin")].join(":");
|
|
3239
3403
|
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
3240
3404
|
const schedulerPath = pathFor(ship);
|
|
3241
3405
|
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
@@ -3247,7 +3411,7 @@ function xmlEscape(s) {
|
|
|
3247
3411
|
}
|
|
3248
3412
|
function planLaunchd(ship, crewHome, job) {
|
|
3249
3413
|
const label = labelFor(crewHome, job);
|
|
3250
|
-
const unitPath =
|
|
3414
|
+
const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3251
3415
|
const crewLog = ship.logFile;
|
|
3252
3416
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3253
3417
|
const persistent = job === "run";
|
|
@@ -3299,8 +3463,8 @@ ${scheduleKeys}
|
|
|
3299
3463
|
}
|
|
3300
3464
|
function planSystemd(ship, crewHome, job) {
|
|
3301
3465
|
const label = labelFor(crewHome, job);
|
|
3302
|
-
const unitDir =
|
|
3303
|
-
const servicePath =
|
|
3466
|
+
const unitDir = join8(homedir5(), ".config", "systemd", "user");
|
|
3467
|
+
const servicePath = join8(unitDir, `${label}.service`);
|
|
3304
3468
|
const crewLog = ship.logFile;
|
|
3305
3469
|
const schedulerLog = schedulerLogFor(crewLog, job);
|
|
3306
3470
|
const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
|
|
@@ -3337,7 +3501,7 @@ WantedBy=default.target
|
|
|
3337
3501
|
schedulerLog
|
|
3338
3502
|
};
|
|
3339
3503
|
}
|
|
3340
|
-
const timerPath =
|
|
3504
|
+
const timerPath = join8(unitDir, `${label}.timer`);
|
|
3341
3505
|
const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
|
|
3342
3506
|
[Unit]
|
|
3343
3507
|
Description=${description}
|
|
@@ -3404,7 +3568,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3404
3568
|
function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
3405
3569
|
const label = labelFor(crewHome, job);
|
|
3406
3570
|
if (hostShip === "macos") {
|
|
3407
|
-
const unitPath =
|
|
3571
|
+
const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3408
3572
|
return {
|
|
3409
3573
|
mechanism: "launchd",
|
|
3410
3574
|
unitPaths: [unitPath],
|
|
@@ -3414,18 +3578,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3414
3578
|
};
|
|
3415
3579
|
}
|
|
3416
3580
|
if (hostShip === "linux" && hasSystemd) {
|
|
3417
|
-
const unitDir =
|
|
3581
|
+
const unitDir = join8(homedir5(), ".config", "systemd", "user");
|
|
3418
3582
|
if (job === "run") {
|
|
3419
3583
|
return {
|
|
3420
3584
|
mechanism: "systemd",
|
|
3421
|
-
unitPaths: [
|
|
3585
|
+
unitPaths: [join8(unitDir, `${label}.service`)],
|
|
3422
3586
|
crontabLine: null,
|
|
3423
3587
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
|
|
3424
3588
|
};
|
|
3425
3589
|
}
|
|
3426
3590
|
return {
|
|
3427
3591
|
mechanism: "systemd",
|
|
3428
|
-
unitPaths: [
|
|
3592
|
+
unitPaths: [join8(unitDir, `${label}.service`), join8(unitDir, `${label}.timer`)],
|
|
3429
3593
|
crontabLine: null,
|
|
3430
3594
|
unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
|
|
3431
3595
|
};
|
|
@@ -3443,7 +3607,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
3443
3607
|
function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
3444
3608
|
const label = labelFor(crewHome, "run");
|
|
3445
3609
|
if (hostShip === "macos") {
|
|
3446
|
-
const unitPath =
|
|
3610
|
+
const unitPath = join8(homedir5(), "Library", "LaunchAgents", `${label}.plist`);
|
|
3447
3611
|
return {
|
|
3448
3612
|
mechanism: "launchd",
|
|
3449
3613
|
startCommand: ["launchctl", "load", unitPath],
|
|
@@ -3467,7 +3631,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
|
|
|
3467
3631
|
}
|
|
3468
3632
|
function runAll(commands, cwd, log) {
|
|
3469
3633
|
for (const [bin, ...args] of commands) {
|
|
3470
|
-
const res =
|
|
3634
|
+
const res = spawnSync4(bin, args, { cwd, encoding: "utf8" });
|
|
3471
3635
|
if (res.error || res.status !== 0) {
|
|
3472
3636
|
log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
|
|
3473
3637
|
output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
|
|
@@ -3484,7 +3648,7 @@ async function applyInstall(plan, crewHome, dryRun, log) {
|
|
|
3484
3648
|
continue;
|
|
3485
3649
|
}
|
|
3486
3650
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
3487
|
-
|
|
3651
|
+
writeFileSync5(path, plan.unitContent[path]);
|
|
3488
3652
|
log.emit(`wrote ${path}`);
|
|
3489
3653
|
}
|
|
3490
3654
|
if (plan.crontabLine) {
|
|
@@ -3532,13 +3696,13 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3532
3696
|
if (dryRun) {
|
|
3533
3697
|
log.emit(`would run: launchctl remove ${plan.removeLabel}`);
|
|
3534
3698
|
} else {
|
|
3535
|
-
if (
|
|
3699
|
+
if (spawnSync4("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
|
|
3536
3700
|
log.emit(`removed loaded job ${plan.removeLabel}`);
|
|
3537
3701
|
}
|
|
3538
3702
|
}
|
|
3539
3703
|
}
|
|
3540
3704
|
for (const path of plan.unitPaths) {
|
|
3541
|
-
if (!
|
|
3705
|
+
if (!existsSync7(path)) continue;
|
|
3542
3706
|
if (dryRun) {
|
|
3543
3707
|
log.emit(`would remove ${path}`);
|
|
3544
3708
|
continue;
|
|
@@ -3557,7 +3721,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
3557
3721
|
}
|
|
3558
3722
|
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
3559
3723
|
function launchAgentsDir() {
|
|
3560
|
-
return
|
|
3724
|
+
return join8(homedir5(), "Library", "LaunchAgents");
|
|
3561
3725
|
}
|
|
3562
3726
|
function xmlUnescape(s) {
|
|
3563
3727
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
@@ -3569,14 +3733,14 @@ function programFromPlist(text) {
|
|
|
3569
3733
|
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
3570
3734
|
}
|
|
3571
3735
|
function loadedCrewLabels() {
|
|
3572
|
-
const list =
|
|
3736
|
+
const list = spawnSync4("launchctl", ["list"], { encoding: "utf8" });
|
|
3573
3737
|
if (list.status !== 0) return [];
|
|
3574
3738
|
return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
|
|
3575
3739
|
}
|
|
3576
3740
|
function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
3577
|
-
const files =
|
|
3741
|
+
const files = existsSync7(dir) ? readdirSync(dir) : [];
|
|
3578
3742
|
const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
3579
|
-
const plistPath =
|
|
3743
|
+
const plistPath = join8(dir, f);
|
|
3580
3744
|
let program = null;
|
|
3581
3745
|
try {
|
|
3582
3746
|
program = programFromPlist(readFileSync7(plistPath, "utf8"));
|
|
@@ -3586,13 +3750,13 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
|
|
|
3586
3750
|
label: f.slice(0, -".plist".length),
|
|
3587
3751
|
plistPath,
|
|
3588
3752
|
program,
|
|
3589
|
-
programExists: program !== null &&
|
|
3753
|
+
programExists: program !== null && existsSync7(program)
|
|
3590
3754
|
};
|
|
3591
3755
|
});
|
|
3592
3756
|
const seen = new Set(units.map((u) => u.label));
|
|
3593
3757
|
for (const label of [...loaded].sort()) {
|
|
3594
3758
|
if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
|
|
3595
|
-
units.push({ label, plistPath:
|
|
3759
|
+
units.push({ label, plistPath: join8(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
|
|
3596
3760
|
}
|
|
3597
3761
|
return units;
|
|
3598
3762
|
}
|
|
@@ -3605,9 +3769,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
|
|
|
3605
3769
|
log.emit(`would unload and remove ${unit.label}`);
|
|
3606
3770
|
return;
|
|
3607
3771
|
}
|
|
3608
|
-
if (
|
|
3609
|
-
|
|
3610
|
-
if (
|
|
3772
|
+
if (existsSync7(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
3773
|
+
spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
3774
|
+
if (existsSync7(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
3611
3775
|
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
3612
3776
|
}
|
|
3613
3777
|
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
|
|
@@ -3639,7 +3803,7 @@ function parseLaunchctlList(text) {
|
|
|
3639
3803
|
return jobs;
|
|
3640
3804
|
}
|
|
3641
3805
|
function describeForeignLaunchdUnits(crewHome, dir) {
|
|
3642
|
-
const list =
|
|
3806
|
+
const list = spawnSync4("launchctl", ["list"], { encoding: "utf8" });
|
|
3643
3807
|
const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
|
|
3644
3808
|
const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
|
|
3645
3809
|
return foreign.map((u) => {
|
|
@@ -3652,11 +3816,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
|
|
|
3652
3816
|
});
|
|
3653
3817
|
}
|
|
3654
3818
|
function readCrontab() {
|
|
3655
|
-
const res =
|
|
3819
|
+
const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
|
|
3656
3820
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
3657
3821
|
}
|
|
3658
3822
|
function writeCrontab(text) {
|
|
3659
|
-
|
|
3823
|
+
spawnSync4("crontab", ["-"], { input: text, encoding: "utf8" });
|
|
3660
3824
|
}
|
|
3661
3825
|
function addCrontabLine(marker, line) {
|
|
3662
3826
|
const existing = readCrontab();
|
|
@@ -3680,7 +3844,7 @@ function removeCrontabLine(marker) {
|
|
|
3680
3844
|
}
|
|
3681
3845
|
function detectSystemd() {
|
|
3682
3846
|
if (process.platform !== "linux") return false;
|
|
3683
|
-
const res =
|
|
3847
|
+
const res = spawnSync4("systemctl", ["--user", "status"], { encoding: "utf8" });
|
|
3684
3848
|
return !res.error;
|
|
3685
3849
|
}
|
|
3686
3850
|
|
|
@@ -3690,7 +3854,7 @@ import { statfsSync } from "fs";
|
|
|
3690
3854
|
// src/git.ts
|
|
3691
3855
|
import { execFileSync } from "child_process";
|
|
3692
3856
|
import {
|
|
3693
|
-
existsSync as
|
|
3857
|
+
existsSync as existsSync8,
|
|
3694
3858
|
mkdirSync as mkdirSync5,
|
|
3695
3859
|
openSync as openSync2,
|
|
3696
3860
|
closeSync as closeSync2,
|
|
@@ -4062,7 +4226,7 @@ function cloneUrlFor(remote) {
|
|
|
4062
4226
|
return `git@github.com:${remote}.git`;
|
|
4063
4227
|
}
|
|
4064
4228
|
function ensureRepoCheckout(dir, remote) {
|
|
4065
|
-
if (
|
|
4229
|
+
if (existsSync8(`${dir}/.git`)) return false;
|
|
4066
4230
|
if (!remote) {
|
|
4067
4231
|
throw new GitError(
|
|
4068
4232
|
`${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`
|
|
@@ -5799,8 +5963,8 @@ function materializeBranchForTicket(dir, cfg, t, role) {
|
|
|
5799
5963
|
}
|
|
5800
5964
|
|
|
5801
5965
|
// src/poll.ts
|
|
5802
|
-
import { writeFileSync as
|
|
5803
|
-
import { join as
|
|
5966
|
+
import { writeFileSync as writeFileSync6 } from "fs";
|
|
5967
|
+
import { join as join9 } from "path";
|
|
5804
5968
|
|
|
5805
5969
|
// src/stalled.ts
|
|
5806
5970
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -6072,7 +6236,7 @@ async function decideCycle(o) {
|
|
|
6072
6236
|
}
|
|
6073
6237
|
return decision;
|
|
6074
6238
|
}
|
|
6075
|
-
var digestPath = (stateDir, route, role) =>
|
|
6239
|
+
var digestPath = (stateDir, route, role) => join9(stateDir, `digest-${route}-${role}.md`);
|
|
6076
6240
|
function writeDigest(o, d, role, stateDir) {
|
|
6077
6241
|
const me = o.route.resolved?.seats[role];
|
|
6078
6242
|
if (!me) return false;
|
|
@@ -6134,7 +6298,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
6134
6298
|
};
|
|
6135
6299
|
const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
6136
6300
|
const path = digestPath(stateDir, routeSlug(o.route.route), role);
|
|
6137
|
-
|
|
6301
|
+
writeFileSync6(path, text);
|
|
6138
6302
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
6139
6303
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
6140
6304
|
});
|
|
@@ -6442,9 +6606,9 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
6442
6606
|
|
|
6443
6607
|
// src/agent.ts
|
|
6444
6608
|
import { spawn as spawn2 } from "child_process";
|
|
6445
|
-
import { readFileSync as readFileSync8, existsSync as
|
|
6609
|
+
import { readFileSync as readFileSync8, existsSync as existsSync9, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, createWriteStream } from "fs";
|
|
6446
6610
|
import { finished } from "stream/promises";
|
|
6447
|
-
import { join as
|
|
6611
|
+
import { join as join10, dirname as dirname6 } from "path";
|
|
6448
6612
|
import { createHash as createHash2 } from "crypto";
|
|
6449
6613
|
|
|
6450
6614
|
// src/environment.ts
|
|
@@ -6842,18 +7006,18 @@ var AgentError = class extends Error {
|
|
|
6842
7006
|
var AgentSpawnError = class extends AgentError {
|
|
6843
7007
|
};
|
|
6844
7008
|
function planAgentRun(o) {
|
|
6845
|
-
const briefPath =
|
|
6846
|
-
if (!
|
|
7009
|
+
const briefPath = join10(o.route.promptsDir, "personas", `lane-${o.role}.md`);
|
|
7010
|
+
if (!existsSync9(briefPath)) {
|
|
6847
7011
|
throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
|
|
6848
7012
|
}
|
|
6849
|
-
const commonPath =
|
|
6850
|
-
if (!
|
|
7013
|
+
const commonPath = join10(o.route.promptsDir, "personas", "common.md");
|
|
7014
|
+
if (!existsSync9(commonPath)) throw new AgentError(`no shared policy at ${commonPath}`);
|
|
6851
7015
|
const now = o.now ?? (() => Date.now());
|
|
6852
7016
|
const routeLabel = routeSlug(o.route.route);
|
|
6853
|
-
const digestPath2 =
|
|
7017
|
+
const digestPath2 = join10(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
|
|
6854
7018
|
let digest;
|
|
6855
7019
|
let ageSeconds;
|
|
6856
|
-
if (
|
|
7020
|
+
if (existsSync9(digestPath2)) {
|
|
6857
7021
|
ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
|
|
6858
7022
|
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync8(digestPath2, "utf8");
|
|
6859
7023
|
}
|
|
@@ -6869,9 +7033,9 @@ function planAgentRun(o) {
|
|
|
6869
7033
|
digest
|
|
6870
7034
|
});
|
|
6871
7035
|
const promptSha = createHash2("sha256").update(`${effectiveCommon}${effectiveBrief}`).digest("hex");
|
|
6872
|
-
const streamsDir =
|
|
7036
|
+
const streamsDir = join10(o.stateDir, "streams");
|
|
6873
7037
|
const base = `${routeLabel}-${o.role}-${o.cycle}`;
|
|
6874
|
-
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}));
|
|
7038
|
+
const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
|
|
6875
7039
|
const { toolchain, model } = seat;
|
|
6876
7040
|
const adapter = adapterFor(toolchain.harness);
|
|
6877
7041
|
const invocation = adapter.invocation(toolchain, {
|
|
@@ -6892,8 +7056,9 @@ function planAgentRun(o) {
|
|
|
6892
7056
|
role: o.role,
|
|
6893
7057
|
route: routeLabel,
|
|
6894
7058
|
// The route's directory, which is a STARTING POINT and not the
|
|
6895
|
-
// ticket's repo: the
|
|
6896
|
-
//
|
|
7059
|
+
// ticket's repo: the session picks its own working directory even when
|
|
7060
|
+
// the runner already selected a ticket (`o.repo` is known then, and
|
|
7061
|
+
// only feeds the per-repo toolchain choice). Where an area spans
|
|
6897
7062
|
// several repos this is whichever one `repos` happened to list first
|
|
6898
7063
|
// (config.ts), and the digest's `repo` column is what actually places
|
|
6899
7064
|
// the work. Kept deliberately rather than guessed at (ISSUE-349).
|
|
@@ -6924,8 +7089,8 @@ function planAgentRun(o) {
|
|
|
6924
7089
|
digestAttached: digest !== void 0,
|
|
6925
7090
|
digestAgeSeconds: ageSeconds,
|
|
6926
7091
|
model,
|
|
6927
|
-
streamPath:
|
|
6928
|
-
eventsPath:
|
|
7092
|
+
streamPath: join10(streamsDir, `${base}.jsonl`),
|
|
7093
|
+
eventsPath: join10(streamsDir, `${base}.events.jsonl`),
|
|
6929
7094
|
ticket: o.ticket,
|
|
6930
7095
|
agentLog,
|
|
6931
7096
|
promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
|
|
@@ -6995,8 +7160,8 @@ function spawnAgent(plan, emit) {
|
|
|
6995
7160
|
return;
|
|
6996
7161
|
}
|
|
6997
7162
|
try {
|
|
6998
|
-
const
|
|
6999
|
-
if (
|
|
7163
|
+
const got = adapter.extractResult(parsed);
|
|
7164
|
+
if (got) result = { ...result, ...Object.fromEntries(Object.entries(got).filter(([, v]) => v !== void 0)) };
|
|
7000
7165
|
consume(parsed, mapper.map(parsed));
|
|
7001
7166
|
} catch {
|
|
7002
7167
|
}
|
|
@@ -7072,7 +7237,7 @@ function spawnAgent(plan, emit) {
|
|
|
7072
7237
|
provider: plan.provider ?? LEGACY_TOOLCHAIN_PROVIDER
|
|
7073
7238
|
};
|
|
7074
7239
|
try {
|
|
7075
|
-
|
|
7240
|
+
writeFileSync7(`${plan.streamPath}.meta.json`, JSON.stringify({
|
|
7076
7241
|
route: plan.route,
|
|
7077
7242
|
role: plan.role,
|
|
7078
7243
|
ticket: plan.ticket ?? null,
|
|
@@ -7131,13 +7296,13 @@ function spawnAgent(plan, emit) {
|
|
|
7131
7296
|
}
|
|
7132
7297
|
|
|
7133
7298
|
// src/run-worktree.ts
|
|
7134
|
-
import { existsSync as
|
|
7135
|
-
import { dirname as dirname7, join as
|
|
7299
|
+
import { existsSync as existsSync11 } from "fs";
|
|
7300
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
7136
7301
|
|
|
7137
7302
|
// src/ports.ts
|
|
7138
7303
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
7139
|
-
import { existsSync as
|
|
7140
|
-
import { join as
|
|
7304
|
+
import { existsSync as existsSync10 } from "fs";
|
|
7305
|
+
import { join as join11 } from "path";
|
|
7141
7306
|
var run = (cmd, args) => {
|
|
7142
7307
|
try {
|
|
7143
7308
|
return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -7182,7 +7347,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
7182
7347
|
}
|
|
7183
7348
|
function worktreeExists(parent, prefix, n) {
|
|
7184
7349
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
7185
|
-
if (
|
|
7350
|
+
if (existsSync10(join11(parent, name, ".git"))) return true;
|
|
7186
7351
|
}
|
|
7187
7352
|
return false;
|
|
7188
7353
|
}
|
|
@@ -7256,7 +7421,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
7256
7421
|
const existing = worktreeForNumber(dir, num);
|
|
7257
7422
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
7258
7423
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
7259
|
-
if (existing && !existing.prunable &&
|
|
7424
|
+
if (existing && !existing.prunable && existsSync11(existing.path)) {
|
|
7260
7425
|
const branch = existing.branch;
|
|
7261
7426
|
if (status(existing.path).length > 0) return { action: "leave-dirty", path: existing.path, branch };
|
|
7262
7427
|
if (!branch) return { action: "leave-unpushed", path: existing.path, branch: "(detached HEAD)" };
|
|
@@ -7270,7 +7435,7 @@ function planRunWorktree(dir, cfg, t, role) {
|
|
|
7270
7435
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
7271
7436
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
7272
7437
|
}
|
|
7273
|
-
const path =
|
|
7438
|
+
const path = join12(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
|
|
7274
7439
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
7275
7440
|
return { action: "cut-remote", path, branch: located.branch };
|
|
7276
7441
|
}
|
|
@@ -7317,7 +7482,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
7317
7482
|
}
|
|
7318
7483
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
7319
7484
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
7320
|
-
if (!wt || !
|
|
7485
|
+
if (!wt || !existsSync11(wt.path)) return { kind: "none" };
|
|
7321
7486
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
7322
7487
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
7323
7488
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -7329,7 +7494,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
7329
7494
|
if (pids.length) await killGently(pids);
|
|
7330
7495
|
}
|
|
7331
7496
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
7332
|
-
return
|
|
7497
|
+
return existsSync11(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
7333
7498
|
}
|
|
7334
7499
|
|
|
7335
7500
|
// src/handoff-guard.ts
|
|
@@ -7478,13 +7643,13 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
7478
7643
|
}
|
|
7479
7644
|
|
|
7480
7645
|
// src/connect-wizard.ts
|
|
7481
|
-
import { delimiter, dirname as dirname8, join as
|
|
7482
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
7646
|
+
import { delimiter, dirname as dirname8, join as join13 } from "path";
|
|
7647
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync12, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
7483
7648
|
import { stringify } from "yaml";
|
|
7484
7649
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
7485
7650
|
function shouldRunWizard(opts) {
|
|
7486
7651
|
if (!opts.isTTY) return false;
|
|
7487
|
-
const exists = opts.configExists ??
|
|
7652
|
+
const exists = opts.configExists ?? existsSync12;
|
|
7488
7653
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
7489
7654
|
}
|
|
7490
7655
|
function firstRunConfigPath() {
|
|
@@ -7501,7 +7666,7 @@ function defaultIsExecutable(p) {
|
|
|
7501
7666
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
7502
7667
|
for (const dir of pathEnv.split(delimiter)) {
|
|
7503
7668
|
if (!dir) continue;
|
|
7504
|
-
const candidate =
|
|
7669
|
+
const candidate = join13(dir, name);
|
|
7505
7670
|
if (isExecutable(candidate)) return candidate;
|
|
7506
7671
|
}
|
|
7507
7672
|
return void 0;
|
|
@@ -7564,7 +7729,7 @@ ${routeBlock}`;
|
|
|
7564
7729
|
function writeNewConfig(path, text) {
|
|
7565
7730
|
mkdirSync7(dirname8(path), { recursive: true });
|
|
7566
7731
|
try {
|
|
7567
|
-
|
|
7732
|
+
writeFileSync8(path, text, { mode: 384, flag: "wx" });
|
|
7568
7733
|
return true;
|
|
7569
7734
|
} catch (e) {
|
|
7570
7735
|
if (e.code === "EEXIST") return false;
|
|
@@ -7596,8 +7761,8 @@ Wrote ${o.configPath}.
|
|
|
7596
7761
|
}
|
|
7597
7762
|
|
|
7598
7763
|
// src/release-run.ts
|
|
7599
|
-
import { readFileSync as readFileSync9, writeFileSync as
|
|
7600
|
-
import { dirname as dirname9, join as
|
|
7764
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync13, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
|
|
7765
|
+
import { dirname as dirname9, join as join14 } from "path";
|
|
7601
7766
|
import { tmpdir as tmpdir2 } from "os";
|
|
7602
7767
|
|
|
7603
7768
|
// src/release.ts
|
|
@@ -8255,9 +8420,9 @@ async function bump(o, size) {
|
|
|
8255
8420
|
}
|
|
8256
8421
|
return printed;
|
|
8257
8422
|
}
|
|
8258
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
8423
|
+
const files = o.repo.release.versionFiles.map((f) => join14(o.cwd, f));
|
|
8259
8424
|
const first = files[0];
|
|
8260
|
-
if (!
|
|
8425
|
+
if (!existsSync13(first)) {
|
|
8261
8426
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
8262
8427
|
return null;
|
|
8263
8428
|
}
|
|
@@ -8280,21 +8445,21 @@ async function bump(o, size) {
|
|
|
8280
8445
|
o.emit.error(`no version field in ${f}`);
|
|
8281
8446
|
return null;
|
|
8282
8447
|
}
|
|
8283
|
-
|
|
8448
|
+
writeFileSync9(f, out);
|
|
8284
8449
|
}
|
|
8285
8450
|
return version;
|
|
8286
8451
|
}
|
|
8287
8452
|
function writeChangelog(o, version, entries) {
|
|
8288
8453
|
const file = o.repo.release.changelog;
|
|
8289
8454
|
if (!file || entries.length === 0) return;
|
|
8290
|
-
const path =
|
|
8455
|
+
const path = join14(o.cwd, file);
|
|
8291
8456
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
8292
8457
|
if (o.dryRun) {
|
|
8293
8458
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
8294
8459
|
return;
|
|
8295
8460
|
}
|
|
8296
|
-
const existing =
|
|
8297
|
-
|
|
8461
|
+
const existing = existsSync13(path) ? readFileSync9(path, "utf8") : "# Changelog\n";
|
|
8462
|
+
writeFileSync9(path, insertChangelogSection(existing, section));
|
|
8298
8463
|
}
|
|
8299
8464
|
async function confirm(o, expected) {
|
|
8300
8465
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -8375,11 +8540,11 @@ async function confirmLastRelease(o, decision) {
|
|
|
8375
8540
|
function openReleaseCheckout(o) {
|
|
8376
8541
|
const { remote, base } = o.repo.branch;
|
|
8377
8542
|
const branch = `crew/release-${Date.now()}`;
|
|
8378
|
-
const root = mkdtempSync(
|
|
8379
|
-
const dir =
|
|
8543
|
+
const root = mkdtempSync(join14(tmpdir2(), "crew-release-"));
|
|
8544
|
+
const dir = join14(root, "checkout");
|
|
8380
8545
|
const close = () => {
|
|
8381
8546
|
gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
|
|
8382
|
-
|
|
8547
|
+
rmSync6(root, { recursive: true, force: true });
|
|
8383
8548
|
gitOk(o.cwd, ["worktree", "prune"]);
|
|
8384
8549
|
gitOk(o.cwd, ["branch", "-D", branch]);
|
|
8385
8550
|
};
|
|
@@ -8390,10 +8555,10 @@ function openReleaseCheckout(o) {
|
|
|
8390
8555
|
throw e;
|
|
8391
8556
|
}
|
|
8392
8557
|
for (const f of o.repo.worktrees.copy) {
|
|
8393
|
-
const src =
|
|
8394
|
-
if (!
|
|
8395
|
-
mkdirSync8(dirname9(
|
|
8396
|
-
copyFileSync(src,
|
|
8558
|
+
const src = join14(o.cwd, f);
|
|
8559
|
+
if (!existsSync13(src)) continue;
|
|
8560
|
+
mkdirSync8(dirname9(join14(dir, f)), { recursive: true });
|
|
8561
|
+
copyFileSync(src, join14(dir, f));
|
|
8397
8562
|
}
|
|
8398
8563
|
return { dir, branch, close };
|
|
8399
8564
|
}
|
|
@@ -8862,7 +9027,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
8862
9027
|
}
|
|
8863
9028
|
|
|
8864
9029
|
// src/watch.ts
|
|
8865
|
-
import { watch as watchFile, existsSync as
|
|
9030
|
+
import { watch as watchFile, existsSync as existsSync14, statSync as statSync6, openSync as openSync3, readSync, closeSync as closeSync3 } from "fs";
|
|
8866
9031
|
var E = "\x1B[";
|
|
8867
9032
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
8868
9033
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -8963,7 +9128,7 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
8963
9128
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
8964
9129
|
}
|
|
8965
9130
|
function readFrom(file, from) {
|
|
8966
|
-
if (!
|
|
9131
|
+
if (!existsSync14(file)) return { events: [], offset: from };
|
|
8967
9132
|
const size = statSync6(file).size;
|
|
8968
9133
|
if (size < from) return readFrom(file, 0);
|
|
8969
9134
|
if (size === from) return { events: [], offset: from };
|
|
@@ -9203,8 +9368,8 @@ function nextRoles(decision, ran) {
|
|
|
9203
9368
|
}
|
|
9204
9369
|
|
|
9205
9370
|
// src/agents.ts
|
|
9206
|
-
import { readFileSync as readFileSync10, existsSync as
|
|
9207
|
-
import { join as
|
|
9371
|
+
import { readFileSync as readFileSync10, existsSync as existsSync15 } from "fs";
|
|
9372
|
+
import { join as join15 } from "path";
|
|
9208
9373
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
9209
9374
|
var PERSONA_NAME = {
|
|
9210
9375
|
dev: "Developer",
|
|
@@ -9218,11 +9383,11 @@ var CREW_AGENT_COLUMN = "agent_id";
|
|
|
9218
9383
|
var AgentsSyncError = class extends Error {
|
|
9219
9384
|
};
|
|
9220
9385
|
function personaDefaultPrompt(promptsDir, role) {
|
|
9221
|
-
const briefPath =
|
|
9222
|
-
if (!
|
|
9386
|
+
const briefPath = join15(promptsDir, "personas", `lane-${role}.md`);
|
|
9387
|
+
if (!existsSync15(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
9223
9388
|
if (role === "pair") return readFileSync10(briefPath, "utf8");
|
|
9224
|
-
const commonPath =
|
|
9225
|
-
if (!
|
|
9389
|
+
const commonPath = join15(promptsDir, "personas", "common.md");
|
|
9390
|
+
if (!existsSync15(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
|
|
9226
9391
|
return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
|
|
9227
9392
|
}
|
|
9228
9393
|
async function resolveAgentId(route, role, opts) {
|
|
@@ -9900,8 +10065,8 @@ import {
|
|
|
9900
10065
|
} from "@tablation/client";
|
|
9901
10066
|
|
|
9902
10067
|
// src/skills.ts
|
|
9903
|
-
import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as
|
|
9904
|
-
import { join as
|
|
10068
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync2, existsSync as existsSync16 } from "fs";
|
|
10069
|
+
import { join as join16 } from "path";
|
|
9905
10070
|
import { parse as parseYaml } from "yaml";
|
|
9906
10071
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
9907
10072
|
var SkillSyncError = class extends Error {
|
|
@@ -9920,9 +10085,9 @@ function parseSkillFile(path) {
|
|
|
9920
10085
|
return { name: meta.name, description: meta.description, prompt: body, file: path };
|
|
9921
10086
|
}
|
|
9922
10087
|
function listLocalSkills(promptsDir) {
|
|
9923
|
-
const dir =
|
|
9924
|
-
if (!
|
|
9925
|
-
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(
|
|
10088
|
+
const dir = join16(promptsDir, "skills");
|
|
10089
|
+
if (!existsSync16(dir)) return [];
|
|
10090
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join16(dir, f)));
|
|
9926
10091
|
}
|
|
9927
10092
|
async function syncSkills(route, opts) {
|
|
9928
10093
|
if (!route.resolved) {
|
|
@@ -10101,7 +10266,7 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
10101
10266
|
}
|
|
10102
10267
|
|
|
10103
10268
|
// src/repos-cmd.ts
|
|
10104
|
-
import { homedir as
|
|
10269
|
+
import { homedir as homedir6 } from "os";
|
|
10105
10270
|
import { basename as basename7, resolve as resolve3 } from "path";
|
|
10106
10271
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
10107
10272
|
var ReposError = class extends Error {
|
|
@@ -10148,7 +10313,7 @@ function parseAddArgs(args, routes, isDir) {
|
|
|
10148
10313
|
return { route, path: "." };
|
|
10149
10314
|
}
|
|
10150
10315
|
function expandDir(p, base) {
|
|
10151
|
-
const home = p === "~" ?
|
|
10316
|
+
const home = p === "~" ? homedir6() : p.startsWith("~/") ? `${homedir6()}/${p.slice(2)}` : p;
|
|
10152
10317
|
return resolve3(base, home);
|
|
10153
10318
|
}
|
|
10154
10319
|
function routeMapOf(doc, route) {
|
|
@@ -10283,7 +10448,7 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
10283
10448
|
}
|
|
10284
10449
|
|
|
10285
10450
|
// src/worktree-sweep.ts
|
|
10286
|
-
import { existsSync as
|
|
10451
|
+
import { existsSync as existsSync17 } from "fs";
|
|
10287
10452
|
import { basename as basename8 } from "path";
|
|
10288
10453
|
function planWorktreeSweep(target, tickets, contract) {
|
|
10289
10454
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
@@ -10311,7 +10476,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
10311
10476
|
let removed = 0;
|
|
10312
10477
|
let keptBranches = 0;
|
|
10313
10478
|
for (const a of actions) {
|
|
10314
|
-
if (!dryRun &&
|
|
10479
|
+
if (!dryRun && existsSync17(a.path)) {
|
|
10315
10480
|
let dirty = false;
|
|
10316
10481
|
try {
|
|
10317
10482
|
dirty = status(a.path).length > 0;
|
|
@@ -10325,7 +10490,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
|
|
|
10325
10490
|
continue;
|
|
10326
10491
|
}
|
|
10327
10492
|
}
|
|
10328
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
10493
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync17(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
10329
10494
|
log.warn(
|
|
10330
10495
|
`not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
|
|
10331
10496
|
{ ticket: a.ticket.issue_id, step: "worktree" }
|
|
@@ -10406,14 +10571,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
10406
10571
|
}
|
|
10407
10572
|
|
|
10408
10573
|
// src/stream-sweep.ts
|
|
10409
|
-
import { existsSync as
|
|
10410
|
-
import { join as
|
|
10574
|
+
import { existsSync as existsSync18, readdirSync as readdirSync3, rmSync as rmSync7, statSync as statSync7 } from "fs";
|
|
10575
|
+
import { join as join17 } from "path";
|
|
10411
10576
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
10412
10577
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
10413
|
-
const dir =
|
|
10414
|
-
if (!
|
|
10578
|
+
const dir = join17(stateDir, "streams");
|
|
10579
|
+
if (!existsSync18(dir)) return [];
|
|
10415
10580
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
10416
|
-
return readdirSync3(dir).map((name) =>
|
|
10581
|
+
return readdirSync3(dir).map((name) => join17(dir, name)).filter((path) => {
|
|
10417
10582
|
try {
|
|
10418
10583
|
return statSync7(path).mtimeMs < cutoff;
|
|
10419
10584
|
} catch {
|
|
@@ -10429,7 +10594,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
10429
10594
|
continue;
|
|
10430
10595
|
}
|
|
10431
10596
|
try {
|
|
10432
|
-
|
|
10597
|
+
rmSync7(path, { force: true });
|
|
10433
10598
|
removed++;
|
|
10434
10599
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
10435
10600
|
} catch (e) {
|
|
@@ -10439,9 +10604,9 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
10439
10604
|
return removed;
|
|
10440
10605
|
}
|
|
10441
10606
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
10442
|
-
if (!
|
|
10607
|
+
if (!existsSync18(stateDir)) return [];
|
|
10443
10608
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
10444
|
-
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) =>
|
|
10609
|
+
return readdirSync3(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join17(stateDir, name)).filter((path) => {
|
|
10445
10610
|
try {
|
|
10446
10611
|
return statSync7(path).mtimeMs < cutoff;
|
|
10447
10612
|
} catch {
|
|
@@ -10451,23 +10616,23 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
10451
10616
|
}
|
|
10452
10617
|
|
|
10453
10618
|
// src/cli.ts
|
|
10454
|
-
import { readFileSync as readFileSync15, existsSync as
|
|
10619
|
+
import { readFileSync as readFileSync15, existsSync as existsSync22, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
|
|
10455
10620
|
import { createInterface } from "readline/promises";
|
|
10456
10621
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
10457
10622
|
|
|
10458
10623
|
// src/ssh-keys.ts
|
|
10459
10624
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
10460
|
-
import { existsSync as
|
|
10461
|
-
import { dirname as dirname11, join as
|
|
10625
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync12, chmodSync } from "fs";
|
|
10626
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
10462
10627
|
var SshKeyError = class extends Error {
|
|
10463
10628
|
};
|
|
10464
10629
|
function shipSshKeyPathFor(stateDir) {
|
|
10465
|
-
return
|
|
10630
|
+
return join18(stateDir, "ssh", "id_ed25519");
|
|
10466
10631
|
}
|
|
10467
10632
|
function ensureShipSshKeypair(stateDir) {
|
|
10468
10633
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
10469
10634
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
10470
|
-
if (!
|
|
10635
|
+
if (!existsSync19(privateKeyPath)) {
|
|
10471
10636
|
mkdirSync9(dirname11(privateKeyPath), { recursive: true });
|
|
10472
10637
|
try {
|
|
10473
10638
|
execFileSync3("ssh-keygen", [
|
|
@@ -10488,7 +10653,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
10488
10653
|
}
|
|
10489
10654
|
chmodSync(privateKeyPath, 384);
|
|
10490
10655
|
}
|
|
10491
|
-
if (!
|
|
10656
|
+
if (!existsSync19(publicKeyPath)) {
|
|
10492
10657
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
10493
10658
|
}
|
|
10494
10659
|
const publicKey = readFileSync12(publicKeyPath, "utf8").trim();
|
|
@@ -10705,9 +10870,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
10705
10870
|
|
|
10706
10871
|
// src/tunnel.ts
|
|
10707
10872
|
import { spawn as spawn3 } from "child_process";
|
|
10708
|
-
import { existsSync as
|
|
10873
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
|
|
10709
10874
|
import { randomInt } from "crypto";
|
|
10710
|
-
import { join as
|
|
10875
|
+
import { join as join19 } from "path";
|
|
10711
10876
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
10712
10877
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
10713
10878
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -10774,11 +10939,11 @@ function startTunnel(opts) {
|
|
|
10774
10939
|
};
|
|
10775
10940
|
}
|
|
10776
10941
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
10777
|
-
return
|
|
10942
|
+
return join19(stateDir, "passengers", `${workspaceId}.json`);
|
|
10778
10943
|
}
|
|
10779
10944
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
10780
10945
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
10781
|
-
if (!
|
|
10946
|
+
if (!existsSync20(path)) return void 0;
|
|
10782
10947
|
try {
|
|
10783
10948
|
return JSON.parse(readFileSync13(path, "utf8"));
|
|
10784
10949
|
} catch {
|
|
@@ -10787,8 +10952,8 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
10787
10952
|
}
|
|
10788
10953
|
function writePersistedTunnel(stateDir, state) {
|
|
10789
10954
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
10790
|
-
mkdirSync10(
|
|
10791
|
-
|
|
10955
|
+
mkdirSync10(join19(stateDir, "passengers"), { recursive: true });
|
|
10956
|
+
writeFileSync10(path, `${JSON.stringify(state, null, 2)}
|
|
10792
10957
|
`);
|
|
10793
10958
|
}
|
|
10794
10959
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -10885,8 +11050,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
10885
11050
|
}
|
|
10886
11051
|
}
|
|
10887
11052
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
10888
|
-
const dir =
|
|
10889
|
-
if (!
|
|
11053
|
+
const dir = join19(stateDir, "passengers");
|
|
11054
|
+
if (!existsSync20(dir)) return [];
|
|
10890
11055
|
return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
10891
11056
|
}
|
|
10892
11057
|
|
|
@@ -11007,8 +11172,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
11007
11172
|
import { createServer as createHttpServer } from "http";
|
|
11008
11173
|
import { spawn as spawn4 } from "child_process";
|
|
11009
11174
|
import { randomBytes } from "crypto";
|
|
11010
|
-
import { existsSync as
|
|
11011
|
-
import { join as
|
|
11175
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
|
|
11176
|
+
import { join as join20 } from "path";
|
|
11012
11177
|
|
|
11013
11178
|
// src/secret-compare.ts
|
|
11014
11179
|
import { timingSafeEqual } from "crypto";
|
|
@@ -11097,11 +11262,11 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
11097
11262
|
return child;
|
|
11098
11263
|
};
|
|
11099
11264
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
11100
|
-
return
|
|
11265
|
+
return join20(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
11101
11266
|
}
|
|
11102
11267
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
11103
11268
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
11104
|
-
if (!
|
|
11269
|
+
if (!existsSync21(path)) return void 0;
|
|
11105
11270
|
try {
|
|
11106
11271
|
return JSON.parse(readFileSync14(path, "utf8"));
|
|
11107
11272
|
} catch {
|
|
@@ -11110,8 +11275,8 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
11110
11275
|
}
|
|
11111
11276
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
11112
11277
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
11113
|
-
mkdirSync11(
|
|
11114
|
-
|
|
11278
|
+
mkdirSync11(join20(stateDir, "passengers"), { recursive: true });
|
|
11279
|
+
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
11115
11280
|
`);
|
|
11116
11281
|
}
|
|
11117
11282
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -11131,8 +11296,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
11131
11296
|
}
|
|
11132
11297
|
}
|
|
11133
11298
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
11134
|
-
const dir =
|
|
11135
|
-
if (!
|
|
11299
|
+
const dir = join20(stateDir, "passengers");
|
|
11300
|
+
if (!existsSync21(dir)) return [];
|
|
11136
11301
|
const suffix = "-sync.json";
|
|
11137
11302
|
return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
11138
11303
|
}
|
|
@@ -11190,7 +11355,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
11190
11355
|
|
|
11191
11356
|
// src/daemon.ts
|
|
11192
11357
|
import { readdirSync as readdirSync6, statSync as statSync8 } from "fs";
|
|
11193
|
-
import { join as
|
|
11358
|
+
import { join as join21 } from "path";
|
|
11194
11359
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
11195
11360
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
11196
11361
|
var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
|
|
@@ -11229,7 +11394,7 @@ function latestMtime(path) {
|
|
|
11229
11394
|
if (!stat.isDirectory()) return 0;
|
|
11230
11395
|
let latest = stat.mtimeMs;
|
|
11231
11396
|
for (const entry of readdirSync6(path, { withFileTypes: true })) {
|
|
11232
|
-
latest = Math.max(latest, latestMtime(
|
|
11397
|
+
latest = Math.max(latest, latestMtime(join21(path, entry.name)));
|
|
11233
11398
|
}
|
|
11234
11399
|
return latest;
|
|
11235
11400
|
}
|
|
@@ -11379,6 +11544,7 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
11379
11544
|
const { route, ship, emit } = ctx;
|
|
11380
11545
|
const tracker = new Tracker(route, ship);
|
|
11381
11546
|
let ticketHint = decision.actionable.top?.issue_id;
|
|
11547
|
+
let ticketRepo = repoTargetFor(route, decision.actionable.top?.repo_id)?.name;
|
|
11382
11548
|
let workingId = decision.actionable.top?.id ?? null;
|
|
11383
11549
|
if (role === "dev" || role === "design" || role === "qa") {
|
|
11384
11550
|
const seat = route.resolved?.seats[role];
|
|
@@ -11433,6 +11599,7 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
11433
11599
|
ticketHint = result.ticket.issue_id;
|
|
11434
11600
|
workingId = result.ticket.id;
|
|
11435
11601
|
const target = repoTargetFor(route, result.ticket.repo_id);
|
|
11602
|
+
ticketRepo = target?.name;
|
|
11436
11603
|
try {
|
|
11437
11604
|
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
11438
11605
|
emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role });
|
|
@@ -11455,6 +11622,7 @@ async function runRoleAgent(role, decision, ctx) {
|
|
|
11455
11622
|
apiKey: resolveApiKey(route),
|
|
11456
11623
|
cycle: emit.cycle,
|
|
11457
11624
|
ticket: ticketHint,
|
|
11625
|
+
repo: ticketRepo,
|
|
11458
11626
|
divergedPrompt: await fetchDivergedPrompt(route, role, { userAgent: ship.userAgent }),
|
|
11459
11627
|
agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
|
|
11460
11628
|
resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
|
|
@@ -12006,14 +12174,14 @@ if (command === "passenger-sync-daemon") {
|
|
|
12006
12174
|
}
|
|
12007
12175
|
if (host === "linux" && hasSystemd) {
|
|
12008
12176
|
const label = labelFor(CREW_HOME, "run");
|
|
12009
|
-
const staleTimer =
|
|
12010
|
-
if (
|
|
12177
|
+
const staleTimer = join22(homedir7(), ".config", "systemd", "user", `${label}.timer`);
|
|
12178
|
+
if (existsSync22(staleTimer)) {
|
|
12011
12179
|
process.stdout.write(
|
|
12012
12180
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
12013
12181
|
`
|
|
12014
12182
|
);
|
|
12015
12183
|
if (!dryRun) {
|
|
12016
|
-
|
|
12184
|
+
spawnSync5("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
12017
12185
|
try {
|
|
12018
12186
|
unlinkSync5(staleTimer);
|
|
12019
12187
|
} catch {
|
|
@@ -12033,7 +12201,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12033
12201
|
const tracker = new Tracker(c, cfg.ship);
|
|
12034
12202
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
12035
12203
|
for (const r of await resolvedRepos(c)) {
|
|
12036
|
-
if (!
|
|
12204
|
+
if (!existsSync22(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
12037
12205
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
12038
12206
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
12039
12207
|
}
|
|
@@ -12051,8 +12219,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
12051
12219
|
}
|
|
12052
12220
|
async function doctorFix(o) {
|
|
12053
12221
|
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))));
|
|
12054
|
-
const unitExists = (p) =>
|
|
12055
|
-
const crontab = o.host === "linux" ?
|
|
12222
|
+
const unitExists = (p) => existsSync22(p);
|
|
12223
|
+
const crontab = o.host === "linux" ? spawnSync5("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
12056
12224
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
12057
12225
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
12058
12226
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -12068,7 +12236,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
12068
12236
|
for (const a of plan.actions) {
|
|
12069
12237
|
if (a.kind === "enable") {
|
|
12070
12238
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
12071
|
-
|
|
12239
|
+
writeFileSync12(cfg.configFile, planEnable(readFileSync15(cfg.configFile, "utf8"), a.route));
|
|
12072
12240
|
enabledSound.add(a.route);
|
|
12073
12241
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
12074
12242
|
} else {
|
|
@@ -12542,6 +12710,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12542
12710
|
apiKey: resolveApiKey(w.route),
|
|
12543
12711
|
cycle: wemit.cycle,
|
|
12544
12712
|
ticket: w.decision.actionable.top?.issue_id,
|
|
12713
|
+
repo: repoTargetFor(w.route, w.decision.actionable.top?.repo_id)?.name,
|
|
12545
12714
|
divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
|
|
12546
12715
|
agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
|
|
12547
12716
|
resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
|
|
@@ -12559,6 +12728,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12559
12728
|
lock = fleetLock;
|
|
12560
12729
|
const fleetTracker = new Tracker(w.route, cfg.ship);
|
|
12561
12730
|
let fleetTicketHint = w.decision.actionable.top?.issue_id;
|
|
12731
|
+
let fleetTicketRepo = repoTargetFor(w.route, w.decision.actionable.top?.repo_id)?.name;
|
|
12562
12732
|
let fleetWorkingId = w.decision.actionable.top?.id ?? null;
|
|
12563
12733
|
if (w.role === "dev" || w.role === "design" || w.role === "qa") {
|
|
12564
12734
|
const seat = w.route.resolved?.seats[w.role];
|
|
@@ -12614,6 +12784,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12614
12784
|
fleetTicketHint = result.ticket.issue_id;
|
|
12615
12785
|
fleetWorkingId = result.ticket.id;
|
|
12616
12786
|
const target = repoTargetFor(w.route, result.ticket.repo_id);
|
|
12787
|
+
fleetTicketRepo = target?.name;
|
|
12617
12788
|
try {
|
|
12618
12789
|
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
12619
12790
|
wemit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: w.role });
|
|
@@ -12635,6 +12806,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12635
12806
|
apiKey: resolveApiKey(w.route),
|
|
12636
12807
|
cycle: wemit.cycle,
|
|
12637
12808
|
ticket: fleetTicketHint,
|
|
12809
|
+
repo: fleetTicketRepo,
|
|
12638
12810
|
divergedPrompt: await fetchDivergedPrompt(w.route, w.role, { userAgent: cfg.ship.userAgent }),
|
|
12639
12811
|
agentPersona: await fetchSeatPersona(w.route, w.role, { userAgent: cfg.ship.userAgent }),
|
|
12640
12812
|
resolvedAgentId: await resolveAgentId(w.route, w.role, { userAgent: cfg.ship.userAgent })
|
|
@@ -12752,6 +12924,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12752
12924
|
lock = got;
|
|
12753
12925
|
}
|
|
12754
12926
|
let ticketHint = decision.actionable.top?.issue_id;
|
|
12927
|
+
let ticketRepo = repoTargetFor(route, decision.actionable.top?.repo_id)?.name;
|
|
12755
12928
|
let workingId = decision.actionable.top?.id ?? null;
|
|
12756
12929
|
if (!dryRun && (current === "dev" || current === "design" || current === "qa")) {
|
|
12757
12930
|
const seat = route.resolved?.seats[current];
|
|
@@ -12813,6 +12986,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12813
12986
|
ticketHint = result.ticket.issue_id;
|
|
12814
12987
|
workingId = result.ticket.id;
|
|
12815
12988
|
const target = repoTargetFor(route, result.ticket.repo_id);
|
|
12989
|
+
ticketRepo = target?.name;
|
|
12816
12990
|
try {
|
|
12817
12991
|
if (target && ensureRepoCheckout(target.dir, target.remote)) {
|
|
12818
12992
|
emit.emit(`cloned ${target.name} into ${target.dir}`, { step: "select", role: current });
|
|
@@ -12840,6 +13014,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12840
13014
|
apiKey: resolveApiKey(route),
|
|
12841
13015
|
cycle: emit.cycle,
|
|
12842
13016
|
ticket: ticketHint,
|
|
13017
|
+
repo: ticketRepo,
|
|
12843
13018
|
divergedPrompt: await fetchDivergedPrompt(route, current, { userAgent: cfg.ship.userAgent }),
|
|
12844
13019
|
agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
|
|
12845
13020
|
resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
|
|
@@ -12924,7 +13099,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12924
13099
|
}
|
|
12925
13100
|
let failed = false;
|
|
12926
13101
|
for (const cmd2 of commands) {
|
|
12927
|
-
const res2 =
|
|
13102
|
+
const res2 = spawnSync5(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
12928
13103
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
12929
13104
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
12930
13105
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -12942,7 +13117,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12942
13117
|
`);
|
|
12943
13118
|
break;
|
|
12944
13119
|
}
|
|
12945
|
-
const res =
|
|
13120
|
+
const res = spawnSync5(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
12946
13121
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
12947
13122
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
12948
13123
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -12965,7 +13140,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
12965
13140
|
process.exit(1);
|
|
12966
13141
|
}
|
|
12967
13142
|
requireArmed("run the daemon loop");
|
|
12968
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
13143
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join22(CREW_HOME, "src"), join22(CREW_HOME, "dist"), join22(CREW_HOME, "bin")];
|
|
12969
13144
|
const spawnBreaker = new SpawnBreaker();
|
|
12970
13145
|
const stopReason = await runDaemonLoop({
|
|
12971
13146
|
runPass: () => runOnePass({
|
|
@@ -13114,11 +13289,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13114
13289
|
`);
|
|
13115
13290
|
if (!dryRun) {
|
|
13116
13291
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13117
|
-
const raw =
|
|
13292
|
+
const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
|
|
13118
13293
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
13119
13294
|
raw.agentSkills = skillResult.agentSkills;
|
|
13120
13295
|
mkdirSync12(dirOf(resolvedPath), { recursive: true });
|
|
13121
|
-
|
|
13296
|
+
writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
13122
13297
|
`);
|
|
13123
13298
|
}
|
|
13124
13299
|
if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
|
|
@@ -13163,10 +13338,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13163
13338
|
`);
|
|
13164
13339
|
if (!dryRun) {
|
|
13165
13340
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
13166
|
-
const raw =
|
|
13341
|
+
const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
|
|
13167
13342
|
raw.agentSkills = result.agentSkills;
|
|
13168
13343
|
mkdirSync12(dirOf(resolvedPath), { recursive: true });
|
|
13169
|
-
|
|
13344
|
+
writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
13170
13345
|
`);
|
|
13171
13346
|
}
|
|
13172
13347
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -13186,7 +13361,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13186
13361
|
if (sub === "list" && !routeName) usage2();
|
|
13187
13362
|
if (sub === "add") {
|
|
13188
13363
|
try {
|
|
13189
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
13364
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync22(resolvePath(p.replace(/^~(?=\/|$)/, homedir7()))));
|
|
13190
13365
|
routeName = a.route;
|
|
13191
13366
|
addPath = a.path;
|
|
13192
13367
|
addName = a.name;
|
|
@@ -13201,7 +13376,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13201
13376
|
}
|
|
13202
13377
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
13203
13378
|
let resolvedRemotes;
|
|
13204
|
-
if (
|
|
13379
|
+
if (existsSync22(resolvedFile)) {
|
|
13205
13380
|
try {
|
|
13206
13381
|
resolvedRemotes = JSON.parse(readFileSync15(resolvedFile, "utf8"));
|
|
13207
13382
|
} catch {
|
|
@@ -13218,14 +13393,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13218
13393
|
for (const r of repos) {
|
|
13219
13394
|
const origin2 = originOf(r.dir);
|
|
13220
13395
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
13221
|
-
const note = !
|
|
13396
|
+
const note = !existsSync22(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
13222
13397
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
13223
13398
|
`);
|
|
13224
13399
|
}
|
|
13225
13400
|
break;
|
|
13226
13401
|
}
|
|
13227
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
13228
|
-
const top =
|
|
13402
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir7()));
|
|
13403
|
+
const top = existsSync22(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
13229
13404
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
13230
13405
|
const origin = originOf(top);
|
|
13231
13406
|
const plan = planRepoAdd({
|
|
@@ -13274,15 +13449,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13274
13449
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
13275
13450
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
13276
13451
|
`);
|
|
13277
|
-
if (
|
|
13452
|
+
if (existsSync22(resolvedFile)) {
|
|
13278
13453
|
const raw = JSON.parse(readFileSync15(resolvedFile, "utf8"));
|
|
13279
13454
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
13280
13455
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
13281
|
-
|
|
13456
|
+
writeFileSync12(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
13282
13457
|
`);
|
|
13283
13458
|
}
|
|
13284
13459
|
}
|
|
13285
|
-
|
|
13460
|
+
writeFileSync12(cfg.configFile, plan.text);
|
|
13286
13461
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
13287
13462
|
`);
|
|
13288
13463
|
} catch (e) {
|
|
@@ -13521,7 +13696,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13521
13696
|
operator = await pickOperator(found.holds);
|
|
13522
13697
|
}
|
|
13523
13698
|
let previous;
|
|
13524
|
-
const hadPreviousFile =
|
|
13699
|
+
const hadPreviousFile = existsSync22(resolvedPath);
|
|
13525
13700
|
if (hadPreviousFile) {
|
|
13526
13701
|
try {
|
|
13527
13702
|
previous = JSON.parse(readFileSync15(resolvedPath, "utf8"));
|
|
@@ -13588,7 +13763,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13588
13763
|
mkdirSync12(dirname12(resolvedPath), { recursive: true });
|
|
13589
13764
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
13590
13765
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
13591
|
-
|
|
13766
|
+
writeFileSync12(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
13592
13767
|
`);
|
|
13593
13768
|
renameSync2(tmpPath, resolvedPath);
|
|
13594
13769
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -13603,7 +13778,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13603
13778
|
`);
|
|
13604
13779
|
} else {
|
|
13605
13780
|
mkdirSync12(dirname12(apiKeyFile), { recursive: true });
|
|
13606
|
-
|
|
13781
|
+
writeFileSync12(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
13607
13782
|
`, { mode: 384 });
|
|
13608
13783
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
13609
13784
|
`);
|
|
@@ -13612,7 +13787,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
13612
13787
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
13613
13788
|
const wizard = firstRun;
|
|
13614
13789
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
13615
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
13790
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join22(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
13616
13791
|
const routeBlock = renderConnection(
|
|
13617
13792
|
found,
|
|
13618
13793
|
route2,
|
|
@@ -14099,7 +14274,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14099
14274
|
}
|
|
14100
14275
|
case "disconnect": {
|
|
14101
14276
|
const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
|
|
14102
|
-
|
|
14277
|
+
const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
|
|
14278
|
+
for (const h of disconnectHarnesses) {
|
|
14103
14279
|
process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
|
|
14104
14280
|
`);
|
|
14105
14281
|
}
|
|
@@ -14250,6 +14426,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
14250
14426
|
repos: repos.map((r) => ({
|
|
14251
14427
|
name: r.name,
|
|
14252
14428
|
dir: r.dir,
|
|
14429
|
+
// Additive (CREW-1529): the toolchain a ticket in this repo runs on.
|
|
14430
|
+
toolchain: resolveToolchain(cfg.ship, route, r.name).name,
|
|
14253
14431
|
// Additive (CREW-1379): `unsafe` means a ship must not cut a
|
|
14254
14432
|
// worktree or release from this base. `version` stays 1.
|
|
14255
14433
|
baseBranch: baseHealth(r),
|
|
@@ -14352,7 +14530,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
14352
14530
|
);
|
|
14353
14531
|
try {
|
|
14354
14532
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
14355
|
-
const evSize =
|
|
14533
|
+
const evSize = existsSync22(evFile) ? statSync9(evFile).size : 0;
|
|
14356
14534
|
process.stdout.write(
|
|
14357
14535
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
14358
14536
|
` : "events log: ok\n"
|
|
@@ -14490,6 +14668,10 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
|
|
|
14490
14668
|
`);
|
|
14491
14669
|
}
|
|
14492
14670
|
}
|
|
14671
|
+
for (const [repoName, ov] of Object.entries(route.repoOverrides)) {
|
|
14672
|
+
if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
|
|
14673
|
+
`);
|
|
14674
|
+
}
|
|
14493
14675
|
process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
|
|
14494
14676
|
`);
|
|
14495
14677
|
if (!allBinsFound || !gitPath) {
|