@triflux/core 10.1.0 → 10.26.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 (145) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +191 -31
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +262 -34
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +1051 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +22 -21
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/schema/delegator-tools.schema.json +1 -1
  35. package/hub/delegator/service.mjs +93 -64
  36. package/hub/delegator/tool-definitions.mjs +35 -35
  37. package/hub/dynamic-routing-engine.mjs +511 -0
  38. package/hub/fullcycle.mjs +34 -18
  39. package/hub/gemini-adapter.mjs +82 -108
  40. package/hub/hitl.mjs +202 -143
  41. package/hub/intent.mjs +167 -38
  42. package/hub/lib/bash-path.mjs +73 -0
  43. package/hub/lib/cache-guard.mjs +43 -17
  44. package/hub/lib/env-detect.mjs +145 -0
  45. package/hub/lib/hosts-compat.mjs +319 -0
  46. package/hub/lib/mcp-response-cache.mjs +239 -0
  47. package/hub/lib/memory-store.mjs +285 -111
  48. package/hub/lib/path-utils.mjs +165 -0
  49. package/hub/lib/phase-manager.mjs +493 -0
  50. package/hub/lib/process-utils.mjs +797 -104
  51. package/hub/lib/prompt-tmp.mjs +33 -0
  52. package/hub/lib/risk-tier.mjs +53 -0
  53. package/hub/lib/spawn-trace.mjs +561 -0
  54. package/hub/lib/ssh-command.mjs +44 -24
  55. package/hub/lib/ssh-retry.mjs +5 -1
  56. package/hub/lib/staged-file-detect.mjs +43 -0
  57. package/hub/lib/state-snapshot.mjs +293 -0
  58. package/hub/lib/tfx-route-args.mjs +309 -0
  59. package/hub/lib/timeline-adapter.mjs +190 -0
  60. package/hub/lib/trace-recorder.mjs +153 -0
  61. package/hub/lib/uuidv7.mjs +4 -3
  62. package/hub/memory-doctor.mjs +266 -106
  63. package/hub/middleware/quota-middleware.mjs +70 -0
  64. package/hub/middleware/request-logger.mjs +61 -34
  65. package/hub/paths.mjs +9 -9
  66. package/hub/pipeline/gates/confidence.mjs +34 -15
  67. package/hub/pipeline/gates/consensus.mjs +27 -15
  68. package/hub/pipeline/gates/index.mjs +7 -3
  69. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  70. package/hub/pipeline/index.mjs +77 -42
  71. package/hub/pipeline/state.mjs +10 -10
  72. package/hub/pipeline/transitions.mjs +40 -23
  73. package/hub/platform.mjs +65 -48
  74. package/hub/quality/deslop.mjs +70 -49
  75. package/hub/reflexion.mjs +87 -13
  76. package/hub/research.mjs +33 -25
  77. package/hub/router.mjs +941 -791
  78. package/hub/routing/complexity.mjs +132 -29
  79. package/hub/routing/index.mjs +17 -12
  80. package/hub/routing/q-learning.mjs +78 -30
  81. package/hub/routing-snapshot.mjs +261 -0
  82. package/hub/session-fingerprint.mjs +127 -61
  83. package/hub/state.mjs +86 -44
  84. package/hub/team/claude-daemon-control.mjs +1130 -0
  85. package/hub/team/retry-state-machine.mjs +336 -0
  86. package/hub/token-mode.mjs +72 -71
  87. package/hub/workers/worker-utils.mjs +23 -17
  88. package/hud/colors.mjs +27 -9
  89. package/hud/constants.mjs +190 -27
  90. package/hud/context-monitor.mjs +133 -44
  91. package/hud/hud-qos-status.mjs +157 -50
  92. package/hud/mission-board.mjs +56 -0
  93. package/hud/providers/claude.mjs +398 -114
  94. package/hud/providers/codex.mjs +96 -21
  95. package/hud/providers/gemini.mjs +210 -52
  96. package/hud/renderers.mjs +440 -127
  97. package/hud/terminal.mjs +101 -31
  98. package/hud/utils.mjs +81 -39
  99. package/mesh/index.mjs +69 -0
  100. package/mesh/mesh-budget.mjs +137 -0
  101. package/mesh/mesh-heartbeat.mjs +100 -0
  102. package/mesh/mesh-protocol.mjs +96 -0
  103. package/mesh/mesh-queue.mjs +163 -0
  104. package/mesh/mesh-registry.mjs +78 -0
  105. package/mesh/mesh-router.mjs +77 -0
  106. package/package.json +5 -1
  107. package/scripts/lib/agent-json.mjs +27 -0
  108. package/scripts/lib/async.mjs +174 -0
  109. package/scripts/lib/claudemd-scanner.mjs +6 -1
  110. package/scripts/lib/cli-agy.mjs +62 -0
  111. package/scripts/lib/cli-claude.mjs +78 -0
  112. package/scripts/lib/cli-codex.mjs +199 -0
  113. package/scripts/lib/cli-gemini.mjs +37 -0
  114. package/scripts/lib/codex-recovery.sh +50 -0
  115. package/scripts/lib/context.mjs +3 -3
  116. package/scripts/lib/cross-review-utils.mjs +13 -5
  117. package/scripts/lib/doctor-env-checks.mjs +121 -0
  118. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  119. package/scripts/lib/env-probe.mjs +58 -30
  120. package/scripts/lib/env.mjs +14 -0
  121. package/scripts/lib/gemini-profiles.mjs +44 -10
  122. package/scripts/lib/handoff.mjs +33 -17
  123. package/scripts/lib/hook-utils.mjs +8 -6
  124. package/scripts/lib/hub.mjs +48 -0
  125. package/scripts/lib/keyword-rules.mjs +49 -20
  126. package/scripts/lib/logger.mjs +24 -24
  127. package/scripts/lib/mcp-filter.mjs +391 -247
  128. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  129. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  130. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  131. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  132. package/scripts/lib/mcp-health.mjs +773 -0
  133. package/scripts/lib/mcp-manifest.mjs +24 -14
  134. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  135. package/scripts/lib/pid.mjs +63 -0
  136. package/scripts/lib/process-utils.mjs +26 -0
  137. package/scripts/lib/psmux-info.mjs +117 -12
  138. package/scripts/lib/quota.mjs +47 -0
  139. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  140. package/scripts/lib/skill-state.mjs +220 -0
  141. package/scripts/lib/skill-template.mjs +32 -7
  142. package/scripts/lib/team.mjs +75 -0
  143. package/scripts/lib/timeout.mjs +66 -0
  144. package/scripts/lib/tmp.mjs +26 -0
  145. package/scripts/lib/toml.mjs +113 -0
