@tablation/crew 0.32.0 → 0.34.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 CHANGED
@@ -29,8 +29,7 @@ ship:
29
29
 
30
30
  # Optional (CREW-1511): named toolchains — a harness adapter plus the binary
31
31
  # and model to run it with. The `agent:` block above keeps working and
32
- # defines an implicit toolchain named `claude`. Only the `claude` harness
33
- # exists today. See docs/toolchains.md.
32
+ # defines an implicit toolchain named `claude`. Harnesses: claude, gemini. See docs/toolchains.md.
34
33
  # toolchain: claude # the ship's default; a route may override it
35
34
  # toolchains:
36
35
  # claude-wrapped:
@@ -42,6 +41,7 @@ ship:
42
41
  # deep: { model: claude-opus-5-5 }
43
42
  # args: [] # appended to the harness argv
44
43
  # env: {} # added to the session environment
44
+ # gemini: { harness: gemini, bin: gemini, model: gemini-2.5-pro }
45
45
 
46
46
  # shell: bash # defaults per platform
47
47
  extraPath: "/opt/homebrew/bin" # prepended for hooks
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { resolve as resolve4, dirname as dirname12, join as join21, basename as basename9 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { hostname as hostname2, homedir as homedir6 } from "os";
7
- import { spawnSync as spawnSync3 } from "child_process";
7
+ import { spawnSync as spawnSync4 } from "child_process";
8
8
 
9
9
  // src/config.ts
10
10
  import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
@@ -159,6 +159,7 @@ var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
159
159
 
160
160
  // src/pair-install.ts
161
161
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync } from "fs";
162
+ import { spawnSync as spawnSync2 } from "child_process";
162
163
  import { homedir as homedir2 } from "os";
163
164
  import { dirname as dirname2, join as join3 } from "path";
164
165
 
@@ -615,6 +616,28 @@ function geminiMcpState(s, url) {
615
616
  if (e.headers?.Authorization !== `Bearer $${PAIR_KEY_VAR}`) return { state: "different", detail: `it does not read its key from $${PAIR_KEY_VAR}` };
616
617
  return { state: "registered" };
617
618
  }
619
+ function inspectGeminiMcp(o) {
620
+ const p = geminiPaths(o.home ?? homedir2());
621
+ const settings = readGeminiSettings(p.config);
622
+ if (!settings) return { state: "different", detail: `${p.config} is not a JSON object` };
623
+ const st = geminiMcpState(settings, o.url);
624
+ return st.state === "registered" ? { state: "registered", url: o.url } : st;
625
+ }
626
+ function ensureGeminiMcp(o) {
627
+ const p = geminiPaths(o.home ?? homedir2());
628
+ const settings = readGeminiSettings(p.config);
629
+ if (!settings) return `gemini: ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
630
+ const st = geminiMcpState(settings, o.url);
631
+ if (st.state === "registered") return `gemini: ${SERVER} MCP server already registered in ${p.config}`;
632
+ if (st.state === "different" && !o.replace) {
633
+ return `gemini: left ${p.config} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`;
634
+ }
635
+ if (!o.dryRun) {
636
+ writeFileMkdir(p.config, `${JSON.stringify({ ...settings, mcpServers: { ...settings.mcpServers, [SERVER]: geminiEntry(o.url) } }, null, 2)}
637
+ `);
638
+ }
639
+ return `gemini: ${o.dryRun ? "(dry run) would register" : "registered"} ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
640
+ }
618
641
  var geminiPair = {
619
642
  harness: "gemini",
620
643
  displayName: "Gemini CLI",
@@ -668,10 +691,144 @@ var geminiPair = {
668
691
  return hadBlock || hadMcp ? `pair(gemini): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(gemini): nothing to remove${kept}`;
669
692
  }
670
693
  };
