triflux 10.43.0 → 10.44.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 (47) hide show
  1. package/README.md +2 -0
  2. package/adapters/codex/skills/tfx-harness/SKILL.md +30 -0
  3. package/adapters/codex/skills/tfx-harness/skill.json +5 -0
  4. package/bin/triflux.mjs +14 -0
  5. package/hooks/keyword-rules.json +139 -19
  6. package/hub/cli-adapter-base.mjs +11 -2
  7. package/hub/codex-adapter.mjs +3 -0
  8. package/hub/intent.mjs +24 -32
  9. package/hub/router.mjs +272 -0
  10. package/hub/schema.sql +61 -2
  11. package/hub/server.mjs +42 -0
  12. package/hub/store.mjs +577 -5
  13. package/hub/team/conductor.mjs +23 -2
  14. package/hub/team/execution-mode.mjs +12 -2
  15. package/hub/team/headless.mjs +16 -0
  16. package/hub/team/retry-state-machine.mjs +8 -3
  17. package/hub/team/swarm-hypervisor.mjs +1 -0
  18. package/hub/team/swarm-preflight.mjs +47 -0
  19. package/hub/team/synapse-http.mjs +149 -4
  20. package/hub/team/worktree-lifecycle.mjs +56 -59
  21. package/hub/tools.mjs +11 -0
  22. package/hub/workers/codex-mcp.mjs +20 -4
  23. package/hub/workers/delegator-mcp.mjs +3 -49
  24. package/package.json +2 -1
  25. package/scripts/__tests__/lint-skills.test.mjs +68 -0
  26. package/scripts/__tests__/tfx-route-node-entry.test.mjs +36 -0
  27. package/scripts/keyword-detector.mjs +55 -8
  28. package/scripts/lib/agent-route-policy.mjs +360 -0
  29. package/scripts/lib/cli-codex.mjs +22 -160
  30. package/scripts/lib/codex-profile-config.mjs +29 -3
  31. package/scripts/lib/keyword-rules.mjs +12 -0
  32. package/scripts/lint-skills.mjs +65 -0
  33. package/scripts/pack.mjs +13 -0
  34. package/scripts/release/check-packages-mirror.mjs +5 -0
  35. package/scripts/setup.mjs +94 -2
  36. package/scripts/tfx-route.mjs +14 -1
  37. package/scripts/tfx-route.sh +233 -70
  38. package/skills/tfx-harness/SKILL.md +19 -76
  39. package/skills/tfx-hub/SKILL.md +1 -1
  40. package/skills/tfx-interview/SKILL.md +3 -1
  41. package/skills/tfx-interview/skill.json +1 -13
  42. package/skills/tfx-profile/SKILL.md +25 -15
  43. package/skills/tfx-prune/SKILL.md +3 -1
  44. package/skills/tfx-prune/skill.json +1 -8
  45. package/skills/tfx-setup/SKILL.md +1 -1
  46. package/tui/codex-profile.mjs +11 -2
  47. package/tui/setup.mjs +2 -0
@@ -11,6 +11,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import * as z from "zod";
14
+ import { resolveCodexAgentPolicy } from "../../scripts/lib/agent-route-policy.mjs";
14
15
  import { resolveBashExecutable } from "../lib/bash-path.mjs";
15
16
  import { runHeadlessWithCleanup } from "../team/headless.mjs";
16
17
  import { buildWorkerSandboxEnv } from "../team/worker-sandbox.mjs";
@@ -33,57 +34,11 @@ const { buildPromptHint, getCodexMcpConfig, SUPPORTED_MCP_PROFILES } =
33
34
  await import(pathToFileURL(mcpFilterPath).href);
34
35
  const SERVER_INFO = { name: "triflux-delegator", version: "1.0.0" };
35
36
  const DEFAULT_CONTEXT_BYTES = 32 * 1024;
36
- const DEFAULT_ROUTE_TIMEOUT_SEC = 120;
37
37
  const DIRECT_PROGRESS_START = 5;
38
38
  const DIRECT_PROGRESS_RUNNING = 60;
39
39
  const DIRECT_PROGRESS_DONE = 100;
40
40
  const SEARCH_ENGINE_CACHE_PATH = [".omc", "state", "search-engines.json"];
41
41
 
