@triflux/remote 10.43.0 → 10.45.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.
Files changed (48) hide show
  1. package/cto/hygiene.mjs +1 -1
  2. package/hub/lib/cto-env.mjs +1 -0
  3. package/hub/pipe.mjs +10 -9
  4. package/hub/schema.sql +62 -2
  5. package/hub/server.mjs +276 -132
  6. package/hub/store.mjs +699 -41
  7. package/hub/team/backend.mjs +4 -2
  8. package/hub/team/claude-daemon-control.mjs +2 -0
  9. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  10. package/hub/team/cli/commands/start/start-mux.mjs +24 -3
  11. package/hub/team/cli/services/runtime-mode.mjs +23 -9
  12. package/hub/team/conductor.mjs +101 -24
  13. package/hub/team/cto-auto-collect.mjs +2 -6
  14. package/hub/team/cto-pull.mjs +4 -0
  15. package/hub/team/dashboard-open.mjs +1 -0
  16. package/hub/team/execution-mode.mjs +50 -9
  17. package/hub/team/headless.mjs +539 -92
  18. package/hub/team/interactive-tui-transport.mjs +15 -1
  19. package/hub/team/intervention.mjs +1 -1
  20. package/hub/team/launcher-template.mjs +17 -1
  21. package/hub/team/notify.mjs +31 -11
  22. package/hub/team/orchestrator.mjs +36 -9
  23. package/hub/team/pane.mjs +176 -14
  24. package/hub/team/psmux.mjs +79 -70
  25. package/hub/team/retry-state-machine.mjs +41 -13
  26. package/hub/team/session.mjs +57 -0
  27. package/hub/team/swarm-hypervisor.mjs +4 -2
  28. package/hub/team/swarm-preflight.mjs +47 -0
  29. package/hub/team/synapse-http.mjs +149 -4
  30. package/hub/team/terminal-opener.mjs +68 -13
  31. package/hub/team/tui-viewer.mjs +65 -14
  32. package/hub/team/worktree-lifecycle.mjs +56 -59
  33. package/hub/tools.mjs +22 -1
  34. package/hub/workers/codex-mcp.mjs +26 -5
  35. package/hub/workers/delegator-mcp.mjs +3 -49
  36. package/package.json +1 -1
  37. package/scripts/lib/agent-route-policy.mjs +367 -0
  38. package/scripts/lib/cli-agy.mjs +24 -4
  39. package/scripts/lib/cli-codex.mjs +22 -160
  40. package/scripts/lib/codex-profile-config.mjs +48 -4
  41. package/scripts/lib/codex-recovery.sh +19 -0
  42. package/scripts/lib/gemini-profiles.mjs +113 -29
  43. package/scripts/lib/keyword-rules.mjs +24 -0
  44. package/scripts/lib/logger.mjs +60 -43
  45. package/scripts/lib/machine-profile.mjs +293 -0
  46. package/scripts/lib/mcp-health.mjs +44 -18
  47. package/cto/steward.mjs +0 -362
  48. package/hub/log-retention.mjs +0 -726
@@ -21,10 +21,12 @@ export function buildAntigravityCommand(
21
21
  // and `"` reach the shell verbatim (P1: shell injection).
22
22
  const promptFile = `${resultFile}.prompt`;
23
23
  writeFileSync(promptFile, prompt);
24
+ // agy 1.1.27 부터 --print 는 값(프롬프트)이 필수이고 stdin 프롬프트는 거부된다.
25
+ // 파일 내용을 셸이 재해석하지 않는 형태(따옴표 안 명령 치환 / Get-Content 식)로 값에 넣는다.
24
26
  if (isWindows) {
25
- return `Get-Content -Raw '${promptFile}' | agy --print --dangerously-skip-permissions > '${resultFile}' 2>'${resultFile}.err'`;
27
+ return `agy --dangerously-skip-permissions --print (Get-Content -Raw '${promptFile}') > '${resultFile}' 2>'${resultFile}.err'`;
26
28
  }
27
- return `agy --print --dangerously-skip-permissions < '${promptFile}' > '${resultFile}' 2>'${resultFile}.err'`;
29
+ return `agy --dangerously-skip-permissions --print "$(cat '${promptFile}')" > '${resultFile}' 2>'${resultFile}.err'`;
28
30
  }
