karajan-code 1.21.1 → 1.22.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.21.1",
3
+ "version": "1.22.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Host Agent — delegates task execution to the MCP host AI via elicitation.
3
+ *
4
+ * Instead of spawning a subprocess, returns the prompt to the host AI
5
+ * (Claude, Codex, etc.) for direct execution. The host has full access
6
+ * to the codebase and tools — no subprocess overhead.
7
+ *
8
+ * Used when: the MCP host IS the same agent configured for a role.
9
+ */
10
+
11
+ import { BaseAgent } from "./base-agent.js";
12
+
13
+ export class HostAgent extends BaseAgent {
14
+ constructor(config, logger, { askHost }) {
15
+ super("host", config, logger);
16
+ this._askHost = askHost;
17
+ }
18
+
19
+ async runTask(task) {
20
+ const { prompt, onOutput } = task;
21
+
22
+ if (!this._askHost) {
23
+ return { ok: false, output: "", error: "Host agent has no askHost callback" };
24
+ }
25
+
26
+ if (onOutput) onOutput({ stream: "info", line: "[host-agent] Delegating to host AI..." });
27
+
28
+ const answer = await this._askHost(prompt);
29
+
30
+ if (!answer) {
31
+ return { ok: false, output: "", error: "Host AI declined or returned no response" };
32
+ }
33
+
34
+ if (onOutput) onOutput({ stream: "info", line: "[host-agent] Host AI completed task" });
35
+
36
+ return { ok: true, output: answer, exitCode: 0 };
37
+ }
38
+ }
@@ -22,6 +22,53 @@ export function setupOrphanGuard({ intervalMs = DEFAULT_INTERVAL_MS, exitFn = ()
22
22
  return { timer, parentPid };
23
23
  }
24
24
 
25
+ const DEFAULT_MEMORY_CHECK_MS = 30_000;
26
+ const DEFAULT_WARN_HEAP_MB = 512;
27
+ const DEFAULT_CRITICAL_HEAP_MB = 768;
28
+
29
+ export function setupMemoryWatchdog({
30
+ intervalMs = DEFAULT_MEMORY_CHECK_MS,
31
+ warnHeapMb = DEFAULT_WARN_HEAP_MB,
32
+ criticalHeapMb = DEFAULT_CRITICAL_HEAP_MB,
33
+ onWarn = null,
34
+ onCritical = null,
35
+ exitFn = () => process.exit(1)
36
+ } = {}) {
37
+ const warnBytes = warnHeapMb * 1024 * 1024;
38
+ const criticalBytes = criticalHeapMb * 1024 * 1024;
39
+ let warned = false;
40
+
41
+ const timer = setInterval(() => {
42
+ const { heapUsed, rss } = process.memoryUsage();
43
+
44
+ if (heapUsed >= criticalBytes) {
45
+ if (global.gc) {
46
+ try { global.gc(); } catch { /* --expose-gc not set */ }
47
+ const after = process.memoryUsage().heapUsed;
48
+ if (after < criticalBytes) return; // GC freed enough
49
+ }
50
+ const msg = `Memory critical: heap ${(heapUsed / 1024 / 1024).toFixed(0)}MB / rss ${(rss / 1024 / 1024).toFixed(0)}MB — exiting to prevent OOM`;
51
+ if (onCritical) onCritical(msg);
52
+ else process.stderr.write(`[karajan-mcp] ${msg}\n`);
53
+ clearInterval(timer);
54
+ exitFn();
55
+ return;
56
+ }
57
+
58
+ if (heapUsed >= warnBytes && !warned) {
59
+ warned = true;
60
+ const msg = `Memory warning: heap ${(heapUsed / 1024 / 1024).toFixed(0)}MB / rss ${(rss / 1024 / 1024).toFixed(0)}MB (critical at ${criticalHeapMb}MB)`;
61
+ if (onWarn) onWarn(msg);
62
+ else process.stderr.write(`[karajan-mcp] ${msg}\n`);
63
+ } else if (heapUsed < warnBytes) {
64
+ warned = false;
65
+ }
66
+ }, intervalMs);
67
+ timer.unref();
68
+
69
+ return { timer };
70
+ }
71
+
25
72
  export function setupVersionWatcher({ pkgPath, currentVersion, exitFn = () => process.exit(0) } = {}) {
26
73
  if (!pkgPath) return null;
27
74
 
@@ -287,22 +287,36 @@ export async function handleResumeDirect(a, server, extra) {
287
287
  const config = await buildConfig(a);
288
288
  const logger = createLogger(config.output.log_level, "mcp");
289
289
 
290
+ const projectDir = await resolveProjectDir(server);
291
+ const runLog = createRunLog(projectDir);
292
+ runLog.logText(`[kj_resume] started — session="${a.sessionId}"`);
293
+
290
294
  const emitter = new EventEmitter();
291
295
  emitter.on("progress", buildProgressHandler(server));
296
+ emitter.on("progress", (event) => runLog.logEvent(event));
292
297
  const progressNotifier = buildProgressNotifier(extra);
293
298
  if (progressNotifier) emitter.on("progress", progressNotifier);
294
299
 
295
300
  const askQuestion = buildAskQuestion(server);
296
- const result = await resumeFlow({
297
- sessionId: a.sessionId,
298
- answer: a.answer || null,
299
- config,
300
- logger,
301
- flags: a,
302
- emitter,
303
- askQuestion
304
- });
305
- return { ok: true, ...result };
301
+ try {
302
+ const result = await resumeFlow({
303
+ sessionId: a.sessionId,
304
+ answer: a.answer || null,
305
+ config,
306
+ logger,
307
+ flags: a,
308
+ emitter,
309
+ askQuestion
310
+ });
311
+ const ok = !result.paused && (result.approved !== false);
312
+ runLog.logText(`[kj_resume] finished — ok=${ok}`);
313
+ return { ok, ...result };
314
+ } catch (err) {
315
+ runLog.logText(`[kj_resume] failed: ${err.message}`);
316
+ throw err;
317
+ } finally {
318
+ runLog.close();
319
+ }
306
320
  }
307
321
 
308
322
  function buildDirectEmitter(server, runLog, extra) {
package/src/mcp/server.js CHANGED
@@ -50,9 +50,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
50
50
  });
51
51
 
52
52
  // --- Orphan process protection + version watcher ---
53
- import { setupOrphanGuard, setupVersionWatcher } from "./orphan-guard.js";
53
+ import { setupOrphanGuard, setupVersionWatcher, setupMemoryWatchdog } from "./orphan-guard.js";
54
54
  setupOrphanGuard();
55
55
  setupVersionWatcher({ pkgPath: PKG_PATH, currentVersion: LOADED_VERSION });
56
+ setupMemoryWatchdog();
56
57
 
57
58
  const transport = new StdioServerTransport();
58
59
  await mcpServer.connect(transport);
@@ -256,8 +256,14 @@ async function handleTddFailure({ tddEval, config, logger, emitter, eventBase, s
256
256
 
257
257
  export async function runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, askQuestion }) {
258
258
  logger.setContext({ iteration, stage: "tdd" });
259
- const tddDiff = await generateDiff({ baseRef: session.session_start_sha });
260
- const untrackedFiles = await getUntrackedFiles();
259
+ let tddDiff, untrackedFiles;
260
+ try {
261
+ tddDiff = await generateDiff({ baseRef: session.session_start_sha });
262
+ untrackedFiles = await getUntrackedFiles();
263
+ } catch (err) {
264
+ logger.warn(`TDD diff generation failed: ${err.message}`);
265
+ return { action: "continue", stageResult: { ok: false, summary: `TDD check failed: ${err.message}` } };
266
+ }
261
267
  const tddEval = evaluateTddPolicy(tddDiff, config.development, untrackedFiles);
262
268
  await addCheckpoint(session, {
263
269
  stage: "tdd-policy",
@@ -366,7 +372,13 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
366
372
  const sonarRole = new SonarRole({ config, logger, emitter });
367
373
  await sonarRole.init({ iteration });
368
374
  const sonarStart = Date.now();
369
- const sonarOutput = await sonarRole.run();
375
+ let sonarOutput;
376
+ try {
377
+ sonarOutput = await sonarRole.run();
378
+ } catch (err) {
379
+ logger.warn(`Sonar threw: ${err.message}`);
380
+ sonarOutput = { ok: false, result: { error: err.message }, summary: `Sonar error: ${err.message}` };
381
+ }
370
382
  trackBudget({ role: "sonar", provider: "sonar", result: sonarOutput, duration_ms: Date.now() - sonarStart });
371
383
  const sonarResult = sonarOutput.result;
372
384
 
@@ -438,7 +450,13 @@ export async function runSonarCloudStage({ config, logger, emitter, eventBase, s
438
450
 
439
451
  const { runSonarCloudScan } = await import("../sonar/cloud-scanner.js");
440
452
  const scanStart = Date.now();
441
- const result = await runSonarCloudScan(config);
453
+ let result;
454
+ try {
455
+ result = await runSonarCloudScan(config);
456
+ } catch (err) {
457
+ logger.warn(`SonarCloud threw: ${err.message}`);
458
+ result = { ok: false, error: err.message };
459
+ }
442
460
  trackBudget({ role: "sonarcloud", provider: "sonarcloud", result: { ok: result.ok }, duration_ms: Date.now() - scanStart });
443
461
 
444
462
  await addCheckpoint(session, {
@@ -550,7 +568,13 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
550
568
  })
551
569
  );
552
570
 
553
- const diff = await fetchReviewDiff(session, logger);
571
+ let diff;
572
+ try {
573
+ diff = await fetchReviewDiff(session, logger);
574
+ } catch (err) {
575
+ logger.warn(`Review diff generation failed: ${err.message}`);
576
+ return { approved: false, blocking_issues: [{ description: `Diff generation failed: ${err.message}` }], non_blocking_suggestions: [], summary: `Reviewer failed: cannot generate diff — ${err.message}`, confidence: 0 };
577
+ }
554
578
  const reviewerOnOutput = ({ stream, line }) => {
555
579
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
556
580
  message: line,
@@ -575,6 +599,9 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
575
599
  trackBudget({ role: "reviewer", provider: reviewer, model: reviewerRole.model, result, duration_ms: Date.now() - reviewerStart });
576
600
  }
577
601
  });
602
+ } catch (err) {
603
+ logger.warn(`Reviewer threw: ${err.message}`);
604
+ reviewerExec = { execResult: { ok: false, error: err.message }, attempts: [{ reviewer: reviewerRole.provider, result: { ok: false, error: err.message } }] };
578
605
  } finally {
579
606
  reviewerStall.stop();
580
607
  }
@@ -16,7 +16,13 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
16
16
  const tester = new TesterRole({ config, logger, emitter });
17
17
  await tester.init({ task, iteration });
18
18
  const testerStart = Date.now();
19
- const testerOutput = await tester.run({ task, diff });
19
+ let testerOutput;
20
+ try {
21
+ testerOutput = await tester.run({ task, diff });
22
+ } catch (err) {
23
+ logger.warn(`Tester threw: ${err.message}`);
24
+ testerOutput = { ok: false, summary: `Tester error: ${err.message}`, result: { error: err.message } };
25
+ }
20
26
  trackBudget({
21
27
  role: "tester",
22
28
  provider: config?.roles?.tester?.provider || coderRole.provider,
@@ -90,7 +96,13 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
90
96
  const security = new SecurityRole({ config, logger, emitter });
91
97
  await security.init({ task, iteration });
92
98
  const securityStart = Date.now();
93
- const securityOutput = await security.run({ task, diff });
99
+ let securityOutput;
100
+ try {
101
+ securityOutput = await security.run({ task, diff });
102
+ } catch (err) {
103
+ logger.warn(`Security threw: ${err.message}`);
104
+ securityOutput = { ok: false, summary: `Security error: ${err.message}`, result: { error: err.message } };
105
+ }
94
106
  trackBudget({
95
107
  role: "security",
96
108
  provider: config?.roles?.security?.provider || coderRole.provider,
@@ -69,6 +69,9 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
69
69
  let triageOutput;
70
70
  try {
71
71
  triageOutput = await triage.run({ task: session.task, onOutput: triageStall.onOutput });
72
+ } catch (err) {
73
+ logger.warn(`Triage threw: ${err.message}`);
74
+ triageOutput = { ok: false, summary: `Triage error: ${err.message}`, result: { error: err.message } };
72
75
  } finally {
73
76
  triageStall.stop();
74
77
  }
@@ -159,6 +162,9 @@ export async function runResearcherStage({ config, logger, emitter, eventBase, s
159
162
  let researchOutput;
160
163
  try {
161
164
  researchOutput = await researcher.run({ task: session.task, onOutput: researcherStall.onOutput });
165
+ } catch (err) {
166
+ logger.warn(`Researcher threw: ${err.message}`);
167
+ researchOutput = { ok: false, summary: `Researcher error: ${err.message}`, result: { error: err.message } };
162
168
  } finally {
163
169
  researcherStall.stop();
164
170
  }
@@ -281,6 +287,9 @@ export async function runArchitectStage({ config, logger, emitter, eventBase, se
281
287
  discoverResult,
282
288
  triageLevel
283
289
  });
290
+ } catch (err) {
291
+ logger.warn(`Architect threw: ${err.message}`);
292
+ architectOutput = { ok: false, summary: `Architect error: ${err.message}`, result: { error: err.message } };
284
293
  } finally {
285
294
  architectStall.stop();
286
295
  }
@@ -380,6 +389,9 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
380
389
  let planResult;
381
390
  try {
382
391
  planResult = await planRole.execute({ task, onOutput: plannerStall.onOutput });
392
+ } catch (err) {
393
+ logger.warn(`Planner threw: ${err.message}`);
394
+ planResult = { ok: false, result: { error: err.message }, summary: `Planner error: ${err.message}` };
383
395
  } finally {
384
396
  plannerStall.stop();
385
397
  }
@@ -453,6 +465,9 @@ export async function runDiscoverStage({ config, logger, emitter, eventBase, ses
453
465
  let discoverOutput;
454
466
  try {
455
467
  discoverOutput = await discover.run({ task: session.task, mode, onOutput: discoverStall.onOutput });
468
+ } catch (err) {
469
+ logger.warn(`Discover threw: ${err.message}`);
470
+ discoverOutput = { ok: false, summary: `Discover error: ${err.message}`, result: { error: err.message } };
456
471
  } finally {
457
472
  discoverStall.stop();
458
473
  }
@@ -16,7 +16,13 @@ export async function runReviewerWithFallback({ reviewerName, config, logger, em
16
16
  const role = new ReviewerRole({ config: reviewerConfig, logger, emitter, createAgentFn: createAgent });
17
17
  await role.init();
18
18
  for (let attempt = 1; attempt <= retries + 1; attempt += 1) {
19
- const execResult = await role.execute(reviewInput);
19
+ let execResult;
20
+ try {
21
+ execResult = await role.execute(reviewInput);
22
+ } catch (err) {
23
+ logger.warn(`Reviewer ${name} attempt ${attempt} threw: ${err.message}`);
24
+ execResult = { ok: false, result: { error: err.message }, summary: `Reviewer error: ${err.message}` };
25
+ }
20
26
  if (onAttemptResult) {
21
27
  await onAttemptResult({ reviewer: name, result: execResult.result });
22
28
  }
@@ -19,7 +19,16 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
19
19
 
20
20
  const solomon = new SolomonRole({ config, logger, emitter });
21
21
  await solomon.init({ task: conflict.task || session.task, iteration });
22
- const ruling = await solomon.run({ conflict });
22
+ let ruling;
23
+ try {
24
+ ruling = await solomon.run({ conflict });
25
+ } catch (err) {
26
+ logger.warn(`Solomon threw: ${err.message}`);
27
+ return escalateToHuman({
28
+ askQuestion, session, emitter, eventBase, stage, iteration,
29
+ conflict: { ...conflict, solomonReason: `Solomon error: ${err.message}` }
30
+ });
31
+ }
23
32
 
24
33
  emitProgress(
25
34
  emitter,
@@ -958,14 +958,45 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
958
958
  return { action: "return", result };
959
959
  }
960
960
 
961
- async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults }) {
961
+ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task }) {
962
+ // Escalate to Solomon / human before giving up
963
+ const solomonResult = await invokeSolomon({
964
+ config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
965
+ iteration: config.max_iterations,
966
+ conflict: {
967
+ stage: "max_iterations",
968
+ task,
969
+ iterationCount: config.max_iterations,
970
+ maxIterations: config.max_iterations,
971
+ history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
972
+ }
973
+ });
974
+
975
+ if (solomonResult.action === "continue") {
976
+ if (solomonResult.humanGuidance) {
977
+ session.last_reviewer_feedback = `User guidance: ${solomonResult.humanGuidance}`;
978
+ }
979
+ session.reviewer_retry_count = 0;
980
+ await saveSession(session);
981
+ return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance };
982
+ }
983
+
984
+ if (solomonResult.action === "pause") {
985
+ return { paused: true, sessionId: session.id, question: solomonResult.question, context: "max_iterations" };
986
+ }
987
+
988
+ if (solomonResult.action === "subtask") {
989
+ return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
990
+ }
991
+
992
+ // Solomon also couldn't resolve — fail
962
993
  session.budget = budgetSummary();
963
994
  await markSessionStatus(session, "failed");
964
995
  emitProgress(
965
996
  emitter,
966
997
  makeEvent("session:end", { ...eventBase, stage: "done" }, {
967
998
  status: "fail",
968
- message: "Max iterations reached",
999
+ message: "Max iterations reached (Solomon could not resolve)",
969
1000
  detail: { approved: false, reason: "max_iterations", iterations: config.max_iterations, stages: stageResults, budget: budgetSummary() }
970
1001
  })
971
1002
  );
@@ -978,7 +1009,7 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
978
1009
  const refactorerRole = resolveRole(config, "refactorer");
979
1010
  const pipelineFlags = resolvePipelineFlags(config);
980
1011
  const repeatDetector = new RepeatDetector({ threshold: getRepeatThreshold(config) });
981
- const coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent });
1012
+ const coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent, askHost: askQuestion });
982
1013
  const startedAt = Date.now();
983
1014
  const eventBase = { sessionId: null, iteration: 0, stage: null, startedAt };
984
1015
  const { budgetTracker, budgetLimit, budgetSummary, trackBudget } = createBudgetManager({ config, emitter, eventBase });
@@ -1109,7 +1140,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1109
1140
  if (iterResult.action === "retry") { i -= 1; }
1110
1141
  }
1111
1142
 
1112
- return handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config, stageResults: ctx.stageResults });
1143
+ return handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config, stageResults: ctx.stageResults, logger, askQuestion, task });
1113
1144
  }