42
- const AGENT_TIMEOUT_SEC = Object.freeze({
43
- executor: 1080,
44
- "build-fixer": 540,
45
- debugger: 900,
46
- "deep-executor": 3600,
47
- architect: 3600,
48
- planner: 3600,
49
- critic: 3600,
50
- analyst: 3600,
51
- "code-reviewer": 1800,
52
- "security-reviewer": 1800,
53
- "quality-reviewer": 1800,
54
- scientist: 1440,
55
- "scientist-deep": 3600,
56
- "document-specialist": 1440,
57
- designer: 900,
58
- writer: 900,
59
- explore: 300,
60
- verifier: 1200,
61
- "test-engineer": 300,
62
- "qa-tester": 300,
63
- spark: 180,
64
- });
65
-
66
- const CODEX_PROFILE_BY_AGENT = Object.freeze({
67
- executor: "gpt56_terra_high",
68
- "build-fixer": "gpt56_luna_low",
69
- debugger: "gpt56_sol_xhigh",
70
- "deep-executor": "gpt56_sol_xhigh",
71
- architect: "gpt56_sol_xhigh",
72
- planner: "gpt56_sol_xhigh",
73
- critic: "gpt56_sol_xhigh",
74
- analyst: "gpt56_sol_xhigh",
75
- "code-reviewer": "gpt56_terra_high",
76
- "security-reviewer": "gpt56_sol_xhigh",
77
- "quality-reviewer": "gpt56_terra_high",
78
- scientist: "gpt56_terra_high",
79
- "scientist-deep": "gpt56_sol_xhigh",
80
- "document-specialist": "gpt56_terra_high",
81
- verifier: "gpt56_terra_high",
82
- designer: "gpt56_sol_xhigh",
83
- writer: "gpt56_luna_low",
84
- spark: "gpt56_luna_low",
85
- });
86
-
87
42
  const REVIEW_INSTRUCTION_BY_AGENT = Object.freeze({
88
43
  "code-reviewer":
89
44
  "코드 리뷰 모드로 동작하라. 버그, 리스크, 회귀, 테스트 누락을 우선 식별하라.",
@@ -137,7 +92,7 @@ function resolveRouteScript(explicitPath, cwd = process.cwd()) {
137
92
  }
138
93
 
139
94
  function resolveCodexProfile(agentType) {
140
- return CODEX_PROFILE_BY_AGENT[agentType] || "high";
95
+ return resolveCodexAgentPolicy(agentType).profile;
141
96
  }
142
97
 
143
98
  function normalizeProvider(provider = "auto") {
@@ -154,8 +109,7 @@ function resolveTimeoutMs(agentType, timeoutMs) {
154
109
  return Math.trunc(timeoutMs);
155
110
  }
156
111
 
157
- const timeoutSec = AGENT_TIMEOUT_SEC[agentType] || DEFAULT_ROUTE_TIMEOUT_SEC;
158
- return timeoutSec * 1000;
112
+ return resolveCodexAgentPolicy(agentType).timeoutSec * 1000;
159
113
  }
160
114
 
161
115
  function resolveTimeoutSec(agentType, timeoutMs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.43.0",
3
+ "version": "10.44.0",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Antigravity, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "playwright-core": "^1.53.0"
29
29
  },
30
30
  "files": [
31
+ "adapters",
31
32
  "bin",
32
33
  "tui",
33
34
  "config",
@@ -98,4 +98,72 @@ describe("lint-skills", () => {
98
98
  rmSync(root, { recursive: true, force: true });
99
99
  }
100
100
  });
101
+
102
+ it("실행형 Agent 호출은 model=이 없으면 실패한다", () => {
103
+ const root = makeTempDir();
104
+ try {
105
+ const skillsDir = join(root, "skills");
106
+ writeSkill(
107
+ root,
108
+ "tfx-agent",
109
+ '---\nname: tfx-agent\ndescription: 한국어 설명\n---\nAgent(subagent_type="explore", prompt="x")',
110
+ );
111
+ const result = lintSkills({ skillsDir });
112
+ assert.equal(result.ok, false);
113
+ assert.equal(result.problems[0]?.code, "agent-model-required");
114
+ } finally {
115
+ rmSync(root, { recursive: true, force: true });
116
+ }
117
+ });
118
+
119
+ it("model=이 있는 실행형 Agent와 prose signature는 통과한다", () => {
120
+ const root = makeTempDir();
121
+ try {
122
+ const skillsDir = join(root, "skills");
123
+ writeSkill(
124
+ root,
125
+ "tfx-agent-ok",
126
+ '---\nname: tfx-agent-ok\ndescription: 한국어 설명\n---\nAgent()\nAgent(subagent_type="explore", model="haiku", prompt="x")',
127
+ );
128
+ const result = lintSkills({ skillsDir });
129
+ assert.equal(result.ok, true);
130
+ } finally {
131
+ rmSync(root, { recursive: true, force: true });
132
+ }
133
+ });
134
+
135
+ it("문자열 안의 괄호가 있어도 model=이 있는 실행형 Agent를 통과시킨다", () => {
136
+ const root = makeTempDir();
137
+ try {
138
+ const skillsDir = join(root, "skills");
139
+ writeSkill(
140
+ root,
141
+ "tfx-agent-nested-model",
142
+ '---\nname: tfx-agent-nested-model\ndescription: 한국어 설명\n---\nAgent(subagent_type="x", prompt="do (this)", model="haiku")',
143
+ );
144
+ const result = lintSkills({ skillsDir });
145
+ assert.equal(result.ok, true);
146
+ assert.deepEqual(result.problems, []);
147
+ } finally {
148
+ rmSync(root, { recursive: true, force: true });
149
+ }
150
+ });
151
+
152
+ it("문자열 안의 괄호가 있어도 model= 없는 실행형 Agent를 실패 처리한다", () => {
153
+ const root = makeTempDir();
154
+ try {
155
+ const skillsDir = join(root, "skills");
156
+ writeSkill(
157
+ root,
158
+ "tfx-agent-nested-missing-model",
159
+ '---\nname: tfx-agent-nested-missing-model\ndescription: 한국어 설명\n---\nAgent(prompt="foo(bar)", subagent_type="x")',
160
+ );
161
+ const result = lintSkills({ skillsDir });
162
+ assert.equal(result.ok, false);
163
+ assert.equal(result.problems.length, 1);
164
+ assert.equal(result.problems[0]?.code, "agent-model-required");
165
+ } finally {
166
+ rmSync(root, { recursive: true, force: true });
167
+ }
168
+ });
101
169
  });
