@triflux/remote 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.
@@ -4,8 +4,16 @@
4
4
  // Remote support: host option → SSH-based git operations via remote-session.mjs.
5
5
 
6
6
  import { execFile } from "node:child_process";
7
- import { access, mkdir, readdir, realpath, rm } from "node:fs/promises";
8
- import { normalize, relative, resolve } from "node:path";
7
+ import {
8
+ access,
9
+ mkdir,
10
+ mkdtemp,
11
+ readdir,
12
+ realpath,
13
+ rm,
14
+ } from "node:fs/promises";
15
+ import { tmpdir } from "node:os";
16
+ import { join, normalize, relative, resolve } from "node:path";
9
17
  import { remoteGit, validateHost } from "./remote-session.mjs";
10
18
 
11
19
  const SWARM_ROOT = ".codex-swarm";
@@ -327,24 +335,23 @@ export async function rebaseShardOntoIntegration({
327
335
  }
328
336
  }
329
337
 
330
- // #127 BUG-E: capture caller's branch BEFORE any checkout. Without
331
- // restoring it in finally, this function leaks main repo HEAD onto
332
- // integrationBranch and every subsequent Edit/commit lands on the swarm
333
- // temp branch silently (observed 2026-04-20: probe + edits + commits all
334
- // ended up on swarm/.../merge while user thought they were on main).
335
- let originalBranch = null;
336
- try {
337
- const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], rootDir);
338
- if (head && head !== "HEAD") originalBranch = head;
339
- } catch {
340
- /* detached HEAD or unknown — skip restore */
341
- }
342
-
343
- // Backup integration HEAD for rollback
338
+ // #127 BUG-E accident history: this function used to checkout the
339
+ // integration branch in rootDir, leak rootDir HEAD, and redirect later user
340
+ // edits/commits into swarm/.../merge. Integration now happens exclusively in
341
+ // a disposable worktree, so rootDir HEAD never moves and needs no restore.
342
+ //
343
+ // #129 BUG-J accident history: checkout failure was followed by a blind
344
+ // reset --hard (or branch -f rollback) against rootDir, which rewound the
345
+ // caller branch. Rollback now resets only the disposable worktree; there is
346
+ // no rootDir reset path and therefore no stash-create last-resort path.
344
347
  const backupCommit = await git(["rev-parse", integrationBranch], rootDir);
345
348
 
346
349
  let shaList = [];
347
350
  let ffError = null;
351
+ let integrationWorktreeAdded = false;
352
+ const integrationWorktree = await mkdtemp(
353
+ join(tmpdir(), "tfx-swarm-integration-"),
354
+ );
348
355
 
