@triflux/core 10.91.0 → 10.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hub/bridge.mjs CHANGED
@@ -1156,6 +1156,7 @@ async function cmdJudgeRoute(args) {
1156
1156
  ? routing.neededMcpServers.join(",")
1157
1157
  : "",
1158
1158
  String(routing.confidence ?? ""),
1159
+ routing.tierShift || "",
1159
1160
  ];
1160
1161
  process.stdout.write(fields.join(RS));
1161
1162
  return true;
@@ -360,8 +360,23 @@ async function callJevCli(request, options = {}) {
360
360
  options.timeoutMs ?? env.TFX_JEV_TIMEOUT_MS ?? DEFAULT_JEV_TIMEOUT_MS,
361
361
  );
362
362
  const args = [request.kind, request.query, "--json"];
363
- if (request.context) {
364
- args.push("--context", request.context);
363
+ // CLI 에는 criteria 인자가 없어 선택지의 뜻을 문맥으로 함께 보낸다.
364
+ const criteria =
365
+ request.criteria &&
366
+ typeof request.criteria === "object" &&
367
+ !Array.isArray(request.criteria)
368
+ ? Object.entries(request.criteria).map(
369
+ ([key, text]) => `- ${key}: ${text}`,
370
+ )
371
+ : [];
372
+ const context = [
373
+ request.context,
374
+ criteria.length > 0 ? `Options:\n${criteria.join("\n")}` : "",
375
+ ]
376
+ .filter(Boolean)
377
+ .join("\n\n");
378
+ if (context) {
379
+ args.push("--context", context);
365
380
  }
366
381
  if (Array.isArray(request.options) && request.options.length > 0) {
367
382
  args.push("--options", request.options.join(","));
@@ -1062,6 +1077,19 @@ const COMPLEXITY_ROUTE_MAP = Object.freeze({
1062
1077
  }),
1063
1078
  });
1064
1079
 
1080
+ // 복잡도로 등급을 고르는 범용 역할이다. 그 밖의 역할은 기본 등급 대비 방향만 답하고
1081
+ // 실제 등급은 agent-route-policy.mjs 의 사다리가 정한다.
1082
+ const TIER_ROUTABLE_ROLES = new Set([
1083
+ "executor",
1084
+ "codex",
1085
+ "antigravity",
1086
+ "agy",
1087
+ "claude",
1088
+ "lead",
1089
+ ]);
1090
+ // 승격은 비용을, 강등은 실패와 재시도를 키우므로 불확실하면 역할 기본 등급을 유지한다.
1091
+ const TIER_CHANGE_MIN_CONFIDENCE = 0.85;
1092
+
1065
1093
  /**
1066
1094
  * 6. Pre-route 통합 판단: 프롬프트 복잡도(모델 및 effort), 실행 권한 모드(read_only / workspace_auto / bypass),
1067
1095
  * 필요한 MCP 프로필 및 서버 목록을 단일 queryJevBatch 호출로 판정한다.
@@ -1166,6 +1194,23 @@ export async function judgeTaskRouting(
1166
1194
  },
1167
1195
  };
1168
1196
 
1197
+ if (!TIER_ROUTABLE_ROLES.has(agentType)) {
1198
+ questions.tier_shift = {
1199
+ hook: "task_routing_tier_shift",
1200
+ kind: "choice",
1201
+ options: ["lower", "keep", "raise"],
1202
+ criteria: {
1203
+ lower:
1204
+ "Mechanical or tiny work needing far less judgment than this role normally applies.",
1205
+ keep: "Normal work for this role, even when it spans several files or sounds important.",
1206
+ raise:
1207
+ "Unusually hard for this role: security-critical, irreversible, or cross-system reasoning a normal run of this role would likely get wrong.",
1208
+ },
1209
+ query:
1210
+ "This worker role already has a default model tier chosen for its usual work. Compared with that default, what does this specific task need?",
1211
+ };
1212
+ }
1213
+
1169
1214
  for (const server of candidateServers) {
1170
1215
  const safeKey = `mcp_${server.replace(/[^a-zA-Z0-9_]/g, "_")}`;
1171
1216
  questions[safeKey] = {
@@ -1185,6 +1230,13 @@ export async function judgeTaskRouting(
1185
1230
  const compRes = results.complexity;
1186
1231
  const modeRes = results.execution_mode;
1187
1232
  const mcpProfRes = results.mcp_profile;
1233
+ const shiftRes = results.tier_shift;
1234
+ const tierShift =
1235
+ shiftRes &&
1236
+ shiftRes.confidence >= TIER_CHANGE_MIN_CONFIDENCE &&
1237
+ (shiftRes.choice === "lower" || shiftRes.choice === "raise")
1238
+ ? shiftRes.choice
1239
+ : null;
1188
1240
 
1189
1241
  const complexity =
1190
1242
  compRes &&
@@ -1230,17 +1282,23 @@ export async function judgeTaskRouting(
1230
1282
  !complexity &&
1231
1283
  !executionMode &&
1232
1284
  !mcpProfile &&
1285
+ !tierShift &&
1233
1286
  neededMcpServers === null
1234
1287
  ) {
1235
1288
  return null;
1236
1289
  }
1237
1290
 
1238
- const routeSpec = complexity
1239
- ? COMPLEXITY_ROUTE_MAP[complexity] || null
1240
- : null;
1291
+ const routeSpec =
1292
+ complexity &&
1293
+ TIER_ROUTABLE_ROLES.has(agentType) &&
1294
+ (complexity === "standard" ||
1295
+ compRes.confidence >= TIER_CHANGE_MIN_CONFIDENCE)
1296
+ ? COMPLEXITY_ROUTE_MAP[complexity] || null
1297
+ : null;
1241
1298
 
1242
1299
  let codexProfile = routeSpec ? routeSpec.codexProfile : null;
1243
1300
  if (
1301
+ routeSpec &&
1244
1302
  complexity === "standard" &&
1245
1303
  /deslop|cleanup|format|lint|rename|typo/i.test(trimmedPrompt)
1246
1304
  ) {
@@ -1278,6 +1336,7 @@ export async function judgeTaskRouting(
1278
1336
  executionArgs: execFlags ? execFlags.args : null,
1279
1337
  mcpProfile,
1280
1338
  neededMcpServers,
1339
+ tierShift,
1281
1340
  confidence: avgConfidence,
1282
1341
  };
1283
1342
  }
@@ -1312,6 +1371,33 @@ export async function judgeMcpSelection(
1312
1371
  };
1313
1372
  }
1314
1373
 
1374
+ /**
1375
+ * 7-1. Claude 워커 MCP 선택 shadow: 적용하지 않고 판정만 기록해 절감량과 도구 손실을 재는 근거를 쌓는다.
1376
+ */
1377
+ export async function recordMcpSelectionShadow(
1378
+ { taskText = "", agentType = "default", availableServers = [] } = {},
1379
+ options = {},
1380
+ ) {
1381
+ if (!isJevEnabled(options) || availableServers.length === 0) return null;
1382
+ const selection = await judgeMcpSelection(
1383
+ { taskText, agentType, availableServers },
1384
+ options,
1385
+ );
1386
+ recordJevStat(
1387
+ {
1388
+ hook: "mcp_selection_shadow",
1389
+ kind: "shadow",
1390
+ intervened: false,
1391
+ agentType,
1392
+ candidates: availableServers,
1393
+ selected: selection?.neededServers ?? null,
1394
+ confidence: selection?.confidence ?? null,
1395
+ },
1396
+ options,
1397
+ );
1398
+ return selection;
1399
+ }
1400
+
1315
1401
  /**
1316
1402
  * 8. 다중 세션 중 프롬프트에 맞는 대상 세션 자동 선택 (--thread auto 모호성 해소).
1317
1403
  */
