triflux 10.46.0 → 10.48.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.
@@ -38,6 +38,8 @@ export function classifyBucket(bucket) {
38
38
 
39
39
  // primary/secondary를 window_minutes 기준으로 정규화
40
40
  // HUD 규약: primary=5h, secondary=1w
41
+ // window_minutes 가 비어 있으면 길이를 모르므로 보고된 칸에 둔다(app-server 스키마에서
42
+ // windowDurationMins 는 선택 필드). 길이를 알지만 5h/1w 가 아닌 버킷은 버린다.
41
43
  export function normalizeBuckets(rl) {
42
44
  let primary = null;
43
45
  let secondary = null;
@@ -46,6 +48,12 @@ export function normalizeBuckets(rl) {
46
48
  if (kind === "five_hour") primary = bucket;
47
49
  else if (kind === "weekly") secondary = bucket;
48
50
  }
51
+ if (!primary && rl.primary && rl.primary.window_minutes == null) {
52
+ primary = rl.primary;
53
+ }
54
+ if (!secondary && rl.secondary && rl.secondary.window_minutes == null) {
55
+ secondary = rl.secondary;
56
+ }
49
57
  return { primary, secondary };
50
58
  }
51
59
 
@@ -104,7 +112,35 @@ function isNewerSnapshot(candidate, current) {
104
112
  );
105
113
  }
106
114
 
107
- function selectCodexWindowSnapshot(snapshots, nowSec, windowKey) {
115
+ function positiveWindowMinutes(bucket) {
116
+ const minutes = Number(bucket?.window_minutes);
117
+ return minutes > 0 ? minutes : null;
118
+ }
119
+
120
+ // 계정 하나에는 같은 종류의 창이 한 번에 하나만 열린다. 브로커 계정이 없으면
121
+ // ~/.codex/sessions 와 HUD 라벨이 모두 ~/.codex/auth.json 계정 하나의 것이다. 그래서
122
+ // 세션 로그에 더 늦게 리셋되는 창이 나타났고 옛 창이 그 창이 열리기 전에만 관측됐다면,
123
+ // 옛 창은 조기 리셋(2026-09-23 실측)이나 계정 전환으로 끝난 창이다. 프로브로만 본 창은
124
+ // 다른 계정일 수 있어 근거로 쓰지 않는다. 새 창이 열린 뒤에도 관측된 창은 함께 도는
125
+ // 계정이므로 max-used 후보로 남긴다.
126
+ function isReplacedWindowGroup(group, other) {
127
+ if (other === group || !other.seenInSessions) return false;
128
+ if (other.minResetAt <= group.maxResetAt || !other.windowMinutes) {
129
+ return false;
130
+ }
131
+ const lastSeenMs = Date.parse(group.latest.timestamp);
132
+ if (!Number.isFinite(lastSeenMs)) return false;
133
+ // resets_at 은 실측으로 아래로 약 30초, 위로 1초 흔들리므로 가장 늦은 값으로 개시 시각을 잡는다.
134
+ const otherOpenedAtMs = (other.maxResetAt - other.windowMinutes * 60) * 1000;
135
+ return lastSeenMs <= otherOpenedAtMs;
136
+ }
137
+
138
+ function selectCodexWindowSnapshot(
139
+ snapshots,
140
+ nowSec,
141
+ windowKey,
142
+ replaceEndedWindows,
143
+ ) {
108
144
  const groups = [];
109
145
  const ungrouped = [];
110
146
 
@@ -126,19 +162,29 @@ function selectCodexWindowSnapshot(snapshots, nowSec, windowKey) {
126
162
  minResetAt: resetAt,
127
163
  maxResetAt: resetAt,
128
164
  latest: snapshot,
165
+ seenInSessions: !snapshot.probe,
166
+ windowMinutes: positiveWindowMinutes(snapshot[windowKey]),
129
167
  };
130
168
  groups.push(group);
131
169
  continue;
132
170
  }
133
171
 
172
+ if (!snapshot.probe) group.seenInSessions = true;
173
+ group.windowMinutes ??= positiveWindowMinutes(snapshot[windowKey]);
134
174
  group.minResetAt = Math.min(group.minResetAt, resetAt);
135
175
  group.maxResetAt = Math.max(group.maxResetAt, resetAt);
136
176
  if (isNewerSnapshot(snapshot, group.latest)) group.latest = snapshot;
137
177
  }
138
178
 
