@triflux/remote 10.51.0 → 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.
Files changed (59) hide show
  1. package/cto/brief.mjs +0 -8
  2. package/cto/collect.mjs +1 -127
  3. package/cto/current.schema.json +2 -19
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -139
  7. package/hub/index.mjs +0 -9
  8. package/hub/team/agents-row-attach.py +120 -0
  9. package/hub/team/agents-row.mjs +200 -0
  10. package/hub/team/claude-daemon-control.mjs +1 -2
  11. package/hub/team/claude-session-projection.mjs +5 -73
  12. package/hub/team/cli/commands/debug.mjs +1 -2
  13. package/hub/team/cli/commands/start/index.mjs +2 -1
  14. package/hub/team/cli/commands/start/start-mux.mjs +2 -4
  15. package/hub/team/cli/services/state-store.mjs +5 -20
  16. package/hub/team/headless.mjs +96 -543
  17. package/hub/team/psmux.mjs +3 -33
  18. package/hub/team/session-context.mjs +1 -1
  19. package/hub/team/tui-viewer.mjs +9 -56
  20. package/hub/team/tui.mjs +0 -37
  21. package/hub/team/uds-orchestrator.mjs +2 -580
  22. package/hub/workers/factory.mjs +6 -51
  23. package/hub/workers/interface.mjs +1 -7
  24. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  25. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
  26. package/package.json +3 -8
  27. package/scripts/lib/agent-route-policy.mjs +0 -67
  28. package/scripts/lib/cli-claude.mjs +0 -12
  29. package/scripts/lib/env-probe.mjs +0 -70
  30. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  31. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  32. package/hub/hub-lifecycle.mjs +0 -248
  33. package/hub/pipe.mjs +0 -825
  34. package/hub/promote-penalties.mjs +0 -138
  35. package/hub/public/dashboard.html +0 -416
  36. package/hub/schema.sql +0 -209
  37. package/hub/server.mjs +0 -3048
  38. package/hub/store-adapter.mjs +0 -182
  39. package/hub/store.mjs +0 -1736
  40. package/hub/team/cli/services/hub-client.mjs +0 -178
  41. package/hub/team/conductor-registry.mjs +0 -121
  42. package/hub/team/cto-auto-collect.mjs +0 -115
  43. package/hub/team/lead-control.mjs +0 -191
  44. package/hub/team/session-sync.mjs +0 -316
  45. package/hub/team/swarm-intent.mjs +0 -18
  46. package/hub/team/synapse-cli.mjs +0 -458
  47. package/hub/team/synapse-http.mjs +0 -311
  48. package/hub/team/synapse-registry.mjs +0 -618
  49. package/hub/team/tui-synapse.mjs +0 -223
  50. package/hub/team/worker-sandbox.mjs +0 -125
  51. package/hub/tools.mjs +0 -900
  52. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  53. package/hub/workers/delegator-mcp.mjs +0 -1098
  54. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  55. package/scripts/lib/claudemd-scanner.mjs +0 -26
  56. package/scripts/lib/context.mjs +0 -51
  57. package/scripts/lib/handoff.mjs +0 -187
  58. package/scripts/lib/logger.mjs +0 -122
  59. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ # claude agents 행의 백킹 PTY 안에서 워커 tmux pane 에 붙는다.