@@ -1,46 +1,55 @@
1
- import { writeFileSync, mkdirSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { tmpdir } from 'node:os';
4
-
5
- import { runPreflight } from './codex-preflight.mjs';
6
- import { withRetry } from './workers/worker-utils.mjs';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { basename, join } from "node:path";
7
4
  import {
8
- createCircuitBreaker,
9
- createResult,
10
- appendWarnings,
11
5
  buildExecCommand,
6
+ createResult,
7
+ executeWithCircuitBroker,
12
8
  normalizePathForShell,
13
- shellQuote,
14
9
  runProcess,
15
- } from './cli-adapter-base.mjs';
16
-
17
- const breaker = createCircuitBreaker();
10
+ shellQuote,
11
+ } from "./cli-adapter-base.mjs";
12
+ import { runPreflight } from "./codex-preflight.mjs";
18
13
 
19
14
  // ── Codex-specific stall inference ──────────────────────────────
20
15
 
21
16
  function inferStallMode(stdout, stderr) {
22
17
  const text = `${stdout}\n${stderr}`.toLowerCase();
23
- if (/(rate.?limit|quota|throttl|too.many.requests|429|usage.limit)/u.test(text)) return 'rate_limited';
24
- if (/(approval|approve|permission|sandbox|bypass)/u.test(text)) return 'approval_stall';
25
- if (/\bmcp\b|context7|playwright|tavily|exa|brave|sequential|server/u.test(text)) return 'mcp_stall';
26
- return 'timeout';
18
+ if (
19
+ /(rate.?limit|quota|throttl|too.many.requests|429|usage.limit)/u.test(text)
20
+ )
21
+ return "rate_limited";
22
+ if (/(approval|approve|permission|sandbox|bypass)/u.test(text))
23
+ return "approval_stall";
24
+ if (
25
+ /\bmcp\b|context7|playwright|tavily|exa|brave|sequential|server/u.test(text)
26
+ )
27
+ return "mcp_stall";
28
+ return "timeout";
27
29
  }
28
30
 
29
31
  // ── Codex command building ──────────────────────────────────────
30
32
 