349
356
  try {
350
357
  const log = await git(
@@ -361,11 +368,15 @@ export async function rebaseShardOntoIntegration({
361
368
  .map((s) => s.trim())
362
369
  .filter(Boolean);
363
370
 
364
- await git(["checkout", integrationBranch], rootDir);
371
+ await git(
372
+ ["worktree", "add", "--quiet", integrationWorktree, integrationBranch],
373
+ rootDir,
374
+ );
375
+ integrationWorktreeAdded = true;
365
376
 
366
377
  try {
367
- await git(["merge", "--ff-only", shardBranch], rootDir);
368
- const headCommit = await git(["rev-parse", "HEAD"], rootDir);
378
+ await git(["merge", "--ff-only", shardBranch], integrationWorktree);
379
+ const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
369
380
  return {
370
381
  ok: true,
371
382
  headCommit,
@@ -376,12 +387,15 @@ export async function rebaseShardOntoIntegration({
376
387
  } catch (err) {
377
388
  ffError = err;
378
389
  try {
379
- await git(["merge", "--abort"], rootDir);
390
+ await git(["merge", "--abort"], integrationWorktree);
380
391
  } catch {
381
392
  /* ff-only usually leaves no merge state */
382
393
  }
383
394
  try {
384
- await git(["reset", "--hard", backupCommit], rootDir);
395
+ // Safe destructive operation: this cwd is the disposable integration
396
+ // worktree, never rootDir. It restores the integration ref before the
397
+ // cherry-pick fallback without touching user files.
398
+ await git(["reset", "--hard", backupCommit], integrationWorktree);
385
399
  } catch {
386
400
  /* fallback below will report if cherry-pick also fails */
387
401
  }
@@ -391,10 +405,10 @@ export async function rebaseShardOntoIntegration({
391
405
  // fail when histories diverge or a shard branch is checked out by a swarm
392
406
  // worktree. Cherry-pick reads commits without moving the shard branch ref.
393
407
  for (const sha of shaList) {
394
- await git(["cherry-pick", sha], rootDir);
408
+ await git(["cherry-pick", sha], integrationWorktree);
395
409
  }
396
410
 
397
- const headCommit = await git(["rev-parse", "HEAD"], rootDir);
411
+ const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
398
412
  return {
399
413
  ok: true,
400
414
  headCommit,
@@ -406,42 +420,17 @@ export async function rebaseShardOntoIntegration({
406
420
  : "cherry-pick fallback",
407
421
  };
408
422
  } catch (err) {
409
- try {
410
- await git(["cherry-pick", "--abort"], rootDir);
411
- } catch {
412
- /* already clean */
413
- }
414
-
415
- // #129 BUG-J: Roll back integrationBranch without mutating whatever
416
- // branch HEAD currently points at. The previous implementation did a
417
- // blind `reset --hard backupCommit` on current HEAD — if the earlier
418
- // `git checkout integrationBranch` inside the try block failed (e.g.
419
- // integrationBranch was already checked out by a swarm worktree) the
420
- // caller's branch silently rewound to integrationBranch's backup commit.
421
- // Observed 2026-04-20: fix branch tree lost to main HEAD during swarm
422
- // probe, requiring `git merge --ff-only origin/<branch>` recovery.
423
- let currentBranch = null;
424
- try {
425
- const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], rootDir);
426
- if (head && head !== "HEAD") currentBranch = head;
427
- } catch {
428
- /* detached — fall through to ref-only update */
429
- }
430
-
431
- if (currentBranch === integrationBranch) {
432
- // HEAD is on integrationBranch — reset advances this branch only.
423
+ if (integrationWorktreeAdded) {
433
424
  try {
434
- await git(["reset", "--hard", backupCommit], rootDir);
425
+ await git(["cherry-pick", "--abort"], integrationWorktree);
435
426
  } catch {
436
- /* best-effort */
427
+ /* already clean */
437
428
  }
438
- } else {
439
- // HEAD is elsewhere (originalBranch, detached, or another branch).
440
- // Update the integrationBranch ref directly; never touch current HEAD.
441
429
  try {
442
- await git(["branch", "-f", integrationBranch, backupCommit], rootDir);
430
+ // #129 rollback is now local to the disposable integration worktree.
431
+ await git(["reset", "--hard", backupCommit], integrationWorktree);
443
432
  } catch {
444
- /* best-effort: branch may be checked out in another worktree */
433
+ /* best-effort */
445
434
  }
446
435
  }
447
436
 
@@ -453,14 +442,22 @@ export async function rebaseShardOntoIntegration({
453
442
  fallbackReason: ffError?.message || null,
454
443
  };
455
444
  } finally {
456
- // Always restore caller's branch. Skip if originalBranch is null
457
- // (detached HEAD case) or already on target.
458
- if (originalBranch) {
445
+ if (integrationWorktreeAdded) {
459
446
  try {
460
- await git(["checkout", originalBranch], rootDir);
447
+ await git(
448
+ ["worktree", "remove", "--force", integrationWorktree],
449
+ rootDir,
450
+ );
461
451
  } catch {
462
- /* best-effort — caller will see HEAD on integrationBranch */
452
+ await rm(integrationWorktree, { recursive: true, force: true });
453
+ try {
454
+ await git(["worktree", "prune"], rootDir);
455
+ } catch {
456
+ /* best-effort metadata cleanup */
457
+ }
463
458
  }
459
+ } else {
460
+ await rm(integrationWorktree, { recursive: true, force: true });
464
461
  }
465
462
  }
466
463
  }
package/hub/tools.mjs CHANGED
@@ -2,6 +2,11 @@
2
2
  // register/status/publish/ask/poll/handoff/HITL + team proxy
3
3
  // 모든 도구 응답: { ok: boolean, error?: { code, message }, data?: ... }
4
4
 
5
+ import {
6
+ DEFAULT_ASSIGN_TIMEOUT_MS,
7
+ DEFAULT_ASSIGN_TTL_MS,
8
+ DEFAULT_HANDOFF_TTL_MS,
9
+ } from "@triflux/core/hub/lib/timeout-defaults.mjs";
5
10
  import { createPipeline, ensurePipelineTable } from "@triflux/core/hub/pipeline/index.mjs";
6
11
  import {
7
12
  initPipelineState,
@@ -10,7 +15,6 @@ import {
10
15
  } from "@triflux/core/hub/pipeline/state.mjs";
11
16
  import { sendInputToConductorSession } from "./team/conductor-registry.mjs";
12
17
  import { getTeamBridge } from "@triflux/core/hub/team-bridge.mjs";
13
- import { DEFAULT_ASSIGN_TIMEOUT_MS, DEFAULT_ASSIGN_TTL_MS, DEFAULT_HANDOFF_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
14
18
 
15
19
  const TEAM_BRIDGE_NOT_REGISTERED = "bridge_not_registered";
16
20
 
@@ -149,6 +153,14 @@ export function createTools(store, router, hitl, pipe = null) {
149
153
  },
150
154
  topics: { type: "array", items: { type: "string" }, maxItems: 64 },
151
155
  metadata: { type: "object" },
156
+ project_id: { type: "string", pattern: "^prj_[A-Za-z0-9_-]{22}$" },
157
+ session_id: { type: "string", minLength: 1, maxLength: 256 },
158
+ host_id: { type: "string", pattern: "^hst_[A-Za-z0-9_-]+$" },
159
+ transport_locators_json: {
160
+ description:
161
+ "tfx.transport-locator.v1 객체 배열 또는 그 JSON 문자열",
162
+ oneOf: [{ type: "array", items: { type: "object" } }, { type: "string" }],
163
+ },
152
164
  heartbeat_ttl_ms: {
153
165
  type: "integer",
154
166
  minimum: 10000,
@@ -146,9 +146,11 @@ export function resolveCodexProfileConfig(profileName) {
146
146
  } catch {
147
147
  // Profile file absent (non-file alias). Derive effort from the naming
148
148
  // convention; leave model unset so codex uses its config.toml default.
149
- const suffix = /_(xhigh|high|med|medium|low)$/.exec(profileName);
149
+ const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(profileName);
150
150
  if (suffix) {
151
151
  const map = {
152
+ ultra: "ultra",
153
+ max: "max",
152
154
  xhigh: "xhigh",
153
155
  high: "high",
154
156
  med: "medium",
@@ -193,16 +195,27 @@ export function buildCodexArguments(prompt, opts = {}) {
193
195
  // profile effort merges on top of any opts.config.
194
196
  if (typeof opts.profile === "string" && opts.profile) {
195
197
  const resolved = resolveCodexProfileConfig(opts.profile);
196
- if (resolved.model && typeof args.model !== "string") {
197
- args.model = resolved.model;
198
+ const profileDerivedUltra =
199
+ resolved.reasoningEffort?.toLowerCase() === "ultra";
200
+ if (typeof args.model !== "string") {
201
+ if (profileDerivedUltra) args.model = "gpt-5.6-sol";
202
+ else if (resolved.model) args.model = resolved.model;
198
203
  }
199
204
  if (resolved.reasoningEffort) {
200
205
  args.config = {
201
206
  ...(args.config || {}),
202
- model_reasoning_effort: resolved.reasoningEffort,
207
+ model_reasoning_effort: profileDerivedUltra
208
+ ? "max"
209
+ : resolved.reasoningEffort,
203
210
  };
204
211
  }
205
212
  }
213
+ if (typeof opts.reasoningEffort === "string" && opts.reasoningEffort) {
214
+ args.config = {
215
+ ...(args.config || {}),
216
+ model_reasoning_effort: opts.reasoningEffort,
217
+ };
218
+ }
206
219
 
207
220
  return args;
208
221
  }
@@ -694,6 +707,9 @@ function parseCliArgs(argv) {
694
707
  case "--model":
695
708
  options.model = next();
696
709
  break;
710
+ case "--reasoning-effort":
711
+ options.reasoningEffort = next();
712
+ break;
697
713
  case "--approval-policy":
698
714
  options.approvalPolicy = next();
699
715
  break;
@@ -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 "@triflux/core/scripts/lib/agent-route-policy.mjs";
14
15
  import { resolveBashExecutable } from "@triflux/core/hub/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/remote",
3
- "version": "10.43.0",
3
+ "version": "10.44.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -0,0 +1,307 @@
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", "gpt56_terra_med", "gpt56_terra_high", "gpt56_sol_xhigh",
211
+ "gpt56_sol_max", "gpt56_sol_ultra",
212
+ ]),
213
+ );
214
+ export const ULTRA_ELIGIBLE_CODEX_AGENTS = Object.freeze(
215
+ new Set(["deep-executor", "scientist-deep"]),
216
+ );
217
+ const NESTED_GLOBAL_PROFILE_OVERRIDES = new Set([
218
+ "max", "ultra", "gpt56_sol_max", "gpt56_sol_ultra",
219
+ ]);
220
+
221
+ export function resolveCodexAgentPolicy(agent) {
222
+ return freezePolicy(
223
+ CODEX_AGENT_POLICY[agent] ?? CODEX_AGENT_POLICY[DEFAULT_CODEX_AGENT],
224
+ );
225
+ }
226
+
227
+ export function resolveCodexAgentProfile(
228
+ agent,
229
+ { profileOverride = "auto", retryProfile = null, nested = false, allowCustom = false } = {},
230
+ ) {
231
+ const defaultProfile = resolveCodexAgentPolicy(agent).profile;
232
+ const retryRequested = String(retryProfile ?? "").trim();
233
+ const requested = retryRequested || String(profileOverride || "auto").trim();
234
+ if (!requested || requested === "auto") return defaultProfile;
235
+ const canonical = requested === "max" ? "gpt56_sol_max" : requested === "ultra" ? "gpt56_sol_ultra" : requested;
236
+ if (!CANONICAL_CODEX_PROFILE_OVERRIDES.has(canonical)) {
237
+ if (allowCustom) return requested;
238
+ throw new Error(`[agent-route-policy] unsupported profile override: ${requested}`);
239
+ }
240
+ if (canonical !== "gpt56_sol_ultra") return canonical;
241
+ return nested || !ULTRA_ELIGIBLE_CODEX_AGENTS.has(agent) ? "gpt56_sol_max" : canonical;
242
+ }
243
+
244
+ export function resolveNestedCodexAgentProfile(
245
+ agent,
246
+ { profileOverride = "auto", globalProfile = "auto", retryProfile = null, codexHome } = {},
247
+ ) {
248
+ const explicitProfile = String(profileOverride || "auto").trim();
249
+ const globalOverride = String(globalProfile || "auto").trim();
250
+ const effectiveOverride = explicitProfile !== "auto" ? explicitProfile : NESTED_GLOBAL_PROFILE_OVERRIDES.has(globalOverride) ? globalOverride : "auto";
251
+ const fallback = resolveCodexAgentProfile(agent);
252
+ const candidate = resolveCodexAgentProfile(agent, { profileOverride: effectiveOverride, retryProfile, nested: true, allowCustom: true });
253
+ const { effort } = resolveCodexProfileConfigValues(candidate, { codexHome });
254
+ if (!effort) return fallback;
255
+ return effort.toLowerCase() === "ultra" ? "gpt56_sol_max" : candidate;
256
+ }
257
+
258
+ export function describeCodexAgentPolicy() {
259
+ return {
260
+ defaultAgent: DEFAULT_CODEX_AGENT,
261
+ agents: Object.keys(CODEX_AGENT_POLICY),
262
+ };
263
+ }
264
+
265
+ function parseCliArgs(argv) {
266
+ const args = [...argv];
267
+ let format = "json";
268
+ let agent = DEFAULT_CODEX_AGENT;
269
+ while (args.length > 0) {
270
+ const flag = args.shift();
271
+ if (flag === "--format") format = args.shift() || "";
272
+ else if (flag === "--agent") agent = args.shift() || "";
273
+ else throw new Error(`unknown argument: ${flag}`);
274
+ }
275
+ if (!new Set(["json", "tsv"]).has(format)) {
276
+ throw new Error(`unsupported format: ${format}`);
277
+ }
278
+ return { format, agent };
279
+ }
280
+
281
+ function runCli() {
282
+ const { format, agent } = parseCliArgs(process.argv.slice(2));
283
+ const resolved = resolveCodexAgentPolicy(agent);
284
+ if (format === "tsv") {
285
+ process.stdout.write(
286
+ [
287
+ resolved.profile,
288
+ resolved.timeoutSec,
289
+ resolved.runMode,
290
+ resolved.opusOversight,
291
+ resolved.mcpHint,
292
+ resolved.subcommand,
293
+ ].join("\t"),
294
+ );
295
+ return;
296
+ }
297
+ process.stdout.write(`${JSON.stringify(resolved)}\n`);
298
+ }
299
+
300
+ if (import.meta.url === new URL(process.argv[1], "file:").href) {
301
+ try {
302
+ runCli();
303
+ } catch (error) {
304
+ process.stderr.write(`[agent-route-policy] ${error.message}\n`);
305
+ process.exitCode = 1;
306
+ }
307
+ }