@@ -1746,3 +1832,60 @@ export async function judgeReviewGate(
1746
1832
  confidence: gateRes.confidence,
1747
1833
  };
1748
1834
  }
1835
+
1836
+ // 라우팅 정본의 결정트리 줄(`- D3: ...; D4 ...`)을 분기별 규칙으로 나눈다.
1837
+ export function parseRoutingBranches(markdown = "") {
1838
+ const branches = {};
1839
+ for (const line of String(markdown).split(/\r?\n/)) {
1840
+ if (!/^- D\d+:/.test(line)) continue;
1841
+ for (const part of line.slice(2).split(/;\s+(?=D\d+\s)/)) {
1842
+ const match = part.match(/^D(\d+):?\s+(.+)$/);
1843
+ if (match) branches[`D${match[1]}`] = match[2].trim();
1844
+ }
1845
+ }
1846
+ return branches;
1847
+ }
1848
+
1849
+ /**
1850
+ * 14. tfx-harness 스킬 선택: 라우팅 정본 전문을 읽지 않도록 맞는 분기 하나를 고른다.
1851
+ * D0(명시 지정)은 호출자가 먼저 처리하고, 기준은 정본에서 읽어 사본을 두지 않는다.
1852
+ */
1853
+ export async function judgeRoutingBranch(
1854
+ { task = "", routingText = "" } = {},
1855
+ options = {},
1856
+ ) {
1857
+ if (!isJevEnabled(options)) return null;
1858
+ const trimmedTask = clampText(task, 1000);
1859
+ if (!trimmedTask) return null;
1860
+
1861
+ const { D0: _explicit, ...branches } = parseRoutingBranches(routingText);
1862
+ const allowed = Object.keys(branches);
1863
+ if (allowed.length < 2) return null;
1864
+ // 맞는 분기가 없을 때 억지로 고르지 않도록 none 을 같은 분포에서 경쟁시킨다.
1865
+ const none = "none";
1866
+
1867
+ const res = await queryJev(
1868
+ {
1869
+ hook: "routing_branch",
1870
+ kind: "choice",
1871
+ options: [...allowed, none],
1872
+ criteria: {
1873
+ ...branches,
1874
+ [none]: "No branch condition clearly matches this request.",
1875
+ },
1876
+ query:
1877
+ "This is an ordered routing decision tree. Choose the lowest-numbered branch whose condition matches the request, or none.",
1878
+ context: `Request:\n${trimmedTask}`,
1879
+ },
1880
+ options,
1881
+ );
1882
+ if (!res || res.confidence < minConfidence(options)) return null;
1883
+ if (!allowed.includes(res.choice)) return null;
1884
+ // D4 소유자는 규칙 한 줄이 아니라 스택 공존 매트릭스가 정하므로 전문 읽기로 돌린다.
1885
+ if (res.choice === "D4") return null;
1886
+ return {
1887
+ branch: res.choice,
1888
+ rule: branches[res.choice],
1889
+ confidence: res.confidence,
1890
+ };
1891
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.91.0",
3
+ "version": "10.92.0",
4
4
  "description": "triflux core — CLI routing, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -187,6 +187,15 @@ const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
187
187
  "astra_ultra",
188
188
  ]),
