triflux 4.2.9 → 5.0.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/bin/tfx-doctor.mjs +1 -1
- package/bin/tfx-setup.mjs +1 -1
- package/bin/triflux.mjs +1 -1
- package/hub/middleware/request-logger.mjs +81 -0
- package/hub/pipeline/index.mjs +19 -0
- package/hub/server.mjs +12 -8
- package/hub/team/native.mjs +82 -32
- package/hub/team/routing.mjs +154 -0
- package/hub/tools.mjs +66 -1
- package/hud/hud-qos-status.mjs +103 -47
- package/package.json +3 -1
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +45 -3
- package/skills/tfx-auto/SKILL.md +72 -14
- package/skills/tfx-multi/SKILL.md +12 -6
- package/skills/tfx-multi/references/thorough-pipeline.md +57 -18
package/hud/hud-qos-status.mjs
CHANGED
|
@@ -151,12 +151,33 @@ function getGeminiRpmLimit(model) {
|
|
|
151
151
|
return 300; // Flash 기본
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// Gemini 모델 ID → HUD 표시 라벨
|
|
154
|
+
// Gemini 모델 ID → HUD 표시 라벨 (동적 매핑)
|
|
155
155
|
function getGeminiModelLabel(model) {
|
|
156
156
|
if (!model) return "";
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return "";
|
|
157
|
+
// 버전 + 티어 추출: gemini-3.1-pro-preview → [3.1Pro], gemini-2.5-flash → [2.5Flash]
|
|
158
|
+
const m = model.match(/gemini-(\d+(?:\.\d+)?)-(\w+)/);
|
|
159
|
+
if (!m) return "";
|
|
160
|
+
const ver = m[1];
|
|
161
|
+
const tier = m[2].charAt(0).toUpperCase() + m[2].slice(1);
|
|
162
|
+
return `[${ver}${tier}]`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Gemini Pro 풀 공유 그룹: 같은 remainingFraction을 공유하는 모델 ID들
|
|
166
|
+
const GEMINI_PRO_POOL = new Set(["gemini-2.5-pro", "gemini-3-pro-preview", "gemini-3.1-pro-preview"]);
|
|
167
|
+
const GEMINI_FLASH_POOL = new Set(["gemini-2.5-flash", "gemini-3-flash-preview"]);
|
|
168
|
+
|
|
169
|
+
// remainingFraction → 사용 퍼센트 변환 (remainingAmount가 있으면 절대값도 제공)
|
|
170
|
+
function deriveGeminiLimits(bucket) {
|
|
171
|
+
if (!bucket || bucket.remainingFraction == null) return null;
|
|
172
|
+
const fraction = bucket.remainingFraction;
|
|
173
|
+
const usedPct = clampPercent(Math.round((1 - fraction) * 100));
|
|
174
|
+
// remainingAmount가 API에서 오면 절대값 역산 (Gemini CLI 방식)
|
|
175
|
+
if (bucket.remainingAmount != null) {
|
|
176
|
+
const remaining = parseInt(bucket.remainingAmount, 10);
|
|
177
|
+
const limit = fraction > 0 ? Math.round(remaining / fraction) : 0;
|
|
178
|
+
return { usedPct, remaining, limit, resetTime: bucket.resetTime, modelId: bucket.modelId };
|
|
179
|
+
}
|
|
180
|
+
return { usedPct, remaining: null, limit: null, resetTime: bucket.resetTime, modelId: bucket.modelId };
|
|
160
181
|
}
|
|
161
182
|
// rows 임계값 상수 (selectTier 에서 tier 결정에 사용)
|
|
162
183
|
const ROWS_BUDGET_FULL = 40;
|
|
@@ -493,10 +514,11 @@ function getMicroLine(stdin, claudeUsage, codexBuckets, geminiSession, geminiBuc
|
|
|
493
514
|
}
|
|
494
515
|
}
|
|
495
516
|
|
|
496
|
-
// Gemini
|
|
517
|
+
// Gemini (일간 쿼터 — P/F/L 3풀)
|
|
497
518
|
let gVal;
|
|
498
519
|
if (geminiBucket) {
|
|
499
|
-
const
|
|
520
|
+
const gl = deriveGeminiLimits(geminiBucket);
|
|
521
|
+
const gU = gl ? gl.usedPct : clampPercent((1 - (geminiBucket.remainingFraction ?? 1)) * 100);
|
|
500
522
|
gVal = colorByProvider(gU, `${gU}`, geminiBlue);
|
|
501
523
|
} else if ((geminiSession?.total || 0) > 0) {
|
|
502
524
|
gVal = geminiBlue("\u221E");
|
|
@@ -536,7 +558,7 @@ function normalizeTimeToken(value) {
|
|
|
536
558
|
}
|
|
537
559
|
const dayHour = text.match(/^(\d+)d(\d+)h$/);
|
|
538
560
|
if (dayHour) {
|
|
539
|
-
return `${Number(dayHour[1])}d${String(Number(dayHour[2])).padStart(2, "0")}h`;
|
|
561
|
+
return `${String(Number(dayHour[1])).padStart(2, "0")}d${String(Number(dayHour[2])).padStart(2, "0")}h`;
|
|
540
562
|
}
|
|
541
563
|
return text;
|
|
542
564
|
}
|
|
@@ -923,7 +945,7 @@ function formatResetRemainingDayHour(isoOrUnix, cycleMs = 0) {
|
|
|
923
945
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
924
946
|
const days = Math.floor(totalMinutes / (60 * 24));
|
|
925
947
|
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
|
|
926
|
-
return `${days}d${hours}h`;
|
|
948
|
+
return `${String(days).padStart(2, "0")}d${String(hours).padStart(2, "0")}h`;
|
|
927
949
|
}
|
|
928
950
|
|
|
929
951
|
function calcCooldownLeftSeconds(isoDatetime) {
|
|
@@ -1024,15 +1046,15 @@ function expireStaleCodexBuckets(buckets) {
|
|
|
1024
1046
|
// ============================================================================
|
|
1025
1047
|
// Codex 세션 JSONL에서 실제 rate limits 추출
|
|
1026
1048
|
// 한계: rate_limits는 세션별 스냅샷이므로 여러 세션 간 토큰 합산은 불가.
|
|
1027
|
-
//
|
|
1028
|
-
// (
|
|
1049
|
+
// 최근 7일간 세션 파일을 스캔해 가장 최신 rate_limits 버킷을 수집한다.
|
|
1050
|
+
// 합성 버킷(token_count 기반)은 2일 이내 데이터만 허용하여 stale 방지.
|
|
1029
1051
|
// ============================================================================
|
|
1030
1052
|
function getCodexRateLimits() {
|
|
1031
1053
|
const now = new Date();
|
|
1032
|
-
let syntheticBucket = null; //
|
|
1054
|
+
let syntheticBucket = null; // 최근 token_count에서 합성 (행 활성화 + 토큰 데이터용)
|
|
1033
1055
|
|
|
1034
|
-
//
|
|
1035
|
-
for (let dayOffset = 0; dayOffset <=
|
|
1056
|
+
// 7일간 스캔: 실제 rate_limits 우선, 합성 버킷은 폴백
|
|
1057
|
+
for (let dayOffset = 0; dayOffset <= 6; dayOffset++) {
|
|
1036
1058
|
const d = new Date(now.getTime() - dayOffset * 86_400_000);
|
|
1037
1059
|
const sessDir = join(
|
|
1038
1060
|
homedir(), ".codex", "sessions",
|
|
@@ -1064,8 +1086,8 @@ function getCodexRateLimits() {
|
|
|
1064
1086
|
contextWindow: evt.payload?.info?.model_context_window,
|
|
1065
1087
|
timestamp: evt.timestamp,
|
|
1066
1088
|
};
|
|
1067
|
-
} else if (dayOffset
|
|
1068
|
-
//
|
|
1089
|
+
} else if (dayOffset <= 1 && !rl && evt?.payload?.info?.total_token_usage && !syntheticBucket) {
|
|
1090
|
+
// 2일 이내 token_count: 합성 버킷 (rate_limits가 null일 때 행 활성화용, stale 방지)
|
|
1069
1091
|
syntheticBucket = {
|
|
1070
1092
|
limitId: "codex", limitName: "codex-session",
|
|
1071
1093
|
primary: null, secondary: null,
|
|
@@ -1080,7 +1102,7 @@ function getCodexRateLimits() {
|
|
|
1080
1102
|
}
|
|
1081
1103
|
} catch { /* 파일 읽기 실패 무시 */ }
|
|
1082
1104
|
}
|
|
1083
|
-
// 실제 rate_limits 발견 →
|
|
1105
|
+
// 실제 rate_limits 발견 → 토큰 데이터 병합 후 즉시 반환
|
|
1084
1106
|
if (Object.keys(mergedBuckets).length > 0) {
|
|
1085
1107
|
if (syntheticBucket) {
|
|
1086
1108
|
const main = mergedBuckets.codex || mergedBuckets[Object.keys(mergedBuckets)[0]];
|
|
@@ -1511,13 +1533,16 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1511
1533
|
}
|
|
1512
1534
|
}
|
|
1513
1535
|
if (realQuota?.type === "gemini") {
|
|
1514
|
-
const
|
|
1515
|
-
if (
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1536
|
+
const pools = realQuota.pools || {};
|
|
1537
|
+
if (pools.pro || pools.flash) {
|
|
1538
|
+
const pP = pools.pro ? clampPercent(Math.round((1 - (pools.pro.remainingFraction ?? 1)) * 100)) : null;
|
|
1539
|
+
const pF = pools.flash ? clampPercent(Math.round((1 - (pools.flash.remainingFraction ?? 1)) * 100)) : null;
|
|
1540
|
+
const pStr = pP != null ? colorByProvider(pP, `${pP}`, provFn) : dim("--");
|
|
1541
|
+
const fStr = pF != null ? colorByProvider(pF, `${pF}`, provFn) : dim("--");
|
|
1542
|
+
return { prefix: minPrefix, left: `${pStr}${dim("/")}${fStr}`, right: "" };
|
|
1518
1543
|
}
|
|
1519
1544
|
}
|
|
1520
|
-
return { prefix: minPrefix, left: dim("
|
|
1545
|
+
return { prefix: minPrefix, left: dim("--/--"), right: "" };
|
|
1521
1546
|
}
|
|
1522
1547
|
|
|
1523
1548
|
if (CURRENT_TIER === "minimal") {
|
|
@@ -1532,12 +1557,17 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1532
1557
|
}
|
|
1533
1558
|
}
|
|
1534
1559
|
if (realQuota?.type === "gemini") {
|
|
1535
|
-
const
|
|
1536
|
-
if (
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1560
|
+
const pools = realQuota.pools || {};
|
|
1561
|
+
if (pools.pro || pools.flash) {
|
|
1562
|
+
const slot = (bucket, label) => {
|
|
1563
|
+
if (!bucket) return `${dim(label + ":")}${dim(formatPlaceholderPercentCell())}`;
|
|
1564
|
+
const gl = deriveGeminiLimits(bucket);
|
|
1565
|
+
const usedP = gl ? gl.usedPct : clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
1566
|
+
return `${dim(label + ":")}${colorByProvider(usedP, formatPercentCell(usedP), provFn)}`;
|
|
1567
|
+
};
|
|
1568
|
+
quotaSection = `${slot(pools.pro, "Pr")} ${slot(pools.flash, "Fl")}`;
|
|
1539
1569
|
} else {
|
|
1540
|
-
quotaSection = `${dim("
|
|
1570
|
+
quotaSection = `${dim("Pr:")}${dim(formatPlaceholderPercentCell())} ${dim("Fl:")}${dim(formatPlaceholderPercentCell())}`;
|
|
1541
1571
|
}
|
|
1542
1572
|
}
|
|
1543
1573
|
if (!quotaSection) {
|
|
@@ -1561,17 +1591,23 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1561
1591
|
}
|
|
1562
1592
|
}
|
|
1563
1593
|
if (realQuota?.type === "gemini") {
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1594
|
+
const pools = realQuota.pools || {};
|
|
1595
|
+
const hasAnyPool = pools.pro || pools.flash;
|
|
1596
|
+
if (hasAnyPool) {
|
|
1597
|
+
const slot = (bucket, label) => {
|
|
1598
|
+
if (!bucket) return `${dim(label + ":")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
|
|
1599
|
+
const gl = deriveGeminiLimits(bucket);
|
|
1600
|
+
const usedP = gl ? gl.usedPct : clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
1601
|
+
const rstRemaining = formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
|
|
1602
|
+
return `${dim(label + ":")}${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim(formatTimeCell(rstRemaining))}`;
|
|
1603
|
+
};
|
|
1604
|
+
quotaSection = `${slot(pools.pro, "Pr")} ${slot(pools.flash, "Fl")}`;
|
|
1569
1605
|
} else {
|
|
1570
|
-
quotaSection = `${dim("
|
|
1606
|
+
quotaSection = `${dim("Pr:")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))} ${dim("Fl:")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
|
|
1571
1607
|
}
|
|
1572
1608
|
}
|
|
1573
1609
|
if (!quotaSection) {
|
|
1574
|
-
quotaSection = `${dim("5h:")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))} ${dim("1w:")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCellDH("
|
|
1610
|
+
quotaSection = `${dim("5h:")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))} ${dim("1w:")}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCellDH("--d--h"))}`;
|
|
1575
1611
|
}
|
|
1576
1612
|
const prefix = `${bold(markerColor(`${marker}`))}:`;
|
|
1577
1613
|
const compactRight = [svStr ? `${dim("sv:")}${svStr}` : "", accountLabel ? markerColor(accountLabel) : ""].filter(Boolean).join(" ");
|
|
@@ -1598,21 +1634,31 @@ function getProviderRow(provider, marker, markerColor, qosProfile, accountsConfi
|
|
|
1598
1634
|
}
|
|
1599
1635
|
|
|
1600
1636
|
if (realQuota?.type === "gemini") {
|
|
1601
|
-
const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1637
|
+
const pools = realQuota.pools || {};
|
|
1638
|
+
const hasAnyPool = pools.pro || pools.flash;
|
|
1639
|
+
|
|
1640
|
+
if (hasAnyPool) {
|
|
1641
|
+
// C/X와 동일한 2슬롯 구조: P:gauge %% (time) F:gauge %% (time)
|
|
1642
|
+
const slot = (bucket, label) => {
|
|
1643
|
+
if (!bucket) {
|
|
1644
|
+
return `${dim(label + ":")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
|
|
1645
|
+
}
|
|
1646
|
+
const gl = deriveGeminiLimits(bucket);
|
|
1647
|
+
const usedP = gl ? gl.usedPct : clampPercent((1 - (bucket.remainingFraction ?? 1)) * 100);
|
|
1648
|
+
const rstRemaining = formatResetRemaining(bucket.resetTime, ONE_DAY_MS) || "n/a";
|
|
1649
|
+
return `${dim(label + ":")}${tierBar(usedP, provAnsi)}${colorByProvider(usedP, formatPercentCell(usedP), provFn)} ${dim(formatTimeCell(rstRemaining))}`;
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
quotaSection = `${slot(pools.pro, "Pr")} ${slot(pools.flash, "Fl")}`;
|
|
1607
1653
|
} else {
|
|
1608
|
-
quotaSection = `${dim("
|
|
1609
|
-
`${dim(
|
|
1654
|
+
quotaSection = `${dim("Pr:")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))} ` +
|
|
1655
|
+
`${dim("Fl:")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))}`;
|
|
1610
1656
|
}
|
|
1611
1657
|
}
|
|
1612
1658
|
|
|
1613
1659
|
// 폴백: 쿼터 데이터 없을 때
|
|
1614
1660
|
if (!quotaSection) {
|
|
1615
|
-
quotaSection = `${dim("5h:")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))} ${dim("1w:")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCellDH("
|
|
1661
|
+
quotaSection = `${dim("5h:")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCell("n/a"))} ${dim("1w:")}${tierDimBar()}${dim(formatPlaceholderPercentCell())} ${dim(formatTimeCellDH("--d--h"))}`;
|
|
1616
1662
|
}
|
|
1617
1663
|
|
|
1618
1664
|
const prefix = `${bold(markerColor(`${marker}`))}:`;
|
|
@@ -1710,11 +1756,15 @@ async function main() {
|
|
|
1710
1756
|
geminiSv = geminiTokens ? geminiTokens / ctxCapacity : null;
|
|
1711
1757
|
}
|
|
1712
1758
|
|
|
1713
|
-
// Gemini:
|
|
1759
|
+
// Gemini: 3풀 버킷 추출 (Pro/Flash/Lite — 각 풀 내 모델들은 쿼터 공유)
|
|
1714
1760
|
const geminiModel = geminiSession?.model || "gemini-3-flash-preview";
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1761
|
+
const geminiBuckets = geminiQuota?.buckets || [];
|
|
1762
|
+
const geminiBucket = geminiBuckets.find((b) => b.modelId === geminiModel)
|
|
1763
|
+
|| geminiBuckets.find((b) => b.modelId === "gemini-3-flash-preview")
|
|
1717
1764
|
|| null;
|
|
1765
|
+
const geminiProBucket = geminiBuckets.find((b) => GEMINI_PRO_POOL.has(b.modelId)) || null;
|
|
1766
|
+
const geminiFlashBucket = geminiBuckets.find((b) => GEMINI_FLASH_POOL.has(b.modelId)) || null;
|
|
1767
|
+
const geminiLiteBucket = geminiBuckets.find((b) => b.modelId?.includes("flash-lite")) || null;
|
|
1718
1768
|
|
|
1719
1769
|
// 합산 절약: Codex+Gemini sv% 합산 (컨텍스트 대비 위임 토큰 비율)
|
|
1720
1770
|
const combinedSvPct = Math.round(((codexSv ?? 0) + (geminiSv ?? 0)) * 100);
|
|
@@ -1731,7 +1781,12 @@ async function main() {
|
|
|
1731
1781
|
}
|
|
1732
1782
|
|
|
1733
1783
|
const codexQuotaData = codexBuckets ? { type: "codex", buckets: codexBuckets } : null;
|
|
1734
|
-
const geminiQuotaData = {
|
|
1784
|
+
const geminiQuotaData = {
|
|
1785
|
+
type: "gemini",
|
|
1786
|
+
quotaBucket: geminiBucket,
|
|
1787
|
+
pools: { pro: geminiProBucket, flash: geminiFlashBucket, lite: geminiLiteBucket },
|
|
1788
|
+
session: geminiSession,
|
|
1789
|
+
};
|
|
1735
1790
|
|
|
1736
1791
|
const rows = [
|
|
1737
1792
|
...getClaudeRows(stdin, claudeUsageSnapshot.data, combinedSvPct),
|
|
@@ -1747,7 +1802,8 @@ async function main() {
|
|
|
1747
1802
|
|
|
1748
1803
|
// 비활성 프로바이더 dim 처리: 데이터 없으면 전체 줄 dim
|
|
1749
1804
|
const codexActive = codexBuckets != null;
|
|
1750
|
-
const geminiActive = (geminiSession?.total || 0) > 0 || geminiBucket != null
|
|
1805
|
+
const geminiActive = (geminiSession?.total || 0) > 0 || geminiBucket != null
|
|
1806
|
+
|| geminiProBucket != null || geminiFlashBucket != null;
|
|
1751
1807
|
|
|
1752
1808
|
let outputLines = renderAlignedRows(rows);
|
|
1753
1809
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triflux",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
49
49
|
"better-sqlite3": "^12.6.2",
|
|
50
|
+
"pino": "^10.3.1",
|
|
51
|
+
"pino-pretty": "^13.1.3",
|
|
50
52
|
"systray2": "^2.1.4"
|
|
51
53
|
},
|
|
52
54
|
"keywords": [
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 요청별 로그 컨텍스트 전파 (AsyncLocalStorage 기반).
|
|
3
|
+
*
|
|
4
|
+
* Hub HTTP 서버의 요청마다 correlationId를 자동 할당하여,
|
|
5
|
+
* 하나의 요청에서 발생한 모든 로그를 추적할 수 있다.
|
|
6
|
+
*
|
|
7
|
+
* 사용법:
|
|
8
|
+
* import { getLogger, getCorrelationId, withRequestContext } from './lib/context.mjs';
|
|
9
|
+
*
|
|
10
|
+
* // 미들웨어에서 컨텍스트 생성
|
|
11
|
+
* withRequestContext({ method: 'POST', path: '/bridge/result' }, () => {
|
|
12
|
+
* const log = getLogger();
|
|
13
|
+
* log.info({ agentId }, 'bridge.result_received');
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // 내부 함수에서 자동 상관 ID
|
|
17
|
+
* function processResult() {
|
|
18
|
+
* const log = getLogger();
|
|
19
|
+
* log.info('result.processed'); // correlationId 자동 포함
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
23
|
+
import { randomUUID } from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
import { logger } from './logger.mjs';
|
|
26
|
+
|
|
27
|
+
/** @type {AsyncLocalStorage<{logger: import('pino').Logger, correlationId: string}>} */
|
|
28
|
+
export const asyncLocalStorage = new AsyncLocalStorage();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 현재 요청 컨텍스트의 로거를 반환한다.
|
|
32
|
+
* 요청 컨텍스트 밖에서 호출하면 기본 로거를 반환한다.
|
|
33
|
+
*
|
|
34
|
+
* @returns {import('pino').Logger}
|
|
35
|
+
*/
|
|
36
|
+
export function getLogger() {
|
|
37
|
+
return asyncLocalStorage.getStore()?.logger || logger;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 현재 요청의 상관 ID를 반환한다.
|
|
42
|
+
*
|
|
43
|
+
* @returns {string|undefined}
|
|
44
|
+
*/
|
|
45
|
+
export function getCorrelationId() {
|
|
46
|
+
return asyncLocalStorage.getStore()?.correlationId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 요청 컨텍스트를 생성하고 콜백을 실행한다.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} context — 컨텍스트 필드 (method, path 등)
|
|
53
|
+
* @param {string} [context.correlationId] — 외부에서 전달된 상관 ID (없으면 자동 생성)
|
|
54
|
+
* @param {function} callback — 컨텍스트 내에서 실행할 함수
|
|
55
|
+
* @returns {*}
|
|
56
|
+
*/
|
|
57
|
+
export function withRequestContext(context, callback) {
|
|
58
|
+
const correlationId = context.correlationId || randomUUID();
|
|
59
|
+
const { correlationId: _, ...rest } = context;
|
|
60
|
+
|
|
61
|
+
const store = {
|
|
62
|
+
correlationId,
|
|
63
|
+
logger: logger.child({ correlationId, ...rest }),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return asyncLocalStorage.run(store, callback);
|
|
67
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logify — triflux 구조화 로깅 설정
|
|
3
|
+
*
|
|
4
|
+
* 사용법:
|
|
5
|
+
* import { logger, createModuleLogger } from './lib/logger.mjs';
|
|
6
|
+
*
|
|
7
|
+
* // 기본 로거
|
|
8
|
+
* logger.info({ taskId: 'abc' }, 'task.started');
|
|
9
|
+
*
|
|
10
|
+
* // 모듈별 로거
|
|
11
|
+
* const log = createModuleLogger('hub');
|
|
12
|
+
* log.info({ port: 27888 }, 'server.started');
|
|
13
|
+
* log.error({ err }, 'server.error');
|
|
14
|
+
*
|
|
15
|
+
* 이벤트 네이밍: {도메인}.{액션} 형식
|
|
16
|
+
* hub.started, hub.stopped, route.started, route.completed,
|
|
17
|
+
* worker.spawned, worker.completed, worker.timeout,
|
|
18
|
+
* mcp.connected, mcp.disconnected, mcp.error,
|
|
19
|
+
* team.created, team.deleted, task.claimed, task.completed,
|
|
20
|
+
* pipe.connected, pipe.message, pipe.error,
|
|
21
|
+
* http.request, http.response, http.error
|
|
22
|
+
*
|
|
23
|
+
* 로그 레벨 가이드:
|
|
24
|
+
* debug — 개발/트러블슈팅용 (변수 값, MCP 메시지, 캐시 키)
|
|
25
|
+
* info — 정상 흐름 상태 변경 (서버 시작, 워커 완료, 팀 생성)
|
|
26
|
+
* warn — 위험 신호 (재시도 발생, 쿼타 임박, 느린 워커)
|
|
27
|
+
* error — 작업 실패 (CLI 실행 실패, MCP 연결 끊김)
|
|
28
|
+
* fatal — 프로세스 위협 (DB 연결 불가, 포트 충돌)
|
|
29
|
+
*/
|
|
30
|
+
import pino from 'pino';
|
|
31
|
+
|
|
32
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
33
|
+
|
|
34
|
+
export const logger = pino({
|
|
35
|
+
level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
|
|
36
|
+
|
|
37
|
+
// 모든 로그에 포함되는 기본 필드
|
|
38
|
+
base: {
|
|
39
|
+
service: process.env.SERVICE_NAME || 'triflux',
|
|
40
|
+
env: process.env.NODE_ENV || 'development',
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// 레벨을 대문자로 출력 (AI 파싱 용이)
|
|
44
|
+
formatters: {
|
|
45
|
+
level: (label) => ({ level: label.toUpperCase() }),
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// ISO 8601 타임스탬프
|
|
49
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
50
|
+
|
|
51
|
+
// 민감정보 자동 필터링
|
|
52
|
+
redact: {
|
|
53
|
+
paths: [
|
|
54
|
+
'password',
|
|
55
|
+
'token',
|
|
56
|
+
'apiKey',
|
|
57
|
+
'secret',
|
|
58
|
+
'authorization',
|
|
59
|
+
'*.password',
|
|
60
|
+
'*.token',
|
|
61
|
+
'*.apiKey',
|
|
62
|
+
'*.secret',
|
|
63
|
+
'req.headers.authorization',
|
|
64
|
+
'req.headers.cookie',
|
|
65
|
+
'hubToken',
|
|
66
|
+
],
|
|
67
|
+
remove: true,
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// 개발 환경: 컬러 콘솔 출력
|
|
71
|
+
transport: isDev
|
|
72
|
+
? {
|
|
73
|
+
target: 'pino-pretty',
|
|
74
|
+
options: {
|
|
75
|
+
colorize: true,
|
|
76
|
+
translateTime: 'yyyy-mm-dd HH:MM:ss',
|
|
77
|
+
ignore: 'pid,hostname',
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
: undefined,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 모듈별 Child Logger 생성.
|
|
85
|
+
* 모듈 이름이 모든 로그에 자동 포함된다.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} module — 모듈 이름 (hub, route, worker, mcp, team 등)
|
|
88
|
+
* @returns {import('pino').Logger}
|
|
89
|
+
*/
|
|
90
|
+
export function createModuleLogger(module) {
|
|
91
|
+
return logger.child({ module });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 정상 종료 시 버퍼 flush 보장
|
|
95
|
+
process.on('uncaughtException', (err) => {
|
|
96
|
+
const finalLogger = pino.final(logger);
|
|
97
|
+
finalLogger.fatal({ err }, 'process.uncaught_exception');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
process.on('unhandledRejection', (reason) => {
|
|
102
|
+
const finalLogger = pino.final(logger);
|
|
103
|
+
finalLogger.fatal({ reason: String(reason) }, 'process.unhandled_rejection');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
@@ -127,6 +127,31 @@ const PROFILE_DEFINITIONS = Object.freeze({
|
|
|
127
127
|
}),
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* 파이프라인 단계별 MCP 서버/도구 제한 (post-filter).
|
|
132
|
+
* role-based 프로필 위에 추가 적용. 빈 배열 = 전체 차단, 미정의 = 제한 없음.
|
|
133
|
+
*/
|
|
134
|
+
export const PHASE_OVERRIDES = Object.freeze({
|
|
135
|
+
plan: Object.freeze({
|
|
136
|
+
description: '계획 단계: 읽기 전용 탐색만 허용',
|
|
137
|
+
allowedServers: Object.freeze(['context7']),
|
|
138
|
+
blockedServers: Object.freeze(['playwright', 'tavily', 'exa']),
|
|
139
|
+
}),
|
|
140
|
+
prd: Object.freeze({
|
|
141
|
+
description: 'PRD 단계: 읽기 전용 탐색 + 문서 조회',
|
|
142
|
+
allowedServers: Object.freeze(['context7', 'brave-search']),
|
|
143
|
+
blockedServers: Object.freeze(['playwright']),
|
|
144
|
+
}),
|
|
145
|
+
exec: Object.freeze({
|
|
146
|
+
description: '실행 단계: 프로필 기반 전체 허용 (제한 없음)',
|
|
147
|
+
}),
|
|
148
|
+
verify: Object.freeze({
|
|
149
|
+
description: '검증 단계: 읽기 전용 + 분석 도구',
|
|
150
|
+
allowedServers: Object.freeze(['context7', 'brave-search', 'exa']),
|
|
151
|
+
blockedServers: Object.freeze(['playwright']),
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
|
|
130
155
|
export const LEGACY_PROFILE_ALIASES = Object.freeze({
|
|
131
156
|
implement: 'executor',
|
|
132
157
|
analyze: 'analyze',
|
|
@@ -553,13 +578,23 @@ export function buildMcpPolicy(options = {}) {
|
|
|
553
578
|
const inventoryIndex = buildInventoryIndex(inventory);
|
|
554
579
|
const resolvedOptions = { ...options, inventory, inventoryIndex };
|
|
555
580
|
const resolvedProfile = resolveMcpProfile(options.agentType, options.requestedProfile);
|
|
556
|
-
|
|
581
|
+
let allowedServers = resolveAllowedServers(resolvedOptions);
|
|
557
582
|
const hint = buildPromptHint(resolvedOptions);
|
|
583
|
+
|
|
584
|
+
// Phase-aware post-filter: 파이프라인 단계별 서버 제한 적용
|
|
585
|
+
const phase = options.phase;
|
|
586
|
+
const phaseOverride = phase && PHASE_OVERRIDES[phase];
|
|
587
|
+
if (phaseOverride && phaseOverride.blockedServers) {
|
|
588
|
+
const blocked = new Set(phaseOverride.blockedServers);
|
|
589
|
+
allowedServers = allowedServers.filter((s) => !blocked.has(s));
|
|
590
|
+
}
|
|
591
|
+
|
|
558
592
|
return {
|
|
559
593
|
requestedProfile: typeof options.requestedProfile === 'string' && options.requestedProfile
|
|
560
594
|
? options.requestedProfile
|
|
561
595
|
: 'auto',
|
|
562
596
|
resolvedProfile,
|
|
597
|
+
resolvedPhase: phase || null,
|
|
563
598
|
allowedServers,
|
|
564
599
|
hint,
|
|
565
600
|
geminiAllowedServers: getGeminiAllowedServers(resolvedOptions),
|
|
@@ -577,14 +612,18 @@ function shellArray(name, values) {
|
|
|
577
612
|
}
|
|
578
613
|
|
|
579
614
|
export function toShellExports(policy) {
|
|
580
|
-
|
|
615
|
+
const lines = [
|
|
581
616
|
`MCP_PROFILE_REQUESTED=${shellEscape(policy.requestedProfile)}`,
|
|
582
617
|
`MCP_RESOLVED_PROFILE=${shellEscape(policy.resolvedProfile)}`,
|
|
583
618
|
`MCP_HINT=${shellEscape(policy.hint)}`,
|
|
584
619
|
shellArray('GEMINI_ALLOWED_SERVERS', policy.geminiAllowedServers),
|
|
585
620
|
shellArray('CODEX_CONFIG_FLAGS', policy.codexConfigOverrides.flatMap((override) => ['-c', override])),
|
|
586
621
|
`CODEX_CONFIG_JSON=${shellEscape(JSON.stringify(policy.codexConfig))}`,
|
|
587
|
-
]
|
|
622
|
+
];
|
|
623
|
+
if (policy.resolvedPhase) {
|
|
624
|
+
lines.push(`MCP_PIPELINE_PHASE=${shellEscape(policy.resolvedPhase)}`);
|
|
625
|
+
}
|
|
626
|
+
return lines.join('\n');
|
|
588
627
|
}
|
|
589
628
|
|
|
590
629
|
function parseCliArgs(argv) {
|
|
@@ -636,6 +675,9 @@ function parseCliArgs(argv) {
|
|
|
636
675
|
case '--worker-index':
|
|
637
676
|
args.workerIndex = Number.parseInt(next(), 10);
|
|
638
677
|
break;
|
|
678
|
+
case '--phase':
|
|
679
|
+
args.phase = next();
|
|
680
|
+
break;
|
|
639
681
|
default:
|
|
640
682
|
throw new Error(`알 수 없는 옵션: ${token}`);
|
|
641
683
|
}
|
package/skills/tfx-auto/SKILL.md
CHANGED
|
@@ -32,14 +32,20 @@ argument-hint: "<command|task> [args...]"
|
|
|
32
32
|
> 2. **비용**: Codex 우선 → Gemini → Claude 최후 수단. `claude` 선택 전 "Codex로 가능한가?" 재확인.
|
|
33
33
|
> 3. **DAG**: SEQUENTIAL/DAG이면 레벨 기반 순차 실행. `.omc/context/{sid}/` 생성, context_output 저장, 실패 시 후속 SKIP.
|
|
34
34
|
> 4. **트리아지**: Codex `--full-auto` 분류 + Opus 인라인 분해. Agent 스폰 금지.
|
|
35
|
+
> 5. **thorough**: `-t`/`--thorough` 시 파이프라인 init 필수. 커맨드 숏컷은 항상 quick.
|
|
35
36
|
|
|
36
37
|
## 모드
|
|
37
38
|
|
|
38
39
|
| 입력 형식 | 모드 | 트리아지 |
|
|
39
40
|
|-----------|------|----------|
|
|
40
|
-
| `/implement JWT 추가` | 커맨드 숏컷 | 없음 (즉시 실행) |
|
|
41
|
-
| `/tfx-auto "리팩터링 + UI"` | 자동 | Codex 분류 → Opus 분해 |
|
|
42
|
-
| `/tfx-auto
|
|
41
|
+
| `/implement JWT 추가` | 커맨드 숏컷 (quick) | 없음 (즉시 실행) |
|
|
42
|
+
| `/tfx-auto "리팩터링 + UI"` | 자동 (quick) | Codex 분류 → Opus 분해 |
|
|
43
|
+
| `/tfx-auto -t "리팩터링 + UI"` | 자동 (thorough) | Codex 분류 → Opus 분해 → Pipeline |
|
|
44
|
+
| `/tfx-auto --thorough "리팩터링"` | 자동 (thorough) | `-t` 동일 |
|
|
45
|
+
| `/tfx-auto 3:codex "리뷰"` | 수동 (quick) | Opus 분해만 |
|
|
46
|
+
|
|
47
|
+
> **tfx-auto는 `--quick`이 기본.** 커맨드 숏컷·단일 실행에서 plan/verify 오버헤드가 불필요하기 때문.
|
|
48
|
+
> 멀티 태스크 시 tfx-multi로 전환되면 tfx-multi의 기본값(`--thorough`)이 적용된다.
|
|
43
49
|
|
|
44
50
|
## 커맨드 숏컷
|
|
45
51
|
|
|
@@ -100,25 +106,77 @@ argument-hint: "<command|task> [args...]"
|
|
|
100
106
|
|
|
101
107
|
**수동 모드 (`N:agent_type`):** Codex 분류 건너뜀 → Opus가 N개 서브태스크 분해. N > 10 거부.
|
|
102
108
|
|
|
109
|
+
## --thorough 모드
|
|
110
|
+
|
|
111
|
+
`-t` 또는 `--thorough` 플래그 시 파이프라인 기반 실행. 커맨드 숏컷에서는 무시된다.
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
분기점은 "실행 전략"이지 "계획"이 아님:
|
|
115
|
+
|
|
116
|
+
TRIAGE
|
|
117
|
+
│
|
|
118
|
+
├─ [thorough] → PIPELINE INIT(plan) → PLAN → PRD → [APPROVAL]
|
|
119
|
+
│ │
|
|
120
|
+
│ ┌───────────────┤
|
|
121
|
+
│ │ │
|
|
122
|
+
│ [1 task] [2+ tasks]
|
|
123
|
+
│ │ │
|
|
124
|
+
│ AUTO 직접 실행 TEAM EXEC (multi Phase 3)
|
|
125
|
+
│ │ │
|
|
126
|
+
│ └───────┬───────┘
|
|
127
|
+
│ │
|
|
128
|
+
│ VERIFY → FIX loop → COMPLETE
|
|
129
|
+
│
|
|
130
|
+
└─ [quick] → [1 task] → fire-and-forget
|
|
131
|
+
[2+ tasks] → TEAM EXEC → COLLECT → CLEANUP
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 단일 태스크 thorough
|
|
135
|
+
|
|
136
|
+
1. `Bash("node hub/bridge.mjs pipeline-init --team ${sid}")` — 파이프라인 초기화 (phase: plan)
|
|
137
|
+
2. Plan: Codex architect → 결과를 `pipeline.writePlanFile()` 저장
|
|
138
|
+
3. PRD: Codex analyst → acceptance criteria 확정
|
|
139
|
+
4. `pipeline_advance_gated` → [Approval Gate] → 사용자 승인 대기
|
|
140
|
+
5. Exec: tfx-auto 직접 실행 (아래 "실행" 섹션)
|
|
141
|
+
6. Verify: Codex verifier → 검증
|
|
142
|
+
7. 실패 시 Fix loop (최대 3회) → Exec 재실행
|
|
143
|
+
8. Complete
|
|
144
|
+
|
|
145
|
+
### 멀티 태스크 thorough
|
|
146
|
+
|
|
147
|
+
Plan/PRD/Approval은 tfx-auto에서 실행, 그 후 tfx-multi Phase 3로 전환.
|
|
148
|
+
서브태스크 배열 + `thorough: true` 신호를 함께 전달하여 multi 측에서 verify/fix를 수행.
|
|
149
|
+
|
|
103
150
|
## 멀티 태스크 라우팅 (트리아지 후)
|
|
104
151
|
|
|
105
|
-
> **트리아지
|
|
152
|
+
> **트리아지 결과에 따라 실행 경로 결정.**
|
|
106
153
|
|
|
107
|
-
|
|
|
108
|
-
|
|
109
|
-
| 1개 | tfx-auto 직접 실행 (
|
|
110
|
-
|
|
|
154
|
+
| 조건 | 실행 경로 | 이유 |
|
|
155
|
+
|------|----------|------|
|
|
156
|
+
| 1개 + quick | tfx-auto 직접 실행 (fire-and-forget) | 팀 오버헤드 불필요 |
|
|
157
|
+
| 1개 + thorough | tfx-auto 직접 실행 + verify/fix loop | plan→exec→verify 단일 경로 |
|
|
158
|
+
| 2개+ + quick | tfx-multi Phase 3 (TeamCreate 직행) | Shift+Down, 상태 추적 |
|
|
159
|
+
| 2개+ + thorough | Plan/PRD/Approval 후 → tfx-multi Phase 3 + verify/fix | 전체 파이프라인 |
|
|
111
160
|
|
|
112
|
-
**전환 방법:** 트리아지 완료 후 서브태스크
|
|
113
|
-
tfx-multi의 Phase 2(트리아지)는
|
|
161
|
+
**전환 방법:** 트리아지 완료 후 서브태스크 배열 + thorough 플래그를 tfx-multi Phase 3에 전달.
|
|
162
|
+
tfx-multi의 Phase 2(트리아지)는 건너뛰고, thorough 시 Phase 2.5-2.6도 건너뛴다 (auto에서 이미 수행).
|
|
114
163
|
|
|
115
164
|
```
|
|
165
|
+
thorough = args에 -t 또는 --thorough 포함
|
|
166
|
+
|
|
116
167
|
if subtasks.length >= 2:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
168
|
+
if thorough:
|
|
169
|
+
→ Pipeline init → Plan → PRD → Approval (tfx-auto에서 수행)
|
|
170
|
+
→ tfx-multi Phase 3 실행 ({ subtasks, thorough: true })
|
|
171
|
+
→ Phase 3.5 verify → Phase 3.6 fix loop → Phase 5 정리
|
|
172
|
+
else:
|
|
173
|
+
→ tfx-multi Phase 3 실행 ({ subtasks, thorough: false })
|
|
174
|
+
→ Phase 4 결과 수집 → Phase 5 정리
|
|
120
175
|
else:
|
|
121
|
-
|
|
176
|
+
if thorough:
|
|
177
|
+
→ Pipeline init → Plan → PRD → Approval → 직접 실행 → Verify → Fix loop
|
|
178
|
+
else:
|
|
179
|
+
→ tfx-auto 직접 실행 (아래)
|
|
122
180
|
```
|
|
123
181
|
|
|
124
182
|
## 실행
|