@triflux/remote 10.50.2 → 10.52.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/cto/brief.mjs +0 -20
- package/cto/collect.mjs +1 -200
- package/cto/current.schema.json +2 -23
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -187
- package/hub/index.mjs +0 -17
- package/hub/team/agent-map.json +0 -1
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +2 -39
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +42 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +2 -7
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +108 -1296
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +3 -34
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/session.mjs +7 -91
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +11 -58
- package/hub/team/tui.mjs +8 -306
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +6 -84
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/hub-client.mjs +0 -264
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-intent.mjs +0 -216
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/tools.mjs +0 -900
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { formatCompletionSuffix } from "../render.mjs";
|
|
4
|
-
import { fetchHubTaskList, nativeGetStatus } from "../services/hub-client.mjs";
|
|
5
|
-
import {
|
|
6
|
-
isNativeMode,
|
|
7
|
-
isTeamAlive,
|
|
8
|
-
isWtMode,
|
|
9
|
-
} from "../services/runtime-mode.mjs";
|
|
1
|
+
import { AMBER, BOLD, DIM, GREEN, RED, RESET } from "../../shared.mjs";
|
|
2
|
+
import { isTeamAlive } from "../services/runtime-mode.mjs";
|
|
10
3
|
import { loadTeamState } from "../services/state-store.mjs";
|
|
11
4
|
|
|
12
5
|
export async function teamStatus(args = []) {
|
|
@@ -41,14 +34,6 @@ export async function teamStatus(args = []) {
|
|
|
41
34
|
})),
|
|
42
35
|
};
|
|
43
36
|
|
|
44
|
-
if (isNativeMode(state) && alive) {
|
|
45
|
-
payload.nativeMembers = (await nativeGetStatus(state))?.data?.members || [];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (alive) {
|
|
49
|
-
payload.hubTasks = await fetchHubTaskList(state);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
37
|
if (json) {
|
|
53
38
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
54
39
|
return;
|
|
@@ -65,11 +50,6 @@ export async function teamStatus(args = []) {
|
|
|
65
50
|
` Uptime: ${alive ? `${Math.round((Date.now() - state.startedAt) / 60000)}분` : "-"}`,
|
|
66
51
|
);
|
|
67
52
|
console.log(` 태스크: ${(state.tasks || []).length}`);
|
|
68
|
-
if (isWtMode(state) && !hasWindowsTerminalSession()) {
|
|
69
|
-
console.log(
|
|
70
|
-
` ${DIM}WT_SESSION 미감지: 생존성은 heuristics로 판정됨${RESET}`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
53
|
|
|
74
54
|
for (const member of state.members || []) {
|
|
75
55
|
console.log(
|
|
@@ -77,40 +57,5 @@ export async function teamStatus(args = []) {
|
|
|
77
57
|
);
|
|
78
58
|
}
|
|
79
59
|
|
|
80
|
-
if (isNativeMode(state) && alive) {
|
|
81
|
-
for (const member of payload.nativeMembers) {
|
|
82
|
-
console.log(
|
|
83
|
-
` • ${member.name}: ${member.status}${formatCompletionSuffix(member)}${member.lastPreview ? ` ${DIM}${member.lastPreview}${RESET}` : ""}`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (alive) {
|
|
89
|
-
const hubTasks = payload.hubTasks || (await fetchHubTaskList(state));
|
|
90
|
-
if (hubTasks.length) {
|
|
91
|
-
const completed = hubTasks.filter(
|
|
92
|
-
(task) => task.status === "completed",
|
|
93
|
-
).length;
|
|
94
|
-
const failed = hubTasks.filter((task) => task.status === "failed").length;
|
|
95
|
-
console.log(
|
|
96
|
-
`\n ${BOLD}Hub Tasks${RESET} ${DIM}(${completed}/${hubTasks.length} done)${RESET}`,
|
|
97
|
-
);
|
|
98
|
-
for (const task of hubTasks) {
|
|
99
|
-
const icon =
|
|
100
|
-
task.status === "completed"
|
|
101
|
-
? `${GREEN}✓${RESET}`
|
|
102
|
-
: task.status === "in_progress"
|
|
103
|
-
? `${AMBER}●${RESET}`
|
|
104
|
-
: task.status === "failed"
|
|
105
|
-
? `${RED}✗${RESET}`
|
|
106
|
-
: `${GRAY}○${RESET}`;
|
|
107
|
-
const owner = task.owner ? ` ${GRAY}[${task.owner}]${RESET}` : "";
|
|
108
|
-
console.log(
|
|
109
|
-
` ${icon} ${task.subject || task.description?.slice(0, 50) || ""}${owner}`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
if (failed > 0) console.log(` ${RED}⚠ ${failed}건 실패${RESET}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
60
|
console.log("");
|
|
116
61
|
}
|
|
@@ -1,31 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { killSession, sessionExists } from "../../session.mjs";
|
|
2
2
|
import { DIM, RESET } from "../../shared.mjs";
|
|
3
3
|
import { ok } from "../render.mjs";
|
|
4
|
-
import { nativeRequest } from "../services/native-control.mjs";
|
|
5
|
-
import { isNativeMode, isWtMode } from "../services/runtime-mode.mjs";
|
|
6
4
|
import { clearTeamState, loadTeamState } from "../services/state-store.mjs";
|
|
7
5
|
|
|
8
|
-
export async function teamStop() {
|
|
9
|
-
const state = loadTeamState();
|
|
10
|
-
if (!state) {
|
|
6
|
+
export async function teamStop(args = []) {
|
|
7
|
+
const state = loadTeamState(args[0]);
|
|
8
|
+
if (!state || (args[0] && state.sessionId !== args[0])) {
|
|
11
9
|
console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
|
|
12
10
|
return;
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
if (
|
|
16
|
-
|
|
13
|
+
if (state.teammateMode === "headless") {
|
|
14
|
+
if (!Number.isInteger(state.ownerPid) || state.ownerPid <= 0) {
|
|
15
|
+
throw new Error("headless ownerPid가 없습니다.");
|
|
16
|
+
}
|
|
17
17
|
try {
|
|
18
|
-
process.kill(state.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
paneCount: state?.wt?.paneCount ?? (state.members || []).length,
|
|
25
|
-
});
|
|
26
|
-
ok(
|
|
27
|
-
`세션 종료: ${state.sessionName}${closed ? ` (${closed} panes closed)` : ""}`,
|
|
28
|
-
);
|
|
18
|
+
process.kill(state.ownerPid, "SIGTERM");
|
|
19
|
+
ok(`headless 종료 요청: ${state.sessionId}`);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code !== "ESRCH") throw error;
|
|
22
|
+
console.log(` ${DIM}세션 이미 종료됨${RESET}`);
|
|
23
|
+
}
|
|
29
24
|
} else if (sessionExists(state.sessionName)) {
|
|
30
25
|
killSession(state.sessionName);
|
|
31
26
|
ok(`세션 종료: ${state.sessionName}`);
|
package/hub/team/cli/help.mjs
CHANGED
|
@@ -4,18 +4,17 @@ export function renderTeamHelp() {
|
|
|
4
4
|
console.log(`
|
|
5
5
|
${AMBER}${BOLD}⬡ tfx multi${RESET} ${DIM}멀티-CLI 팀 모드 (Lead + Teammates)${RESET}
|
|
6
6
|
|
|
7
|
-
${BOLD}
|
|
7
|
+
${BOLD}Usage${RESET}
|
|
8
8
|
${WHITE}tfx multi "작업 설명"${RESET}
|
|
9
9
|
${WHITE}tfx multi --agents codex,antigravity --lead claude "작업"${RESET}
|
|
10
10
|
${WHITE}tfx multi --teammate-mode tmux "작업"${RESET}
|
|
11
|
-
${WHITE}tfx multi --teammate-mode
|
|
11
|
+
${WHITE}tfx multi --teammate-mode headless "작업"${RESET}
|
|
12
12
|
${WHITE}tfx multi --layout 1xN "작업"${RESET} ${DIM}(세로 분할 컬럼)${RESET}
|
|
13
13
|
${WHITE}tfx multi --layout Nx1 "작업"${RESET} ${DIM}(가로 분할 스택)${RESET}
|
|
14
14
|
${WHITE}tfx multi --dashboard-layout lite "작업"${RESET} ${DIM}(dashboard-lite 기본 뷰)${RESET}
|
|
15
15
|
${WHITE}tfx multi --dashboard-layout auto "작업"${RESET} ${DIM}(dashboard viewer 레이아웃 자동 결정)${RESET}
|
|
16
|
-
${WHITE}tfx multi --dashboard-size 0.4 "작업"${RESET} ${DIM}(대시보드 분할 비율 0.2~0.8, 기본 0.
|
|
16
|
+
${WHITE}tfx multi --dashboard-size 0.4 "작업"${RESET} ${DIM}(대시보드 분할 비율 0.2~0.8, 기본 0.40)${RESET}
|
|
17
17
|
${WHITE}tfx multi --dashboard-anchor window "작업"${RESET} ${DIM}(대시보드 고정 위치: window|tab, 기본 window)${RESET}
|
|
18
|
-
${WHITE}tfx multi --teammate-mode in-process "작업"${RESET} ${DIM}(tmux 불필요)${RESET}
|
|
19
18
|
|
|
20
19
|
${BOLD}제어${RESET}
|
|
21
20
|
${WHITE}tfx multi status${RESET} ${GRAY}현재 팀 상태${RESET}
|
|
@@ -27,7 +26,7 @@ export function renderTeamHelp() {
|
|
|
27
26
|
${WHITE}tfx multi send${RESET} ${DIM}<lead|이름|번호> "msg"${RESET} ${GRAY}팀메이트에 메시지 주입${RESET}
|
|
28
27
|
${WHITE}tfx multi interrupt${RESET} ${DIM}<대상>${RESET} ${GRAY}팀메이트 인터럽트(C-c)${RESET}
|
|
29
28
|
${WHITE}tfx multi control${RESET} ${DIM}<대상> <cmd>${RESET} ${GRAY}리드 제어명령(interrupt|stop|pause|resume)${RESET}
|
|
30
|
-
${WHITE}tfx multi stop${RESET}
|
|
29
|
+
${WHITE}tfx multi stop${RESET} ${DIM}[session-id]${RESET} ${GRAY}선택한 팀 세션 종료${RESET}
|
|
31
30
|
${WHITE}tfx multi kill${RESET} ${GRAY}모든 팀 세션 강제 종료${RESET}
|
|
32
31
|
${WHITE}tfx multi list${RESET} ${GRAY}활성 세션 목록${RESET}
|
|
33
32
|
|
|
@@ -32,7 +32,11 @@ export const TEAM_COMMANDS = [
|
|
|
32
32
|
usage: "tfx multi control <대상> <cmd> [사유]",
|
|
33
33
|
desc: "리드 제어명령 전송",
|
|
34
34
|
},
|
|
35
|
-
{
|
|
35
|
+
{
|
|
36
|
+
name: "stop",
|
|
37
|
+
usage: "tfx multi stop [session-id]",
|
|
38
|
+
desc: "선택한 팀 세션 종료",
|
|
39
|
+
},
|
|
36
40
|
{ name: "kill", usage: "tfx multi kill", desc: "모든 팀 세션 강제 종료" },
|
|
37
41
|
{ name: "list", usage: "tfx multi list", desc: "활성 세션 목록" },
|
|
38
42
|
{ name: "help", usage: "tfx multi help", desc: "도움말" },
|
package/hub/team/cli/render.mjs
CHANGED
|
@@ -38,15 +38,3 @@ export function renderTasks(tasks = []) {
|
|
|
38
38
|
}
|
|
39
39
|
console.log("");
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
export function formatCompletionSuffix(member) {
|
|
43
|
-
if (!member?.completionStatus) return "";
|
|
44
|
-
if (member.completionStatus === "abnormal") {
|
|
45
|
-
return ` ${RED}[abnormal:${member.completionReason || "unknown"}]${RESET}`;
|
|
46
|
-
}
|
|
47
|
-
if (member.completionStatus === "normal")
|
|
48
|
-
return ` ${GREEN}[route-ok]${RESET}`;
|
|
49
|
-
if (member.completionStatus === "unchecked")
|
|
50
|
-
return ` ${GRAY}[route-unchecked]${RESET}`;
|
|
51
|
-
return "";
|
|
52
|
-
}
|
|
@@ -1,37 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
detectMultiplexer,
|
|
3
|
-
hasWindowsTerminal,
|
|
4
|
-
hasWindowsTerminalSession,
|
|
5
|
-
sessionExists,
|
|
6
|
-
} from "../../session.mjs";
|
|
1
|
+
import { detectMultiplexer, sessionExists } from "../../session.mjs";
|
|
7
2
|
|
|
8
3
|
export function normalizeTeammateMode(mode = "auto", deps = {}) {
|
|
9
4
|
const platform = deps.platform || process.platform;
|
|
10
5
|
const env = deps.env || process.env;
|
|
11
6
|
const detectMux = deps.detectMultiplexer || detectMultiplexer;
|
|
12
7
|
const raw = String(mode).toLowerCase();
|
|
13
|
-
if (raw === "inline" || raw === "native") return "in-process";
|
|
14
8
|
if (raw === "headless" || raw === "hl") return "headless";
|
|
15
|
-
if (raw === "
|
|
16
|
-
if (raw === "psmux"
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
) {
|
|
22
|
-
return platform === "win32" ? "wt" : "in-process";
|
|
23
|
-
}
|
|
24
|
-
if (raw === "auto") {
|
|
25
|
-
if (env.TMUX) return "tmux";
|
|
26
|
-
const mux = detectMux();
|
|
27
|
-
if (platform !== "win32") {
|
|
28
|
-
return mux === "tmux" ? "tmux" : "in-process";
|
|
29
|
-
}
|
|
30
|
-
if (mux === "psmux") return "psmux";
|
|
31
|
-
if (mux === "tmux") return "tmux";
|
|
32
|
-
return "in-process";
|
|
9
|
+
if (raw === "tmux") return raw;
|
|
10
|
+
if (raw === "psmux" && platform === "win32") return raw;
|
|
11
|
+
if (raw !== "auto") {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`지원하지 않는 teammate mode: ${mode}; auto, headless, tmux${platform === "win32" ? ", psmux" : ""}를 사용하세요.`,
|
|
14
|
+
);
|
|
33
15
|
}
|
|
34
|
-
return "
|
|
16
|
+
if (env.TMUX) return "tmux";
|
|
17
|
+
const mux = detectMux();
|
|
18
|
+
if (mux === "tmux") return "tmux";
|
|
19
|
+
if (platform === "win32" && mux === "psmux") return "psmux";
|
|
20
|
+
if (platform === "win32" && mux === "git-bash-tmux") return "tmux";
|
|
21
|
+
if (!(deps.isTTY ?? Boolean(process.stdout.isTTY))) return "headless";
|
|
22
|
+
return ensureTmuxOrExit(deps);
|
|
35
23
|
}
|
|
36
24
|
|
|
37
25
|
export function normalizeLayout(layout = "2x2") {
|
|
@@ -43,91 +31,32 @@ export function normalizeLayout(layout = "2x2") {
|
|
|
43
31
|
return "2x2";
|
|
44
32
|
}
|
|
45
33
|
|
|
46
|
-
export function isNativeMode(state) {
|
|
47
|
-
return state?.teammateMode === "in-process" && !!state?.native?.controlUrl;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function isWtMode(state) {
|
|
51
|
-
return state?.teammateMode === "wt";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
34
|
export function isTeamAlive(state) {
|
|
55
35
|
if (!state) return false;
|
|
56
|
-
if (
|
|
36
|
+
if (state.teammateMode === "headless" && state.ownerPid) {
|
|
57
37
|
try {
|
|
58
|
-
process.kill(state.
|
|
38
|
+
process.kill(state.ownerPid, 0);
|
|
59
39
|
return true;
|
|
60
40
|
} catch {
|
|
61
41
|
return false;
|
|
62
42
|
}
|
|
63
43
|
}
|
|
64
|
-
if (isWtMode(state)) {
|
|
65
|
-
if (!hasWindowsTerminal()) return false;
|
|
66
|
-
if (hasWindowsTerminalSession()) return true;
|
|
67
|
-
return Array.isArray(state.members) && state.members.length > 0;
|
|
68
|
-
}
|
|
69
44
|
return sessionExists(state.sessionName);
|
|
70
45
|
}
|
|
71
46
|
|
|
72
47
|
export function ensureTmuxOrExit(deps = {}) {
|
|
73
48
|
const platform = deps.platform || process.platform;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
49
|
+
const mux = (deps.detectMultiplexer || detectMultiplexer)();
|
|
50
|
+
if (
|
|
51
|
+
mux === "tmux" ||
|
|
52
|
+
(platform === "win32" && ["psmux", "git-bash-tmux"].includes(mux))
|
|
53
|
+
)
|
|
54
|
+
return mux;
|
|
55
|
+
const error = new Error(
|
|
56
|
+
platform === "win32"
|
|
57
|
+
? "멀티플렉서 미발견. psmux를 설치하세요: winget install psmux"
|
|
58
|
+
: `멀티플렉서 미발견. tmux를 설치하세요: ${platform === "darwin" ? "brew install tmux" : "apt install tmux"}`,
|
|
59
|
+
);
|
|
78
60
|
error.code = "TMUX_REQUIRED";
|
|
79
61
|
throw error;
|
|
80
62
|
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* teammateMode + 환경 기반 effective mode 결정 (pure function).
|
|
84
|
-
*
|
|
85
|
-
* - wt → wt.exe 없으면 in-process
|
|
86
|
-
* - wt → WT_SESSION 없으면 in-process
|
|
87
|
-
* - in-process → non-TTY 감지 시 headless (#114: run_in_background 부모 종료 시 자식 SIGTERM 방지)
|
|
88
|
-
* - override: TFX_FORCE_IN_PROCESS=1 로 in-process 유지
|
|
89
|
-
*
|
|
90
|
-
* @param {string} teammateMode
|
|
91
|
-
* @param {{
|
|
92
|
-
* hasWt?: () => boolean,
|
|
93
|
-
* hasWtSession?: () => boolean,
|
|
94
|
-
* isTTY?: boolean,
|
|
95
|
-
* env?: Record<string,string|undefined>,
|
|
96
|
-
* }} [deps]
|
|
97
|
-
* @returns {{ mode: string, warnings: string[] }}
|
|
98
|
-
*/
|
|
99
|
-
export function resolveEffectiveMode(teammateMode, deps = {}) {
|
|
100
|
-
const hasWt = deps.hasWt || hasWindowsTerminal;
|
|
101
|
-
const hasWtSession = deps.hasWtSession || hasWindowsTerminalSession;
|
|
102
|
-
const isTTY =
|
|
103
|
-
typeof deps.isTTY === "boolean"
|
|
104
|
-
? deps.isTTY
|
|
105
|
-
: Boolean(process.stdout.isTTY);
|
|
106
|
-
const env = deps.env || process.env;
|
|
107
|
-
|
|
108
|
-
const warnings = [];
|
|
109
|
-
let effective = teammateMode;
|
|
110
|
-
|
|
111
|
-
if (effective === "wt" && !hasWt()) {
|
|
112
|
-
warnings.push("wt.exe 미발견 — in-process 모드로 자동 fallback");
|
|
113
|
-
effective = "in-process";
|
|
114
|
-
}
|
|
115
|
-
if (effective === "wt" && !hasWtSession()) {
|
|
116
|
-
warnings.push(
|
|
117
|
-
"WT_SESSION 미감지(Windows Terminal 외부) — in-process 모드로 자동 fallback",
|
|
118
|
-
);
|
|
119
|
-
effective = "in-process";
|
|
120
|
-
}
|
|
121
|
-
if (
|
|
122
|
-
effective === "in-process" &&
|
|
123
|
-
!isTTY &&
|
|
124
|
-
env.TFX_FORCE_IN_PROCESS !== "1"
|
|
125
|
-
) {
|
|
126
|
-
warnings.push(
|
|
127
|
-
"non-TTY 환경 감지 (run_in_background 등) — headless 모드로 자동 fallback (#114). 강제 유지: TFX_FORCE_IN_PROCESS=1",
|
|
128
|
-
);
|
|
129
|
-
effective = "headless";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return { mode: effective, warnings };
|
|
133
|
-
}
|
|
@@ -14,26 +14,14 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
export const PKG_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
15
15
|
export const HUB_PID_DIR =
|
|
16
16
|
process.env.TFX_HUB_PID_DIR || join(homedir(), ".claude", "cache", "tfx-hub");
|
|
17
|
-
export const TEAM_PROFILE = (() => {
|
|
18
|
-
const raw = String(process.env.TFX_TEAM_PROFILE || "team")
|
|
19
|
-
.trim()
|
|
20
|
-
.toLowerCase();
|
|
21
|
-
return raw === "codex-team" ? "codex-team" : "team";
|
|
22
|
-
})();
|
|
23
|
-
|
|
24
17
|
export const SESSION_ID = process.env.CLAUDE_SESSION_ID || `s${Date.now()}`;
|
|
25
18
|
|
|
26
19
|
function getStatePath(sessionId) {
|
|
27
20
|
if (sessionId) return join(HUB_PID_DIR, `team-state-${sessionId}.json`);
|
|
28
|
-
return join(
|
|
29
|
-
HUB_PID_DIR,
|
|
30
|
-
TEAM_PROFILE === "codex-team"
|
|
31
|
-
? "team-state-codex-team.json"
|
|
32
|
-
: "team-state.json",
|
|
33
|
-
);
|
|
21
|
+
return join(HUB_PID_DIR, "team-state.json");
|
|
34
22
|
}
|
|
35
23
|
|
|
36
|
-
function
|
|
24
|
+
function findLatestTeamState() {
|
|
37
25
|
try {
|
|
38
26
|
if (!existsSync(HUB_PID_DIR)) return null;
|
|
39
27
|
const entries = readdirSync(HUB_PID_DIR)
|
|
@@ -51,7 +39,7 @@ function findLatestMatchingTeamState() {
|
|
|
51
39
|
for (const { path } of entries) {
|
|
52
40
|
try {
|
|
53
41
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
54
|
-
if (
|
|
42
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
55
43
|
} catch {
|
|
56
44
|
// corrupt entry — skip
|
|
57
45
|
}
|
|
@@ -84,7 +72,7 @@ export function loadTeamState(sessionId) {
|
|
|
84
72
|
// team-state-*.json 을 tfx multi status 가 못 찾던 false-negative 우회.
|
|
85
73
|
// 명시 sessionId lookup 은 의도적 조회이므로 auto-discover 하지 않는다.
|
|
86
74
|
if (!explicit) {
|
|
87
|
-
const latest =
|
|
75
|
+
const latest = findLatestTeamState();
|
|
88
76
|
if (latest) return latest;
|
|
89
77
|
}
|
|
90
78
|
return null;
|
|
@@ -93,10 +81,7 @@ export function loadTeamState(sessionId) {
|
|
|
93
81
|
export function saveTeamState(state, sessionId) {
|
|
94
82
|
const path = getStatePath(sessionId || state.sessionId || SESSION_ID);
|
|
95
83
|
mkdirSync(dirname(path), { recursive: true });
|
|
96
|
-
writeFileSync(
|
|
97
|
-
path,
|
|
98
|
-
JSON.stringify({ ...state, profile: TEAM_PROFILE }, null, 2) + "\n",
|
|
99
|
-
);
|
|
84
|
+
writeFileSync(path, JSON.stringify(state, null, 2) + "\n");
|
|
100
85
|
}
|
|
101
86
|
|
|
102
87
|
export function clearTeamState(sessionId) {
|
package/hub/team/handoff.mjs
CHANGED
|
@@ -13,37 +13,6 @@ const YES_NO = ["yes", "no"];
|
|
|
13
13
|
const TOKEN_HARD_CAP = 150;
|
|
14
14
|
const HANDOFF_MARKER = "--- HANDOFF ---";
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* 워커 프롬프트에 삽입할 HANDOFF 블록 생성 지시
|
|
18
|
-
*/
|
|
19
|
-
export const HANDOFF_INSTRUCTION = `
|
|
20
|
-
After completing the task, you MUST output a HANDOFF block in exactly this format at the end of your response:
|
|
21
|
-
|
|
22
|
-
--- HANDOFF ---
|
|
23
|
-
status: ok | partial | failed
|
|
24
|
-
lead_action: accept | needs_read | retry | reassign
|
|
25
|
-
task: <1-3 word task type>
|
|
26
|
-
files_changed: <comma-separated repo-root relative file paths, or "none">
|
|
27
|
-
verdict: <one sentence conclusion>
|
|
28
|
-
confidence: high | medium | low
|
|
29
|
-
risk: low | med | high
|
|
30
|
-
detail: <result file path if available, or "none">
|
|
31
|
-
|
|
32
|
-
If the task failed, also include:
|
|
33
|
-
error_stage: dispatch | execution | timeout
|
|
34
|
-
retryable: yes | no
|
|
35
|
-
partial_output: yes | no
|
|
36
|
-
|
|
37
|
-
Rules:
|
|
38
|
-
- The HANDOFF block must start with exactly "--- HANDOFF ---"
|
|
39
|
-
- Set status: ok only after you have verified the work (file/diff/test evidence) — unverified claims must use partial or failed.
|
|
40
|
-
- Each field must be on its own line as "key: value"
|
|
41
|
-
- Report files_changed as repo-root relative paths
|
|
42
|
-
- verdict must be a single concise sentence
|
|
43
|
-
- Do not skip any required field
|
|
44
|
-
- This block owns the final output position; this block must be the last thing you output
|
|
45
|
-
`.trim();
|
|
46
|
-
|
|
47
16
|
/**
|
|
48
17
|
* CLI 프롬프트 길이 제한을 고려한 축약 HANDOFF 지시
|
|
49
18
|
*/
|
|
@@ -61,7 +30,7 @@ Set status: ok only after you have verified the work (file/diff/test evidence)
|
|
|
61
30
|
* @param {string} rawText
|
|
62
31
|
* @returns {object|null} 파싱된 필드 객체, 블록이 없으면 null
|
|
63
32
|
*/
|
|
64
|
-
|
|
33
|
+
function parseHandoff(rawText) {
|
|
65
34
|
if (!rawText || typeof rawText !== "string") return null;
|
|
66
35
|
|
|
67
36
|
// P1 fix: 마지막 HANDOFF 블록을 파싱 (프롬프트 에코로 인한 중복 마커 대응)
|
|
@@ -137,7 +106,7 @@ function validateEnum(value, allowed) {
|
|
|
137
106
|
* @param {string[]} [context.gitDiffFiles]
|
|
138
107
|
* @returns {{ handoff: object, valid: boolean, warnings: string[] }}
|
|
139
108
|
*/
|
|
140
|
-
|
|
109
|
+
function validateHandoff(parsed, context = {}) {
|
|
141
110
|
const warnings = [];
|
|
142
111
|
const h = { ...parsed };
|
|
143
112
|
|
|
@@ -247,7 +216,7 @@ export function validateHandoff(parsed, context = {}) {
|
|
|
247
216
|
* @param {string[]} [context.filesChanged]
|
|
248
217
|
* @returns {object}
|
|
249
218
|
*/
|
|
250
|
-
|
|
219
|
+
function buildFallbackHandoff(exitCode, resultFile, cli, context = {}) {
|
|
251
220
|
const ok = exitCode === 0;
|
|
252
221
|
const filesChanged = Array.isArray(context.filesChanged)
|
|
253
222
|
? context.filesChanged.filter(Boolean)
|