triflux 10.9.21 → 10.9.23
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/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +22 -0
- package/config/mcp-registry.json +29 -0
- package/hub/account-broker.mjs +6 -4
- package/hub/cli-adapter-base.mjs +14 -14
- package/hub/lib/env-detect.mjs +47 -20
- package/hub/server.mjs +17 -15
- package/hub/team/headless.mjs +10 -0
- package/hub/team/swarm-hypervisor.mjs +2 -2
- package/hub/workers/delegator-mcp.mjs +129 -1
- package/hud/constants.mjs +24 -13
- package/hud/renderers.mjs +2 -1
- package/package.json +62 -21
- package/scripts/__tests__/keyword-detector.test.mjs +4 -4
- package/scripts/__tests__/release-governance.test.mjs +148 -0
- package/scripts/doctor-diagnose.mjs +6 -7
- package/scripts/lib/cross-review-utils.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +12 -24
- package/scripts/release/bump-version.mjs +77 -0
- package/scripts/release/check-sync.mjs +51 -0
- package/scripts/release/lib.mjs +303 -0
- package/scripts/release/prepare.mjs +85 -0
- package/scripts/release/publish.mjs +87 -0
- package/scripts/release/verify.mjs +81 -0
- package/scripts/release/version-manifest.json +26 -0
- package/scripts/remote-spawn.mjs +3 -3
- package/scripts/setup.mjs +18 -15
- package/scripts/tfx-route.sh +64 -8
- package/tui/codex-profile.mjs +457 -0
- package/tui/core.mjs +266 -0
- package/tui/doctor.mjs +375 -0
- package/tui/gemini-profile.mjs +299 -0
- package/tui/monitor-data.mjs +152 -0
- package/tui/monitor.mjs +339 -0
- package/tui/setup.mjs +598 -0
- package/CLAUDE.md +0 -212
- package/references/hosts.json +0 -46
- package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
- package/skills/tfx-workspace/evals/evals.json +0 -79
- package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/review.html +0 -1325
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/review.html +0 -1325
- package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
- package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
- package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
- package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
- package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
- package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
- package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
- package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "triflux",
|
|
4
|
+
"description": "CLI-first multi-model orchestrator — Codex/Gemini/Claude routing with DAG execution, auto-triage, and cost optimization",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "tellang"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "triflux",
|
|
11
|
+
"description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
|
|
12
|
+
"version": "10.9.21",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "tellang"
|
|
15
|
+
},
|
|
16
|
+
"source": {
|
|
17
|
+
"source": "npm",
|
|
18
|
+
"package": "triflux"
|
|
19
|
+
},
|
|
20
|
+
"category": "productivity",
|
|
21
|
+
"homepage": "https://github.com/tellang/triflux",
|
|
22
|
+
"tags": [
|
|
23
|
+
"multi-model",
|
|
24
|
+
"codex",
|
|
25
|
+
"gemini",
|
|
26
|
+
"cli-routing",
|
|
27
|
+
"orchestration",
|
|
28
|
+
"cost-optimization",
|
|
29
|
+
"dag-execution"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"version": "10.9.21"
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "triflux",
|
|
3
|
+
"version": "10.9.22",
|
|
4
|
+
"description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "tellang"
|
|
7
|
+
},
|
|
8
|
+
"repository": "https://github.com/tellang/triflux",
|
|
9
|
+
"homepage": "https://github.com/tellang/triflux",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"claude-code",
|
|
13
|
+
"plugin",
|
|
14
|
+
"codex",
|
|
15
|
+
"gemini",
|
|
16
|
+
"cli-routing",
|
|
17
|
+
"orchestration",
|
|
18
|
+
"multi-model"
|
|
19
|
+
],
|
|
20
|
+
"skills": "./skills/",
|
|
21
|
+
"hooks": "./hooks/hooks.json"
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "mcp-registry-schema",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "MCP 서버 중앙 레지스트리 — 진실의 원천",
|
|
5
|
+
"defaults": {
|
|
6
|
+
"transport": "hub-url",
|
|
7
|
+
"hub_base": "http://127.0.0.1:27888"
|
|
8
|
+
},
|
|
9
|
+
"servers": {
|
|
10
|
+
"tfx-hub": {
|
|
11
|
+
"transport": "hub-url",
|
|
12
|
+
"url": "http://127.0.0.1:27888/mcp",
|
|
13
|
+
"safe": true,
|
|
14
|
+
"targets": ["claude", "gemini", "codex"],
|
|
15
|
+
"description": "triflux Hub MCP 서버"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"policies": {
|
|
19
|
+
"stdio_action": "replace-with-hub",
|
|
20
|
+
"unknown_server_action": "warn",
|
|
21
|
+
"watched_paths": [
|
|
22
|
+
"~/.gemini/settings.json",
|
|
23
|
+
"~/.codex/config.toml",
|
|
24
|
+
"~/.claude/settings.json",
|
|
25
|
+
"~/.claude/settings.local.json",
|
|
26
|
+
".mcp.json"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
package/hub/account-broker.mjs
CHANGED
|
@@ -120,11 +120,13 @@ class AccountBroker extends EventEmitter {
|
|
|
120
120
|
#config;
|
|
121
121
|
#state; // Map<accountId, accountState>
|
|
122
122
|
#roundRobinIndex; // Map<provider, number>
|
|
123
|
+
#persist; // boolean — disable persistence for tests
|
|
123
124
|
|
|
124
|
-
constructor(config) {
|
|
125
|
+
constructor(config, { _skipPersistence = false } = {}) {
|
|
125
126
|
super();
|
|
126
127
|
const parsed = ConfigSchema.parse(config);
|
|
127
128
|
this.#config = parsed;
|
|
129
|
+
this.#persist = !_skipPersistence;
|
|
128
130
|
|
|
129
131
|
this.#state = new Map();
|
|
130
132
|
this.#roundRobinIndex = new Map();
|
|
@@ -134,7 +136,7 @@ class AccountBroker extends EventEmitter {
|
|
|
134
136
|
...(parsed.gemini || []).map((a) => ({ ...a, provider: "gemini" })),
|
|
135
137
|
];
|
|
136
138
|
|
|
137
|
-
const persisted = loadPersistedState();
|
|
139
|
+
const persisted = this.#persist ? loadPersistedState() : null;
|
|
138
140
|
const pEntries = persisted?.entries || {};
|
|
139
141
|
|
|
140
142
|
for (const account of allAccounts) {
|
|
@@ -386,7 +388,7 @@ class AccountBroker extends EventEmitter {
|
|
|
386
388
|
};
|
|
387
389
|
|
|
388
390
|
this.#state.set(accountId, updated);
|
|
389
|
-
persistState(this.#state);
|
|
391
|
+
if (this.#persist) persistState(this.#state);
|
|
390
392
|
this.emit("release", { id: accountId, ok });
|
|
391
393
|
}
|
|
392
394
|
|
|
@@ -401,7 +403,7 @@ class AccountBroker extends EventEmitter {
|
|
|
401
403
|
leasedAt: null,
|
|
402
404
|
cooldownUntil: Date.now() + coolMs,
|
|
403
405
|
});
|
|
404
|
-
persistState(this.#state);
|
|
406
|
+
if (this.#persist) persistState(this.#state);
|
|
405
407
|
}
|
|
406
408
|
|
|
407
409
|
// ── snapshot ──────────────────────────────────────────────────
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -218,14 +218,15 @@ export async function executeWithCircuitBroker({
|
|
|
218
218
|
const { withRetry } = await import("./workers/worker-utils.mjs");
|
|
219
219
|
|
|
220
220
|
// access broker as live binding property (not destructured) so reloadBroker() propagates
|
|
221
|
-
const
|
|
222
|
-
|
|
221
|
+
const hasBroker = brokerMod.broker != null;
|
|
222
|
+
const lease = hasBroker ? brokerMod.broker.lease({ provider }) : null;
|
|
223
|
+
if (hasBroker && !lease) {
|
|
223
224
|
return createResult(false, { fellBack: true, failureMode: "circuit_open" });
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
const preflight = await preflightFn(opts);
|
|
227
228
|
if (!preflight.ok) {
|
|
228
|
-
brokerMod.broker.release(lease.id, { ok: false });
|
|
229
|
+
if (lease) brokerMod.broker.release(lease.id, { ok: false });
|
|
229
230
|
return createResult(false, {
|
|
230
231
|
stderr: appendWarnings("", preflight.warnings),
|
|
231
232
|
fellBack: opts.fallbackToClaude !== false,
|
|
@@ -273,20 +274,19 @@ export async function executeWithCircuitBroker({
|
|
|
273
274
|
}
|
|
274
275
|
|
|
275
276
|
if (lastResult.ok) {
|
|
276
|
-
brokerMod.broker.release(lease.id, { ok: true });
|
|
277
|
+
if (lease) brokerMod.broker.release(lease.id, { ok: true });
|
|
277
278
|
return lastResult;
|
|
278
279
|
}
|
|
279
280
|
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
brokerMod.broker.release(lease.id, { ok: false });
|
|
281
|
+
if (lease) {
|
|
282
|
+
if (lastResult.failureMode === "rate_limited") {
|
|
283
|
+
const text = `${lastResult.output || ""}\n${lastResult.stderr || ""}`;
|
|
284
|
+
const coolMs = parseRetryAfterMs(text, provider);
|
|
285
|
+
brokerMod.broker.markRateLimited(lease.id, coolMs);
|
|
286
|
+
brokerMod.broker.emit("cooldown", { id: lease.id, provider, coolMs, reason: "quota_exhausted" });
|
|
287
|
+
} else {
|
|
288
|
+
brokerMod.broker.release(lease.id, { ok: false });
|
|
289
|
+
}
|
|
290
290
|
}
|
|
291
291
|
return {
|
|
292
292
|
...lastResult,
|
package/hub/lib/env-detect.mjs
CHANGED
|
@@ -3,68 +3,95 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { platform as osPlatform } from "node:os";
|
|
4
4
|
|
|
5
5
|
let _cached = null;
|
|
6
|
+
let _shellCache = null;
|
|
7
|
+
let _terminalCache = null;
|
|
8
|
+
let _multiplexerCache = null;
|
|
9
|
+
|
|
10
|
+
const PIPE_OPTS = { encoding: "utf8", timeout: 3000, stdio: "pipe" };
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* 기본 쉘 감지
|
|
9
|
-
* Windows: pwsh → powershell
|
|
10
|
-
* Unix: $SHELL → /bin/sh
|
|
14
|
+
* Windows: pwsh → powershell (full path + version)
|
|
15
|
+
* Unix: $SHELL → /bin/sh (+ version)
|
|
11
16
|
*/
|
|
12
17
|
export function detectShell() {
|
|
18
|
+
if (_shellCache) return _shellCache;
|
|
19
|
+
|
|
13
20
|
const platform = osPlatform();
|
|
21
|
+
|
|
14
22
|
if (platform === "win32") {
|
|
15
23
|
try {
|
|
16
|
-
execFileSync("where", ["pwsh.exe"],
|
|
17
|
-
|
|
24
|
+
const path = execFileSync("where", ["pwsh.exe"], PIPE_OPTS).trim().split(/\r?\n/)[0];
|
|
25
|
+
let version = null;
|
|
26
|
+
try {
|
|
27
|
+
version = execFileSync(path, ["-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()"], PIPE_OPTS).trim();
|
|
28
|
+
} catch {}
|
|
29
|
+
_shellCache = { name: "pwsh", path, version };
|
|
18
30
|
} catch {
|
|
19
31
|
try {
|
|
20
|
-
execFileSync("where", ["powershell.exe"],
|
|
21
|
-
|
|
32
|
+
const path = execFileSync("where", ["powershell.exe"], PIPE_OPTS).trim().split(/\r?\n/)[0];
|
|
33
|
+
_shellCache = { name: "powershell", path, version: null };
|
|
22
34
|
} catch {
|
|
23
|
-
|
|
35
|
+
_shellCache = { name: "powershell", path: "", version: null, installHint: "pwsh: winget install Microsoft.PowerShell" };
|
|
24
36
|
}
|
|
25
37
|
}
|
|
38
|
+
return _shellCache;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
const shellPath = process.env.SHELL || "/bin/sh";
|
|
29
42
|
const name = shellPath.split("/").pop() || "sh";
|
|
30
|
-
|
|
43
|
+
let version = null;
|
|
44
|
+
try {
|
|
45
|
+
version = execFileSync(shellPath, ["--version"], PIPE_OPTS).trim();
|
|
46
|
+
} catch {}
|
|
47
|
+
_shellCache = { name, path: shellPath, version };
|
|
48
|
+
return _shellCache;
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
/**
|
|
34
52
|
* 터미널 에뮬레이터 감지
|
|
35
53
|
*/
|
|
36
54
|
export function detectTerminal() {
|
|
55
|
+
if (_terminalCache) return _terminalCache;
|
|
56
|
+
|
|
37
57
|
const platform = osPlatform();
|
|
38
58
|
if (platform === "win32") {
|
|
39
59
|
try {
|
|
40
|
-
execFileSync("where", ["wt.exe"],
|
|
41
|
-
|
|
60
|
+
execFileSync("where", ["wt.exe"], PIPE_OPTS);
|
|
61
|
+
_terminalCache = { name: "windows-terminal", hasWt: true };
|
|
42
62
|
} catch {
|
|
43
|
-
|
|
63
|
+
_terminalCache = { name: "unknown", hasWt: false, installHint: "wt: winget install Microsoft.WindowsTerminal" };
|
|
44
64
|
}
|
|
65
|
+
return _terminalCache;
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
if (process.env.TERM_PROGRAM === "iTerm.app") {
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
_terminalCache = { name: "iterm2", hasWt: false };
|
|
70
|
+
} else if (process.env.TERM_PROGRAM === "Apple_Terminal") {
|
|
71
|
+
_terminalCache = { name: "terminal-app", hasWt: false };
|
|
72
|
+
} else {
|
|
73
|
+
_terminalCache = { name: "unknown", hasWt: false };
|
|
52
74
|
}
|
|
53
|
-
|
|
54
|
-
return { name: "unknown", hasWt: false };
|
|
75
|
+
return _terminalCache;
|
|
55
76
|
}
|
|
56
77
|
|
|
57
78
|
/**
|
|
58
79
|
* 멀티플렉서 감지 (tmux)
|
|
59
80
|
*/
|
|
60
81
|
export function detectMultiplexer() {
|
|
82
|
+
if (_multiplexerCache) return _multiplexerCache;
|
|
83
|
+
|
|
61
84
|
try {
|
|
62
85
|
const cmd = osPlatform() === "win32" ? "where" : "which";
|
|
63
|
-
const path = execFileSync(cmd, ["tmux"],
|
|
64
|
-
|
|
86
|
+
const path = execFileSync(cmd, ["tmux"], PIPE_OPTS).trim();
|
|
87
|
+
_multiplexerCache = { name: "tmux", path };
|
|
65
88
|
} catch {
|
|
66
|
-
|
|
89
|
+
const hint = osPlatform() === "win32"
|
|
90
|
+
? "tmux: install tmux in WSL or MSYS2"
|
|
91
|
+
: undefined;
|
|
92
|
+
_multiplexerCache = { name: "none", path: null, ...(hint ? { installHint: hint } : {}) };
|
|
67
93
|
}
|
|
94
|
+
return _multiplexerCache;
|
|
68
95
|
}
|
|
69
96
|
|
|
70
97
|
/**
|
package/hub/server.mjs
CHANGED
|
@@ -80,7 +80,7 @@ const AIMD_WINDOW_MS = 30 * 60 * 1000;
|
|
|
80
80
|
const AIMD_INITIAL_BATCH_SIZE = 3;
|
|
81
81
|
const AIMD_MIN_BATCH_SIZE = 1;
|
|
82
82
|
const AIMD_MAX_BATCH_SIZE = 10;
|
|
83
|
-
const HUB_IDLE_TIMEOUT_DEFAULT_MS =
|
|
83
|
+
const HUB_IDLE_TIMEOUT_DEFAULT_MS = 0; // 0 = 영구 실행 (idle shutdown 비활성). TFX_HUB_IDLE_TIMEOUT_MS 환경변수로 오버라이드 가능
|
|
84
84
|
const HUB_IDLE_SWEEP_DEFAULT_MS = 60 * 1000;
|
|
85
85
|
const STATIC_CONTENT_TYPES = Object.freeze({
|
|
86
86
|
".html": "text/html",
|
|
@@ -1655,21 +1655,23 @@ export async function startHub({
|
|
|
1655
1655
|
return stopPromise;
|
|
1656
1656
|
};
|
|
1657
1657
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
void stopFn().catch((error) => {
|
|
1666
|
-
hubLog.error(
|
|
1667
|
-
{ err: error, idleMs, idleTimeoutMs: hubIdleTimeoutMs, port },
|
|
1668
|
-
"hub.idle_timeout_shutdown_failed",
|
|
1658
|
+
if (hubIdleTimeoutMs > 0) {
|
|
1659
|
+
idleTimer = setInterval(() => {
|
|
1660
|
+
const idleMs = Date.now() - lastRequestAt;
|
|
1661
|
+
if (idleMs < hubIdleTimeoutMs) return;
|
|
1662
|
+
hubLog.warn(
|
|
1663
|
+
{ idleMs, idleTimeoutMs: hubIdleTimeoutMs, port },
|
|
1664
|
+
"hub.idle_timeout_shutdown",
|
|
1669
1665
|
);
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1666
|
+
void stopFn().catch((error) => {
|
|
1667
|
+
hubLog.error(
|
|
1668
|
+
{ err: error, idleMs, idleTimeoutMs: hubIdleTimeoutMs, port },
|
|
1669
|
+
"hub.idle_timeout_shutdown_failed",
|
|
1670
|
+
);
|
|
1671
|
+
});
|
|
1672
|
+
}, hubIdleSweepMs);
|
|
1673
|
+
idleTimer.unref();
|
|
1674
|
+
}
|
|
1673
1675
|
|
|
1674
1676
|
resolveHub({
|
|
1675
1677
|
reused: false,
|
package/hub/team/headless.mjs
CHANGED
|
@@ -161,6 +161,16 @@ const MCP_PROFILE_HINTS = {
|
|
|
161
161
|
* @param {string} [opts.contextFile] — 컨텍스트 파일 경로 (최대 32KB, UTF-8 안전 절단)
|
|
162
162
|
* @returns {string} PowerShell 명령
|
|
163
163
|
*/
|
|
164
|
+
// ── Dashboard attach args for WT ────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export function buildDashboardAttachArgs(sessionName, layout, workerCount, anchor = "window") {
|
|
167
|
+
const safeName = String(sessionName).replace(/[^a-zA-Z0-9_-]/g, "");
|
|
168
|
+
const base = anchor === "tab"
|
|
169
|
+
? ["-w", "0", "nt"]
|
|
170
|
+
: ["-w", "new"];
|
|
171
|
+
return [...base, "--session", safeName, "--layout", layout, "--workers", String(workerCount)];
|
|
172
|
+
}
|
|
173
|
+
|
|
164
174
|
export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
165
175
|
const { handoff = true, mcp, contextFile, model, cwd } = opts;
|
|
166
176
|
const resolvedCli = resolveCliType(cli);
|
|
@@ -1065,10 +1065,10 @@ export function createSwarmHypervisor(opts) {
|
|
|
1065
1065
|
* @param {SwarmPlan} swarmPlan — from planSwarm()
|
|
1066
1066
|
* @returns {Promise<SwarmStatus>}
|
|
1067
1067
|
*/
|
|
1068
|
-
/** Hub keepalive —
|
|
1068
|
+
/** Hub keepalive — Hub crash recovery (idle timeout은 기본 비활성이므로 crash 복구 용도) */
|
|
1069
1069
|
let hubKeepaliveTimer = null;
|
|
1070
1070
|
function startHubKeepalive() {
|
|
1071
|
-
// 5분마다 Hub /status 핑
|
|
1071
|
+
// 5분마다 Hub /status 핑 — crash 감지 시 ensureHubAlive로 재시작
|
|
1072
1072
|
hubKeepaliveTimer = setInterval(
|
|
1073
1073
|
async () => {
|
|
1074
1074
|
try {
|
|
@@ -14,6 +14,7 @@ import * as z from "zod";
|
|
|
14
14
|
import { resolveBashExecutable } from "../lib/bash-path.mjs";
|
|
15
15
|
import { CodexMcpWorker } from "./codex-mcp.mjs";
|
|
16
16
|
import { GeminiWorker } from "./gemini-worker.mjs";
|
|
17
|
+
import { runHeadlessWithCleanup } from "../team/headless.mjs";
|
|
17
18
|
|
|
18
19
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
|
|
@@ -321,6 +322,18 @@ const DelegateInputSchema = z.object({
|
|
|
321
322
|
teamAgentName: z.string().optional().describe("TFX_TEAM_AGENT_NAME"),
|
|
322
323
|
teamLeadName: z.string().optional().describe("TFX_TEAM_LEAD_NAME"),
|
|
323
324
|
hubUrl: z.string().optional().describe("TFX_HUB_URL"),
|
|
325
|
+
workers: z
|
|
326
|
+
.array(
|
|
327
|
+
z.object({
|
|
328
|
+
provider: z.enum(["codex", "gemini"]).describe("워커 provider"),
|
|
329
|
+
agentType: z.string().default("executor").describe("역할명"),
|
|
330
|
+
prompt: z.string().describe("워커별 프롬프트"),
|
|
331
|
+
mcpProfile: z.string().default("auto").describe("MCP 프로필"),
|
|
332
|
+
model: z.string().optional().describe("모델 오버라이드"),
|
|
333
|
+
}),
|
|
334
|
+
)
|
|
335
|
+
.optional()
|
|
336
|
+
.describe("병렬 멀티워커 목록. 지정 시 psmux 기반 병렬 실행"),
|
|
324
337
|
});
|
|
325
338
|
|
|
326
339
|
const DelegateStatusInputSchema = z.object({
|
|
@@ -341,7 +354,7 @@ const DelegateOutputSchema = z.object({
|
|
|
341
354
|
jobId: z.string().optional(),
|
|
342
355
|
job_id: z.string().optional(),
|
|
343
356
|
mode: z.enum(["sync", "async"]).optional(),
|
|
344
|
-
status: z.enum(["running", "completed", "failed"]).optional(),
|
|
357
|
+
status: z.enum(["running", "completed", "failed", "partial"]).optional(),
|
|
345
358
|
error: z.string().optional(),
|
|
346
359
|
providerRequested: z.string().optional(),
|
|
347
360
|
providerResolved: z.string().nullable().optional(),
|
|
@@ -357,6 +370,20 @@ const DelegateOutputSchema = z.object({
|
|
|
357
370
|
threadId: z.string().nullable().optional(),
|
|
358
371
|
sessionKey: z.string().nullable().optional(),
|
|
359
372
|
conversationOpen: z.boolean().optional(),
|
|
373
|
+
workerResults: z
|
|
374
|
+
.array(
|
|
375
|
+
z.object({
|
|
376
|
+
cli: z.string(),
|
|
377
|
+
role: z.string().optional(),
|
|
378
|
+
paneName: z.string(),
|
|
379
|
+
matched: z.boolean(),
|
|
380
|
+
exitCode: z.number().nullable(),
|
|
381
|
+
output: z.string(),
|
|
382
|
+
}),
|
|
383
|
+
)
|
|
384
|
+
.optional()
|
|
385
|
+
.describe("멀티워커 개별 결과"),
|
|
386
|
+
sessionName: z.string().optional().describe("psmux 세션명"),
|
|
360
387
|
});
|
|
361
388
|
|
|
362
389
|
function isTeamRouteRequested(args) {
|
|
@@ -791,6 +818,23 @@ export class DelegatorMcpWorker {
|
|
|
791
818
|
"위임 실행을 시작합니다.",
|
|
792
819
|
);
|
|
793
820
|
|
|
821
|
+
// 멀티워커 분기: workers 배열이 있으면 headless psmux 병렬 실행
|
|
822
|
+
if (Array.isArray(args.workers) && args.workers.length > 0) {
|
|
823
|
+
try {
|
|
824
|
+
const result = await this._executeMultiWorker(args, extra);
|
|
825
|
+
await emitProgress(extra, DIRECT_PROGRESS_DONE, 100, "위임이 완료되었습니다.");
|
|
826
|
+
return result;
|
|
827
|
+
} catch (error) {
|
|
828
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
829
|
+
return createErrorPayload(message, {
|
|
830
|
+
mode: "sync",
|
|
831
|
+
providerRequested: "multi",
|
|
832
|
+
agentType: args.agentType,
|
|
833
|
+
transport: "headless-psmux",
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
794
838
|
const runViaRoute = this._shouldUseRoute(args);
|
|
795
839
|
|
|
796
840
|
try {
|
|
@@ -816,6 +860,90 @@ export class DelegatorMcpWorker {
|
|
|
816
860
|
}
|
|
817
861
|
}
|
|
818
862
|
|
|
863
|
+
async _executeMultiWorker(args, extra) {
|
|
864
|
+
await emitProgress(
|
|
865
|
+
extra,
|
|
866
|
+
DIRECT_PROGRESS_START,
|
|
867
|
+
100,
|
|
868
|
+
"멀티워커 psmux 병렬 실행을 시작합니다.",
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
// workers 배열을 headless assignments 형식으로 변환
|
|
872
|
+
const assignments = args.workers.map((w) => {
|
|
873
|
+
// provider를 CLI 이름으로 매핑 (codex/gemini 그대로)
|
|
874
|
+
const cli = w.provider;
|
|
875
|
+
const role = w.agentType || "executor";
|
|
876
|
+
// buildDirectPrompt와 동일한 context 처리
|
|
877
|
+
const prompt = withContext(String(w.prompt || ""), args.contextFile);
|
|
878
|
+
return { cli, prompt, role };
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// 타임아웃: 워커 중 가장 긴 타임아웃 사용
|
|
882
|
+
const maxTimeoutSec = Math.max(
|
|
883
|
+
...args.workers.map((w) =>
|
|
884
|
+
Math.ceil(resolveTimeoutMs(w.agentType || "executor", args.timeoutMs) / 1000),
|
|
885
|
+
),
|
|
886
|
+
300,
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
try {
|
|
890
|
+
const { results, sessionName } = await runHeadlessWithCleanup(assignments, {
|
|
891
|
+
sessionPrefix: "dlg",
|
|
892
|
+
timeoutSec: maxTimeoutSec,
|
|
893
|
+
layout: assignments.length <= 2 ? "even-horizontal" : "2x2",
|
|
894
|
+
progressive: true,
|
|
895
|
+
dashboard: true,
|
|
896
|
+
dashboardLayout: "single",
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
await emitProgress(
|
|
900
|
+
extra,
|
|
901
|
+
DIRECT_PROGRESS_DONE,
|
|
902
|
+
100,
|
|
903
|
+
`멀티워커 실행 완료: ${results.length}개 워커`,
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
// 전체 성공 판단: 모든 워커가 matched && exitCode === 0
|
|
907
|
+
const allOk = results.every((r) => r.matched && r.exitCode === 0);
|
|
908
|
+
// 개별 출력을 합산
|
|
909
|
+
const combinedOutput = results
|
|
910
|
+
.map(
|
|
911
|
+
(r, i) =>
|
|
912
|
+
`=== Worker ${i + 1} (${r.cli}/${assignments[i].role}) ===\n${r.output || "(no output)"}`,
|
|
913
|
+
)
|
|
914
|
+
.join("\n\n");
|
|
915
|
+
|
|
916
|
+
return {
|
|
917
|
+
ok: allOk,
|
|
918
|
+
mode: "sync",
|
|
919
|
+
status: allOk ? "completed" : "partial",
|
|
920
|
+
providerRequested: "multi",
|
|
921
|
+
providerResolved: "multi",
|
|
922
|
+
agentType: args.agentType || "executor",
|
|
923
|
+
transport: "headless-psmux",
|
|
924
|
+
exitCode: allOk ? 0 : 1,
|
|
925
|
+
output: combinedOutput,
|
|
926
|
+
workerResults: results.map((r) => ({
|
|
927
|
+
cli: r.cli,
|
|
928
|
+
role: r.role || "",
|
|
929
|
+
paneName: r.paneName,
|
|
930
|
+
matched: r.matched,
|
|
931
|
+
exitCode: r.exitCode,
|
|
932
|
+
output: r.output || "",
|
|
933
|
+
})),
|
|
934
|
+
sessionName,
|
|
935
|
+
};
|
|
936
|
+
} catch (error) {
|
|
937
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
938
|
+
return createErrorPayload(message, {
|
|
939
|
+
mode: "sync",
|
|
940
|
+
providerRequested: "multi",
|
|
941
|
+
agentType: args.agentType || "executor",
|
|
942
|
+
transport: "headless-psmux",
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
819
947
|
async _executeWorker(args, extra) {
|
|
820
948
|
await emitProgress(
|
|
821
949
|
extra,
|
package/hud/constants.mjs
CHANGED
|
@@ -8,20 +8,23 @@ export const VERSION = "2.0";
|
|
|
8
8
|
|
|
9
9
|
export const QOS_PATH = join(
|
|
10
10
|
homedir(),
|
|
11
|
-
".
|
|
12
|
-
"
|
|
11
|
+
".claude",
|
|
12
|
+
"cache",
|
|
13
|
+
"tfx-hub",
|
|
13
14
|
"cli_qos_profile.json",
|
|
14
15
|
);
|
|
15
16
|
export const ACCOUNTS_CONFIG_PATH = join(
|
|
16
17
|
homedir(),
|
|
17
|
-
".
|
|
18
|
-
"
|
|
18
|
+
".claude",
|
|
19
|
+
"cache",
|
|
20
|
+
"tfx-hub",
|
|
19
21
|
"accounts.json",
|
|
20
22
|
);
|
|
21
23
|
export const ACCOUNTS_STATE_PATH = join(
|
|
22
24
|
homedir(),
|
|
23
|
-
".
|
|
24
|
-
"
|
|
25
|
+
".claude",
|
|
26
|
+
"cache",
|
|
27
|
+
"tfx-hub",
|
|
25
28
|
"cli_accounts_state.json",
|
|
26
29
|
);
|
|
27
30
|
|
|
@@ -46,7 +49,13 @@ export const CONTEXT_MONITOR_LEGACY_PATH = join(
|
|
|
46
49
|
"state",
|
|
47
50
|
"context-monitor.json",
|
|
48
51
|
);
|
|
49
|
-
export const CONTEXT_MONITOR_LOG_DIR = join(
|
|
52
|
+
export const CONTEXT_MONITOR_LOG_DIR = join(
|
|
53
|
+
homedir(),
|
|
54
|
+
".claude",
|
|
55
|
+
"cache",
|
|
56
|
+
"tfx-hub",
|
|
57
|
+
"logs",
|
|
58
|
+
);
|
|
50
59
|
|
|
51
60
|
// 원격 프로브 캐시 (tfx-remote-spawn)
|
|
52
61
|
export const REMOTE_ENV_CACHE_DIR = join(
|
|
@@ -70,15 +79,17 @@ export const CLAUDE_USAGE_CACHE_PATH = join(
|
|
|
70
79
|
"cache",
|
|
71
80
|
"claude-usage-cache.json",
|
|
72
81
|
);
|
|
73
|
-
export const
|
|
82
|
+
export const PLUGIN_USAGE_CACHE_PATH = join(
|
|
74
83
|
homedir(),
|
|
75
84
|
".claude",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
85
|
+
"cache",
|
|
86
|
+
"tfx-hub",
|
|
87
|
+
"plugin-usage-cache.json",
|
|
79
88
|
);
|
|
80
|
-
export const
|
|
81
|
-
export const
|
|
89
|
+
export const OMC_PLUGIN_USAGE_CACHE_PATH = PLUGIN_USAGE_CACHE_PATH; // OMC alias
|
|
90
|
+
export const CLAUDE_USAGE_STALE_MS_SOLO = 5 * 60 * 1000; // 플러그인 없을 때: 5분 캐시
|
|
91
|
+
export const CLAUDE_USAGE_STALE_MS_WITH_PLUGIN = 15 * 60 * 1000; // 플러그인 있을 때: 15분
|
|
92
|
+
export const CLAUDE_USAGE_STALE_MS_WITH_OMC = CLAUDE_USAGE_STALE_MS_WITH_PLUGIN; // OMC alias
|
|
82
93
|
export const CLAUDE_USAGE_429_BACKOFF_MS = 10 * 60 * 1000; // 429 에러 시 10분 backoff
|
|
83
94
|
export const GEMINI_429_BASE_DELAY_MS = 2000;
|
|
84
95
|
export const GEMINI_429_MAX_RETRIES = 3;
|
package/hud/renderers.mjs
CHANGED
|
@@ -389,7 +389,8 @@ export function getClaudeRows(
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
if (currentTier === "minimal") {
|
|
392
|
-
const
|
|
392
|
+
const staleTag = claudeUsage?.stale ? ` ${dim("[stale]")}` : "";
|
|
393
|
+
const quotaSection = `${dim("5h:")}${fStr} ${dim("1w:")}${wStr}${staleTag}`;
|
|
393
394
|
const right = `${dim("CTX:")}${colorByPercent(ctxView.percent, ctxView.display)}`;
|
|
394
395
|
return [{ prefix, left: quotaSection, right }];
|
|
395
396
|
}
|