1114
1145
 
1115
1146
  export async function resumeFlow({ sessionId, answer, config, logger, flags = {}, emitter = null, askQuestion = null }) {
@@ -1162,5 +1193,10 @@ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}
1162
1193
  await saveSession(session);
1163
1194
 
1164
1195
  // Re-run the flow with the existing session context
1165
- return runFlow({ task, config: sessionConfig, logger, flags, emitter, askQuestion });
1196
+ try {
1197
+ return await runFlow({ task, config: sessionConfig, logger, flags, emitter, askQuestion });
1198
+ } catch (err) {
1199
+ await markSessionStatus(session, "failed");
1200
+ throw err;
1201
+ }
1166
1202
  }
@@ -1,6 +1,8 @@
1
1
  import { BaseRole } from "./base-role.js";
2
2
  import { createAgent as defaultCreateAgent } from "../agents/index.js";
3
3
  import { buildCoderPrompt } from "../prompts/coder.js";
4
+ import { isHostAgent } from "../utils/agent-detect.js";
5
+ import { HostAgent } from "../agents/host-agent.js";
4
6
 
5
7
  function resolveProvider(config) {
6
8
  return (
@@ -11,9 +13,10 @@ function resolveProvider(config) {
11
13
  }
12
14
 
13
15
  export class CoderRole extends BaseRole {
14
- constructor({ config, logger, emitter = null, createAgentFn = null }) {
16
+ constructor({ config, logger, emitter = null, createAgentFn = null, askHost = null }) {
15
17
  super({ name: "coder", config, logger, emitter });
16
18
  this._createAgent = createAgentFn || defaultCreateAgent;
19
+ this._askHost = askHost;
17
20
  }
18
21
 
19
22
  async execute(input) {
@@ -22,7 +25,14 @@ export class CoderRole extends BaseRole {
22
25
  : input || {};
23
26
 
24
27
  const provider = resolveProvider(this.config);
25
- const agent = this._createAgent(provider, this.config, this.logger);
28
+ const useHost = this._askHost && isHostAgent(provider);
29
+ const agent = useHost
30
+ ? new HostAgent(this.config, this.logger, { askHost: this._askHost })
31
+ : this._createAgent(provider, this.config, this.logger);
32
+
33
+ if (useHost) {
34
+ this.logger.info(`Host-as-coder: delegating to host AI (skipping ${provider} subprocess)`);
35
+ }
26
36
 
27
37
  const prompt = buildCoderPrompt({
28
38
  task: task || this.context?.task || "",
@@ -83,8 +83,9 @@ export async function loadMostRecentSession() {
83
83
 
84
84
  export async function resumeSessionWithAnswer(sessionId, answer) {
85
85
  const session = await loadSession(sessionId);
86
- if (session.status !== "paused") {
87
- throw new Error(`Session ${sessionId} is not paused (status: ${session.status})`);
86
+ const resumable = new Set(["paused", "running", "failed", "stopped"]);
87
+ if (!resumable.has(session.status)) {
88
+ throw new Error(`Session ${sessionId} cannot be resumed (status: ${session.status})`);
88
89
  }
89
90
  const pausedState = session.paused_state;
90
91
  if (!pausedState) {
@@ -30,4 +30,25 @@ export async function detectAvailableAgents() {
30
30
  return results;
31
31
  }
32
32
 
33
+ /**
34
+ * Detect which AI agent is the current MCP host (if any).
35
+ * Returns the agent name ("claude", "codex", etc.) or null if not inside an agent.
36
+ */
37
+ export function detectHostAgent() {
38
+ if (process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE === "1") return "claude";
39
+ if (process.env.CODEX_CLI === "1" || process.env.CODEX === "1") return "codex";
40
+ if (process.env.GEMINI_CLI === "1") return "gemini";
41
+ if (process.env.OPENCODE === "1") return "opencode";
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Check if a given provider matches the current host agent.
47
+ * When true, we can skip subprocess spawning and delegate to the host.
48
+ */
49
+ export function isHostAgent(provider) {
50
+ const host = detectHostAgent();
51
+ return host !== null && host === provider;
52
+ }
53
+
33
54
  export { KNOWN_AGENTS };