3
+ # bg-pty-host 가 출력 이력을 다시 재생하면 바깥 터미널이 tmux 의 오래된 질의에 또 답한다.
4
+ # tmux 는 늦게 온 응답을 키 입력으로 넘기므로 여기서 응답 시퀀스를 걸러 낸다.
5
+ import fcntl
6
+ import os
7
+ import pty
8
+ import re
9
+ import select
10
+ import signal
11
+ import sys
12
+ import termios
13
+ import time
14
+ import tty
15
+
16
+ # DA1/DA2/DA3, 창 크기 보고(CSI ... t), DECRQM, kitty 키보드 플래그 응답, DCS, OSC
17
+ REPLY = re.compile(
18
+ rb"\x1b\[[?>=][0-9;]*c"
19
+ rb"|\x1b\[[0-9]+(?:;[0-9]+)*t"
20
+ rb"|\x1b\[\?[0-9;]*\$y"
21
+ rb"|\x1b\[\?[0-9]*u"
22
+ rb"|\x1bP[^\x1b]*\x1b\\"
23
+ rb"|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)"
24
+ )
25
+ # 응답이 read 경계에서 잘렸을 때 잠깐 붙잡아 둘 접두
26
+ PARTIAL = re.compile(
27
+ rb"(?:\x1b\[[?>=]?[0-9;$]*|\x1bP[^\x1b]*\x1b?|\x1b\][^\x07\x1b]*\x1b?)\Z"
28
+ )
29
+ HOLD_SEC = 0.05
30
+
31
+
32
+ def strip_replies(buf):
33
+ """걸러 낸 바이트와, 다음 read 까지 보류할 꼬리를 돌려준다."""
34
+ cleaned = REPLY.sub(b"", buf)
35
+ m = PARTIAL.search(cleaned)
36
+ if m and cleaned[m.start():] != b"\x1b":
37
+ return cleaned[: m.start()], cleaned[m.start():]
38
+ return cleaned, b""
39
+
40
+
41
+ def copy_winsize(src, dst):
42
+ try:
43
+ size = fcntl.ioctl(src, termios.TIOCGWINSZ, b"\0" * 8)
44
+ fcntl.ioctl(dst, termios.TIOCSWINSZ, size)
45
+ except OSError:
46
+ pass
47
+
48
+
49
+ def relay(argv, env):
50
+ pid, fd = pty.fork()
51
+ if pid == 0:
52
+ os.execvpe(argv[0], argv, env)
53
+ stdin = sys.stdin.fileno()
54
+ stdout = sys.stdout.fileno()
55
+ copy_winsize(stdin, fd)
56
+ signal.signal(signal.SIGWINCH, lambda *_: copy_winsize(stdin, fd))
57
+ saved = termios.tcgetattr(stdin) if os.isatty(stdin) else None
58
+ if saved:
59
+ tty.setraw(stdin)
60
+ pending = b""
61
+ deadline = 0.0
62
+ try:
63
+ while True:
64
+ # 자식 출력이 계속 바빠도 보류한 바이트가 HOLD_SEC 를 넘겨 묶이지 않게 마감을 둔다.
65
+ timeout = max(0.0, deadline - time.monotonic()) if pending else None
66
+ try:
67
+ ready, _, _ = select.select([stdin, fd], [], [], timeout)
68
+ except InterruptedError:
69
+ continue
70
+ if pending and time.monotonic() >= deadline:
71
+ os.write(fd, pending)
72
+ pending = b""
73
+ if fd in ready:
74
+ try:
75
+ data = os.read(fd, 65536)
76
+ except OSError:
77
+ break
78
+ if not data:
79
+ break
80
+ os.write(stdout, data)
81
+ if stdin in ready:
82
+ # 행 PTY 가 닫히면 slave 읽기는 EIO 를 던진다.
83
+ try:
84
+ data = os.read(stdin, 65536)
85
+ except OSError:
86
+ break
87
+ if not data:
88
+ break
89
+ out, held = strip_replies(pending + data)
90
+ if held and not pending:
91
+ deadline = time.monotonic() + HOLD_SEC
92
+ pending = held
93
+ if out:
94
+ os.write(fd, out)
95
+ finally:
96
+ if saved:
97
+ try:
98
+ termios.tcsetattr(stdin, termios.TCSADRAIN, saved)
99
+ except termios.error:
100
+ pass
101
+ # 행 PTY 가 닫히면 attach client 도 끝낸다. 워커 방은 그대로 남는다.
102
+ os.close(fd)
103
+ _, status = os.waitpid(pid, 0)
104
+ return os.waitstatus_to_exitcode(status)
105
+
106
+
107
+ def main():
108
+ # 사용법: agents-row-attach.py -- <tmux attach 명령>
109
+ argv = sys.argv[1:]
110
+ if argv[:1] == ["--"]:
111
+ argv = argv[1:]
112
+ if not argv:
113
+ print("usage: agents-row-attach.py -- <command>", file=sys.stderr)
114
+ return 2
115
+ env = {k: v for k, v in os.environ.items() if k not in ("TMUX", "TMUX_PANE")}
116
+ return relay(argv, env)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ sys.exit(main())
@@ -0,0 +1,200 @@
1
+ // hub/team/agents-row.mjs: 워커 tmux pane 을 `claude agents` 행으로 노출한다.
2
+ // 행의 백킹 job 은 공식 `claude --bg --exec` 로 띄운 tmux attach client 다.
3
+ // Enter 를 누르면 그 PTY, 곧 워커가 도는 tmux 방이 열린다(ADR-0026).
4
+
5
+ import { execFile } from "node:child_process";
6
+ import { accessSync, constants } from "node:fs";
7
+ import { delimiter, dirname, isAbsolute, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { promisify } from "node:util";
10
+ import { getMultiplexerType, psmuxExec } from "./psmux.mjs";
11
+
12
+ const execFileAsync = promisify(execFile);
13
+ const ATTACH_HELPER = join(
14
+ dirname(fileURLToPath(import.meta.url)),
15
+ "agents-row-attach.py",
16
+ );
17
+ const READ_ONLY_SUFFIX = " [read-only]";
18
+
19
+ function shellQuote(value) {
20
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
21
+ }
22
+
23
+ // daemon 이 띄운 셸의 PATH 는 호출자와 다를 수 있어 절대 경로로 넘긴다.
24
+ export function resolveExecutable(name, envPath = process.env.PATH || "") {
25
+ if (isAbsolute(name)) return name;
26
+ for (const dir of envPath.split(delimiter)) {
27
+ if (!dir) continue;
28
+ const candidate = join(dir, name);
29
+ try {
30
+ accessSync(candidate, constants.X_OK);
31
+ return candidate;
32
+ } catch {
33
+ /* 다음 후보 */
34
+ }
35
+ }
36
+ return "";
37
+ }
38
+
39
+ /** tmux target 을 서버 안에서 유일한 socket/session/pane ID 로 고정한다. */
40
+ export function resolveTmuxPane(target, { tmux = psmuxExec } = {}) {
41
+ const out = tmux([
42
+ "display-message",
43
+ "-p",
44
+ "-t",
45
+ String(target),
46
+ "#{socket_path}\t#{session_id}\t#{pane_id}\t#{session_name}",
47
+ ]);
48
+ const [socketPath, sessionId, paneId, sessionName] = String(out)
49
+ .trim()
50
+ .split("\t");
51
+ if (!socketPath || !/^\$\d+$/u.test(sessionId) || !/^%\d+$/u.test(paneId)) {
52
+ throw new Error(`tmux pane을 확인할 수 없습니다: ${target}`);
53
+ }
54
+ return { socketPath, sessionId, paneId, sessionName };
55
+ }
56
+
57
+ /**
58
+ * 행의 백킹 명령. pane 이 등록한 세션에 그대로 있는 동안 attach 를 반복하고,
59
+ * 방이 사라지면 자기 행을 지운다. 대화형은 python3 중계로 터미널 응답 누수를 걸러 내고,
60
+ * 그 밖에는 읽기 전용 attach 로 입력 자체를 막는다.
61
+ */
62
+ export function buildAgentsRowCommand({
63
+ tmuxBin,
64
+ claudeBin = "claude",
65
+ pane,
66
+ interactive = false,
67
+ python = "",
68
+ }) {
69
+ const q = shellQuote;
70
+ const tmux = `${q(tmuxBin)} -S ${q(pane.socketPath)}`;
71
+ const readOnly = !(interactive && python);
72
+ const attach = [
73
+ ...(readOnly ? [] : [q(python), q(ATTACH_HELPER), "--"]),
74
+ "env -u TMUX -u TMUX_PANE",
75
+ tmux,
76
+ `attach-session -t ${q(pane.paneId)}`,
77
+ // -r 은 read-only,ignore-size 와 같고 tmux 3.2 이전에도 있다.
78
+ ...(readOnly ? ["-r"] : []),
79
+ ].join(" ");
80
+ // 같은 번호의 다른 방에 붙지 않도록 pane 의 세션 ID 를 매번 대조한다.
81
+ const alive = `[ "$(${tmux} display-message -p -t ${q(pane.paneId)} '#{session_id}' 2>/dev/null)" = ${q(pane.sessionId)} ]`;
82
+ // CLAUDE_JOB_DIR 의 끝이 이 job 의 short id 다. job 이 끝난 뒤 지우도록 nohup 으로 띄운다.
83
+ const removeSelf = `nohup sh -c 'sleep 1; exec "$0" rm "\${CLAUDE_JOB_DIR##*/}"' ${q(claudeBin)} >/dev/null 2>&1 &`;
84
+ // attach 가 바로 끝나기를 5번 연달아 반복하면 고칠 수 없는 실패로 보고 멈춘다.
85
+ const loop = `n=0; while ${alive}; do t=$(date +%s); ${attach}; if [ $(($(date +%s) - t)) -lt 2 ]; then n=$((n + 1)); else n=0; fi; [ "$n" -ge 5 ] && break; sleep 1; done`;
86
+ return { readOnly, command: `${loop}; ${removeSelf}` };
87
+ }
88
+
89
+ // macOS 에는 CLT 설치 안내만 띄우는 /usr/bin/python3 스텁이 있어 실제로 한 번 돌려 본다.
90
+ async function usablePython(resolve, run) {
91
+ const python = resolve("python3");
92
+ if (!python) return "";
93
+ try {
94
+ await run(python, ["-c", "import pty"], { timeout: 5_000 });
95
+ return python;
96
+ } catch {
97
+ return "";
98
+ }
99
+ }
100
+
101
+ export function parseBackgroundShort(stdout) {
102
+ const match = /backgrounded · ([0-9a-f]{6,}) ·/u.exec(String(stdout));
103
+ return match ? match[1] : "";
104
+ }
105
+
106
+ /**
107
+ * 워커 pane 하나를 `claude agents` 행으로 연다.
108
+ * @returns {Promise<{short:string, name:string, readOnly:boolean, pane:object}>}
109
+ */
110
+ export async function openAgentsRow({
111
+ name,
112
+ target,
113
+ cwd = process.cwd(),
114
+ interactive = false,
115
+ _deps = {},
116
+ } = {}) {
117
+ if (!name) throw new Error("name is required");
118
+ const tmux = _deps.tmux || psmuxExec;
119
+ const run = _deps.execFile || execFileAsync;
120
+ const resolve = _deps.resolveExecutable || resolveExecutable;
121
+ const pane = resolveTmuxPane(target, { tmux });
122
+ const tmuxBin = resolve(getMultiplexerType()) || getMultiplexerType();
123
+ const claudeBin = _deps.claudeBin || resolve("claude") || "claude";
124
+ const python = interactive ? await usablePython(resolve, run) : "";
125
+ const { command, readOnly } = buildAgentsRowCommand({
126
+ tmuxBin,
127
+ claudeBin,
128
+ pane,
129
+ interactive,
130
+ python,
131
+ });
132
+ // 대화형인데 읽기 전용으로 떨어졌으면 행 이름에서 바로 보이게 한다.
133
+ const rowName = interactive && readOnly ? `${name}${READ_ONLY_SUFFIX}` : name;
134
+ const { stdout = "", stderr = "" } = await run(
135
+ claudeBin,
136
+ ["--bg", "--name", rowName, "--exec", command],
137
+ { cwd, timeout: 20_000, encoding: "utf8" },
138
+ );
139
+ const short = parseBackgroundShort(`${stdout}\n${stderr}`);
140
+ if (!short) {
141
+ throw new Error(`claude --bg 출력에서 id를 찾지 못했습니다: ${stdout}`);
142
+ }
143
+ return { short, name: rowName, readOnly, pane };
144
+ }
145
+
146
+ /** 행을 지운다. rm 은 실행 중인 job 도 멈춘다. 워커 tmux 방은 건드리지 않는다. */
147
+ export async function closeAgentsRow(short, { _deps = {} } = {}) {
148
+ if (!short) return;
149
+ const run = _deps.execFile || execFileAsync;
150
+ await run(_deps.claudeBin || "claude", ["rm", short], {
151
+ timeout: 10_000,
152
+ }).catch(() => {});
153
+ }
154
+
155
+ /** 실행이 연 행만 정확히 한 번 닫는다. */
156
+ export function createAgentsRowOwnership(close = closeAgentsRow) {
157
+ const shorts = new Set();
158
+ return {
159
+ get size() {
160
+ return shorts.size;
161
+ },
162
+ add(short) {
163
+ if (short) shorts.add(short);
164
+ },
165
+ async release() {
166
+ const owned = [...shorts];
167
+ shorts.clear();
168
+ await Promise.all(owned.map((short) => close(short)));
169
+ },
170
+ };
171
+ }
172
+
173
+ /**
174
+ * tfx-live start 가 띄운 Claude 외 CLI 세션을 대화형 행으로 노출한다.
175
+ * Claude 세션은 원래 목록에 뜨므로 건너뛴다. 실패해도 start 는 성공으로 두고 경고만 돌려준다.
176
+ */
177
+ export async function exposeLiveSession(
178
+ { cli, session, name, cwd, remote, ready },
179
+ _deps,
180
+ ) {
181
+ if (process.env.TFX_AGENTS_ROW === "0") return {};
182
+ if (remote || cli === "claude" || !ready) return {};
183
+ try {
184
+ const row = await openAgentsRow({
185
+ name: name || session,
186
+ target: session,
187
+ cwd,
188
+ interactive: true,
189
+ _deps,
190
+ });
191
+ return {
192
+ agentsRow: row.short,
193
+ ...(row.readOnly
194
+ ? { agentsRowWarning: "python3 not found; row is read-only" }
195
+ : {}),
196
+ };
197
+ } catch (error) {
198
+ return { agentsRowWarning: error?.message || String(error) };
199
+ }
200
+ }
@@ -1775,8 +1775,7 @@ export async function dispatchClaudeDaemonJob({
1775
1775
  /**
1776
1776
  * dispatchClaudeDaemonJob 의 대칭 정리 경로.
1777
1777
  * removeProjection + killDaemonJob (+ optional removeClaudeJobState) 를
1778
- * 모두 시도하며, 각 스텝 오류는 기존 close()/cleanupDaemonDispatches 와
1779
- * 동일하게 .catch 로 삼킨다.
1778
+ * 모두 시도하며, 각 스텝 오류는 .catch 로 삼킨다.
1780
1779
  */
1781
1780
  export async function teardownClaudeDaemonJob({
1782
1781
  controlSock,
@@ -1,12 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- export function normalizeClaudeBridgeSessionId(bridgeSessionId, short) {
4
+ // 실제 bridge ID 가 없으면 만들어 내지 않는다. 추정 ID 는 붙을 수 있는 세션처럼 보이게 한다.
5
+ export function normalizeClaudeBridgeSessionId(bridgeSessionId) {
5
6
  const value = String(bridgeSessionId || "").trim();
6
- if (value.startsWith("session_")) return value;
7
7
  if (value.startsWith("cse_")) return `session_${value.slice(4)}`;
8
- if (value) return value;
9
- return `session_${short}`;
8
+ return value;
10
9
  }
11
10
 
12
11
  export function buildClaudeSessionProjection({
@@ -30,6 +29,7 @@ export function buildClaudeSessionProjection({
30
29
  if (!cwd) throw new Error("cwd is required");
31
30
  if (!name) throw new Error("name is required");
32
31
  if (!agent) throw new Error("agent is required");
32
+ const bridge = normalizeClaudeBridgeSessionId(bridgeSessionId);
33
33
  return {
34
34
  pid,
35
35
  sessionId,
@@ -45,7 +45,7 @@ export function buildClaudeSessionProjection({
45
45
  jobId: short,
46
46
  status,
47
47
  updatedAt,
48
- bridgeSessionId: normalizeClaudeBridgeSessionId(bridgeSessionId, short),
48
+ ...(bridge ? { bridgeSessionId: bridge } : {}),
49
49
  };
50
50
  }
51
51
 
@@ -58,74 +58,6 @@ export async function writeClaudeSessionProjection(sessionsDir, projection) {
58
58
  return sessionPath;
59
59
  }
60
60
 
61
- export async function updateClaudeSessionProjection(sessionPath, patch) {
62
- const parsed = JSON.parse(await fs.readFile(sessionPath, "utf8"));
63
- const next = {
64
- ...parsed,
65
- ...patch,
66
- updatedAt: patch.updatedAt || Date.now(),
67
- };
68
- await fs.writeFile(sessionPath, `${JSON.stringify(next)}\n`, "utf8");
69
- return next;
70
- }
71
-
72
- function projectionSessionId(projection) {
73
- return String(
74
- projection?.sessionId ?? projection?.session_id ?? projection?.id ?? "",
75
- ).trim();
76
- }
77
-
78
- export async function findClaudeSessionProjectionBySessionId(
79
- sessionsDir,
80
- sessionId,
81
- ) {
82
- const expected = String(sessionId || "").trim();
83
- if (!sessionsDir || !expected) return null;
84
- let entries;
85
- try {
86
- entries = await fs.readdir(sessionsDir, { withFileTypes: true });
87
- } catch (error) {
88
- if (error?.code === "ENOENT") return null;
89
- throw error;
90
- }
91
-
92
- for (const entry of entries) {
93
- if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
94
- const filePath = path.join(sessionsDir, entry.name);
95
- try {
96
- const projection = JSON.parse(await fs.readFile(filePath, "utf8"));
97
- if (projectionSessionId(projection) === expected) {
98
- return { path: filePath, projection };
99
- }
100
- } catch (error) {
101
- if (error instanceof SyntaxError) continue;
102
- if (error?.code === "ENOENT") continue;
103
- throw error;
104
- }
105
- }
106
- return null;
107
- }
108
-
109
- export async function refreshClaudeSessionProjectionCwd({
110
- sessionsDir,
111
- sessionId,
112
- cwd,
113
- updatedAt = Date.now(),
114
- } = {}) {
115
- const nextCwd = String(cwd || "").trim();
116
- if (!nextCwd) return { updated: false, reason: "missing_cwd" };
117
- const found = await findClaudeSessionProjectionBySessionId(
118
- sessionsDir,
119
- sessionId,
120
- );
121
- if (!found) return { updated: false, reason: "projection_not_found" };
122
- const projection = await updateClaudeSessionProjection(found.path, {
123
- cwd: nextCwd,
124
- updatedAt,
125
- });
126
- return { updated: true, path: found.path, projection };
127
- }
128
-
129
61
  export async function removeClaudeSessionProjection(sessionPath) {
130
62
  await fs.rm(sessionPath, { force: true });
131
63
  }
@@ -6,7 +6,7 @@ import {
6
6
  } from "../../session.mjs";
7
7
  import { AMBER, BOLD, DIM, RESET } from "../../shared.mjs";
8
8
  import { isTeamAlive } from "../services/runtime-mode.mjs";
9
- import { loadTeamState, TEAM_PROFILE } from "../services/state-store.mjs";
9
+ import { loadTeamState } from "../services/state-store.mjs";
10
10
 
11
11
  export async function teamDebug(args = []) {
12
12
  const state = loadTeamState();
@@ -33,7 +33,6 @@ export async function teamDebug(args = []) {
33
33
 
34
34
  console.log(`\n ${BOLD}state${RESET}`);
35
35
  console.log(` session: ${state.sessionName}`);
36
- console.log(` profile: ${state.profile || TEAM_PROFILE}`);
37
36
  console.log(` mode: ${state.teammateMode || "tmux"}`);
38
37
  console.log(` lead: ${state.lead}`);
39
38
  console.log(` agents: ${(state.agents || []).join(", ")}`);
@@ -88,7 +88,8 @@ export async function teamStart(args = []) {
88
88
  });
89
89
 
90
90
  const effectiveMode = normalizeTeammateMode(teammateMode);
91
- if (effectiveMode !== "headless") ensureTmuxOrExit();
91
+ // headless 워커도 tmux 방에서 돈다. 방이 없으면 claude agents 행도 열 수 없다.
92
+ ensureTmuxOrExit();
92
93
  const effectiveNativeBridge =
93
94
  effectiveMode === "headless" && !nativeBridgeUiOptOut ? true : nativeBridge;
94
95
 
@@ -10,7 +10,7 @@ import {
10
10
  import { BOLD, DIM, GREEN, RESET } from "../../../shared.mjs";
11
11
  import { ok, warn } from "../../render.mjs";
12
12
  import { toAgentId } from "../../services/member-selector.mjs";
13
- import { PKG_ROOT, TEAM_PROFILE } from "../../services/state-store.mjs";
13
+ import { PKG_ROOT } from "../../services/state-store.mjs";
14
14
  import { buildTasks } from "../../services/task-model.mjs";
15
15
 
16
16
  export async function startMuxTeam({
@@ -106,9 +106,7 @@ export async function startMuxTeam({
106
106
  members.filter((member) => member.role === "worker"),
107
107
  ),
108
108
  postSave() {
109
- const profilePrefix =
110
- TEAM_PROFILE === "team" ? "" : `TFX_TEAM_PROFILE=${TEAM_PROFILE} `;
111
- const taskListCommand = `${profilePrefix}${process.execPath} ${join(PKG_ROOT, "bin", "triflux.mjs")} team tasks`;
109
+ const taskListCommand = `${process.execPath} ${join(PKG_ROOT, "bin", "triflux.mjs")} team tasks`;
112
110
  configureTeammateKeybindings(sessionId, {
113
111
  inProcess: false,
114
112
  taskListCommand,
@@ -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 findLatestMatchingTeamState() {
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 ((parsed.profile || "team") === TEAM_PROFILE) return parsed;
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 = findLatestMatchingTeamState();
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) {