@@ -262,6 +262,42 @@ describe("cli-codex.mjs — 핵심 매핑", () => {
262
262
  assert.equal(p.mcpProfile, "analyze");
263
263
  });
264
264
 
265
+ test("explicit max override selects the canonical Sol max profile", () => {
266
+ const p = codexAdapter.plan({
267
+ agent: "architect",
268
+ prompt: "x",
269
+ mcpProfile: "auto",
270
+ profileOverride: "max",
271
+ });
272
+ assert.equal(p.profile, "gpt56_sol_max");
273
+ assert.equal(p.effort, "gpt56_sol_max");
274
+ });
275
+
276
+ test("ultra is limited to top-level deep execution and research", () => {
277
+ const topLevel = codexAdapter.plan({
278
+ agent: "deep-executor",
279
+ prompt: "x",
280
+ profileOverride: "ultra",
281
+ nested: false,
282
+ });
283
+ const nested = codexAdapter.plan({
284
+ agent: "deep-executor",
285
+ prompt: "x",
286
+ profileOverride: "ultra",
287
+ nested: true,
288
+ });
289
+ const ineligible = codexAdapter.plan({
290
+ agent: "architect",
291
+ prompt: "x",
292
+ profileOverride: "ultra",
293
+ nested: false,
294
+ });
295
+
296
+ assert.equal(topLevel.profile, "gpt56_sol_ultra");
297
+ assert.equal(nested.profile, "gpt56_sol_max");
298
+ assert.equal(ineligible.profile, "gpt56_sol_max");
299
+ });
300
+
265
301
  test("security-reviewer → review 서브커맨드 + opus oversight", () => {
266
302
  const p = codexAdapter.plan({
267
303
  agent: "security-reviewer",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
3
+ import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import {
7
7
  compileRules,
@@ -90,6 +90,27 @@ export function sanitizeForKeywordDetection(text) {
90
90
  .trim();
91
91
  }
92
92
 
93
+ // repo_scope 는 path segment 단위로 매칭한다 — "triflux" 는 .../tools/triflux/... 에만
94
+ // 매칭되고 .../triflux-ideas 같은 부분 문자열에는 매칭되지 않는다. 빈 scope = 전역.
95
+ export function matchesRepoScope(path, scopes) {
96
+ if (!Array.isArray(scopes) || scopes.length === 0) return true;
97
+ if (typeof path !== "string" || !path) return false;
98
+ const segments = path.split(/[\\/]+/).filter(Boolean);
99
+ return scopes.some((scope) => segments.includes(scope));
100
+ }
101
+
102
+ // resolvedMatches[0] 단일 승자의 우선순위 역전 방지 — explicit(종결 라우팅) 규칙이
103
+ // 있으면 정렬 순서와 무관하게 그 규칙이 이긴다. 없으면 정렬 선두 유지.
104
+ export function selectPrimaryMatch(resolvedMatches) {
105
+ if (!Array.isArray(resolvedMatches) || resolvedMatches.length === 0) {
106
+ return null;
107
+ }
108
+ const explicitMatch = resolvedMatches.find(
109
+ (match) => match?.explicit === true,
110
+ );
111
+ return explicitMatch ?? resolvedMatches[0];
112
+ }
113
+
93
114
  function createHookOutput(additionalContext) {
94
115
  return {
95
116
  continue: true,
@@ -263,7 +284,6 @@ function main() {
263
284
  return;
264
285
  }
265
286
 
266
- const selected = resolvedMatches[0];
267
287
  const baseDir =
268
288
  typeof payload.cwd === "string" && payload.cwd
269
289
  ? payload.cwd
@@ -271,6 +291,15 @@ function main() {
271
291
  ? payload.directory
272
292
  : process.cwd();
273
293
 
294
+ const scopedMatches = resolvedMatches.filter((match) =>
295
+ matchesRepoScope(baseDir, match.repo_scope),
296
+ );
297
+ const selected = selectPrimaryMatch(scopedMatches);
298
+ if (!selected) {
299
+ console.log(JSON.stringify(createSuppressOutput()));
300
+ return;
301
+ }
302
+
274
303
  activateState(baseDir, selected.state, prompt, payload);
275
304
 
276
305
  if (selected.action === "context_hint" && selected.hint) {
@@ -308,9 +337,27 @@ function main() {
308
337
  console.log(JSON.stringify(createSuppressOutput()));
309
338
  }
310
339
 
311
- try {
312
- main();
313
- } catch (error) {
314
- console.error(`[triflux-keyword-detector] 예외 발생: ${error.message}`);
315
- console.log(JSON.stringify(createSuppressOutput()));
340
+ // 훅 실행(node scripts/keyword-detector.mjs)일 때만 main 구동 — 테스트 import 시
341
+ // stdin 대기로 행이 걸리지 않도록 direct-run 가드.
342
+ const isDirectRun = (() => {
343
+ try {
344
+ if (typeof process.argv[1] !== "string" || !process.argv[1]) return false;
345
+ const selfPath = fileURLToPath(import.meta.url);
346
+ const argvPath = resolve(process.argv[1]);
347
+ if (selfPath === argvPath) return true;
348
+ // 심링크 설치 경로: argv[1]이 심링크면 import.meta.url(실경로)과 어긋난다.
349
+ // canonical 경로로 재비교 (realpath 실패 시 non-direct 취급).
350
+ return selfPath === realpathSync(argvPath);
351
+ } catch {
352
+ return false;
353
+ }
354
+ })();
355
+
356
+ if (isDirectRun) {
357
+ try {
358
+ main();
359
+ } catch (error) {
360
+ console.error(`[triflux-keyword-detector] 예외 발생: ${error.message}`);
361
+ console.log(JSON.stringify(createSuppressOutput()));
362
+ }
316
363
  }
@@ -0,0 +1,360 @@
1
+ // Canonical Codex role policy. Concrete model IDs stay in profile config files.
2
+
3
+ import { resolveCodexProfileConfigValues } from "./codex-profile-config.mjs";
4
+
5
+ const policy = {
6
+ executor: {
7
+ profile: "gpt56_terra_high",
8
+ timeoutSec: 1080,
9
+ runMode: "fg",
10
+ opusOversight: false,
11
+ mcpHint: "implement",
12
+ subcommand: "exec",
13
+ },
14
+ codex: {
15
+ profile: "gpt56_terra_high",
16
+ timeoutSec: 1080,
17
+ runMode: "fg",
18
+ opusOversight: false,
19
+ mcpHint: "implement",
20
+ subcommand: "exec",
21
+ },
22
+ "build-fixer": {
23
+ profile: "gpt56_luna_low",
24
+ timeoutSec: 540,
25
+ runMode: "fg",
26
+ opusOversight: false,
27
+ mcpHint: "implement",
28
+ subcommand: "exec",
29
+ },
30
+ cleanup: {
31
+ profile: "gpt56_terra_med",
32
+ timeoutSec: 540,
33
+ runMode: "fg",
34
+ opusOversight: false,
35
+ mcpHint: "implement",
36
+ subcommand: "exec",
37
+ },
38
+ deslop: {
39
+ profile: "gpt56_terra_med",
40
+ timeoutSec: 540,
41
+ runMode: "fg",
42
+ opusOversight: false,
43
+ mcpHint: "implement",
44
+ subcommand: "exec",
45
+ },
46
+ debugger: {
47
+ profile: "gpt56_sol_xhigh",
48
+ timeoutSec: 900,
49
+ runMode: "bg",
50
+ opusOversight: false,
51
+ mcpHint: "implement",
52
+ subcommand: "exec",
53
+ },
54
+ "deep-executor": {
55
+ profile: "gpt56_sol_xhigh",
56
+ timeoutSec: 3600,
57
+ runMode: "bg",
58
+ opusOversight: true,
59
+ mcpHint: "implement",
60
+ subcommand: "exec",
61
+ },
62
+ architect: {
63
+ profile: "gpt56_sol_xhigh",
64
+ timeoutSec: 3600,
65
+ runMode: "bg",
66
+ opusOversight: true,
67
+ mcpHint: "analyze",
68
+ subcommand: "exec",
69
+ },
70
+ critic: {
71
+ profile: "gpt56_sol_xhigh",
72
+ timeoutSec: 3600,
73
+ runMode: "bg",
74
+ opusOversight: true,
75
+ mcpHint: "analyze",
76
+ subcommand: "exec",
77
+ },
78
+ planner: {
79
+ profile: "gpt56_sol_xhigh",
80
+ timeoutSec: 3600,
81
+ runMode: "fg",
82
+ opusOversight: true,
83
+ mcpHint: "analyze",
84
+ subcommand: "exec",
85
+ },
86
+ analyst: {
87
+ profile: "gpt56_sol_xhigh",
88
+ timeoutSec: 3600,
89
+ runMode: "fg",
90
+ opusOversight: true,
91
+ mcpHint: "analyze",
92
+ subcommand: "exec",
93
+ },
94
+ "code-reviewer": {
95
+ profile: "gpt56_terra_high",
96
+ timeoutSec: 1800,
97
+ runMode: "bg",
98
+ opusOversight: false,
99
+ mcpHint: "review",
100
+ subcommand: "review",
101
+ },
102
+ "quality-reviewer": {
103
+ profile: "gpt56_terra_high",
104
+ timeoutSec: 1800,
105
+ runMode: "bg",
106
+ opusOversight: false,
107
+ mcpHint: "review",
108
+ subcommand: "review",
109
+ },
110
+ "security-reviewer": {
111
+ profile: "gpt56_sol_xhigh",
112
+ timeoutSec: 1800,
113
+ runMode: "bg",
114
+ opusOversight: true,
115
+ mcpHint: "review",
116
+ subcommand: "review",
117
+ },
118
+ scientist: {
119
+ profile: "gpt56_terra_high",
120
+ timeoutSec: 1440,
121
+ runMode: "bg",
122
+ opusOversight: false,
123
+ mcpHint: "analyze",
124
+ subcommand: "exec",
125
+ },
126
+ "document-specialist": {
127
+ profile: "gpt56_terra_high",
128
+ timeoutSec: 1440,
129
+ runMode: "bg",
130
+ opusOversight: false,
131
+ mcpHint: "analyze",
132
+ subcommand: "exec",
133
+ },
134
+ "scientist-deep": {
135
+ profile: "gpt56_sol_xhigh",
136
+ timeoutSec: 3600,
137
+ runMode: "bg",
138
+ opusOversight: false,
139
+ mcpHint: "analyze",
140
+ subcommand: "exec",
141
+ },
142
+ verifier: {
143
+ profile: "gpt56_terra_high",
144
+ timeoutSec: 1200,
145
+ runMode: "fg",
146
+ opusOversight: false,
147
+ mcpHint: "review",
148
+ subcommand: "review",
149
+ },
150
+ "test-engineer": {
151
+ profile: "gpt56_terra_high",
152
+ timeoutSec: 1200,
153
+ runMode: "bg",
154
+ opusOversight: false,
155
+ mcpHint: "implement",
156
+ subcommand: "exec",
157
+ },
158
+ "qa-tester": {
159
+ profile: "gpt56_terra_high",
160
+ timeoutSec: 1200,
161
+ runMode: "bg",
162
+ opusOversight: false,
163
+ mcpHint: "review",
164
+ subcommand: "review",
165
+ },
166
+ spark: {
167
+ profile: "gpt56_luna_low",
168
+ timeoutSec: 180,
169
+ runMode: "fg",
170
+ opusOversight: false,
171
+ mcpHint: "implement",
172
+ subcommand: "exec",
173
+ },
174
+ // These entries are only for explicit direct-Codex overrides. agent-map.json
175
+ // remains the provider-selection authority for their normal execution.
176
+ designer: {
177
+ profile: "gpt56_sol_xhigh",
178
+ timeoutSec: 900,
179
+ runMode: "bg",
180
+ opusOversight: false,
181
+ mcpHint: "implement",
182
+ subcommand: "exec",
183
+ },
184
+ writer: {
185
+ profile: "gpt56_luna_low",
186
+ timeoutSec: 900,
187
+ runMode: "bg",
188
+ opusOversight: false,
189
+ mcpHint: "implement",
190
+ subcommand: "exec",
191
+ },
192
+ };
193
+
194
+ function freezePolicy(entry) {
195
+ return Object.freeze({ ...entry });
196
+ }
197
+
198
+ export const CODEX_AGENT_POLICY = Object.freeze(
199
+ Object.fromEntries(
200
+ Object.entries(policy).map(([agent, entry]) => [
201
+ agent,
202
+ freezePolicy(entry),
203
+ ]),
204
+ ),
205
+ );
206
+ export const DEFAULT_CODEX_AGENT = "executor";
207
+
208
+ export const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
209
+ new Set([
210
+ "gpt56_luna_low",
211
+ "gpt56_terra_med",
212
+ "gpt56_terra_high",
213
+ "gpt56_sol_xhigh",
214
+ "gpt56_sol_max",
215
+ "gpt56_sol_ultra",
216
+ ]),
217
+ );
218
+ export const ULTRA_ELIGIBLE_CODEX_AGENTS = Object.freeze(
219
+ new Set(["deep-executor", "scientist-deep"]),
220
+ );
221
+
222
+ const NESTED_GLOBAL_PROFILE_OVERRIDES = new Set([
223
+ "max",
224
+ "ultra",
225
+ "gpt56_sol_max",
226
+ "gpt56_sol_ultra",
227
+ ]);
228
+
229
+ export function resolveCodexAgentPolicy(agent) {
230
+ return freezePolicy(
231
+ CODEX_AGENT_POLICY[agent] ?? CODEX_AGENT_POLICY[DEFAULT_CODEX_AGENT],
232
+ );
233
+ }
234
+
235
+ /**
236
+ * Resolves a requested Codex profile against the canonical role policy.
237
+ *
238
+ * `retryProfile` intentionally wins over `profileOverride`: retry snapshots
239
+ * represent an already-selected escalation step and must not be replaced by a
240
+ * later process-level `TFX_CODEX_PROFILE`. Ultra is reserved for an eligible
241
+ * top-level role; every nested/sandboxed or ineligible request is max.
242
+ */
243
+ export function resolveCodexAgentProfile(
244
+ agent,
245
+ {
246
+ profileOverride = "auto",
247
+ retryProfile = null,
248
+ nested = false,
249
+ allowCustom = false,
250
+ } = {},
251
+ ) {
252
+ const defaultProfile = resolveCodexAgentPolicy(agent).profile;
253
+ const retryRequested = String(retryProfile ?? "").trim();
254
+ const requested = retryRequested || String(profileOverride || "auto").trim();
255
+ if (!requested || requested === "auto") return defaultProfile;
256
+
257
+ const canonical =
258
+ requested === "max"
259
+ ? "gpt56_sol_max"
260
+ : requested === "ultra"
261
+ ? "gpt56_sol_ultra"
262
+ : requested;
263
+ if (!CANONICAL_CODEX_PROFILE_OVERRIDES.has(canonical)) {
264
+ if (allowCustom) return requested;
265
+ throw new Error(
266
+ `[agent-route-policy] unsupported profile override: ${requested}`,
267
+ );
268
+ }
269
+ if (canonical !== "gpt56_sol_ultra") return canonical;
270
+ return nested || !ULTRA_ELIGIBLE_CODEX_AGENTS.has(agent)
271
+ ? "gpt56_sol_max"
272
+ : canonical;
273
+ }
274
+
275
+ /**
276
+ * Nested headless, team, worker, and delegator launches always select an
277
+ * explicit role profile rather than inheriting the global Codex config. A
278
+ * custom profile that resolves to ultra is also downgraded to max.
279
+ */
280
+ export function resolveNestedCodexAgentProfile(
281
+ agent,
282
+ {
283
+ profileOverride = "auto",
284
+ globalProfile = "auto",
285
+ retryProfile = null,
286
+ codexHome,
287
+ } = {},
288
+ ) {
289
+ const explicitProfile = String(profileOverride || "auto").trim();
290
+ const globalOverride = String(globalProfile || "auto").trim();
291
+ // Headless lanes must select their role policy explicitly. Only the max and
292
+ // ultra exception lanes may cross the process environment boundary.
293
+ const effectiveOverride =
294
+ explicitProfile !== "auto"
295
+ ? explicitProfile
296
+ : NESTED_GLOBAL_PROFILE_OVERRIDES.has(globalOverride)
297
+ ? globalOverride
298
+ : "auto";
299
+ const fallback = resolveCodexAgentProfile(agent);
300
+ const candidate = resolveCodexAgentProfile(agent, {
301
+ profileOverride: effectiveOverride,
302
+ retryProfile,
303
+ nested: true,
304
+ allowCustom: true,
305
+ });
306
+ const { effort } = resolveCodexProfileConfigValues(candidate, { codexHome });
307
+ if (!effort) return fallback;
308
+ return effort.toLowerCase() === "ultra" ? "gpt56_sol_max" : candidate;
309
+ }
310
+
311
+ export function describeCodexAgentPolicy() {
312
+ return {
313
+ defaultAgent: DEFAULT_CODEX_AGENT,
314
+ agents: Object.keys(CODEX_AGENT_POLICY),
315
+ };
316
+ }
317
+
318
+ function parseCliArgs(argv) {
319
+ const args = [...argv];
320
+ let format = "json";
321
+ let agent = DEFAULT_CODEX_AGENT;
322
+ while (args.length > 0) {
323
+ const flag = args.shift();
324
+ if (flag === "--format") format = args.shift() || "";
325
+ else if (flag === "--agent") agent = args.shift() || "";
326
+ else throw new Error(`unknown argument: ${flag}`);
327
+ }
328
+ if (!new Set(["json", "tsv"]).has(format)) {
329
+ throw new Error(`unsupported format: ${format}`);
330
+ }
331
+ return { format, agent };
332
+ }
333
+
334
+ function runCli() {
335
+ const { format, agent } = parseCliArgs(process.argv.slice(2));
336
+ const resolved = resolveCodexAgentPolicy(agent);
337
+ if (format === "tsv") {
338
+ process.stdout.write(
339
+ [
340
+ resolved.profile,
341
+ resolved.timeoutSec,
342
+ resolved.runMode,
343
+ resolved.opusOversight,
344
+ resolved.mcpHint,
345
+ resolved.subcommand,
346
+ ].join("\t"),
347
+ );
348
+ return;
349
+ }
350
+ process.stdout.write(`${JSON.stringify(resolved)}\n`);
351
+ }
352
+
353
+ if (import.meta.url === new URL(process.argv[1], "file:").href) {
354
+ try {
355
+ runCli();
356
+ } catch (error) {
357
+ process.stderr.write(`[agent-route-policy] ${error.message}\n`);
358
+ process.exitCode = 1;
359
+ }
360
+ }