29
31
 
30
32
  const _require = createRequire(import.meta.url);
@@ -321,10 +321,12 @@ export async function probeClaudeDaemonCandidates({
321
321
  short,
322
322
  sessionId,
323
323
  timeoutMs = 6000,
324
+ tmpRoot = "/tmp",
324
325
  } = {}) {
325
326
  const candidates = await buildClaudeDaemonDiscoveryCandidates({
326
327
  configDir,
327
328
  env,
329
+ tmpRoot,
328
330
  });
329
331
  const callerProvenance = detectCallerProvenance(env);
330
332
  const targetRequested = Boolean(short || sessionId);
@@ -88,6 +88,8 @@ export async function startHeadlessTeam({
88
88
  timeoutSec: timeoutSec || 900,
89
89
  layout,
90
90
  autoAttach: !!autoAttach,
91
+ leadPane: process.env.TMUX_PANE,
92
+ leadTmux: process.env.TMUX,
91
93
  dashboard: !!dashboard,
92
94
  dashboardLayout,
93
95
  dashboardSize: dashboardSize ?? 0.5,
@@ -5,6 +5,7 @@ import {
5
5
  attachSession,
6
6
  configureTeammateKeybindings,
7
7
  createSession,
8
+ splitAttachToClient,
8
9
  } from "../../../session.mjs";
9
10
  import { BOLD, DIM, GREEN, RESET } from "../../../shared.mjs";
10
11
  import { ok, warn } from "../../render.mjs";
@@ -62,12 +63,23 @@ export async function startMuxTeam({
62
63
 
63
64
  ok("CLI 초기화 대기 (3초)...");
64
65
  await new Promise((resolve) => setTimeout(resolve, 3000));
65
- await orchestrate(sessionId, assignments, {
66
+ const promptInjectionFailures = await orchestrate(sessionId, assignments, {
66
67
  hubUrl,
67
68
  teammateMode,
68
69
  lead: { target: leadTarget, cli: lead, task },
70
+ onInjectionFailure(failure) {
71
+ warn(
72
+ `${failure.cli} ${failure.role} prompt 제출 실패 (${failure.target}): ${failure.message}`,
73
+ );
74
+ },
69
75
  });
70
- ok("리드/워커 프롬프트 주입 완료");
76
+ if (promptInjectionFailures.length === 0) {
77
+ ok("리드/워커 프롬프트 주입 완료");
78
+ } else {
79
+ warn(
80
+ `prompt 제출 실패 ${promptInjectionFailures.length}건 — 세션 상태에 기록함`,
81
+ );
82
+ }
71
83
 
72
84
  return {
73
85
  sessionName: sessionId,
@@ -78,6 +90,7 @@ export async function startMuxTeam({
78
90
  teammateMode,
79
91
  startedAt: Date.now(),
80
92
  hubUrl,
93
+ promptInjectionFailures,
81
94
  members,
82
95
  panes: Object.fromEntries(
83
96
  members.map((member) => [
@@ -113,7 +126,15 @@ export async function startMuxTeam({
113
126
  );
114
127
  console.log(` ${DIM}Ctrl+B → D: 세션 분리 (백그라운드)${RESET}\n`);
115
128
  if (process.stdout.isTTY && process.stdin.isTTY) attachSession(sessionId);
116
- else {
129
+ else if (
130
+ splitAttachToClient(sessionId, {
131
+ targetPane: process.env.TMUX_PANE,
132
+ tmuxEnv: process.env.TMUX,
133
+ })
134
+ ) {
135
+ ok("non-TTY 환경: 바깥 tmux 클라이언트 창을 분할해 attach함");
136
+ console.log(` ${DIM}분할 pane에서 나가기: Ctrl+B → D${RESET}\n`);
137
+ } else {
117
138
  warn("TTY 미지원 환경이라 자동 attach를 생략함");
118
139
  console.log(` ${DIM}수동 연결: tfx multi attach${RESET}\n`);
119
140
  }
@@ -5,16 +5,28 @@ import {
5
5
  sessionExists,
6
6
  } from "../../session.mjs";
7
7
 
8
- export function normalizeTeammateMode(mode = "auto") {
8
+ export function normalizeTeammateMode(mode = "auto", deps = {}) {
9
+ const platform = deps.platform || process.platform;
10
+ const env = deps.env || process.env;
11
+ const detectMux = deps.detectMultiplexer || detectMultiplexer;
9
12
  const raw = String(mode).toLowerCase();
10
13
  if (raw === "inline" || raw === "native") return "in-process";
11
14
  if (raw === "headless" || raw === "hl") return "headless";
12
- if (raw === "in-process" || raw === "tmux" || raw === "psmux" || raw === "wt")
13
- return raw;
14
- if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
15
+ if (raw === "in-process" || raw === "tmux") return raw;
16
+ if (raw === "psmux") return platform === "win32" ? "psmux" : "in-process";
17
+ if (
18
+ raw === "wt" ||
19
+ raw === "windows-terminal" ||
20
+ raw === "windows_terminal"
21
+ ) {
22
+ return platform === "win32" ? "wt" : "in-process";
23
+ }
15
24
  if (raw === "auto") {
16
- if (process.env.TMUX) return "tmux";
17
- const mux = detectMultiplexer();
25
+ if (env.TMUX) return "tmux";
26
+ const mux = detectMux();
27
+ if (platform !== "win32") {
28
+ return mux === "tmux" ? "tmux" : "in-process";
29
+ }
18
30
  if (mux === "psmux") return "psmux";
19
31
  if (mux === "tmux") return "tmux";
20
32
  return "in-process";
@@ -57,9 +69,11 @@ export function isTeamAlive(state) {
57
69
  return sessionExists(state.sessionName);
58
70
  }
59
71
 
60
- export function ensureTmuxOrExit() {
61
- const mux = detectMultiplexer();
62
- if (mux) return mux;
72
+ export function ensureTmuxOrExit(deps = {}) {
73
+ const platform = deps.platform || process.platform;
74
+ const detectMux = deps.detectMultiplexer || detectMultiplexer;
75
+ const mux = detectMux();
76
+ if (mux && (platform === "win32" || mux === "tmux")) return mux;
63
77
  const error = new Error("tmux 미발견");
64
78
  error.code = "TMUX_REQUIRED";
65
79
  throw error;
@@ -19,6 +19,13 @@ import {
19
19
  import { homedir } from "node:os";
20
20
  import { dirname, join } from "node:path";
21
21
  import { createRegistry } from "@triflux/core/mesh/mesh-registry.mjs";
22
+ import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
23
+ import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
24
+ import {
25
+ buildDisabledCliError,
26
+ normalizeCliName,
27
+ resolveCliPolicy,
28
+ } from "../../scripts/lib/machine-profile.mjs";
22
29
  import { broker } from "@triflux/core/hub/account-broker.mjs";
23
30
  import { runPreflight as runCodexPreflight } from "@triflux/core/hub/codex-preflight.mjs";
24
31
  import {
@@ -604,28 +611,53 @@ export function createConductor(opts = {}) {
604
611
  }
605
612
  }
606
613
 
607
- const spawnSpec = buildSpawnSpecForMode(MODES.HEADLESS, {
608
- cli: session.config.agent,
609
- prompt: session.config.prompt,
610
- profile: session.config.profile,
611
- model: session.config.model,
612
- mcpServers: session.config.mcpServers,
613
- excludeMcpServers,
614
- resolveCommand: opts.deps?.resolveCliExecutable,
615
- onWarning: (message, details = {}) => {
616
- eventLog.append("mcp_server_skipped", {
617
- session: session.id,
618
- message,
619
- ...details,
620
- });
621
- emitter.emit("warning", {
622
- type: "mcp_server_skipped",
623
- session: session.id,
624
- message,
625
- ...details,
626
- });
627
- },
628
- });
614
+ let spawnSpec;
615
+ try {
616
+ spawnSpec = buildSpawnSpecForMode(MODES.HEADLESS, {
617
+ cli: session.config.agent,
618
+ prompt: session.config.prompt,
619
+ profile: session.config.profile,
620
+ role: session.config.role,
621
+ model: session.config.model,
622
+ env: session.config.env,
623
+ mcpServers: session.config.mcpServers,
624
+ excludeMcpServers,
625
+ resolveCommand: opts.deps?.resolveCliExecutable,
626
+ onWarning: (message, details = {}) => {
627
+ eventLog.append("mcp_server_skipped", {
628
+ session: session.id,
629
+ message,
630
+ ...details,
631
+ });
632
+ emitter.emit("warning", {
633
+ type: "mcp_server_skipped",
634
+ session: session.id,
635
+ message,
636
+ ...details,
637
+ });
638
+ },
639
+ });
640
+ } catch (err) {
641
+ // respawnSession 은 `void` 로 호출되므로 여기서 던지면 처리되지 않은 거부가
642
+ // 된다. spec 생성 실패를 명시 실패로 접어 세션이 매달리지 않게 한다.
643
+ // 정책 차단과 그 밖의 spec 생성 실패는 원인이 다르므로 이벤트를 나눈다.
644
+ const message = err?.message || String(err);
645
+ const blockedByPolicy = err?.code === "TFX_CLI_DISABLED";
646
+ const eventName = blockedByPolicy
647
+ ? "session_spawn_blocked"
648
+ : "session_spawn_failed";
649
+ const reason = blockedByPolicy ? "spawn_blocked" : "spawn_failed";
650
+ const detail = {
651
+ session: session.id,
652
+ agent: session.config.agent,
653
+ code: err?.code || null,
654
+ error: message,
655
+ };
656
+ eventLog.append(eventName, detail);
657
+ emitter.emit("warning", { type: eventName, ...detail });
658
+ handleFailure(session, `${reason}:${message}`);
659
+ return;
660
+ }
629
661
 
630
662
  // #90 branch guard: shard spawn cwd가 main 브랜치면 즉시 abort.
631
663
  // swarm shard는 반드시 shard 전용 worktree 브랜치에서 실행되어야 한다.
@@ -902,8 +934,25 @@ export function createConductor(opts = {}) {
902
934
  } else if (agent === "gemini") {
903
935
  remoteBin = "gemini -y";
904
936
  } else {
905
- remoteBin =
906
- "codex exec -s danger-full-access --dangerously-bypass-approvals-and-sandbox";
937
+ const codexHome = session.config.env?.CODEX_HOME;
938
+ const profile = resolveNestedCodexAgentProfile(
939
+ session.config.role || "executor",
940
+ {
941
+ profileOverride: session.config.profile ?? "auto",
942
+ globalProfile:
943
+ session.config.env?.TFX_CODEX_PROFILE ??
944
+ process.env.TFX_CODEX_PROFILE,
945
+ codexHome,
946
+ },
947
+ );
948
+ const configArgs = codexProfileConfigOverrides(profile, {
949
+ codexHome,
950
+ disallowUltra: true,
951
+ enforceCanonicalProfile: true,
952
+ })
953
+ .map((override) => `-c '${override.replace(/'/g, `'\\''`)}'`)
954
+ .join(" ");
955
+ remoteBin = `codex exec${configArgs ? ` ${configArgs}` : ""} -s danger-full-access --dangerously-bypass-approvals-and-sandbox`;
907
956
  }
908
957
 
909
958
  // prompt는 stdin으로 전달 — 셸 이스케이프 문제 완전 회피
@@ -1155,6 +1204,34 @@ export function createConductor(opts = {}) {
1155
1204
  }
1156
1205
  }
1157
1206
 
1207
+ // TFX_DISABLE_* SSOT 집행. 계정 임대와 worker sandbox 생성보다 앞에 둔다.
1208
+ // 뒤에 두면 정책으로 거부된 세션이 샌드박스 디렉터리와 auth.json 사본을
1209
+ // 남기고, 임대 반납이 회로 차단기 실패로 집계돼 멀쩡한 계정이 cooldown 에
1210
+ // 들어간다. 다른 CLI 로 자동 전환하지 않고 호출자에게 그대로 던진다.
1211
+ // claude 처럼 정책 대상이 아닌 agent 는 프로파일 파일을 읽지 않는다 (F4 와 동일).
1212
+ if (!config.remote && normalizeCliName(config.agent)) {
1213
+ const blockedCli = buildDisabledCliError(
1214
+ config.agent,
1215
+ resolveCliPolicy(process.env),
1216
+ );
1217
+ if (blockedCli) {
1218
+ eventLog.append("session_spawn_blocked", {
1219
+ session: config.id,
1220
+ agent: config.agent,
1221
+ code: blockedCli.code || null,
1222
+ error: blockedCli.message,
1223
+ });
1224
+ emitter.emit("warning", {
1225
+ type: "session_spawn_blocked",
1226
+ session: config.id,
1227
+ agent: config.agent,
1228
+ code: blockedCli.code || null,
1229
+ error: blockedCli.message,
1230
+ });
1231
+ throw blockedCli;
1232
+ }
1233
+ }
1234
+
1158
1235
  // broker lease (graceful — broker null if accounts.json absent)
1159
1236
  let lease = null;
1160
1237
  const activeBroker = getActiveBroker();
@@ -2,16 +2,12 @@ import { existsSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { resolveLakeRootDir } from "../../cto/lake-root.mjs";
5
+ import { resolveRoleControlSnapshot } from "@triflux/core/hub/lib/cto-env.mjs";
5
6
 
6
7
  const DEFAULT_DEBOUNCE_MS = 120_000;
7
- const OFF_VALUES = new Set(["0", "false", "off", "no"]);
8
8
 
9
9
  export function isCtoAutoCollectDisabled(env = process.env) {
10
- return OFF_VALUES.has(
11
- String(env?.TFX_CTO_AUTO_COLLECT ?? "")
12
- .trim()
13
- .toLowerCase(),
14
- );
10
+ return !resolveRoleControlSnapshot(env).auto_collect_enabled;
15
11
  }
16
12
 
17
13
  function freshCurrentMd(projectRoot, now, debounceMs) {
@@ -1,6 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
+ import { resolveRoleControlSnapshot } from "@triflux/core/hub/lib/cto-env.mjs";
5
+
4
6
  function defaultLakeRoot() {
5
7
  return join(process.cwd(), ".triflux", "lake");
6
8
  }
@@ -9,9 +11,11 @@ function defaultLakeRoot() {
9
11
  * Read the CTO north-star lake snapshot without affecting runtime control flow.
10
12
  * @param {object} [opts]
11
13
  * @param {string} [opts.lakeRoot] Directory containing current.json.
14
+ * @param {object} [opts.env] Environment values used for the CTO kill-switch.
12
15
  * @returns {object|null}
13
16
  */
14
17
  export function readCtoSnapshot(opts = {}) {
18
+ if (!resolveRoleControlSnapshot(opts.env).north_star_enabled) return null;
15
19
  try {
16
20
  const lakeRoot = opts.lakeRoot || defaultLakeRoot();
17
21
  const parsed = JSON.parse(
@@ -47,6 +47,7 @@ export async function openHeadlessDashboardTarget(sessionName, opts = {}) {
47
47
  title: title || `▲ ${safeSession}`,
48
48
  cwd,
49
49
  profile: opts.profile ?? "triflux",
50
+ targetPane: opts.targetPane,
50
51
  });
51
52
  return await opened;
52
53
  } catch {
@@ -2,13 +2,17 @@
2
2
 
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { dirname, resolve as pathResolve } from "node:path";
5
-
6
- // scripts/lib is bundled into @triflux/remote itself (package.json files), so
7
- // import the local mirror relatively rather than via @triflux/core. The core
8
- // peer range (^10.0.0-alpha.1) could resolve to a build that predates this
9
- // export, which would fail at import time; the bundled copy is version-skew
10
- // safe and always carries the matching codexProfileConfigOverrides.
5
+ import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
11
6
  import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
7
+ import {
8
+ resolveGeminiModel,
9
+ resolveGeminiProfileForPurpose,
10
+ } from "../../scripts/lib/gemini-profiles.mjs";
11
+ import {
12
+ buildDisabledCliError,
13
+ normalizeCliName,
14
+ resolveCliPolicy,
15
+ } from "../../scripts/lib/machine-profile.mjs";
12
16
  import { whichCommand } from "@triflux/core/hub/platform.mjs";
13
17
 
14
18
  const WIN32_EXT_PRECEDENCE = [".cmd", ".exe", ".bat", ".ps1"];
@@ -101,6 +105,19 @@ export function resolveCliExecutable(cli, opts = {}) {
101
105
 
102
106
  export function buildSpawnSpecForMode(mode, opts = {}) {
103
107
  const cli = normalizeCli(opts.cli || "codex");
108
+
109
+ // TFX_DISABLE_* SSOT 집행. route 를 우회하는 spawn 경로도 tfx-route.sh 와 같은
110
+ // 정책을 따른다. 정책 판독은 런타임 process.env 와 machine profile 만 쓰고,
111
+ // 자식에게 넘길 세션 env(opts.env)는 쓰지 않는다. 주입 시임은 opts.cliPolicy
112
+ // (계산된 정책)와 opts.policyEnv(env 객체) 둘뿐이다.
113
+ // claude 처럼 정책 대상이 아닌 CLI 는 프로파일 파일을 읽지 않는다.
114
+ if (normalizeCliName(cli)) {
115
+ const cliPolicy =
116
+ opts.cliPolicy ?? resolveCliPolicy(opts.policyEnv ?? process.env);
117
+ const blockedCli = buildDisabledCliError(cli, cliPolicy);
118
+ if (blockedCli) throw blockedCli;
119
+ }
120
+
104
121
  const prompt = asPrompt(opts.prompt);
105
122
  const commandName = cli === "antigravity" ? "agy" : cli;
106
123
  const resolvedCommand = resolveCliExecutable(commandName, opts);
@@ -131,12 +148,26 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
131
148
  };
132
149
 
133
150
  if (cli === "antigravity") {
134
- const args = ["--print", "--dangerously-skip-permissions"];
151
+ // agy 1.1.27: --print 는 값(프롬프트)을 받는 마지막 인자다. stdin 프롬프트는
152
+ // "empty prompt" 로 거부되므로 argv 로 넘긴다.
153
+ // effort 는 역할별 SSOT(scripts/lib/gemini-profiles.mjs). 명시 model 이 우선한다.
154
+ const model =
155
+ typeof opts.model === "string" && opts.model.trim()
156
+ ? resolveGeminiModel(opts.model.trim(), {
157
+ profilesPath: opts.geminiProfilesPath,
158
+ })
159
+ : resolveGeminiModel(
160
+ resolveGeminiProfileForPurpose(opts.role || opts.agent),
161
+ { profilesPath: opts.geminiProfilesPath },
162
+ );
163
+ const args = ["--dangerously-skip-permissions"];
164
+ if (model) args.push("--model", model);
165
+ args.push("--print", prompt);
135
166
  return {
136
167
  ...wrap(args),
137
168
  useExec: true,
138
169
  shell: false,
139
- stdinPrompt: prompt.length > 0,
170
+ stdinPrompt: false,
140
171
  prompt,
141
172
  };
142
173
  }
@@ -165,7 +196,17 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
165
196
  // still contains an inline [profiles.X] table (which codex re-injects on
166
197
  // config rewrite). These go straight to spawn argv (shell: false), so push
167
198
  // the raw TOML-scalar overrides without shell-quoting.
168
- for (const override of codexProfileConfigOverrides(opts.profile)) {
199
+ const codexHome = opts.env?.CODEX_HOME;
200
+ const codexProfile = resolveNestedCodexAgentProfile(opts.role || "executor", {
201
+ profileOverride: opts.profile ?? "auto",
202
+ globalProfile: opts.env?.TFX_CODEX_PROFILE ?? process.env.TFX_CODEX_PROFILE,
203
+ codexHome,
204
+ });
205
+ for (const override of codexProfileConfigOverrides(codexProfile, {
206
+ codexHome,
207
+ disallowUltra: true,
208
+ enforceCanonicalProfile: true,
209
+ })) {
169
210
  args.push("-c", override);
170
211
  }
171
212
  if (Array.isArray(opts.mcpServers)) {