31
33
  function commandWithOverrides(command, prompt, codexPath, overrides = []) {
32
- const next = codexPath ? command.replace(/^codex\b/u, shellQuote(codexPath)) : command;
34
+ const next = codexPath
35
+ ? command.replace(/^codex\b/u, shellQuote(codexPath))
36
+ : command;
33
37
  if (!overrides.length) return next;
34
38
  const promptArg = JSON.stringify(prompt);
35
- const flags = overrides.flatMap((value) => ['-c', shellQuote(value)]).join(' ');
39
+ const flags = overrides
40
+ .flatMap((value) => ["-c", shellQuote(value)])
41
+ .join(" ");
36
42
  return next.endsWith(promptArg)
37
43
  ? `${next.slice(0, -promptArg.length)}${flags} ${promptArg}`
38
44
  : `${next} ${flags}`;
39
45
  }
40
46
 
41
47
  function buildOverrides(requested, excluded) {
42
- return [...new Set((requested || []).filter((name) => (excluded || []).includes(name)))]
43
- .map((name) => `mcp_servers.${name}.enabled=false`);
48
+ return [
49
+ ...new Set(
50
+ (requested || []).filter((name) => (excluded || []).includes(name)),
51
+ ),
52
+ ].map((name) => `mcp_servers.${name}.enabled=false`);
44
53
  }
45
54
 