139
- const activeGroups = groups.filter(
179
+ const openGroups = groups.filter(
140
180
  (group) => Number(group.latest[windowKey]?.resets_at) > nowSec,
141
181
  );
182
+ const activeGroups = replaceEndedWindows
183
+ ? openGroups.filter(
184
+ (group) =>
185
+ !openGroups.some((other) => isReplacedWindowGroup(group, other)),
186
+ )
187
+ : openGroups;
142
188
  let selected = null;
143
189
  if (activeGroups.length > 0) {
144
190
  selected = activeGroups
@@ -168,16 +214,18 @@ function selectCodexWindowSnapshot(snapshots, nowSec, windowKey) {
168
214
  return { snapshot: selected, activeGroupCount: activeGroups.length };
169
215
  }
170
216
 
171
- function selectCodexSnapshot(snapshots, nowSec) {
217
+ function selectCodexSnapshot(snapshots, nowSec, replaceEndedWindows) {
172
218
  const primarySelection = selectCodexWindowSnapshot(
173
219
  snapshots,
174
220
  nowSec,
175
221
  "primary",
222
+ replaceEndedWindows,
176
223
  );
177
224
  const secondarySelection = selectCodexWindowSnapshot(
178
225
  snapshots,
179
226
  nowSec,
180
227
  "secondary",
228
+ replaceEndedWindows,
181
229
  );
182
230
  const primarySnapshot = primarySelection.snapshot;
183
231
  if (!primarySnapshot) return null;
@@ -198,7 +246,7 @@ function selectCodexSnapshot(snapshots, nowSec) {
198
246
  return selected;
199
247
  }
200
248
 
201
- function mergeRateLimitSnapshots(snapshots, nowSec) {
249
+ function mergeRateLimitSnapshots(snapshots, nowSec, replaceEndedWindows) {
202
250
  const merged = {};
203
251
  const codexSnapshots = [];
204
252
  for (const snapshot of snapshots) {
@@ -211,7 +259,11 @@ function mergeRateLimitSnapshots(snapshots, nowSec) {
211
259
  }
212
260
  }
213
261
 
214
- const codex = selectCodexSnapshot(codexSnapshots, nowSec);
262
+ const codex = selectCodexSnapshot(
263
+ codexSnapshots,
264
+ nowSec,
265
+ replaceEndedWindows,
266
+ );
215
267
  if (codex) merged.codex = codex;
216
268
  return merged;
217
269
  }
@@ -227,9 +279,16 @@ export function getCodexRateLimits({
227
279
  now = new Date(),
228
280
  maxLinesPerFile = 800,
229
281
  extraSnapshots = [],
282
+ // 교체된 창 판정(isReplacedWindowGroup). 브로커 Codex 계정이 있으면 끈다.
283
+ replaceEndedWindows = true,
230
284
  } = {}) {
231
285
  let syntheticBucket = null; // 최근 token_count에서 합성 (행 활성화 + 토큰 데이터용)
232
- const recentSnapshots = [...extraSnapshots];
286
+ // 프로브도 세션 로그와 같은 window_minutes 기준으로 5h/1w 슬롯을 정한다.
287
+ // app-server 는 5h 창이 없으면 주간 창을 primary 로 보낸다(2026-09-23 실측).
288
+ const recentSnapshots = extraSnapshots.map((snapshot) => ({
289
+ ...snapshot,
290
+ ...normalizeBuckets(snapshot),
291
+ }));
233
292
  const nowSec = Math.floor(now.getTime() / 1000);
234
293
 
235
294
  // 7일간 스캔: 실제 rate_limits 우선, 합성 버킷은 폴백
@@ -313,7 +372,11 @@ export function getCodexRateLimits({
313
372
  }
314
373
 
315
374
  const snapshots = dayOffset <= 1 ? recentSnapshots : daySnapshots;
316
- const mergedBuckets = mergeRateLimitSnapshots(snapshots, nowSec);
375
+ const mergedBuckets = mergeRateLimitSnapshots(
376
+ snapshots,
377
+ nowSec,
378
+ replaceEndedWindows,
379
+ );
317
380
  if (Object.keys(mergedBuckets).length > 0) {
318
381
  if (syntheticBucket) {
319
382
  const main =
@@ -369,10 +432,30 @@ export async function collectProbeSnapshots(now = new Date()) {
369
432
  );
370
433
  }
371
434
 
435
+ // 브로커 계정은 자기 CODEX_HOME 에서 돌기 때문에, 옛 창이 전환 전 계정의 창이어도 그
436
+ // 계정이 아직 쓰이고 있을 수 있다. accounts.json 의 codex 항목과 프로브용 인증 캐시로 판정한다.
437
+ export function hasBrokerCodexAccounts({
438
+ brokerDir = CODEX_BROKER_AUTH_CACHE_DIR,
439
+ } = {}) {
440
+ const accounts = readJson(join(brokerDir, "accounts.json"), null);
441
+ if (Array.isArray(accounts?.codex) && accounts.codex.length > 0) return true;
442
+ try {
443
+ return readdirSync(brokerDir).some((file) =>
444
+ /^codex-auth-.+\.json$/.test(file),
445
+ );
446
+ } catch {
447
+ return false;
448
+ }
449
+ }
450
+
372
451
  export async function refreshCodexRateLimitsCache() {
373
452
  const now = new Date();
374
453
  const extraSnapshots = await collectProbeSnapshots(now);
375
- const buckets = getCodexRateLimits({ now, extraSnapshots });
454
+ const buckets = getCodexRateLimits({
455
+ now,
456
+ extraSnapshots,
457
+ replaceEndedWindows: !hasBrokerCodexAccounts(),
458
+ });
376
459
  // buckets가 null이어도 캐시 갱신 (stale 데이터 제거)
377
460
  writeJsonSafe(CODEX_QUOTA_CACHE_PATH, { timestamp: Date.now(), buckets });
378
461
  return buckets;
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { resolveRoleControlSnapshot } from "../../hub/lib/cto-env.mjs";
3
4
  import {
4
5
  CTO_LAKE_CURRENT_RELATIVE_PATH,
5
6
  CTO_STATUS_LINE_MAX_CHARS,
@@ -53,6 +54,8 @@ function extractStatusLine(lines) {
53
54
  }
54
55
 
55
56
  export function readCtoStatus(options = {}) {
57
+ if (!resolveRoleControlSnapshot(options.env).auto_collect_enabled)
58
+ return null;
56
59
  try {
57
60
  const currentPath = resolveCurrentPath(options);
58
61
  const stat = statSync(currentPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.46.0",
3
+ "version": "10.48.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": {
@@ -87,7 +87,7 @@ describe("tfx-route bash/node parity — Phase 0 회귀가드", () => {
87
87
  assert.equal(plan.agent, "executor");
88
88
  assert.equal(plan.cliType, "codex");
89
89
  assert.equal(plan.adapter, "codex");
90
- assert.equal(plan.effort, "gpt6_sol_high");
90
+ assert.equal(plan.effort, "gpt61_sol_high");
91
91
  assert.equal(plan.runMode, "fg");
92
92
  });
93
93
 
@@ -234,14 +234,14 @@ describe("tfx-route.mjs — buildRoutePlan (agent-map.json 전수)", () => {
234
234
  });
235
235
 
236
236
  describe("cli-codex.mjs — 핵심 매핑", () => {
237
- test("executor → gpt6_sol_high / 1080s / fg / mcp=implement", () => {
237
+ test("executor → gpt61_sol_high / 1080s / fg / mcp=implement", () => {
238
238
  const p = codexAdapter.plan({
239
239
  agent: "executor",
240
240
  prompt: "x",
241
241
  mcpProfile: "auto",
242
242
  });
243
- assert.equal(p.effort, "gpt6_sol_high");
244
- assert.equal(p.profile, "gpt6_sol_high");
243
+ assert.equal(p.effort, "gpt61_sol_high");
244
+ assert.equal(p.profile, "gpt61_sol_high");
245
245
  assert.equal(p.timeoutMs, 1_080_000);
246
246
  assert.equal(p.runMode, "fg");
247
247
  assert.equal(p.opusOversight, false);
@@ -334,7 +334,7 @@ describe("cli-codex.mjs — 핵심 매핑", () => {
334
334
  prompt: "x",
335
335
  mcpProfile: "auto",
336
336
  });
337
- assert.equal(p.effort, "gpt6_sol_high");
337
+ assert.equal(p.effort, "gpt61_sol_high");
338
338
  assert.equal(p.timeoutMs, 1_080_000);
339
339
  });
340
340
 
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  const policy = {
9
9
  executor: {
10
- profile: "gpt6_sol_high",
10
+ profile: "gpt61_sol_high",
11
11
  timeoutSec: 1080,
12
12
  runMode: "fg",
13
13
  opusOversight: false,
@@ -15,7 +15,7 @@ const policy = {
15
15
  subcommand: "exec",
16
16
  },
17
17
  codex: {
18
- profile: "gpt6_sol_high",
18
+ profile: "gpt61_sol_high",
19
19
  timeoutSec: 1080,
20
20
  runMode: "fg",
21
21
  opusOversight: false,
@@ -31,7 +31,7 @@ const policy = {
31
31
  subcommand: "exec",
32
32
  },
33
33
  cleanup: {
34
- profile: "gpt6_sol_med",
34
+ profile: "gpt61_sol_med",
35
35
  timeoutSec: 540,
36
36
  runMode: "fg",
37
37
  opusOversight: false,
@@ -39,7 +39,7 @@ const policy = {
39
39
  subcommand: "exec",
40
40
  },
41
41
  deslop: {
42
- profile: "gpt6_sol_med",
42
+ profile: "gpt61_sol_med",
43
43
  timeoutSec: 540,
44
44
  runMode: "fg",
45
45
  opusOversight: false,
@@ -95,7 +95,7 @@ const policy = {
95
95
  subcommand: "exec",
96
96
  },
97
97
  "code-reviewer": {
98
- profile: "gpt6_sol_high",
98
+ profile: "gpt61_sol_high",
99
99
  timeoutSec: 1800,
100
100
  runMode: "bg",
101
101
  opusOversight: false,
@@ -103,7 +103,7 @@ const policy = {
103
103
  subcommand: "review",
104
104
  },
105
105
  "quality-reviewer": {
106
- profile: "gpt6_sol_high",
106
+ profile: "gpt61_sol_high",
107
107
  timeoutSec: 1800,
108
108
  runMode: "bg",
109
109
  opusOversight: false,
@@ -119,7 +119,7 @@ const policy = {
119
119
  subcommand: "review",
120
120
  },
121
121
  scientist: {
122
- profile: "gpt6_sol_high",
122
+ profile: "gpt61_sol_high",
123
123
  timeoutSec: 1440,
124
124
  runMode: "bg",
125
125
  opusOversight: false,
@@ -127,7 +127,7 @@ const policy = {
127
127
  subcommand: "exec",
128
128
  },
129
129
  "document-specialist": {
130
- profile: "gpt6_sol_high",
130
+ profile: "gpt61_sol_high",
131
131
  timeoutSec: 1440,
132
132
  runMode: "bg",
133
133
  opusOversight: false,
@@ -143,7 +143,7 @@ const policy = {
143
143
  subcommand: "exec",
144
144
  },
145
145
  verifier: {
146
- profile: "gpt6_sol_high",
146
+ profile: "gpt61_sol_high",
147
147
  timeoutSec: 1200,
148
148
  runMode: "fg",
149
149
  opusOversight: false,
@@ -151,7 +151,7 @@ const policy = {
151
151
  subcommand: "review",
152
152
  },
153
153
  "test-engineer": {
154
- profile: "gpt6_sol_high",
154
+ profile: "gpt61_sol_high",
155
155
  timeoutSec: 1200,
156
156
  runMode: "bg",
157
157
  opusOversight: false,
@@ -159,7 +159,7 @@ const policy = {
159
159
  subcommand: "exec",
160
160
  },
161
161
  "qa-tester": {
162
- profile: "gpt6_sol_high",
162
+ profile: "gpt61_sol_high",
163
163
  timeoutSec: 1200,
164
164
  runMode: "bg",
165
165
  opusOversight: false,
@@ -212,8 +212,8 @@ export const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
212
212
  new Set([
213
213
  "gpt6_luna_low",
214
214
  "gpt6_luna_high",
215
- "gpt6_sol_med",
216
- "gpt6_sol_high",
215
+ "gpt61_sol_med",
216
+ "gpt61_sol_high",
217
217
  "gpt6_astra_xhigh",
218
218
  "gpt6_astra_max",
219
219
  "gpt6_astra_ultra",
@@ -21,22 +21,25 @@ const EFFORT_BY_SUFFIX = {
21
21
  const CANONICAL_PROFILE_VALUES = Object.freeze({
22
22
  gpt6_luna_low: { model: "gpt-6-luna", effort: "low" },
23
23
  gpt6_luna_high: { model: "gpt-6-luna", effort: "high" },
24
- gpt6_sol_med: { model: "gpt-6-sol", effort: "medium" },
25
- gpt6_sol_high: { model: "gpt-6-sol", effort: "high" },
24
+ gpt61_sol_med: { model: "gpt-6.1-sol", effort: "medium" },
25
+ gpt61_sol_high: { model: "gpt-6.1-sol", effort: "high" },
26
26
  gpt6_astra_xhigh: { model: "gpt-6-astra", effort: "xhigh" },
27
27
  gpt6_astra_max: { model: "gpt-6-astra", effort: "max" },
28
28
  gpt6_astra_ultra: { model: "gpt-6-astra", effort: "ultra" },
29
29
  });
30
30
 
31
31
  // GPT-5.6 lane names map to the GPT-6 lane with the same effort. GPT-6 has no
32
- // Terra tier, so Terra lanes move to Sol.
32
+ // Terra tier, so Terra lanes move to Sol. GPT-6 Sol lane names map to GPT-6.1
33
+ // Sol with the same effort.
33
34
  const LEGACY_CANONICAL_PROFILE_ALIASES = Object.freeze({
34
35
  gpt56_sol_xhigh: "gpt6_astra_xhigh",
35
36
  gpt56_sol_max: "gpt6_astra_max",
36
37
  gpt56_sol_ultra: "gpt6_astra_ultra",
37
- gpt56_terra_high: "gpt6_sol_high",
38
- gpt56_terra_med: "gpt6_sol_med",
38
+ gpt56_terra_high: "gpt61_sol_high",
39
+ gpt56_terra_med: "gpt61_sol_med",
39
40
  gpt56_luna_low: "gpt6_luna_low",
41
+ gpt6_sol_high: "gpt61_sol_high",
42
+ gpt6_sol_med: "gpt61_sol_med",
40
43
  });
41
44
 
42
45
  export function normalizeCodexProfileName(profileName) {
@@ -65,7 +68,7 @@ function readProfileScalar(raw, key) {
65
68
  }
66
69
 
67
70
  /**
68
- * Resolve a Codex effort profile name (e.g. "gpt6_sol_high") to the `-c key=value`
71
+ * Resolve a Codex effort profile name (e.g. "gpt61_sol_high") to the `-c key=value`
69
72
  * config overrides that `codex exec` accepts, so the headless CLI lanes can
70
73
  * select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
71
74
  * `--profile X` whenever config.toml still contains an inline `[profiles.X]`
@@ -83,7 +86,7 @@ function readProfileScalar(raw, key) {
83
86
  *
84
87
  * @param {string} profileName
85
88
  * @param {{ codexHome?: string }} [opts]
86
- * @returns {string[]} e.g. ['model="gpt-6-sol"', 'model_reasoning_effort="high"']
89
+ * @returns {string[]} e.g. ['model="gpt-6.1-sol"', 'model_reasoning_effort="high"']
87
90
  */
88
91
  export function resolveCodexProfileConfigValues(profileName, opts = {}) {
89
92
  if (typeof profileName !== "string" || !profileName) {
package/scripts/setup.mjs CHANGED
@@ -437,14 +437,14 @@ const REQUIRED_CODEX_PROFILES = [
437
437
  name: "gpt6_astra_xhigh",
438
438
  lines: ['model = "gpt-6-astra"', 'model_reasoning_effort = "xhigh"'],
439
439
  },
440
- // GPT-6 Sol: implementation, review, and verification role defaults.
440
+ // GPT-6.1 Sol: implementation, review, and verification role defaults.
441
441
  {
442
- name: "gpt6_sol_high",
443
- lines: ['model = "gpt-6-sol"', 'model_reasoning_effort = "high"'],
442
+ name: "gpt61_sol_high",
443
+ lines: ['model = "gpt-6.1-sol"', 'model_reasoning_effort = "high"'],
444
444
  },
445
445
  {
446
- name: "gpt6_sol_med",
447
- lines: ['model = "gpt-6-sol"', 'model_reasoning_effort = "medium"'],
446
+ name: "gpt61_sol_med",
447
+ lines: ['model = "gpt-6.1-sol"', 'model_reasoning_effort = "medium"'],
448
448
  },
449
449
  // GPT-6 Luna: high for focused lanes, low for latency-first lanes.
450
450
  {
@@ -636,6 +636,13 @@ const SYNC_MAP = [
636
636
  // 설치본이 남지 않게 한다. hooks/ 는 플러그인 루트에서 직접 import 되는 경로라
637
637
  // 이 복사 목록의 대상이 아니다.
638
638
  ...scanLibFiles(PLUGIN_ROOT, CLAUDE_DIR),
639
+ // hud/providers/cto.mjs 가 ../../hub/lib/cto-env.mjs 를 정적 import 한다(ADR-0018).
640
+ // 의존성 없는 env 판독 모듈이라 이 파일 하나만 hud 보다 먼저 복사한다.
641
+ {
642
+ src: join(PLUGIN_ROOT, "hub", "lib", "cto-env.mjs"),
643
+ dst: join(CLAUDE_DIR, "hub", "lib", "cto-env.mjs"),
644
+ label: "hub/lib/cto-env.mjs",
645
+ },
639
646
  ...scanHudFiles(PLUGIN_ROOT, CLAUDE_DIR),
640
647
  {
641
648
  src: join(PLUGIN_ROOT, "scripts", "notion-read.mjs"),
@@ -1424,7 +1424,7 @@ route_agent() {
1424
1424
  exit 1
1425
1425
  }
1426
1426
  IFS=$'\t' read -r policy_profile policy_timeout policy_mode policy_oversight policy_mcp policy_subcommand <<< "$policy_row"
1427
- case "$policy_profile" in gpt6_astra_xhigh|gpt6_sol_high|gpt6_sol_med|gpt6_luna_high|gpt6_luna_low) ;; *) echo "ERROR: invalid Codex profile policy" >&2; exit 1 ;; esac
1427
+ case "$policy_profile" in gpt6_astra_xhigh|gpt61_sol_high|gpt61_sol_med|gpt6_luna_high|gpt6_luna_low) ;; *) echo "ERROR: invalid Codex profile policy" >&2; exit 1 ;; esac
1428
1428
  [[ "$policy_timeout" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: invalid Codex timeout policy" >&2; exit 1; }
1429
1429
  case "$policy_mode" in fg|bg) ;; *) echo "ERROR: invalid Codex run mode policy" >&2; exit 1 ;; esac
1430
1430
  case "$policy_oversight" in true|false) ;; *) echo "ERROR: invalid Codex oversight policy" >&2; exit 1 ;; esac
@@ -1495,9 +1495,11 @@ normalize_codex_profile_name() {
1495
1495
  gpt56_sol_xhigh) echo "gpt6_astra_xhigh" ;;
1496
1496
  gpt56_sol_max) echo "gpt6_astra_max" ;;
1497
1497
  gpt56_sol_ultra) echo "gpt6_astra_ultra" ;;
1498
- gpt56_terra_high) echo "gpt6_sol_high" ;;
1499
- gpt56_terra_med) echo "gpt6_sol_med" ;;
1498
+ gpt56_terra_high) echo "gpt61_sol_high" ;;
1499
+ gpt56_terra_med) echo "gpt61_sol_med" ;;
1500
1500
  gpt56_luna_low) echo "gpt6_luna_low" ;;
1501
+ gpt6_sol_high) echo "gpt61_sol_high" ;;
1502
+ gpt6_sol_med) echo "gpt61_sol_med" ;;
1501
1503
  *) echo "$1" ;;
1502
1504
  esac
1503
1505
  }
@@ -1588,9 +1590,9 @@ case "$TFX_CODEX_TRANSPORT" in
1588
1590
  ;;
1589
1591
  esac
1590
1592
  case "$TFX_CODEX_PROFILE" in
1591
- auto|max|ultra|gpt6_luna_low|gpt6_luna_high|gpt6_sol_med|gpt6_sol_high|gpt6_astra_xhigh|gpt6_astra_max|gpt6_astra_ultra) ;;
1593
+ auto|max|ultra|gpt6_luna_low|gpt6_luna_high|gpt61_sol_med|gpt61_sol_high|gpt6_astra_xhigh|gpt6_astra_max|gpt6_astra_ultra) ;;
1592
1594
  *)
1593
- echo "ERROR: TFX_CODEX_PROFILE 값은 auto, max, ultra 또는 canonical gpt6_* profile이어야 합니다. (현재: $TFX_CODEX_PROFILE)" >&2
1595
+ echo "ERROR: TFX_CODEX_PROFILE 값은 auto, max, ultra 또는 canonical gpt6_*/gpt61_* profile이어야 합니다. (현재: $TFX_CODEX_PROFILE)" >&2
1594
1596
  exit 1
1595
1597
  ;;
1596
1598
  esac
@@ -1645,7 +1647,7 @@ apply_cli_mode() {
1645
1647
  writer)
1646
1648
  CLI_ARGS="exec --profile gpt6_luna_high ${codex_base}"; CLI_EFFORT="gpt6_luna_high"; DEFAULT_TIMEOUT=900 ;;
1647
1649
  *)
1648
- CLI_ARGS="exec --profile gpt6_sol_high ${codex_base}"; CLI_EFFORT="gpt6_sol_high"; DEFAULT_TIMEOUT=1080 ;;
1650
+ CLI_ARGS="exec --profile gpt61_sol_high ${codex_base}"; CLI_EFFORT="gpt61_sol_high"; DEFAULT_TIMEOUT=1080 ;;
1649
1651
  esac
1650
1652
  echo "[tfx-route] TFX_CLI_MODE=codex: $AGENT_TYPE → codex($CLI_EFFORT)로 리매핑" >&2
1651
1653
  fi ;;
@@ -1792,9 +1794,9 @@ apply_plan_guard() {
1792
1794
  replacement=""
1793
1795
  case "$CLI_EFFORT" in
1794
1796
  gpt55_low|spark53_low|codex53_low|gpt54_low|mini54_low) replacement="gpt6_luna_low" ;;
1795
- gpt55_med|spark53_med|codex53_med|mini54_med) replacement="gpt6_sol_med" ;;
1797
+ gpt55_med|spark53_med|codex53_med|mini54_med) replacement="gpt61_sol_med" ;;
1796
1798
  gpt55_xhigh|codex53_xhigh|gpt54_xhigh) replacement="gpt6_astra_xhigh" ;;
1797
- gpt55_high|spark53_*|codex53_*|gpt54_*|mini54_*) replacement="gpt6_sol_high" ;;
1799
+ gpt55_high|spark53_*|codex53_*|gpt54_*|mini54_*) replacement="gpt61_sol_high" ;;
1798
1800
  esac
1799
1801
  fi
1800
1802
  [[ -z "$replacement" ]] && return
@@ -1832,22 +1834,22 @@ apply_no_claude_native_mode() {
1832
1834
  OPUS_OVERSIGHT="false"
1833
1835
  ;;
1834
1836
  verifier)
1835
- CLI_ARGS="exec --profile gpt6_sol_high ${codex_base} review"
1836
- CLI_EFFORT="gpt6_sol_high"
1837
+ CLI_ARGS="exec --profile gpt61_sol_high ${codex_base} review"
1838
+ CLI_EFFORT="gpt61_sol_high"
1837
1839
  DEFAULT_TIMEOUT=1200
1838
1840
  RUN_MODE="fg"
1839
1841
  OPUS_OVERSIGHT="false"
1840
1842
  ;;
1841
1843
  test-engineer)
1842
- CLI_ARGS="exec --profile gpt6_sol_high ${codex_base}"
1843
- CLI_EFFORT="gpt6_sol_high"
1844
+ CLI_ARGS="exec --profile gpt61_sol_high ${codex_base}"
1845
+ CLI_EFFORT="gpt61_sol_high"
1844
1846
  DEFAULT_TIMEOUT=1200
1845
1847
  RUN_MODE="bg"
1846
1848
  OPUS_OVERSIGHT="false"
1847
1849
  ;;
1848
1850
  qa-tester)
1849
- CLI_ARGS="exec --profile gpt6_sol_high ${codex_base} review"
1850
- CLI_EFFORT="gpt6_sol_high"
1851
+ CLI_ARGS="exec --profile gpt61_sol_high ${codex_base} review"
1852
+ CLI_EFFORT="gpt61_sol_high"
1851
1853
  DEFAULT_TIMEOUT=1200
1852
1854
  RUN_MODE="bg"
1853
1855
  OPUS_OVERSIGHT="false"
@@ -1879,7 +1881,7 @@ resolve_safe_retry_codex_profile() {
1879
1881
  echo "gpt6_astra_max"
1880
1882
  return
1881
1883
  ;;
1882
- gpt6_luna_low|gpt6_luna_high|gpt6_sol_med|gpt6_sol_high|gpt6_astra_xhigh|gpt6_astra_max)
1884
+ gpt6_luna_low|gpt6_luna_high|gpt61_sol_med|gpt61_sol_high|gpt6_astra_xhigh|gpt6_astra_max)
1883
1885
  echo "$profile"
1884
1886
  return
1885
1887
  ;;
@@ -1889,7 +1891,7 @@ resolve_safe_retry_codex_profile() {
1889
1891
  codex_home="${CODEX_HOME:-${TFX_CODEX_HOME:-${HOME:-}/.codex}}"
1890
1892
  profile_file="${codex_home}/${profile}.config.toml"
1891
1893
  if [[ ! -f "$profile_file" ]]; then
1892
- echo "gpt6_sol_high"
1894
+ echo "gpt61_sol_high"
1893
1895
  return
1894
1896
  fi
1895
1897
  effort=$(awk -F= '
@@ -1903,7 +1905,7 @@ resolve_safe_retry_codex_profile() {
1903
1905
  ' "$profile_file" 2>/dev/null || true)
1904
1906
  case "$effort" in
1905
1907
  ultra) echo "gpt6_astra_max" ;;
1906
- "") echo "gpt6_sol_high" ;;
1908
+ "") echo "gpt61_sol_high" ;;
1907
1909
  *) echo "$profile" ;;
1908
1910
  esac
1909
1911
  }
@@ -2116,8 +2118,8 @@ apply_dynamic_routing_override() {
2116
2118
  local codex_base
2117
2119
  codex_base="$(build_codex_base)"
2118
2120
  CLI_CMD="codex"
2119
- CLI_ARGS="exec --profile gpt6_sol_high ${codex_base}"
2120
- CLI_EFFORT="gpt6_sol_high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false"
2121
+ CLI_ARGS="exec --profile gpt61_sol_high ${codex_base}"
2122
+ CLI_EFFORT="gpt61_sol_high"; DEFAULT_TIMEOUT=1080; RUN_MODE="fg"; OPUS_OVERSIGHT="false"
2121
2123
  ;;
2122
2124
  antigravity)
2123
2125
  CLI_CMD="agy"
@@ -3079,7 +3081,7 @@ run_codex_exec() {
3079
3081
  if [[ "$exit_code_local" -ne 0 ]] && grep -qE "is not supported when using Codex with a ChatGPT account" "$STDERR_LOG" 2>/dev/null; then
3080
3082
  local fallback_profile=""
3081
3083
  case "$CLI_EFFORT" in
3082
- gpt6_astra_ultra|gpt6_astra_max|gpt6_astra_xhigh|gpt6_sol_high|gpt6_sol_med|gpt6_luna_high) fallback_profile="gpt6_luna_low" ;;
3084
+ gpt6_astra_ultra|gpt6_astra_max|gpt6_astra_xhigh|gpt61_sol_high|gpt61_sol_med|gpt6_luna_high) fallback_profile="gpt6_luna_low" ;;
3083
3085
  esac
3084
3086
  if [[ -n "$fallback_profile" ]]; then
3085
3087
  echo "[tfx-route] tier fallback: $CLI_EFFORT not supported on ChatGPT account → retry with $fallback_profile" >&2
@@ -3539,7 +3541,7 @@ EOF
3539
3541
  FULL_PROMPT="$PROMPT"
3540
3542
  fi
3541
3543
  # CTO north-star: codex lane(line ~2670)과 동일하게 brief 를 prompt 앞에 주입.
3542
- # prepend_codex_north_star 는 CLI-agnostic (TFX_CTO_NORTH_STAR opt-out +
3544
+ # prepend_codex_north_star 는 CLI-agnostic (TFX_CTO_NORTH_STAR opt-in +
3543
3545
  # .triflux/lake/current.md 만 참조) 하므로 agy 워커에서도 그대로 재사용한다.
3544
3546
  # sentinel 은 command-substitution 이 trailing newline 을 삼키는 것을 막는다.
3545
3547
  local _agy_north_star_file="${WORKDIR:-$PWD}/.triflux/lake/current.md"
@@ -1009,7 +1009,7 @@ Agent(subagent_type="oh-my-claudecode:{agent}", model="{model}", prompt="{prompt
1009
1009
 
1010
1010
  | 입력 | CLI | MCP |
1011
1011
  |------|-----|-----|
1012
- | codex / executor | Codex (`gpt6_sol_high`; 복잡 구현은 deep-executor/xhigh) | implement |
1012
+ | codex / executor | Codex (`gpt61_sol_high`; 복잡 구현은 deep-executor/xhigh) | implement |
1013
1013
  | debugger / deep-executor | Codex (`gpt6_astra_xhigh`) | implement |
1014
1014
  | build-fixer | Codex (`gpt6_luna_high`) | implement |
1015
1015
  | spark | Codex (`gpt6_luna_low`) | implement |
@@ -79,6 +79,34 @@ the npm `node .../codex` shim, `codex.js`, and native `codex` executables.
79
79
  on that host; unavailable `ps` produces a peer `preflightWarning` and allows
80
80
  attach to continue.
81
81
 
82
+ ### Claude discovery and Codex registry
83
+
84
+ `tfx-live list-sessions --cli claude [--cwd DIR]` lists local tmux sessions from
85
+ `~/.claude/sessions/*.json` (`TFX_CLAUDE_SESSIONS_DIR` overrides the directory).
86
+ It returns `session`, `target`, `paneId`, pane `cwd`, `pid`, `sessionId`, `short`,
87
+ `name`, `title`, `nameSource`, and `status` only for live PIDs matching the pane process
88
+ or a descendant within four levels. Claude discovery rejects `--remote` and
89
+ `--transport uds`; `--transport auto` uses local tmux, and `--cwd` matches the
90
+ pane cwd exactly.
91
+
92
+ `title` equals `name` unless `nameSource` is `derived` or the name is empty.
93
+ In those cases it uses the latest non-empty, trimmed `custom-title` or `ai-title`
94
+ from the last 262144 bytes of `~/.claude/projects/*/<sessionId>.jsonl`
95
+ (`TFX_CLAUDE_PROJECTS_DIR` overrides the projects directory). `customTitle`
96
+ takes precedence over `aiTitle` on the same line; no matching title yields null.
97
+
98
+ Codex SessionStart and UserPromptSubmit hooks atomically write `<pid>.json` to
99
+ `${XDG_STATE_HOME:-$HOME/.local/state}/triflux/codex-sessions`
100
+ (`TFX_CODEX_SESSION_REGISTRY_DIR` overrides it). Version 1 contains
101
+ `{ version: 1, writer: "triflux", pid, sessionId, cwd, tmux, tmuxPane, source,
102
+ startedAt, updatedAt }`: `tmux` is `<session>:@<window_id>.%<pane_id>` or null,
103
+ `tmuxPane` is `%<pane_id>` or null, `source` defaults to null, and timestamps
104
+ are milliseconds. `startedAt` stays unchanged for the same PID and thread.
105
+ Local Codex discovery adds `threadId` and `name` to matching entries in `panes`;
106
+ names come from `$CODEX_HOME/session_index.jsonl` (default `~/.codex`), and are
107
+ null when absent. Dead PID records are pruned after writes.
108
+ Records are written only when the Codex process is inside the hook's tmux pane, so TUIs attached to a shared app-server daemon (`daemon_auto_start`) are not recorded.
109
+
82
110
  `ask` and tmux `interrupt` accept session names, `name:window`, and
83
111
  `name:window.pane`. `start` and `stop` require a session name only; `stop`
84
112
  rejects pane targets because it kills the entire session. `--remote HOST`
@@ -10,7 +10,7 @@ argument-hint: "<구현할 기능> [--quick]"
10
10
  > **ARGUMENTS 처리**: `--quick` → Quick 모드. 그 외 → Deep 모드 (기본).
11
11
 
12
12
  > AI makes completeness near-free. 기본은 Opus 4.6(Planner) + Codex(Architect) + Claude Opus(Critic) 합의.
13
- > Antigravity는 `agy --print` stdin 경로가 단순 프롬프트에서만 검증됐으므로, schema-driven critic에는 쓰지 않고 `--quick` freeform 보조로만 사용한다.
13
+ > Antigravity는 `agy --print` 경로가 단순 프롬프트에서만 검증됐으므로, schema-driven critic에는 쓰지 않고 `--quick` freeform 보조로만 사용한다.
14
14
 
15
15
  ---
16
16
 
@@ -49,7 +49,7 @@ Tier 3:
49
49
  3. Claude → `Agent(run_in_background=true)`
50
50
  4. Bash + Agent 동시 호출
51
51
  5. deprecated Gemini CLI 또는 legacy Gemini assign 금지. Antigravity는 `antigravity:`/`agy` 이름만 사용한다.
52
- 6. `agy --print`는 stdin prompt만 사용한다. positional prompt 및 strict JSON critic 검증에 실패하면 Claude critic으로 대체한다.
52
+ 6. `agy --print`는 프롬프트를 옵션 값으로 받는다(`--print '<prompt>'`). 표준 입력 프롬프트는 agy 1.1.27부터 동작하지 않는다. strict JSON critic 검증에 실패하면 Claude critic으로 대체한다.
53
53
 
54
54
  ### 모델 역할
55
55