694
+ var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
695
+ function vscodeUserDirs(o) {
696
+ const home = o.home ?? homedir2();
697
+ const platform2 = o.platform ?? process.platform;
698
+ const root = platform2 === "darwin" ? join3(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join3(home, "AppData", "Roaming") : join3(home, ".config");
699
+ const all = VSCODE_FLAVOURS.map((f) => join3(root, f, "User"));
700
+ const present = all.filter((d) => existsSync2(d));
701
+ return present.length > 0 ? present : [all[0]];
702
+ }
703
+ var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
704
+ function readVscodeMcp(path) {
705
+ const text = readIfExists(path);
706
+ if (text === void 0 || text.trim() === "") return {};
707
+ try {
708
+ const j = JSON.parse(text);
709
+ return j && typeof j === "object" && !Array.isArray(j) ? j : void 0;
710
+ } catch {
711
+ return void 0;
712
+ }
713
+ }
714
+ var COPILOT_AUTH = `Bearer \${env:${PAIR_KEY_VAR}}`;
715
+ var copilotEntry = (url) => ({ type: "http", url, headers: { Authorization: COPILOT_AUTH } });
716
+ function copilotMcpState(s, url) {
717
+ const e = s.servers?.[SERVER];
718
+ if (!e) return { state: "missing" };
719
+ if (e.url !== url) return { state: "different", detail: `it points at ${e.url ?? "(no url)"}, not ${url}` };
720
+ if (e.headers?.Authorization !== COPILOT_AUTH) return { state: "different", detail: `it does not read its key from \${env:${PAIR_KEY_VAR}}` };
721
+ return { state: "registered" };
722
+ }
723
+ function installCopilotInstructions(path, dryRun) {
724
+ const before = readIfExists(path);
725
+ const base = before ?? COPILOT_FRONTMATTER;
726
+ if (pairBlockIsBroken(base)) return { changed: false, error: `${path} has an unmatched ${PAIR_BLOCK_START}/${PAIR_BLOCK_END} marker \u2014 fix it by hand` };
727
+ const after = upsertPairBlock(base, pairBlock("copilot"));
728
+ if (after === before) return { changed: false };
729
+ if (!dryRun) writeFileMkdir(path, after);
730
+ return { changed: true };
731
+ }
732
+ function removeCopilotInstructions(path, dryRun) {
733
+ const before = readIfExists(path);
734
+ if (before === void 0 || !hasPairBlock(before)) return false;
735
+ if (dryRun) return true;
736
+ const after = removePairBlock(before);
737
+ if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync2(path);
738
+ else writeFileSync(path, after);
739
+ return true;
740
+ }
741
+ var copilotPair = {
742
+ harness: "copilot",
743
+ displayName: "GitHub Copilot (VS Code)",
744
+ installPair(o) {
745
+ const lines = [];
746
+ for (const dir of vscodeUserDirs(o)) {
747
+ const mcpPath = join3(dir, "mcp.json");
748
+ const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
749
+ const settings = readVscodeMcp(mcpPath);
750
+ if (!settings) {
751
+ lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
752
+ continue;
753
+ }
754
+ const st = copilotMcpState(settings, o.url);
755
+ if (st.state === "different" && !o.replace) {
756
+ lines.push(`pair(copilot): left ${mcpPath} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`);
757
+ continue;
758
+ }
759
+ const instr = installCopilotInstructions(instrPath, o.dryRun);
760
+ if (instr.error) {
761
+ lines.push(`pair(copilot): ${instr.error}`);
762
+ continue;
763
+ }
764
+ const mcpChanged = st.state !== "registered";
765
+ if (mcpChanged && !o.dryRun) {
766
+ const next = { ...settings, servers: { ...settings.servers, [SERVER]: copilotEntry(o.url) } };
767
+ writeFileMkdir(mcpPath, `${JSON.stringify(next, null, 2)}
768
+ `);
769
+ }
770
+ lines.push(!instr.changed && !mcpChanged ? `pair(copilot): already installed (${instrPath}, ${mcpPath})` : `pair(copilot): ${o.dryRun ? "(dry run) would install" : "installed"} the Pair instructions in ${instrPath} and ${SERVER} in ${mcpPath} (key read from \${env:${PAIR_KEY_VAR}})`);
771
+ }
772
+ return lines.join("\n");
773
+ },
774
+ verifyPair(o) {
775
+ const verdicts = vscodeUserDirs(o).map((dir) => {
776
+ const instrPath = join3(dir, "prompts", "crew-pair.instructions.md");
777
+ const mcpPath = join3(dir, "mcp.json");
778
+ const b = blockVerdict(instrPath, "copilot");
779
+ if (!b.ok) return b;
780
+ const settings = readVscodeMcp(mcpPath);
781
+ if (!settings) return { ok: false, detail: `${mcpPath} is not a plain JSON object` };
782
+ const st = copilotMcpState(settings, o.url);
783
+ if (st.state === "missing") return { ok: false, detail: `no ${SERVER} server in ${mcpPath}` };
784
+ if (st.state === "different") return { ok: false, detail: `${SERVER} in ${mcpPath} ${st.detail}` };
785
+ return { ok: true, detail: `${instrPath}, ${mcpPath}` };
786
+ });
787
+ return verdicts.find((v) => !v.ok) ?? { ok: true, detail: verdicts.map((v) => v.detail).join("; ") };
788
+ },
789
+ removePair(o) {
790
+ const lines = [];
791
+ for (const dir of vscodeUserDirs(o)) {
792
+ const mcpPath = join3(dir, "mcp.json");
793
+ const hadBlock = removeCopilotInstructions(join3(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
794
+ const settings = readVscodeMcp(mcpPath);
795
+ const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
796
+ const hadMcp = st.state === "registered";
797
+ const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
798
+ if (hadMcp && settings && !o.dryRun) {
799
+ const { [SERVER]: _gone, ...rest } = settings.servers ?? {};
800
+ const next = { ...settings };
801
+ if (Object.keys(rest).length > 0) next.servers = rest;
802
+ else delete next.servers;
803
+ if (Object.keys(next).length === 0) rmSync2(mcpPath);
804
+ else writeFileSync(mcpPath, `${JSON.stringify(next, null, 2)}
805
+ `);
806
+ }
807
+ const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
808
+ lines.push(hadBlock || hadMcp ? `pair(copilot): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what} (${dir})${kept}` : `pair(copilot): nothing to remove in ${dir}${kept}`);
809
+ }
810
+ return lines.join("\n");
811
+ },
812
+ /**
813
+ * VS Code launched from the Dock or Spotlight does not inherit a shell's
814
+ * exports, so `${env:CREW_API_KEY}` expands to nothing there. macOS only: the
815
+ * login environment is `launchctl`'s, and it is the one place we can ask.
816
+ */
817
+ envWarning(o) {
818
+ if ((o.platform ?? process.platform) !== "darwin") return void 0;
819
+ const get2 = o.loginEnv ?? ((name) => {
820
+ const r = spawnSync2("launchctl", ["getenv", name], { encoding: "utf8" });
821
+ return r.status === 0 ? r.stdout.trim() : void 0;
822
+ });
823
+ if (get2(PAIR_KEY_VAR)) return void 0;
824
+ return `$${PAIR_KEY_VAR} is not in the login environment, so VS Code started from the Dock will not send the tracker key. Launch VS Code from a terminal (\`code .\`), or run \`launchctl setenv ${PAIR_KEY_VAR} <key>\` once per login.`;
825
+ }
826
+ };
671
827
  var PAIR_INSTALLERS = {
672
828
  [claudePair.harness]: claudePair,
673
829
  [codexPair.harness]: codexPair,
674
- [geminiPair.harness]: geminiPair
830
+ [geminiPair.harness]: geminiPair,
831
+ [copilotPair.harness]: copilotPair
675
832
  };
676
833
  var PAIR_HARNESS_NAMES = Object.keys(PAIR_INSTALLERS);
677
834
  var DEFAULT_PAIR = ["claude"];
@@ -693,9 +850,120 @@ function harnessPromptNote(harness) {
693
850
 
694
851
  // src/toolchain.ts
695
852
  import { basename } from "path";
853
+
854
+ // src/adapters/gemini.ts
855
+ var BILLING_VARS_TO_UNSET2 = [
856
+ "GEMINI_API_KEY",
857
+ "GOOGLE_API_KEY",
858
+ "GOOGLE_GENAI_USE_VERTEXAI",
859
+ "GOOGLE_GENAI_USE_GCA",
860
+ "GOOGLE_CLOUD_PROJECT",
861
+ "GOOGLE_CLOUD_PROJECT_ID",
862
+ "GOOGLE_CLOUD_LOCATION",
863
+ "GOOGLE_APPLICATION_CREDENTIALS",
864
+ "GOOGLE_GEMINI_BASE_URL",
865
+ "GEMINI_MODEL"
866
+ ];
867
+ var READ_TOOLS = ["read_file", "read_many_files", "list_directory", "glob", "search_file_content"];
868
+ function geminiGrantFor(p) {
869
+ if (p.level === "read") return { approvalMode: "default", allowed: [...READ_TOOLS, "run_shell_command"] };
870
+ if (p.level === "write") return { approvalMode: "default", allowed: [...READ_TOOLS, "run_shell_command", "write_file"] };
871
+ return { approvalMode: "yolo", allowed: [] };
872
+ }
873
+ function geminiGrantArgs(role) {
874
+ const g = geminiGrantFor(profileFor(role));
875
+ return ["--approval-mode", g.approvalMode, ...g.allowed.length ? ["--allowed-tools", g.allowed.join(",")] : []];
876
+ }
877
+ function mapGeminiLine(line) {
878
+ const obj = line;
879
+ if (!obj || typeof obj !== "object") return [];
880
+ if (obj.type === "message" && obj.role === "assistant" && obj.content) return [{ kind: "text", text: obj.content }];
881
+ if (obj.type === "tool_use") return [{ kind: "tool", tool: obj.tool_name, target: toolTarget(obj.tool_name === "run_shell_command" ? "Bash" : obj.tool_name, obj.parameters) }];
882
+ return [];
883
+ }
884
+ function createGeminiStreamMapper() {
885
+ let pending = "";
886
+ const flush = () => {
887
+ const text = pending;
888
+ pending = "";
889
+ return text ? [{ kind: "text", text }] : [];
890
+ };
891
+ return {
892
+ flush,
893
+ map(line) {
894
+ const obj = line;
895
+ if (obj && typeof obj === "object" && obj.type === "message" && obj.role === "assistant") {
896
+ if (obj.content) pending += obj.content;
897
+ return [];
898
+ }
899
+ return [...flush(), ...mapGeminiLine(line)];
900
+ }
901
+ };
902
+ }
903
+ function extractGeminiResult(line) {
904
+ const obj = line;
905
+ if (obj?.type === "init") return obj.session_id ? { sessionId: obj.session_id } : void 0;
906
+ if (!obj || obj.type !== "result") return void 0;
907
+ const s = obj.stats;
908
+ return {
909
+ inputTokens: s?.input_tokens,
910
+ outputTokens: s?.output_tokens,
911
+ cacheReadTokens: s?.cached
912
+ };
913
+ }
914
+ var geminiAdapter = {
915
+ harness: "gemini",
916
+ displayName: "Gemini CLI",
917
+ vendor: "google",
918
+ ownsModel: (model) => model.startsWith("gemini-"),
919
+ // Gemini CLI has no per-run reasoning knob, so a tier's `reasoning` is ignored.
920
+ defaultBin: "gemini",
921
+ defaultModel: "gemini-2.5-pro",
922
+ defaultTiers: {
923
+ light: { model: "gemini-2.5-flash" },
924
+ standard: { model: "gemini-2.5-pro" },
925
+ deep: { model: "gemini-2.5-pro" }
926
+ },
927
+ limitations: [
928
+ "the tool grant is an allow list of tools that skip confirmation, not a sandbox: run_shell_command is allowed in every profile and sub-commands cannot be restricted",
929
+ "there is no deny list: destructive shell patterns (rm -rf, forced pushes, curl | sh) are not refused at the tool layer, unlike the Claude adapter",
930
+ "the edit profile runs in yolo mode (auto-approves every tool); the write profile (QA) withholds replace, not shell writes",
931
+ "design tools (skills, design canvas) are unavailable on this harness",
932
+ "the tracker MCP server is read from ~/.gemini/settings.json (there is no per-run --mcp-config), and MCP tool calls in the read and write profiles are not on the allow list",
933
+ "the stream carries no thinking blocks and streams assistant text in fragments, so the runner coalesces fragments into one Agent Log cycle per assistant turn; no turn count or cost is reported"
934
+ ],
935
+ invocation(spec, o) {
936
+ const args = [
937
+ // The prompt arrives on stdin; `-p` puts the CLI in non-interactive mode.
938
+ "-p",
939
+ "",
940
+ "--model",
941
+ o.model,
942
+ "--output-format",
943
+ "stream-json",
944
+ ...geminiGrantArgs(o.role),
945
+ ...spec.args
946
+ ];
947
+ return spec.launcher ? { bin: spec.launcherBin ?? spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
948
+ },
949
+ runEnv: (spec) => ({ ...spec.env }),
950
+ provider: () => "google",
951
+ unsetEnv: BILLING_VARS_TO_UNSET2,
952
+ mapStreamLine: mapGeminiLine,
953
+ createStreamMapper: createGeminiStreamMapper,
954
+ extractResult: extractGeminiResult,
955
+ extractTurnTokens: () => void 0,
956
+ inspectMcp(o) {
957
+ return inspectGeminiMcp(o);
958
+ },
959
+ ensureMcp: (o) => ensureGeminiMcp(o)
960
+ };
961
+
962
+ // src/toolchain.ts
696
963
  var TIERS = ["light", "standard", "deep"];
697
964
  var ADAPTERS = {
698
- [claudeAdapter.harness]: claudeAdapter
965
+ [claudeAdapter.harness]: claudeAdapter,
966
+ [geminiAdapter.harness]: geminiAdapter
699
967
  };
700
968
  var HARNESS_NAMES = Object.keys(ADAPTERS);
701
969
  function isHarness(name) {
@@ -2908,7 +3176,7 @@ function describeRelease(o, route) {
2908
3176
  import { dirname as dirname4, join as join7, extname } from "path";
2909
3177
  import { homedir as homedir4 } from "os";
2910
3178
  import { accessSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync } from "fs";
2911
- import { spawnSync as spawnSync2 } from "child_process";
3179
+ import { spawnSync as spawnSync3 } from "child_process";
2912
3180
  import { createHash } from "crypto";
2913
3181
 
2914
3182
  // src/runtime-info.ts
@@ -3199,7 +3467,7 @@ function planDaemonControl(crewHome, hostShip, hasSystemd) {
3199
3467
  }
3200
3468
  function runAll(commands, cwd, log) {
3201
3469
  for (const [bin, ...args] of commands) {
3202
- const res = spawnSync2(bin, args, { cwd, encoding: "utf8" });
3470
+ const res = spawnSync3(bin, args, { cwd, encoding: "utf8" });
3203
3471
  if (res.error || res.status !== 0) {
3204
3472
  log.warn(`${bin} ${args.join(" ")} exited ${res.status ?? "error"}`, {
3205
3473
  output: `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? String(res.error) : ""}`
@@ -3264,7 +3532,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3264
3532
  if (dryRun) {
3265
3533
  log.emit(`would run: launchctl remove ${plan.removeLabel}`);
3266
3534
  } else {
3267
- if (spawnSync2("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
3535
+ if (spawnSync3("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
3268
3536
  log.emit(`removed loaded job ${plan.removeLabel}`);
3269
3537
  }
3270
3538
  }
@@ -3301,7 +3569,7 @@ function programFromPlist(text) {
3301
3569
  return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
3302
3570
  }
3303
3571
  function loadedCrewLabels() {
3304
- const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
3572
+ const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
3305
3573
  if (list.status !== 0) return [];
3306
3574
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3307
3575
  }
@@ -3337,8 +3605,8 @@ function removeLaunchdUnit(unit, dryRun, log) {
3337
3605
  log.emit(`would unload and remove ${unit.label}`);
3338
3606
  return;
3339
3607
  }
3340
- if (existsSync6(unit.plistPath)) spawnSync2("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3341
- spawnSync2("launchctl", ["remove", unit.label], { encoding: "utf8" });
3608
+ if (existsSync6(unit.plistPath)) spawnSync3("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3609
+ spawnSync3("launchctl", ["remove", unit.label], { encoding: "utf8" });
3342
3610
  if (existsSync6(unit.plistPath)) unlinkSync(unit.plistPath);
3343
3611
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3344
3612
  }
@@ -3371,7 +3639,7 @@ function parseLaunchctlList(text) {
3371
3639
  return jobs;
3372
3640
  }
3373
3641
  function describeForeignLaunchdUnits(crewHome, dir) {
3374
- const list = spawnSync2("launchctl", ["list"], { encoding: "utf8" });
3642
+ const list = spawnSync3("launchctl", ["list"], { encoding: "utf8" });
3375
3643
  const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
3376
3644
  const foreign = foreignLaunchdUnits(crewHome, dir, jobs.keys());
3377
3645
  return foreign.map((u) => {
@@ -3384,11 +3652,11 @@ function describeForeignLaunchdUnits(crewHome, dir) {
3384
3652
  });
3385
3653
  }
3386
3654
  function readCrontab() {
3387
- const res = spawnSync2("crontab", ["-l"], { encoding: "utf8" });
3655
+ const res = spawnSync3("crontab", ["-l"], { encoding: "utf8" });
3388
3656
  return res.status === 0 ? res.stdout ?? "" : "";
3389
3657
  }
3390
3658
  function writeCrontab(text) {
3391
- spawnSync2("crontab", ["-"], { input: text, encoding: "utf8" });
3659
+ spawnSync3("crontab", ["-"], { input: text, encoding: "utf8" });
3392
3660
  }
3393
3661
  function addCrontabLine(marker, line) {
3394
3662
  const existing = readCrontab();
@@ -3412,7 +3680,7 @@ function removeCrontabLine(marker) {
3412
3680
  }
3413
3681
  function detectSystemd() {
3414
3682
  if (process.platform !== "linux") return false;
3415
- const res = spawnSync2("systemctl", ["--user", "status"], { encoding: "utf8" });
3683
+ const res = spawnSync3("systemctl", ["--user", "status"], { encoding: "utf8" });
3416
3684
  return !res.error;
3417
3685
  }
3418
3686
 
@@ -6712,6 +6980,7 @@ function spawnAgent(plan, emit) {
6712
6980
  let result;
6713
6981
  const cycles = [];
6714
6982
  const attribution = new TicketAttributionTracker();
6983
+ const mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
6715
6984
  const handleLine = (line) => {
6716
6985
  if (!line) return;
6717
6986
  try {
@@ -6726,9 +6995,16 @@ function spawnAgent(plan, emit) {
6726
6995
  return;
6727
6996
  }
6728
6997
  try {
6729
- result = adapter.extractResult(parsed) ?? result;
6998
+ const extracted = adapter.extractResult(parsed);
6999
+ if (extracted) result = { ...result, ...extracted };
7000
+ consume(parsed, mapper.map(parsed));
7001
+ } catch {
7002
+ }
7003
+ };
7004
+ const consume = (parsed, events) => {
7005
+ {
6730
7006
  let turnText = "";
6731
- for (const ev of adapter.mapStreamLine(parsed)) {
7007
+ for (const ev of events) {
6732
7008
  const e = {
6733
7009
  at: (/* @__PURE__ */ new Date()).toISOString(),
6734
7010
  cycle: emit.cycle,
@@ -6766,9 +7042,8 @@ function spawnAgent(plan, emit) {
6766
7042
  `);
6767
7043
  }
6768
7044
  }
6769
- const turnTokens = adapter.extractTurnTokens(parsed);
7045
+ const turnTokens = parsed === void 0 ? void 0 : adapter.extractTurnTokens(parsed);
6770
7046
  if (turnTokens !== void 0) attribution.add(turnText, turnTokens);
6771
- } catch {
6772
7047
  }
6773
7048
  };
6774
7049
  child.stdout.on("data", (chunk) => {
@@ -6784,6 +7059,10 @@ function spawnAgent(plan, emit) {
6784
7059
  });
6785
7060
  child.on("close", async (code) => {
6786
7061
  if (buf) handleLine(buf);
7062
+ try {
7063
+ consume(void 0, mapper.flush());
7064
+ } catch {
7065
+ }
6787
7066
  await Promise.all([endSink(rawSink), endSink(eventsSink)]);
6788
7067
  if (spawnFailed) return;
6789
7068
  const attributedTicket = attribution.winner(plan.ticket);
@@ -11734,7 +12013,7 @@ if (command === "passenger-sync-daemon") {
11734
12013
  `
11735
12014
  );
11736
12015
  if (!dryRun) {
11737
- spawnSync3("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
12016
+ spawnSync4("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
11738
12017
  try {
11739
12018
  unlinkSync5(staleTimer);
11740
12019
  } catch {
@@ -11773,7 +12052,7 @@ if (command === "passenger-sync-daemon") {
11773
12052
  async function doctorFix(o) {
11774
12053
  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))));
11775
12054
  const unitExists = (p) => existsSync21(p);
11776
- const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
12055
+ const crontab = o.host === "linux" ? spawnSync4("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
11777
12056
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
11778
12057
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
11779
12058
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -12645,7 +12924,7 @@ ${changed2.map((c) => ` - ${c}
12645
12924
  }
12646
12925
  let failed = false;
12647
12926
  for (const cmd2 of commands) {
12648
- const res2 = spawnSync3(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12927
+ const res2 = spawnSync4(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12649
12928
  if (res2.stdout) process.stdout.write(res2.stdout);
12650
12929
  if (res2.stderr) process.stderr.write(res2.stderr);
12651
12930
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -12663,7 +12942,7 @@ ${changed2.map((c) => ` - ${c}
12663
12942
  `);
12664
12943
  break;
12665
12944
  }
12666
- const res = spawnSync3(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12945
+ const res = spawnSync4(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
12667
12946
  if (res.stdout) process.stdout.write(res.stdout);
12668
12947
  if (res.stderr) process.stderr.write(res.stderr);
12669
12948
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -14242,6 +14521,9 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
14242
14521
  process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
14243
14522
  ` : ` NOT installed \u2014 ${v.detail}; run crew connect
14244
14523
  `));
14524
+ const envWarn = inst.envWarning?.({ url: mcpUrlFor(route.baseUrl) });
14525
+ if (envWarn) process.stdout.write(`pair ${h} env:`.padEnd(19) + ` WARN \u2014 ${envWarn}
14526
+ `);
14245
14527
  }
14246
14528
  const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
14247
14529
  if (route.apiKey && route.resolved) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",