189
189
  );
190
+ // 비용 순서다. Jev 는 방향만 답하고 등급은 여기서 한 단계만 옮긴다.
191
+ const CODEX_TIER_LADDER = Object.freeze([
192
+ "luna_low",
193
+ "luna_high",
194
+ "sol_med",
195
+ "sol_high",
196
+ "astra_xhigh",
197
+ ]);
198
+ const NO_LOWER_CODEX_AGENTS = Object.freeze(new Set(["security-reviewer"]));
190
199
  const ULTRA_ELIGIBLE_CODEX_AGENTS = Object.freeze(
191
200
  new Set(["deep-executor", "scientist-deep"]),
192
201
  );
@@ -212,6 +221,16 @@ export function resolveCodexAgentPolicy(agent) {
212
221
  * later process-level `TFX_CODEX_PROFILE`. Ultra is reserved for an eligible
213
222
  * top-level role; every nested/sandboxed or ineligible request is max.
214
223
  */
224
+ export function shiftCodexAgentProfile(agent, tierShift) {
225
+ const { profile } = resolveDirectCodexRoutePolicy(agent);
226
+ const index = CODEX_TIER_LADDER.indexOf(profile);
227
+ if (index === -1) return profile;
228
+ if (tierShift === "raise") return CODEX_TIER_LADDER[index + 1] ?? profile;
229
+ if (tierShift === "lower" && !NO_LOWER_CODEX_AGENTS.has(agent))
230
+ return CODEX_TIER_LADDER[index - 1] ?? profile;
231
+ return profile;
232
+ }
233
+
215
234
  export function resolveCodexAgentProfile(
216
235
  agent,
217
236
  {
@@ -400,6 +419,7 @@ function parseCliArgs(argv) {
400
419
  let profileOverride = "auto";
401
420
  let geminiProfilesModule = "";
402
421
  let nested = false;
422
+ let tierShift = "";
403
423
  while (args.length > 0) {
404
424
  const flag = args.shift();
405
425
  if (flag === "--format") format = args.shift() || "";
@@ -410,6 +430,7 @@ function parseCliArgs(argv) {
410
430
  else if (flag === "--gemini-profiles-module")
411
431
  geminiProfilesModule = args.shift() || "";
412
432
  else if (flag === "--nested") nested = true;
433
+ else if (flag === "--tier-shift") tierShift = args.shift() || "";
413
434
  else throw new Error(`unknown argument: ${flag}`);
414
435
  }
415
436
  if (!new Set(["json", "tsv", "route"]).has(format)) {
@@ -422,6 +443,7 @@ function parseCliArgs(argv) {
422
443
  profileOverride,
423
444
  geminiProfilesModule,
424
445
  nested,
446
+ tierShift,
425
447
  };
426
448
  }
427
449
 
@@ -433,9 +455,14 @@ async function runCli() {
433
455
  profileOverride,
434
456
  geminiProfilesModule,
435
457
  nested,
458
+ tierShift,
436
459
  } = parseCliArgs(process.argv.slice(2));
437
460
  if (format === "route") {
438
461
  if (!mapFile) throw new Error("--map-file is required for route format");
462
+ const routedProfile =
463
+ tierShift && normalizeCodexProfileName(profileOverride) === "auto"
464
+ ? shiftCodexAgentProfile(agent, tierShift)
465
+ : profileOverride;
439
466
  const map = JSON.parse(readFileSync(mapFile, "utf8"));
440
467
  let weeklyPercent = 0;
441
468
  if (agent === "code-reviewer" || agent === "quality-reviewer") {
@@ -470,7 +497,7 @@ async function runCli() {
470
497
  }
471
498
  }
472
499
  process.stdout.write(
473
- resolveAgentRoute(agent, map, profileOverride, nested, resolver, {
500
+ resolveAgentRoute(agent, map, routedProfile, nested, resolver, {
474
501
  reviewAuthor: process.env.TFX_REVIEW_AUTHOR ?? "codex",
475
502
  weeklyPercent,
476
503
  }).join("\x1e"),