@triflux/remote 10.0.1 → 10.1.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/public/dashboard.html +7 -7
- package/hub/server.mjs +23 -0
- package/hub/store-adapter.mjs +51 -20
- package/hub/store.mjs +8 -8
- package/hub/team/cli/commands/start/start-headless.mjs +5 -3
- package/hub/team/cli/services/hub-client.mjs +1 -1
- package/hub/team/conductor.mjs +1 -1
- package/hub/team/dashboard-open.mjs +1 -1
- package/hub/team/headless.mjs +4 -4
- package/hub/team/orchestrator.mjs +2 -2
- package/hub/team/psmux.mjs +67 -5
- package/hub/team/remote-probe.mjs +1 -1
- package/hub/team/remote-watcher.mjs +1 -1
- package/hub/team/session.mjs +1 -1
- package/hub/team/swarm-hypervisor.mjs +3 -4
- package/hub/team/tui-remote-adapter.mjs +3 -3
- package/hub/team/tui-viewer.mjs +0 -1
- package/hub/team/tui.mjs +12 -15
- package/hub/team/worktree-lifecycle.mjs +1 -1
- package/hub/workers/delegator-mcp.mjs +1 -2
- package/package.json +1 -1
|
@@ -42,7 +42,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
42
42
|
.token-item .calls{font-size:.78rem;color:#8b949e;margin-top:2px}
|
|
43
43
|
|
|
44
44
|
.chart-wrap{position:relative;width:100%;height:160px}
|
|
45
|
-
.chart-wrap canvas{width:100
|
|
45
|
+
.chart-wrap canvas{width:100%;height:100%}
|
|
46
46
|
.chart-legend{display:flex;gap:14px;margin-top:8px;font-size:.78rem;color:#8b949e}
|
|
47
47
|
.legend-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px;vertical-align:middle}
|
|
48
48
|
|
|
@@ -152,7 +152,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
152
152
|
function remain(rt) {
|
|
153
153
|
if (!rt) return '';
|
|
154
154
|
var ts = typeof rt === 'number' && rt < 1e10 ? rt * 1000 : new Date(rt).getTime();
|
|
155
|
-
if (!isFinite(ts)) return '';
|
|
155
|
+
if (!Number.isFinite(ts)) return '';
|
|
156
156
|
var ms = ts - Date.now();
|
|
157
157
|
if (ms <= 0) return '만료됨';
|
|
158
158
|
var h = Math.floor(ms / 3600000);
|
|
@@ -199,7 +199,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
199
199
|
var s = '';
|
|
200
200
|
|
|
201
201
|
// Claude
|
|
202
|
-
var cl = d.claude
|
|
202
|
+
var cl = d.claude?.data;
|
|
203
203
|
if (cl) {
|
|
204
204
|
s += quotaBlock('Claude (c)', '#d19a66', [
|
|
205
205
|
['5시간', cl.fiveHourPercent || 0],
|
|
@@ -208,16 +208,16 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// Codex
|
|
211
|
-
var cx2 = d.codex
|
|
211
|
+
var cx2 = d.codex?.buckets?.codex;
|
|
212
212
|
if (cx2) {
|
|
213
213
|
s += quotaBlock('Codex (x)', '#ffffff', [
|
|
214
214
|
['Primary', cx2.primary ? cx2.primary.used_percent || 0 : 0],
|
|
215
215
|
['Secondary', cx2.secondary ? cx2.secondary.used_percent || 0 : 0]
|
|
216
|
-
], cx2.primary
|
|
216
|
+
], cx2.primary?.resets_at);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
// Gemini
|
|
220
|
-
var gm = d.gemini
|
|
220
|
+
var gm = d.gemini?.buckets;
|
|
221
221
|
if (gm && gm.length > 0) {
|
|
222
222
|
var frac = gm[0].remainingFraction;
|
|
223
223
|
s += quotaBlock('Gemini (g)', '#61afef', [
|
|
@@ -330,7 +330,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
330
330
|
gauge(data.aimd ? data.aimd.batchSize : 0);
|
|
331
331
|
renderQuota(data);
|
|
332
332
|
|
|
333
|
-
cachedEvents = (data.aimd
|
|
333
|
+
cachedEvents = (data.aimd?.events) || [];
|
|
334
334
|
drawEvents(cachedEvents);
|
|
335
335
|
|
|
336
336
|
renderTokens(data.accumulator);
|
package/hub/server.mjs
CHANGED
|
@@ -418,6 +418,29 @@ export async function startHub({
|
|
|
418
418
|
const adaptiveEngine = createAdaptiveEngine({ repoRoot: PROJECT_ROOT, fingerprintService });
|
|
419
419
|
adaptiveEngine.startSession();
|
|
420
420
|
|
|
421
|
+
// safety-guard → reflexion 패널티 승격 + 적응형 규칙 유지보수
|
|
422
|
+
const projectSlug = PROJECT_ROOT.split(/[\\/]/).pop();
|
|
423
|
+
try {
|
|
424
|
+
const { promotePenalties } = await import("./promote-penalties.mjs");
|
|
425
|
+
const result = promotePenalties(store, { projectSlug });
|
|
426
|
+
if (result.promoted > 0) {
|
|
427
|
+
console.log(`[reflexion] ${result.promoted} penalties promoted to adaptive rules`);
|
|
428
|
+
}
|
|
429
|
+
} catch { /* promote-penalties 실패는 Hub 시작을 막지 않음 */ }
|
|
430
|
+
|
|
431
|
+
// stale adaptive_rules 정리 (30일 초과 + confidence 0.2 미만)
|
|
432
|
+
try {
|
|
433
|
+
const pruned = store.pruneStaleRules();
|
|
434
|
+
if (pruned > 0) console.log(`[reflexion] ${pruned} stale adaptive rules pruned`);
|
|
435
|
+
} catch { /* prune 실패 무시 */ }
|
|
436
|
+
|
|
437
|
+
// adaptive rule confidence decay (7일 이상 미관측 규칙 -0.1 감소)
|
|
438
|
+
try {
|
|
439
|
+
const { decayRules } = await import("./reflexion.mjs");
|
|
440
|
+
const decay = decayRules(store, adaptiveEngine.sessionCount?.() || 1, projectSlug);
|
|
441
|
+
if (decay.deleted.length > 0) console.log(`[reflexion] ${decay.deleted.length} low-confidence rules removed`);
|
|
442
|
+
} catch { /* decay 실패 무시 */ }
|
|
443
|
+
|
|
421
444
|
// Delegator MCP resident service 초기화
|
|
422
445
|
const delegatorWorker = createDelegatorWorker({ cwd: PROJECT_ROOT });
|
|
423
446
|
try {
|
package/hub/store-adapter.mjs
CHANGED
|
@@ -15,23 +15,35 @@ export { createMemoryStore };
|
|
|
15
15
|
function ensureAdaptiveRulesSchema(db) {
|
|
16
16
|
const schemaKey = 'adaptive_rules_schema_version';
|
|
17
17
|
const currentVersion = db.prepare('SELECT value FROM _meta WHERE key = ?').pluck().get(schemaKey);
|
|
18
|
-
if (currentVersion === '
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
if (currentVersion === '2') return;
|
|
19
|
+
if (!currentVersion) {
|
|
20
|
+
// 신규 생성
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS adaptive_rules (
|
|
23
|
+
project_slug TEXT NOT NULL,
|
|
24
|
+
pattern TEXT NOT NULL,
|
|
25
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
26
|
+
hit_count INTEGER NOT NULL DEFAULT 1,
|
|
27
|
+
last_seen_ms INTEGER NOT NULL,
|
|
28
|
+
created_ms INTEGER NOT NULL,
|
|
29
|
+
error_message TEXT,
|
|
30
|
+
solution TEXT,
|
|
31
|
+
context TEXT,
|
|
32
|
+
PRIMARY KEY (project_slug, pattern)
|
|
33
|
+
);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_adaptive_rules_last_seen
|
|
35
|
+
ON adaptive_rules(project_slug, last_seen_ms DESC);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_adaptive_rules_confidence
|
|
37
|
+
ON adaptive_rules(project_slug, confidence DESC);
|
|
38
|
+
`);
|
|
39
|
+
} else {
|
|
40
|
+
// v1 → v2 마이그레이션: 컬럼 추가
|
|
41
|
+
const cols = db.pragma('table_info(adaptive_rules)').map(c => c.name);
|
|
42
|
+
if (!cols.includes('error_message')) db.exec('ALTER TABLE adaptive_rules ADD COLUMN error_message TEXT');
|
|
43
|
+
if (!cols.includes('solution')) db.exec('ALTER TABLE adaptive_rules ADD COLUMN solution TEXT');
|
|
44
|
+
if (!cols.includes('context')) db.exec('ALTER TABLE adaptive_rules ADD COLUMN context TEXT');
|
|
45
|
+
}
|
|
46
|
+
db.prepare('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)').run(schemaKey, '2');
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
function attachAdaptiveRuleStore(store) {
|
|
@@ -40,15 +52,20 @@ function attachAdaptiveRuleStore(store) {
|
|
|
40
52
|
const statements = {
|
|
41
53
|
upsertRule: store.db.prepare(`
|
|
42
54
|
INSERT INTO adaptive_rules (
|
|
43
|
-
project_slug, pattern, confidence, hit_count, last_seen_ms, created_ms
|
|
55
|
+
project_slug, pattern, confidence, hit_count, last_seen_ms, created_ms,
|
|
56
|
+
error_message, solution, context
|
|
44
57
|
) VALUES (
|
|
45
|
-
@project_slug, @pattern, @confidence, @hit_count, @last_seen_ms, @created_ms
|
|
58
|
+
@project_slug, @pattern, @confidence, @hit_count, @last_seen_ms, @created_ms,
|
|
59
|
+
@error_message, @solution, @context
|
|
46
60
|
)
|
|
47
61
|
ON CONFLICT(project_slug, pattern) DO UPDATE SET
|
|
48
62
|
confidence = excluded.confidence,
|
|
49
63
|
hit_count = excluded.hit_count,
|
|
50
64
|
last_seen_ms = excluded.last_seen_ms,
|
|
51
|
-
created_ms = MIN(adaptive_rules.created_ms, excluded.created_ms)
|
|
65
|
+
created_ms = MIN(adaptive_rules.created_ms, excluded.created_ms),
|
|
66
|
+
error_message = COALESCE(excluded.error_message, adaptive_rules.error_message),
|
|
67
|
+
solution = COALESCE(excluded.solution, adaptive_rules.solution),
|
|
68
|
+
context = COALESCE(excluded.context, adaptive_rules.context)`),
|
|
52
69
|
getRule: store.db.prepare('SELECT * FROM adaptive_rules WHERE project_slug = ? AND pattern = ?'),
|
|
53
70
|
updateRule: store.db.prepare(`
|
|
54
71
|
UPDATE adaptive_rules SET
|
|
@@ -93,6 +110,20 @@ function attachAdaptiveRuleStore(store) {
|
|
|
93
110
|
return statements.pruneRules.run(cutoff, minConfidence).changes;
|
|
94
111
|
};
|
|
95
112
|
|
|
113
|
+
// listAdaptiveRules: decayRules/getActiveAdaptiveRules에서 사용
|
|
114
|
+
const listStmt = store.db.prepare('SELECT * FROM adaptive_rules WHERE project_slug = ? ORDER BY confidence DESC');
|
|
115
|
+
const listAllStmt = store.db.prepare('SELECT * FROM adaptive_rules ORDER BY confidence DESC');
|
|
116
|
+
const deleteRuleStmt = store.db.prepare('DELETE FROM adaptive_rules WHERE project_slug = ? AND pattern = ?');
|
|
117
|
+
|
|
118
|
+
store.listAdaptiveRules = function listAdaptiveRules(projectSlug) {
|
|
119
|
+
if (projectSlug) return listStmt.all(projectSlug).map(clone);
|
|
120
|
+
return listAllStmt.all().map(clone);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
store.deleteAdaptiveRule = function deleteAdaptiveRule(projectSlug, pattern) {
|
|
124
|
+
return deleteRuleStmt.run(projectSlug, pattern).changes > 0;
|
|
125
|
+
};
|
|
126
|
+
|
|
96
127
|
return store;
|
|
97
128
|
}
|
|
98
129
|
|
package/hub/store.mjs
CHANGED
|
@@ -551,7 +551,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
551
551
|
ttl_ms: clampDuration(patch.ttl_ms ?? current.ttl_ms, current.ttl_ms || nextTimeout),
|
|
552
552
|
timeout_ms: nextTimeout,
|
|
553
553
|
deadline_ms: (() => {
|
|
554
|
-
if (Object.
|
|
554
|
+
if (Object.hasOwn(patch, 'deadline_ms')) {
|
|
555
555
|
return patch.deadline_ms == null ? null : Math.trunc(Number(patch.deadline_ms));
|
|
556
556
|
}
|
|
557
557
|
if (isTerminal) return null;
|
|
@@ -560,23 +560,23 @@ export function createStore(dbPath, options = {}) {
|
|
|
560
560
|
})(),
|
|
561
561
|
trace_id: patch.trace_id ?? current.trace_id,
|
|
562
562
|
correlation_id: patch.correlation_id ?? current.correlation_id,
|
|
563
|
-
last_message_id: Object.
|
|
563
|
+
last_message_id: Object.hasOwn(patch, 'last_message_id')
|
|
564
564
|
? patch.last_message_id
|
|
565
565
|
: current.last_message_id,
|
|
566
|
-
result_json: Object.
|
|
566
|
+
result_json: Object.hasOwn(patch, 'result')
|
|
567
567
|
? (patch.result == null ? null : JSON.stringify(patch.result))
|
|
568
568
|
: (current.result == null ? null : JSON.stringify(current.result)),
|
|
569
|
-
error_json: Object.
|
|
569
|
+
error_json: Object.hasOwn(patch, 'error')
|
|
570
570
|
? (patch.error == null ? null : JSON.stringify(patch.error))
|
|
571
571
|
: (current.error == null ? null : JSON.stringify(current.error)),
|
|
572
572
|
updated_at_ms: now,
|
|
573
|
-
started_at_ms: Object.
|
|
573
|
+
started_at_ms: Object.hasOwn(patch, 'started_at_ms')
|
|
574
574
|
? patch.started_at_ms
|
|
575
575
|
: (nextStatus === 'running' ? (current.started_at_ms || now) : current.started_at_ms),
|
|
576
|
-
completed_at_ms: Object.
|
|
576
|
+
completed_at_ms: Object.hasOwn(patch, 'completed_at_ms')
|
|
577
577
|
? patch.completed_at_ms
|
|
578
578
|
: (isTerminal ? (current.completed_at_ms || now) : current.completed_at_ms),
|
|
579
|
-
last_retry_at_ms: Object.
|
|
579
|
+
last_retry_at_ms: Object.hasOwn(patch, 'last_retry_at_ms')
|
|
580
580
|
? patch.last_retry_at_ms
|
|
581
581
|
: current.last_retry_at_ms,
|
|
582
582
|
};
|
|
@@ -661,7 +661,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
661
661
|
started_at_ms: null,
|
|
662
662
|
last_retry_at_ms: Date.now(),
|
|
663
663
|
result: patch.result ?? null,
|
|
664
|
-
error: Object.
|
|
664
|
+
error: Object.hasOwn(patch, 'error') ? patch.error : current.error,
|
|
665
665
|
last_message_id: null,
|
|
666
666
|
});
|
|
667
667
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DIM, GREEN, RESET, AMBER } from "../../../shared.mjs";
|
|
2
2
|
import { runHeadlessInteractive, resolveCliType } from "../../../headless.mjs";
|
|
3
|
-
import { ok,
|
|
3
|
+
import { ok, } from "../../render.mjs";
|
|
4
4
|
import { buildTasks } from "../../services/task-model.mjs";
|
|
5
5
|
import { clearTeamState } from "../../services/state-store.mjs";
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
|
|
|
10
10
|
? assigns.map((a, i) => ({ cli: resolveCliType(a.cli), prompt: a.prompt, role: a.role || `worker-${i + 1}`, mcp: mcpProfile, model }))
|
|
11
11
|
: subtasks.map((subtask, i) => ({ cli: resolveCliType(agents[i] || agents[0]), prompt: subtask, role: `worker-${i + 1}`, mcp: mcpProfile, model }));
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const _startedAt = Date.now();
|
|
14
14
|
ok(`headless ${assignments.length}워커 시작`);
|
|
15
15
|
|
|
16
16
|
const handle = await runHeadlessInteractive(sessionId, assignments, {
|
|
@@ -46,6 +46,8 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
|
|
|
46
46
|
const failed = results.filter((r) => !r.matched || r.exitCode !== 0);
|
|
47
47
|
|
|
48
48
|
ok(`헤드리스 완료: ${succeeded.length}성공 / ${failed.length}실패 / ${results.length}전체`);
|
|
49
|
+
// 머신 파싱용 완료 마커 — background task에서 결과 회수 시점 판단에 사용
|
|
50
|
+
console.log(`=== HEADLESS_COMPLETE succeeded=${succeeded.length} failed=${failed.length} total=${results.length} ===`);
|
|
49
51
|
|
|
50
52
|
if (failed.length > 0) {
|
|
51
53
|
for (const r of failed) console.log(` ${AMBER}✗${RESET} ${r.paneName} (${r.cli}) exit=${r.exitCode}`);
|
|
@@ -154,7 +154,7 @@ export async function ensureHubAlive(maxRetries = 3) {
|
|
|
154
154
|
}
|
|
155
155
|
// 다음 재시도 전 대기: 1초, 2초, 4초 (마지막 시도 후에는 대기 없음)
|
|
156
156
|
if (i < maxRetries - 1) {
|
|
157
|
-
const backoffMs =
|
|
157
|
+
const backoffMs = 2 ** i * 1000; // i=0: 1초, i=1: 2초, i=2: 4초
|
|
158
158
|
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
159
159
|
}
|
|
160
160
|
}
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { homedir } from 'node:os';
|
|
|
14
14
|
import { mkdirSync, createWriteStream, readFileSync, copyFileSync } from 'node:fs';
|
|
15
15
|
import { EventEmitter } from 'node:events';
|
|
16
16
|
|
|
17
|
-
import { killProcess,
|
|
17
|
+
import { killProcess, } from '@triflux/core/hub/platform.mjs';
|
|
18
18
|
import { createEventLog } from './event-log.mjs';
|
|
19
19
|
import { createHealthProbe } from './health-probe.mjs';
|
|
20
20
|
import { createRemoteProbe } from './remote-probe.mjs';
|
|
@@ -15,7 +15,7 @@ function sanitizeWindowTitle(value, fallback = "triflux") {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function sanitizeSessionName(value) {
|
|
18
|
-
return String(value || "").replace(/[^a-zA-Z0-9_
|
|
18
|
+
return String(value || "").replace(/[^a-zA-Z0-9_-]/g, "") || "tfx-session";
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function sanitizeWorkingDirectory(value) {
|
package/hub/team/headless.mjs
CHANGED
|
@@ -33,8 +33,8 @@ const CLI_BRAND = {
|
|
|
33
33
|
gemini: { emoji: "\u{1F535}", label: "Gemini", ansi: "\x1b[38;5;39m" }, // 🔵 geminiBlue
|
|
34
34
|
claude: { emoji: "\u{1F7E0}", label: "Claude", ansi: "\x1b[38;2;232;112;64m" }, // 🟠 claudeOrange
|
|
35
35
|
};
|
|
36
|
-
const
|
|
37
|
-
const
|
|
36
|
+
const _ANSI_RESET = "\x1b[0m";
|
|
37
|
+
const _ANSI_DIM = "\x1b[2m";
|
|
38
38
|
|
|
39
39
|
/** 에이전트 역할명 → CLI 타입 매핑 (단일 소스: agent-map.json) */
|
|
40
40
|
const _require = createRequire(import.meta.url);
|
|
@@ -478,7 +478,7 @@ async function awaitAll(sessionName, dispatches, timeoutSec, safeProgress, progr
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
let completion;
|
|
481
|
-
if (stallOpts
|
|
481
|
+
if (stallOpts?.enabled) {
|
|
482
482
|
// 하이브리드 stall detection 모드
|
|
483
483
|
try {
|
|
484
484
|
const stallPollCb = safeProgress && progressIntervalSec > 0
|
|
@@ -809,7 +809,7 @@ function atomicWriteSync(filePath, data) {
|
|
|
809
809
|
}
|
|
810
810
|
|
|
811
811
|
function sanitizeSessionName(value) {
|
|
812
|
-
return String(value || "").replace(/[^a-zA-Z0-9_
|
|
812
|
+
return String(value || "").replace(/[^a-zA-Z0-9_-]/g, "") || "tfx-session";
|
|
813
813
|
}
|
|
814
814
|
|
|
815
815
|
function sanitizeWindowTitle(value, fallback = "triflux") {
|
|
@@ -50,7 +50,7 @@ export function decomposeTask(taskDescription, agentCount) {
|
|
|
50
50
|
* @returns {string}
|
|
51
51
|
*/
|
|
52
52
|
export function buildLeadPrompt(taskDescription, config) {
|
|
53
|
-
const { agentId,
|
|
53
|
+
const { agentId, teammateMode = "tmux", workers = [] } = config;
|
|
54
54
|
|
|
55
55
|
const roster = workers
|
|
56
56
|
.map((w, i) => `${i + 1}. ${w.agentId} (${w.cli}) — ${w.subtask}`)
|
|
@@ -92,7 +92,7 @@ ${roster}
|
|
|
92
92
|
export function buildPrompt(subtask, config) {
|
|
93
93
|
const { cli, agentId, hubUrl } = config;
|
|
94
94
|
|
|
95
|
-
const
|
|
95
|
+
const _hubBase = hubUrl.replace("/mcp", "");
|
|
96
96
|
|
|
97
97
|
const bridgePath = "node hub/bridge.mjs";
|
|
98
98
|
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -196,7 +196,12 @@ function ensureCaptureHelper() {
|
|
|
196
196
|
"}",
|
|
197
197
|
"",
|
|
198
198
|
"# Force UTF-8 encoding — CP949 등 non-UTF-8 codepage에서 Gemini stdout 캡처 실패 방지",
|
|
199
|
+
"# InputEncoding: pipe-pane에서 읽어들이는 바이트 해석",
|
|
200
|
+
"# OutputEncoding: 네이티브 명령(gemini/codex CLI)의 stdout 디코딩",
|
|
201
|
+
"# $OutputEncoding: PowerShell 파이프라인 기본 인코딩 (BOM 없는 UTF-8)",
|
|
199
202
|
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8",
|
|
203
|
+
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
204
|
+
"$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
|
|
200
205
|
"",
|
|
201
206
|
"$reader = [Console]::In",
|
|
202
207
|
"while (($line = $reader.ReadLine()) -ne $null) {",
|
|
@@ -514,6 +519,23 @@ export function createPsmuxSession(sessionName, opts = {}) {
|
|
|
514
519
|
psmuxExec(["select-pane", "-t", pane, "-T", toPaneTitle(index)]);
|
|
515
520
|
});
|
|
516
521
|
|
|
522
|
+
// CP949 등 non-UTF-8 codepage 환경에서 CLI stdout 깨짐 방지:
|
|
523
|
+
// pane 생성 직후 UTF-8 인코딩을 강제 설정한다.
|
|
524
|
+
// chcp 65001: 프로세스 코드 페이지를 UTF-8로 전환
|
|
525
|
+
// OutputEncoding: 네이티브 명령(gemini/codex) stdout 디코딩
|
|
526
|
+
// $OutputEncoding: PowerShell 파이프라인 기본 인코딩
|
|
527
|
+
if (IS_WINDOWS) {
|
|
528
|
+
const encodingInit = [
|
|
529
|
+
"chcp 65001 > $null",
|
|
530
|
+
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8",
|
|
531
|
+
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
532
|
+
"$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
|
|
533
|
+
].join("; ");
|
|
534
|
+
panes.forEach((paneId) => {
|
|
535
|
+
sendLiteralToPane(paneId, encodingInit, true);
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
517
539
|
return { sessionName, panes };
|
|
518
540
|
}
|
|
519
541
|
|
|
@@ -857,15 +879,55 @@ export function startCapture(sessionName, paneNameOrTarget) {
|
|
|
857
879
|
* @param {string} cmd
|
|
858
880
|
* @returns {string}
|
|
859
881
|
*/
|
|
882
|
+
// Windows: PowerShell의 bash가 WSL을 가리킬 수 있음. Git Bash를 명시적으로 사용.
|
|
883
|
+
// WSL bash에서는 /c/Users/... 경로가 유효하지 않아 exit 127 발생.
|
|
884
|
+
// psmux pane 용 (PowerShell → bash 실행): & "path" 형식
|
|
885
|
+
// Node.js execSync 용 (직접 실행): "path" 형식
|
|
886
|
+
const _gitBashPath = (() => {
|
|
887
|
+
if (!IS_WINDOWS) return null;
|
|
888
|
+
const candidates = [
|
|
889
|
+
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
890
|
+
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
|
|
891
|
+
];
|
|
892
|
+
for (const p of candidates) {
|
|
893
|
+
if (existsSync(p)) return p;
|
|
894
|
+
}
|
|
895
|
+
return null;
|
|
896
|
+
})();
|
|
897
|
+
// PowerShell call operator(&)가 필요: & "C:\...\bash.exe" -c '...'
|
|
898
|
+
const GIT_BASH_BIN_PS = _gitBashPath ? `& "${_gitBashPath}"` : "bash";
|
|
899
|
+
// Node.js execSync에서 직접 실행할 때는 따옴표만
|
|
900
|
+
const GIT_BASH_BIN_EXEC = _gitBashPath ? `"${_gitBashPath}"` : "bash";
|
|
901
|
+
|
|
902
|
+
// CLI 바이너리 절대 경로 캐시 (세션 중 1회만 resolve)
|
|
903
|
+
const _cliPathCache = new Map();
|
|
904
|
+
function resolveCliAbsPath(name) {
|
|
905
|
+
if (_cliPathCache.has(name)) return _cliPathCache.get(name);
|
|
906
|
+
try {
|
|
907
|
+
const resolved = childProcess
|
|
908
|
+
.execSync(`${GIT_BASH_BIN_EXEC} -c "which ${name}"`, { encoding: "utf8", timeout: 3000 })
|
|
909
|
+
.trim();
|
|
910
|
+
if (resolved) _cliPathCache.set(name, resolved);
|
|
911
|
+
return resolved || name;
|
|
912
|
+
} catch {
|
|
913
|
+
return name;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
860
917
|
function wrapCliForBash(cmd) {
|
|
861
918
|
const trimmed = cmd.trimStart();
|
|
862
919
|
// PowerShell 구문(Clear-Host, Get-Content 등) 또는 completion token이 포함되면 PowerShell 직통
|
|
863
920
|
if (/Clear-Host|Get-Content|__TRIFLUX_DONE__/i.test(trimmed)) return cmd;
|
|
864
|
-
const
|
|
865
|
-
if (!
|
|
921
|
+
const cliMatch = trimmed.match(/^(codex|gemini)\b/u);
|
|
922
|
+
if (!cliMatch) return cmd;
|
|
923
|
+
// Node.js 측에서 절대 경로 resolve → psmux pane의 bash PATH 불일치 문제 해결 (exit 127)
|
|
924
|
+
const absPath = resolveCliAbsPath(cliMatch[1]);
|
|
925
|
+
const resolved = absPath !== cliMatch[1]
|
|
926
|
+
? trimmed.replace(new RegExp(`^${cliMatch[1]}\\b`), absPath)
|
|
927
|
+
: trimmed;
|
|
866
928
|
// 단일 따옴표 이스케이프: ' → '\''
|
|
867
|
-
const escaped =
|
|
868
|
-
return
|
|
929
|
+
const escaped = resolved.replace(/'/g, "'\\''");
|
|
930
|
+
return `${GIT_BASH_BIN_PS} -c '${escaped}'`;
|
|
869
931
|
}
|
|
870
932
|
|
|
871
933
|
export function dispatchCommand(sessionName, paneNameOrTarget, commandText) {
|
|
@@ -1230,7 +1292,7 @@ export function captureWorkerOutput(sessionName, workerName, lines = 50) {
|
|
|
1230
1292
|
|
|
1231
1293
|
// ─── CLI 진입점 ───
|
|
1232
1294
|
|
|
1233
|
-
if (process.argv[1]
|
|
1295
|
+
if (process.argv[1]?.endsWith("psmux.mjs")) {
|
|
1234
1296
|
(async () => {
|
|
1235
1297
|
const [, , cmd, ...args] = process.argv;
|
|
1236
1298
|
|
|
@@ -14,7 +14,7 @@ import { detectInputWait, PROBE_DEFAULTS } from './health-probe.mjs';
|
|
|
14
14
|
const COMPLETION_TOKEN_RE = /__TRIFLUX_DONE__/;
|
|
15
15
|
|
|
16
16
|
/** 프롬프트 idle 패턴 (Claude Code 프롬프트 복귀) */
|
|
17
|
-
const
|
|
17
|
+
const _PROMPT_IDLE_RE = /(\u276f|\u2795|>\s*$)/;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* SSH 경유로 원격 psmux capture-pane 실행.
|
|
@@ -40,7 +40,7 @@ const PROMPT_IDLE_PATTERNS = Object.freeze([
|
|
|
40
40
|
]);
|
|
41
41
|
|
|
42
42
|
/** @deprecated shellQuoteForHost(value, os) 사용 권장 — OS-aware 쿼팅 */
|
|
43
|
-
function
|
|
43
|
+
function _shellQuote(value, os) {
|
|
44
44
|
if (os) return shellQuoteForHost(value, os);
|
|
45
45
|
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
46
46
|
}
|
package/hub/team/session.mjs
CHANGED
|
@@ -481,7 +481,7 @@ export function configureTeammateKeybindings(sessionName, opts = {}) {
|
|
|
481
481
|
if (taskListCommand) {
|
|
482
482
|
const escaped = taskListCommand.replace(/'/g, "'\\''");
|
|
483
483
|
try {
|
|
484
|
-
tmux(`bind-key -T root -n C-t if-shell -F '${cond}'
|
|
484
|
+
tmux(`bind-key -T root -n C-t if-shell -F '${cond}' "display-popup -E '${escaped}'" "send-keys C-t"`);
|
|
485
485
|
} catch {
|
|
486
486
|
tmux(`bind-key -T root -n C-t if-shell -F '${cond}' 'display-message "tfx multi tasks 명령으로 태스크 확인"' 'send-keys C-t'`);
|
|
487
487
|
}
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
import { EventEmitter } from 'node:events';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { mkdirSync, readFileSync, existsSync } from 'node:fs';
|
|
16
|
-
import { execSync } from 'node:child_process';
|
|
17
16
|
|
|
18
17
|
import { createConductor, STATES } from './conductor.mjs';
|
|
19
18
|
import { createSwarmLocks } from './swarm-locks.mjs';
|
|
@@ -68,9 +67,9 @@ export function createSwarmHypervisor(opts) {
|
|
|
68
67
|
logsDir,
|
|
69
68
|
maxRestarts = 2,
|
|
70
69
|
graceMs = 10_000,
|
|
71
|
-
|
|
70
|
+
_integrationTimeoutMs = 60_000,
|
|
72
71
|
probeOpts = {},
|
|
73
|
-
|
|
72
|
+
_deps = {},
|
|
74
73
|
} = opts;
|
|
75
74
|
|
|
76
75
|
if (!workdir) throw new Error('workdir is required');
|
|
@@ -415,7 +414,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
415
414
|
*/
|
|
416
415
|
function detectChangedFiles(shardName, worker) {
|
|
417
416
|
// Best-effort: parse output log for file paths
|
|
418
|
-
const
|
|
417
|
+
const _outPath = join(logsDir, shardName);
|
|
419
418
|
try {
|
|
420
419
|
const snap = worker.conductor.getSnapshot();
|
|
421
420
|
for (const session of snap) {
|
|
@@ -57,7 +57,7 @@ function resolveSshUser(hostsData, host) {
|
|
|
57
57
|
* @returns {string|null}
|
|
58
58
|
*/
|
|
59
59
|
function resolveHostFromSessionName(sessionName) {
|
|
60
|
-
if (!sessionName
|
|
60
|
+
if (!sessionName?.startsWith(SESSION_PREFIX)) return null;
|
|
61
61
|
const rest = sessionName.slice(SESSION_PREFIX.length);
|
|
62
62
|
const dashIdx = rest.indexOf("-");
|
|
63
63
|
if (dashIdx === -1) return rest || null;
|
|
@@ -162,7 +162,7 @@ export function createRemoteAdapter(opts = {}) {
|
|
|
162
162
|
const loadHosts = deps.loadHostsJson || loadHostsJson;
|
|
163
163
|
const setIntervalFn = deps.setInterval || setInterval;
|
|
164
164
|
const clearIntervalFn = deps.clearInterval || clearInterval;
|
|
165
|
-
const
|
|
165
|
+
const _nowFn = deps.now || Date.now;
|
|
166
166
|
|
|
167
167
|
const emitter = new EventEmitter();
|
|
168
168
|
let hostsData = loadHosts(hostsJsonPath);
|
|
@@ -175,7 +175,7 @@ export function createRemoteAdapter(opts = {}) {
|
|
|
175
175
|
function handleStateChange({ sessionId, from, to, reason }) {
|
|
176
176
|
const snapshots = conductor.getSnapshot();
|
|
177
177
|
const entry = snapshots.find((s) => s.id === sessionId);
|
|
178
|
-
if (!entry
|
|
178
|
+
if (!entry?.remote) return;
|
|
179
179
|
|
|
180
180
|
const watcherStatus = getWatcherSession(sessionId);
|
|
181
181
|
const workerData = buildWorkerData(entry, watcherStatus, hostsData);
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -11,7 +11,6 @@ import { createLogDashboard } from "./tui.mjs";
|
|
|
11
11
|
import { createLiteDashboard } from "./tui-lite.mjs";
|
|
12
12
|
import { openHeadlessDashboardTarget } from "./dashboard-open.mjs";
|
|
13
13
|
import { processHandoff } from "./handoff.mjs";
|
|
14
|
-
import { statusBadge } from "./ansi.mjs";
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
// ── CLI 인자 파싱 ──
|
package/hub/team/tui.mjs
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import {
|
|
6
6
|
RESET,
|
|
7
7
|
FG,
|
|
8
|
-
BG,
|
|
9
8
|
MOCHA,
|
|
10
9
|
color,
|
|
11
10
|
dim,
|
|
@@ -18,7 +17,6 @@ import {
|
|
|
18
17
|
wcswidth,
|
|
19
18
|
progressBar,
|
|
20
19
|
statusBadge,
|
|
21
|
-
STATUS_ICON,
|
|
22
20
|
altScreenOn,
|
|
23
21
|
altScreenOff,
|
|
24
22
|
clearScreen,
|
|
@@ -27,7 +25,6 @@ import {
|
|
|
27
25
|
cursorShow,
|
|
28
26
|
moveTo,
|
|
29
27
|
clearLine,
|
|
30
|
-
clearToEnd,
|
|
31
28
|
} from "./ansi.mjs";
|
|
32
29
|
|
|
33
30
|
import { execFile as _execFile } from "node:child_process";
|
|
@@ -52,7 +49,7 @@ const SPINNER_FRAMES = [...SPINNER_FRAMES_RAW, ...[...SPINNER_FRAMES_RAW].revers
|
|
|
52
49
|
const SPINNER_CYCLE_MS = 2000;
|
|
53
50
|
const SPINNER_BASE_COLOR = { r: 203, g: 166, b: 247 }; // Catppuccin Mocha mauve
|
|
54
51
|
const SPINNER_SHIMMER = { r: 171, g: 43, b: 63 }; // Claude shimmer #ab2b3f
|
|
55
|
-
|
|
52
|
+
const spinnerStart = Date.now();
|
|
56
53
|
let spinnerTick = 0;
|
|
57
54
|
|
|
58
55
|
function lerpRgb(a, b, t) {
|
|
@@ -119,7 +116,7 @@ function activityWave(tick, count = 4) {
|
|
|
119
116
|
const GRID_GAP = 2;
|
|
120
117
|
const DEFAULT_DETAIL_LINES = 10;
|
|
121
118
|
// Tier1 상단 고정 행 수
|
|
122
|
-
const
|
|
119
|
+
const _TIER1_ROWS = 2;
|
|
123
120
|
|
|
124
121
|
const SUMMARY_KEYS = [
|
|
125
122
|
"status", "lead_action", "verdict", "files_changed",
|
|
@@ -128,14 +125,14 @@ const SUMMARY_KEYS = [
|
|
|
128
125
|
|
|
129
126
|
// ── 레이아웃 브레이크포인트 ──────────────────────────────────────────────
|
|
130
127
|
// 80-119: 28col rail, 120-159: 36col rail, 160+: 균등
|
|
131
|
-
function
|
|
128
|
+
function _resolveRailWidth(totalCols, columnCount) {
|
|
132
129
|
if (columnCount <= 1) return totalCols;
|
|
133
130
|
if (totalCols >= 160) return Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount);
|
|
134
131
|
if (totalCols >= 120) return Math.min(36, Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount));
|
|
135
132
|
return Math.min(28, Math.floor((totalCols - GRID_GAP * (columnCount - 1)) / columnCount));
|
|
136
133
|
}
|
|
137
134
|
|
|
138
|
-
function
|
|
135
|
+
function _autoColumnCount(totalCols, workerCount) {
|
|
139
136
|
if (workerCount <= 1) return 1;
|
|
140
137
|
if (totalCols >= 160) return Math.min(workerCount, 3);
|
|
141
138
|
if (totalCols >= 120) return Math.min(workerCount, 2);
|
|
@@ -154,7 +151,7 @@ function stripCodeBlocks(text) {
|
|
|
154
151
|
.replace(/```[\s\S]*?(?:```|$)/g, "\n")
|
|
155
152
|
.replace(/^\s*```.*$/gm, "")
|
|
156
153
|
// indented code blocks (4+ spaces or tab at line start)
|
|
157
|
-
.replace(/^(?:
|
|
154
|
+
.replace(/^(?: {4}|\t).+$/gm, "")
|
|
158
155
|
// shell prompts: PS C:\...>, >, $
|
|
159
156
|
.replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "")
|
|
160
157
|
.trim();
|
|
@@ -275,7 +272,7 @@ function gradientBorderFn(topRgb, bottomRgb) {
|
|
|
275
272
|
}
|
|
276
273
|
|
|
277
274
|
// Effect 3: Flash-fade border — 상태 변경 시 백색 플래시 → 페이드아웃
|
|
278
|
-
function
|
|
275
|
+
function _flashFadeBorderColor(currentStatus, prevStatus, changedAt) {
|
|
279
276
|
const elapsed = Date.now() - (changedAt || 0);
|
|
280
277
|
if (elapsed >= FADE_DURATION_MS || !prevStatus) return null;
|
|
281
278
|
const statusRgb = statusToRgb(currentStatus);
|
|
@@ -319,11 +316,11 @@ function dedupeRole(role, name, cli) {
|
|
|
319
316
|
r = r.replace(new RegExp(esc(cli), "gi"), "").trim();
|
|
320
317
|
r = r.replace(new RegExp(esc(name), "gi"), "").trim();
|
|
321
318
|
// CLI indicator emojis 제거
|
|
322
|
-
r = r.replace(/[⚪⚫🔴🟠🟡🟢🔵🟣🟤⭕🔘]/
|
|
319
|
+
r = r.replace(/[⚪⚫🔴🟠🟡🟢🔵🟣🟤⭕🔘]/gu, "").trim();
|
|
323
320
|
// 빈 괄호 제거 + 중첩 괄호 정리
|
|
324
321
|
r = r.replace(/\(\s*\)/g, "").trim();
|
|
325
322
|
r = r.replace(/^\(([^()]+)\)$/, "$1").trim();
|
|
326
|
-
r = r.replace(/^\s*[
|
|
323
|
+
r = r.replace(/^\s*[•·-]\s*/, "").trim();
|
|
327
324
|
return r;
|
|
328
325
|
}
|
|
329
326
|
|
|
@@ -350,7 +347,7 @@ function wrapLine(text, width) {
|
|
|
350
347
|
return lines.length > 0 ? lines : [source.slice(0, limit)];
|
|
351
348
|
}
|
|
352
349
|
|
|
353
|
-
function
|
|
350
|
+
function _wrapText(text, width, maxLines = DEFAULT_DETAIL_LINES, rawMode = false) {
|
|
354
351
|
if (maxLines <= 0) return [];
|
|
355
352
|
const input = sanitizeTextBlock(text, rawMode);
|
|
356
353
|
if (!input) return [];
|
|
@@ -465,7 +462,7 @@ function buildWorkerRail(name, st, opts = {}) {
|
|
|
465
462
|
selected = false,
|
|
466
463
|
focused = false, // rail 포커스 여부
|
|
467
464
|
previousSelected = false,
|
|
468
|
-
|
|
465
|
+
_rawMode = false,
|
|
469
466
|
compact = false,
|
|
470
467
|
time = Date.now(),
|
|
471
468
|
} = opts;
|
|
@@ -713,7 +710,7 @@ function buildHelpOverlay(width, height) {
|
|
|
713
710
|
}
|
|
714
711
|
|
|
715
712
|
// ── joinColumns ───────────────────────────────────────────────────────────
|
|
716
|
-
function
|
|
713
|
+
function _joinColumns(blocks, gap = GRID_GAP) {
|
|
717
714
|
const maxHeight = Math.max(...blocks.map((b) => b.length));
|
|
718
715
|
return Array.from({ length: maxHeight }, (_, rowIdx) =>
|
|
719
716
|
blocks
|
|
@@ -1418,7 +1415,7 @@ export function renderConductorTier(snapshot, cols = 100) {
|
|
|
1418
1415
|
const dataLines = [];
|
|
1419
1416
|
if (!snapshot || snapshot.length === 0) {
|
|
1420
1417
|
const emptyMsg = color('(no sessions)', FG.muted);
|
|
1421
|
-
const
|
|
1418
|
+
const _emptyPad = clip(stripAnsi(emptyMsg) === '(no sessions)' ? emptyMsg : emptyMsg, inner);
|
|
1422
1419
|
dataLines.push(`${borderSeq}│${RESET} ${padRight(emptyMsg, inner - 2)} ${borderSeq}│${RESET}`);
|
|
1423
1420
|
} else {
|
|
1424
1421
|
for (const s of snapshot) {
|
|
@@ -173,7 +173,7 @@ export async function pruneWorktree({ worktreePath, branchName, rootDir = proces
|
|
|
173
173
|
try {
|
|
174
174
|
await git(['worktree', 'remove', worktreePath, ...(forceFlag ? [forceFlag] : [])], rootDir);
|
|
175
175
|
break;
|
|
176
|
-
} catch (
|
|
176
|
+
} catch (_err) {
|
|
177
177
|
if (attempt === 2) {
|
|
178
178
|
// Last resort: rm the directory and prune
|
|
179
179
|
try { await rm(worktreePath, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
@@ -30,7 +30,6 @@ const {
|
|
|
30
30
|
buildPromptHint,
|
|
31
31
|
getCodexMcpConfig,
|
|
32
32
|
getGeminiAllowedServers,
|
|
33
|
-
resolveMcpProfile,
|
|
34
33
|
SUPPORTED_MCP_PROFILES,
|
|
35
34
|
} = await import(pathToFileURL(mcpFilterPath).href);
|
|
36
35
|
const SERVER_INFO = { name: 'triflux-delegator', version: '1.0.0' };
|
|
@@ -198,7 +197,7 @@ function joinInstructions(...values) {
|
|
|
198
197
|
.trim();
|
|
199
198
|
}
|
|
200
199
|
|
|
201
|
-
function
|
|
200
|
+
function _loadAvailableServersFromSearchEngineCache(cwd = process.cwd()) {
|
|
202
201
|
const cacheFile = resolve(cwd, ...SEARCH_ENGINE_CACHE_PATH);
|
|
203
202
|
if (!existsSync(cacheFile)) return null;
|
|
204
203
|
|