46
55
  function buildAttempts(opts, preflight) {
@@ -63,49 +72,58 @@ function buildAttempts(opts, preflight) {
63
72
  // ── Launch script ───────────────────────────────────────────────
64
73
 
65
74
  function createLaunchScriptText(opts) {
66
- const parts = ['codex'];
67
- if (opts.profile) parts.push('--profile', shellQuote(opts.profile));
75
+ const parts = ["codex"];
76
+ if (opts.profile) parts.push("--profile", shellQuote(opts.profile));
68
77
  parts.push(
69
- 'exec',
70
- '--dangerously-bypass-approvals-and-sandbox',
71
- '--skip-git-repo-check',
78
+ "exec",
79
+ "--dangerously-bypass-approvals-and-sandbox",
80
+ "--skip-git-repo-check",
72
81
  '$(cat "$PROMPT_FILE")',
73
82
  );
74
83
  return [
75
- '#!/usr/bin/env bash',
76
- 'set -euo pipefail',
84
+ "#!/usr/bin/env bash",
85
+ "set -euo pipefail",
77
86
  `cd ${shellQuote(normalizePathForShell(opts.workdir))}`,
78
87
  `PROMPT_FILE=${shellQuote(normalizePathForShell(opts.promptFile))}`,
79
- `TFX_CODEX_TIMEOUT_MS=${shellQuote(String(opts.timeout ?? ''))}`,
80
- parts.join(' '),
81
- '',
82
- ].join('\n');
88
+ `TFX_CODEX_TIMEOUT_MS=${shellQuote(String(opts.timeout ?? ""))}`,
89
+ parts.join(" "),
90
+ "",
91
+ ].join("\n");
83
92
  }
84
93
 
85
94
  export function buildLaunchScript(opts = {}) {
86
- const dir = join(tmpdir(), 'triflux-codex-launch');
95
+ const dir = join(tmpdir(), "triflux-codex-launch");
87
96
  mkdirSync(dir, { recursive: true });
88
- const path = join(dir, `${String(opts.id || 'launch')}.sh`);
89
- writeFileSync(path, createLaunchScriptText(opts), 'utf8');
97
+ const path = join(dir, `${String(opts.id || "launch")}.sh`);
98
+ writeFileSync(path, createLaunchScriptText(opts), "utf8");
90
99
  return path;
91
100
  }
92
101
 
93
102
  // ── Exec args builder ───────────────────────────────────────────
94
103
 
95
104
  export function buildExecArgs(opts = {}) {
96
- const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
105
+ const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
97
106
  const command = buildExecCommand(prompt, opts.resultFile || null, {
98
107
  profile: opts.profile,
99
108
  skipGitRepoCheck: true,
100
109
  sandboxBypass: true,
101
110
  cwd: opts.cwd,
111
+ // stdinPrompt 가 명시되면 buildExecCommand 로 forward.
112
+ // headless 워커 spawn (backend.mjs) 은 default (stdin on, macOS hook fix).
113
+ // launcher path (launcher-template.mjs) 는 결정론 위해 false 전달.
114
+ stdinPrompt: opts.stdinPrompt,
102
115
  });
103
116
 
104
- if (!prompt) return command.replace(/\s+""$/u, '');
117
+ if (!prompt) return command.replace(/\s+""$/u, "");
105
118
 
106
119
  let result;
107
120
  const quotedPrompt = JSON.stringify(prompt);
108
- if (/^\(Get-Content\b[\s\S]*\)$/u.test(prompt) && command.endsWith(quotedPrompt)) {
121
+ // PowerShell: (Get-Content -Raw '...'), bash: "$(cat '...')"
122
+ if (
123
+ (/^\(Get-Content\b[\s\S]*\)$/u.test(prompt) ||
124
+ /^"\$\(cat\b[\s\S]*\)"$/u.test(prompt)) &&
125
+ command.endsWith(quotedPrompt)
126
+ ) {
109
127
  result = `${command.slice(0, -quotedPrompt.length)}${prompt}`;
110
128
  } else {
111
129
  result = command;
@@ -120,13 +138,16 @@ export function buildExecArgs(opts = {}) {
120
138
 
121
139
  // ── Codex execution ─────────────────────────────────────────────
122
140
 
123
- async function runCodex(prompt, workdir, preflight, attempt) {
124
- const dir = join(tmpdir(), 'triflux-codex-exec');
141
+ async function runCodex(prompt, workdir, preflight, attempt, lease) {
142
+ const dir = join(tmpdir(), "triflux-codex-exec");
125
143
  mkdirSync(dir, { recursive: true });
126
- const resultFile = join(dir, `codex-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
144
+ const resultFile = join(
145
+ dir,
146
+ `codex-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`,
147
+ );
127
148
  const command = commandWithOverrides(
128
149
  buildExecCommand(prompt, resultFile, {
129
- profile: attempt.profile,
150
+ profile: lease?.profile ?? attempt.profile,
130
151
  skipGitRepoCheck: true,
131
152
  sandboxBypass: attempt.forceBypass,
132
153
  }),
@@ -134,66 +155,131 @@ async function runCodex(prompt, workdir, preflight, attempt) {
134
155
  preflight.codexPath,
135
156
  buildOverrides(attempt.requested, attempt.excluded),
136
157
  );
137
- return runProcess(command, workdir, attempt.timeout, { resultFile, inferStallMode });
158
+ // PRD A1 — lease 메타데이터를 spawn env 에 적용한다. lease.authFile 이 있으면
159
+ // 해당 파일이 위치한 디렉토리를 CODEX_HOME 으로 export 해서 codex CLI 가 그
160
+ // account 의 auth.json 을 사용하게 한다. lease 가 null 이면 default ~/.codex
161
+ // 동작 유지 (회귀 없음). lease.env 는 추가 환경변수 (provider 고정 등) 주입용.
162
+ const spawnEnv = lease ? buildLeaseSpawnEnv(lease) : undefined;
163
+ const authCheck = validateLeaseAuth(lease, spawnEnv);
164
+ if (!authCheck.ok) {
165
+ return createResult(false, {
166
+ stderr: authCheck.error,
167
+ failureMode: "auth_sync",
168
+ skipCircuit: true,
169
+ });
170
+ }
171
+ return runProcess(command, workdir, attempt.timeout, {
172
+ resultFile,
173
+ inferStallMode,
174
+ spawnEnv,
175
+ });
138
176
  }
139
177
 
140
- // ── Public API ──────────────────────────────────────────────────
141
-
142
- export function getCircuitState(now) {
143
- return breaker.getState(now);
178
+ function buildLeaseSpawnEnv(lease) {
179
+ const extra = {};
180
+ if (lease.authFile) {
181
+ // lease.authFile 은 보통 ~/.claude/cache/tfx-hub/codex-auth-<account>.json 형식.
182
+ // codex CLI 는 CODEX_HOME 을 통해 auth.json 위치를 결정하므로, 이 cache 파일을
183
+ // 직접 CODEX_HOME 후보 디렉토리로 사용한다. cache 파일 자체가 auth.json 이름이
184
+ // 아니라면 후속 PR 에서 isolated dir + symlink/복사 처리한다 (PRD Open Question).
185
+ // 현재는 lease.authFile 의 dirname 을 export 하되, 그 dirname 안에 auth.json 이
186
+ // 실제로 있을 때만 적용한다 (false-positive 회피).
187
+ try {
188
+ const dir = dirnameOf(lease.authFile);
189
+ if (dir) extra.CODEX_HOME = dir;
190
+ } catch {}
191
+ }
192
+ if (lease.env && typeof lease.env === "object") {
193
+ Object.assign(extra, lease.env);
194
+ }
195
+ return Object.keys(extra).length ? { ...process.env, ...extra } : undefined;
144
196
  }
145
197
 
146
- export async function execute(opts = {}) {
147
- const entry = breaker.canExecute();
148
- if (!entry.allowed) {
149
- return createResult(false, { fellBack: true, failureMode: 'circuit_open' });
198
+ function validateLeaseAuth(lease, spawnEnv) {
199
+ if (!lease?.authFile) return { ok: true };
200
+ if (basename(lease.authFile) !== "auth.json") {
201
+ return {
202
+ ok: false,
203
+ error: `lease authFile must resolve to auth.json for account ${lease.id}`,
204
+ };
150
205
  }
151
-
152
- const preflight = await runPreflight({ mcpServers: opts.mcpServers, subcommand: 'exec' });
153
- if (!preflight.ok) {
154
- breaker.clearTrial();
155
- breaker.recordFailure(entry.halfOpen);
156
- return createResult(false, {
157
- stderr: appendWarnings('', preflight.warnings),
158
- fellBack: opts.fallbackToClaude !== false,
159
- failureMode: 'crash',
160
- });
206
+ if (!existsSync(lease.authFile)) {
207
+ return {
208
+ ok: false,
209
+ error: `lease authFile missing for account ${lease.id}: ${lease.authFile}`,
210
+ };
211
+ }
212
+ if (dirnameOf(lease.authFile) !== spawnEnv?.CODEX_HOME) {
213
+ return {
214
+ ok: false,
215
+ // This is currently expected to match because buildLeaseSpawnEnv derives
216
+ // CODEX_HOME from authFile. Keep the guard so future lease.env overrides
217
+ // cannot silently point Codex at a different account home.
218
+ error: `CODEX_HOME mismatch for account ${lease.id}`,
219
+ };
161
220
  }
162
-
163
- const attempts = buildAttempts(opts, preflight);
164
- let attemptIndex = 0;
165
- let lastResult = createResult(false);
166
-
167
221
  try {
168
- lastResult = await withRetry(async () => {
169
- const result = await runCodex(opts.prompt || '', opts.workdir || process.cwd(), preflight, attempts[attemptIndex]);
170
- const current = { ...result, stderr: appendWarnings(result.stderr, preflight.warnings), retried: attemptIndex > 0 };
171
- const canRetry = !current.ok && attemptIndex < attempts.length - 1;
172
- attemptIndex += 1;
173
- if (!canRetry) return current;
174
- const error = new Error('retry');
175
- error.retryable = true;
176
- error.result = current;
177
- throw error;
178
- }, {
179
- maxAttempts: attempts.length,
180
- baseDelayMs: 250,
181
- maxDelayMs: 750,
182
- shouldRetry: (error) => error?.retryable === true,
183
- });
222
+ const parsed = JSON.parse(readFileSync(lease.authFile, "utf8"));
223
+ const accountId =
224
+ parsed?.tokens?.account_id ??
225
+ parsed?.account_id ??
226
+ parsed?.accountId ??
227
+ null;
228
+ if (!accountId) {
229
+ return {
230
+ ok: false,
231
+ error: `lease authFile has no account_id for account ${lease.id}`,
232
+ };
233
+ }
234
+ if (accountId !== lease.id) {
235
+ return {
236
+ ok: false,
237
+ error: `lease authFile account mismatch: expected ${lease.id}, got ${accountId}`,
238
+ };
239
+ }
184
240
  } catch (error) {
185
- lastResult = error?.result || createResult(false, { stderr: String(error?.message || error) });
241
+ return {
242
+ ok: false,
243
+ error: `lease authFile unreadable for account ${lease.id}: ${error?.message || error}`,
244
+ };
186
245
  }
246
+ return { ok: true };
247
+ }
187
248
 
188
- if (lastResult.ok) {
189
- breaker.reset();
190
- return lastResult;
191
- }
249
+ function dirnameOf(filePath) {
250
+ if (typeof filePath !== "string" || !filePath) return null;
251
+ const lastSep = Math.max(
252
+ filePath.lastIndexOf("/"),
253
+ filePath.lastIndexOf("\\"),
254
+ );
255
+ if (lastSep < 0) return null;
256
+ return filePath.slice(0, lastSep);
257
+ }
192
258
 
193
- breaker.recordFailure(entry.halfOpen);
194
- return {
195
- ...lastResult,
196
- retried: attempts.length > 1,
197
- fellBack: opts.fallbackToClaude !== false,
198
- };
259
+ // ── Public API ──────────────────────────────────────────────────
260
+
261
+ export async function getCircuitState() {
262
+ const brokerMod = await import("./account-broker.mjs");
263
+ if (!brokerMod.broker) return { state: "closed", failures: [] };
264
+ const snap = brokerMod.broker
265
+ .snapshot()
266
+ .filter((a) => a.provider === "codex");
267
+ return snap.length
268
+ ? { state: snap[0].circuitState, accounts: snap }
269
+ : { state: "closed", failures: [] };
270
+ }
271
+
272
+ export function execute(opts = {}) {
273
+ return executeWithCircuitBroker({
274
+ provider: "codex",
275
+ runFn: runCodex,
276
+ preflightFn: (o) =>
277
+ runPreflight({
278
+ mcpServers: o.mcpServers,
279
+ subcommand: "exec",
280
+ workdir: o.workdir,
281
+ }),
282
+ buildAttemptsFn: buildAttempts,
283
+ opts,
284
+ });
199
285
  }
@@ -1,12 +1,12 @@
1
1
  // hub/codex-compat.mjs — backward-compatible facade for legacy imports
2
2
 
3
- /** @experimental */
3
+ /** @experimental 런타임 미연결 — 향후 통합 예정 */
4
4
  export {
5
+ buildExecCommand,
5
6
  CODEX_MCP_EXECUTION_EXIT_CODE,
6
7
  CODEX_MCP_TRANSPORT_EXIT_CODE,
7
- FEATURES,
8
- buildExecCommand,
9
8
  escapePwshSingleQuoted,
9
+ FEATURES,
10
10
  getCodexVersion,
11
11
  gte,
12
- } from './cli-adapter-base.mjs';
12
+ } from "./cli-adapter-base.mjs";
@@ -1,28 +1,30 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { homedir } from 'node:os';
4
- import { execSync } from 'node:child_process';
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
5
 
6
- import { whichCommandAsync } from './platform.mjs';
6
+ import { whichCommandAsync } from "./platform.mjs";
7
7
 
8
8
  const MIN_RECOMMENDED_MINOR = 118;
9
9
  let _cachedVersion = null;
10
10
 
11
11
  function escapeRegExp(value) {
12
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
13
  }
14
14
 
15
- function readConfigText(configPath = join(homedir(), '.codex', 'config.toml')) {
16
- if (!existsSync(configPath)) return '';
15
+ function readConfigText(configPath = join(homedir(), ".codex", "config.toml")) {
16
+ if (!existsSync(configPath)) return "";
17
17
  try {
18
- return readFileSync(configPath, 'utf8');
18
+ return readFileSync(configPath, "utf8");
19
19
  } catch {
20
- return '';
20
+ return "";
21
21
  }
22
22
  }
23
23
 
24
24
  function readTomlString(text, key) {
25
- const match = String(text).match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, 'mu'));
25
+ const match = String(text).match(
26
+ new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "mu"),
27
+ );
26
28
  return match?.[1] ?? null;
27
29
  }
28
30
 
@@ -30,13 +32,13 @@ function readSection(text, name) {
30
32
  const lines = String(text).split(/\r?\n/u);
31
33
  const header = `[${name}]`;
32
34
  const start = lines.findIndex((line) => line.trim() === header);
33
- if (start < 0) return '';
35
+ if (start < 0) return "";
34
36
  const body = [];
35
37
  for (const line of lines.slice(start + 1)) {
36
38
  if (/^\s*\[[^\]]+\]\s*$/u.test(line)) break;
37
39
  body.push(line);
38
40
  }
39
- return body.join('\n');
41
+ return body.join("\n");
40
42
  }
41
43
 
42
44
  /**
@@ -47,7 +49,10 @@ function readSection(text, name) {
47
49
  export function getCodexVersion() {
48
50
  if (_cachedVersion !== null) return _cachedVersion;
49
51
  try {
50
- const out = execSync('codex --version', { encoding: 'utf8', timeout: 5000 }).trim();
52
+ const out = execSync("codex --version", {
53
+ encoding: "utf8",
54
+ timeout: 5000,
55
+ }).trim();
51
56
  // "codex 0.117.0" 또는 "0.117.0" 형식 대응
52
57
  const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
53
58
  _cachedVersion = match ? parseInt(match[2], 10) : 0;
@@ -58,29 +63,34 @@ export function getCodexVersion() {
58
63
  }
59
64
 
60
65
  async function checkCodexInstalled() {
61
- const codexPath = await whichCommandAsync('codex');
66
+ const codexPath = await whichCommandAsync("codex");
62
67
  if (codexPath) return { codexPath, ok: true, warnings: [] };
63
68
  return {
64
69
  codexPath: null,
65
70
  ok: false,
66
- warnings: ['Codex CLI not found. Install Codex and ensure `codex` is available on PATH.'],
71
+ warnings: [
72
+ "Codex CLI not found. Install Codex and ensure `codex` is available on PATH.",
73
+ ],
67
74
  };
68
75
  }
69
76
 
70
77
  function checkCodexVersion() {
71
78
  const version = getCodexVersion();
72
- const warnings = version >= MIN_RECOMMENDED_MINOR
73
- ? []
74
- : [`Codex CLI 0.${version}.x detected; 0.${MIN_RECOMMENDED_MINOR}.x or newer is recommended.`];
79
+ const warnings =
80
+ version >= MIN_RECOMMENDED_MINOR
81
+ ? []
82
+ : [
83
+ `Codex CLI 0.${version}.x detected; 0.${MIN_RECOMMENDED_MINOR}.x or newer is recommended.`,
84
+ ];
75
85
  return { version, warnings };
76
86
  }
77
87
 
78
88
  function checkApprovalMode(configText, opts = {}) {
79
- const approvalMode = readTomlString(configText, 'approval_mode');
80
- const sandbox = readTomlString(configText, 'sandbox');
81
- const subcommand = opts.subcommand || 'exec';
89
+ const approvalMode = readTomlString(configText, "approval_mode");
90
+ const sandbox = readTomlString(configText, "sandbox");
91
+ const subcommand = opts.subcommand || "exec";
82
92
  return {
83
- needsBypass: subcommand === 'exec' || approvalMode !== 'full-auto',
93
+ needsBypass: subcommand === "exec" || approvalMode !== "full-auto",
84
94
  approvalMode,
85
95
  sandbox,
86
96
  };
@@ -88,28 +98,41 @@ function checkApprovalMode(configText, opts = {}) {
88
98
 
89
99
  async function verifyServerHealth(name, configText) {
90
100
  const section = readSection(configText, `mcp_servers.${name}`);
91
- if (!section) return { ok: false, warning: `MCP server '${name}' is not configured.` };
101
+ if (!section)
102
+ return { ok: false, warning: `MCP server '${name}' is not configured.` };
92
103
  if (/^\s*enabled\s*=\s*false\s*$/mu.test(section)) {
93
- return { ok: false, warning: `MCP server '${name}' is disabled in config.toml.` };
104
+ return {
105
+ ok: false,
106
+ warning: `MCP server '${name}' is disabled in config.toml.`,
107
+ };
94
108
  }
95
109
 
96
- const command = readTomlString(section, 'command');
110
+ const command = readTomlString(section, "command");
97
111
  if (command) {
98
112
  const resolved = await whichCommandAsync(command);
99
113
  return resolved
100
- ? { ok: true, warning: '' }
101
- : { ok: false, warning: `MCP server '${name}' command not found: ${command}` };
114
+ ? { ok: true, warning: "" }
115
+ : {
116
+ ok: false,
117
+ warning: `MCP server '${name}' command not found: ${command}`,
118
+ };
102
119
  }
103
120
 
104
- const url = readTomlString(section, 'url');
105
- if (!url || !/^https?:\/\//u.test(url)) return { ok: true, warning: '' };
121
+ const url = readTomlString(section, "url");
122
+ if (!url || !/^https?:\/\//u.test(url)) return { ok: true, warning: "" };
106
123
  try {
107
124
  const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
108
125
  return response.status < 500
109
- ? { ok: true, warning: '' }
110
- : { ok: false, warning: `MCP server '${name}' returned HTTP ${response.status}.` };
126
+ ? { ok: true, warning: "" }
127
+ : {
128
+ ok: false,
129
+ warning: `MCP server '${name}' returned HTTP ${response.status}.`,
130
+ };
111
131
  } catch {
112
- return { ok: false, warning: `MCP server '${name}' is unreachable at ${url}.` };
132
+ return {
133
+ ok: false,
134
+ warning: `MCP server '${name}' is unreachable at ${url}.`,
135
+ };
113
136
  }
114
137
  }
115
138
 
@@ -118,7 +141,7 @@ async function checkMcpHealth(mcpServers, configText) {
118
141
  const warnings = [];
119
142
 
120
143
  for (const name of Array.isArray(mcpServers) ? mcpServers : []) {
121
- const server = String(name ?? '').trim();
144
+ const server = String(name ?? "").trim();
122
145
  if (!server) continue;
123
146
  const result = await verifyServerHealth(server, configText);
124
147
  if (!result.ok) excludeMcpServers.push(server);
@@ -128,6 +151,53 @@ async function checkMcpHealth(mcpServers, configText) {
128
151
  return { excludeMcpServers, warnings };
129
152
  }
130
153
 
154
+ /**
155
+ * Detects codex config drift between global (~/.codex/config.toml) and the
156
+ * target workdir. Covers:
157
+ * 1. workdir-local `.codex/config.toml` overriding global approval_mode/sandbox
158
+ * 2. instruction-file asymmetry (AGENTS.md vs CLAUDE.md)
159
+ *
160
+ * @param {string} [workdir] absolute path to the invocation workdir
161
+ * @param {string} [globalConfigText] already-read ~/.codex/config.toml content
162
+ * @returns {{ warnings: string[] }}
163
+ */
164
+ export function detectWorkdirDrift(workdir, globalConfigText = "") {
165
+ if (!workdir || typeof workdir !== "string") return { warnings: [] };
166
+ const warnings = [];
167
+
168
+ const localCodexConfig = join(workdir, ".codex", "config.toml");
169
+ if (existsSync(localCodexConfig)) {
170
+ let localText = "";
171
+ try {
172
+ localText = readFileSync(localCodexConfig, "utf8");
173
+ } catch {
174
+ localText = "";
175
+ }
176
+ const keys = ["approval_mode", "sandbox", "model"];
177
+ for (const key of keys) {
178
+ const local = readTomlString(localText, key);
179
+ const global = readTomlString(globalConfigText, key);
180
+ if (local && local !== global) {
181
+ warnings.push(
182
+ `workdir '.codex/config.toml' overrides ${key}='${local}' (global='${global || "unset"}').`,
183
+ );
184
+ }
185
+ }
186
+ }
187
+
188
+ const hasAgentsMd = existsSync(join(workdir, "AGENTS.md"));
189
+ const hasClaudeMd = existsSync(join(workdir, "CLAUDE.md"));
190
+ if (hasAgentsMd !== hasClaudeMd) {
191
+ const present = hasAgentsMd ? "AGENTS.md" : "CLAUDE.md";
192
+ const missing = hasAgentsMd ? "CLAUDE.md" : "AGENTS.md";
193
+ warnings.push(
194
+ `workdir has ${present} but not ${missing}; codex and other CLIs may read different instruction sources.`,
195
+ );
196
+ }
197
+
198
+ return { warnings };
199
+ }
200
+
131
201
  export async function runPreflight(opts = {}) {
132
202
  const install = await checkCodexInstalled();
133
203
  if (!install.ok) {
@@ -147,14 +217,20 @@ export async function runPreflight(opts = {}) {
147
217
 
148
218
  const configText = readConfigText(opts.configPath);
149
219
  const approval = checkApprovalMode(configText, opts);
150
- if (approval.approvalMode !== 'full-auto') {
151
- warnings.push(`approval_mode is '${approval.approvalMode || 'unset'}'; bypass flag will be used.`);
220
+ if (approval.approvalMode !== "full-auto") {
221
+ warnings.push(
222
+ `approval_mode is '${approval.approvalMode || "unset"}'; bypass flag will be used.`,
223
+ );
152
224
  }
153
- if (approval.sandbox) warnings.push(`sandbox mode from config.toml: ${approval.sandbox}`);
225
+ if (approval.sandbox)
226
+ warnings.push(`sandbox mode from config.toml: ${approval.sandbox}`);
154
227
 
155
228
  const mcp = await checkMcpHealth(opts.mcpServers, configText);
156
229
  warnings.push(...mcp.warnings);
157
230
 
231
+ const drift = detectWorkdirDrift(opts.workdir, configText);
232
+ warnings.push(...drift.warnings);
233
+
158
234
  return {
159
235
  codexPath: install.codexPath,
160
236
  version,