@triflux/remote 10.50.1 → 10.51.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 (120) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +16 -247
  3. package/cto/current.schema.json +4 -20
  4. package/cto/events.mjs +13 -294
  5. package/cto/hygiene.mjs +26 -346
  6. package/cto/lake-root.mjs +114 -16
  7. package/cto/status.mjs +33 -128
  8. package/hub/index.mjs +0 -8
  9. package/hub/store-adapter.mjs +1 -1
  10. package/hub/team/agent-map.json +0 -1
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/cli/commands/attach.mjs +1 -17
  14. package/hub/team/cli/commands/control.mjs +12 -38
  15. package/hub/team/cli/commands/debug.mjs +1 -37
  16. package/hub/team/cli/commands/focus.mjs +5 -31
  17. package/hub/team/cli/commands/interrupt.mjs +6 -25
  18. package/hub/team/cli/commands/kill.mjs +1 -30
  19. package/hub/team/cli/commands/list.mjs +0 -16
  20. package/hub/team/cli/commands/send.mjs +6 -26
  21. package/hub/team/cli/commands/start/index.mjs +41 -133
  22. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  23. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  24. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  25. package/hub/team/cli/commands/status.mjs +2 -57
  26. package/hub/team/cli/commands/stop.mjs +14 -19
  27. package/hub/team/cli/help.mjs +4 -5
  28. package/hub/team/cli/manifest.mjs +5 -1
  29. package/hub/team/cli/render.mjs +0 -12
  30. package/hub/team/cli/services/hub-client.mjs +2 -88
  31. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  32. package/hub/team/handoff.mjs +3 -34
  33. package/hub/team/headless.mjs +14 -755
  34. package/hub/team/orchestrator.mjs +4 -30
  35. package/hub/team/pane.mjs +6 -28
  36. package/hub/team/psmux.mjs +16 -403
  37. package/hub/team/remote-session.mjs +1 -2
  38. package/hub/team/session.mjs +8 -119
  39. package/hub/team/swarm-intent.mjs +0 -198
  40. package/hub/team/terminal-opener.mjs +11 -35
  41. package/hub/team/tui-core.mjs +1 -1
  42. package/hub/team/tui-viewer.mjs +2 -2
  43. package/hub/team/tui.mjs +8 -269
  44. package/hub/team/wt-manager.mjs +2 -2
  45. package/hub/workers/factory.mjs +4 -37
  46. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  47. package/package.json +1 -1
  48. package/scripts/lib/agent-route-policy.mjs +162 -14
  49. package/scripts/lib/claudemd-scanner.mjs +2 -199
  50. package/scripts/lib/cli-claude.mjs +0 -2
  51. package/scripts/lib/context.mjs +3 -19
  52. package/scripts/lib/env-probe.mjs +5 -64
  53. package/scripts/lib/gemini-profiles.mjs +2 -69
  54. package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
  55. package/scripts/lib/machine-profile.mjs +3 -17
  56. package/scripts/lib/mcp-filter.mjs +109 -53
  57. package/scripts/lib/mcp-health.mjs +2 -2
  58. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  59. package/scripts/lib/psmux-info.mjs +25 -104
  60. package/scripts/lib/skill-template.mjs +0 -271
  61. package/cto/hygiene-actions.mjs +0 -355
  62. package/cto/hygiene-notify.mjs +0 -170
  63. package/hub/team/build-worker-prompt.mjs +0 -92
  64. package/hub/team/check-mcp-hub.mjs +0 -49
  65. package/hub/team/claude-native-bridge.mjs +0 -987
  66. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  67. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  68. package/hub/team/cli/services/native-control.mjs +0 -145
  69. package/hub/team/codex-review.mjs +0 -520
  70. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  71. package/hub/team/conductor.mjs +0 -1529
  72. package/hub/team/consensus-meta.mjs +0 -176
  73. package/hub/team/cto-pull.mjs +0 -31
  74. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  75. package/hub/team/dashboard.mjs +0 -322
  76. package/hub/team/event-log.mjs +0 -84
  77. package/hub/team/execution-mode.mjs +0 -324
  78. package/hub/team/extract-completion-payload.mjs +0 -110
  79. package/hub/team/headless-bridge-session.mjs +0 -17
  80. package/hub/team/health-probe.mjs +0 -435
  81. package/hub/team/interactive-native-launcher.mjs +0 -119
  82. package/hub/team/interactive-native-worker.mjs +0 -80
  83. package/hub/team/interactive-tui-transport.mjs +0 -479
  84. package/hub/team/launcher-template.mjs +0 -144
  85. package/hub/team/mcp-selector.mjs +0 -145
  86. package/hub/team/native-supervisor.mjs +0 -460
  87. package/hub/team/native.mjs +0 -727
  88. package/hub/team/recovery-store.mjs +0 -149
  89. package/hub/team/remote-probe.mjs +0 -292
  90. package/hub/team/remote-watcher.mjs +0 -506
  91. package/hub/team/runtime-strategy.mjs +0 -200
  92. package/hub/team/schemas/worker-completion.json +0 -39
  93. package/hub/team/sentinel-capture.mjs +0 -127
  94. package/hub/team/swarm-cli.mjs +0 -390
  95. package/hub/team/swarm-hypervisor.mjs +0 -2206
  96. package/hub/team/swarm-locks.mjs +0 -335
  97. package/hub/team/swarm-planner.mjs +0 -371
  98. package/hub/team/swarm-preflight.mjs +0 -366
  99. package/hub/team/swarm-reconciler.mjs +0 -242
  100. package/hub/team/tui-remote-adapter.mjs +0 -377
  101. package/hub/team/worker-completion-validator.mjs +0 -79
  102. package/hub/team/worktree-lifecycle.mjs +0 -639
  103. package/hub/team/wt-templates.json +0 -43
  104. package/hub/workers/antigravity-route-worker.mjs +0 -179
  105. package/hub/workers/gemini-worker.mjs +0 -217
  106. package/scripts/lib/agent-json.mjs +0 -27
  107. package/scripts/lib/async.mjs +0 -174
  108. package/scripts/lib/cli-agy.mjs +0 -85
  109. package/scripts/lib/cli-codex.mjs +0 -62
  110. package/scripts/lib/cli-gemini.mjs +0 -37
  111. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  112. package/scripts/lib/env.mjs +0 -14
  113. package/scripts/lib/hub.mjs +0 -48
  114. package/scripts/lib/pid.mjs +0 -63
  115. package/scripts/lib/quota.mjs +0 -47
  116. package/scripts/lib/skill-state.mjs +0 -220
  117. package/scripts/lib/team.mjs +0 -75
  118. package/scripts/lib/timeout.mjs +0 -66
  119. package/scripts/lib/tmp.mjs +0 -26
  120. package/scripts/lib/toml.mjs +0 -132
package/cto/status.mjs CHANGED
@@ -1,9 +1,14 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { join } from "node:path";
4
3
 
5
4
  import { compactHygieneCounts, projectCtoHygiene } from "./hygiene.mjs";
6
- import { resolveLakeRootDir } from "./lake-root.mjs";
5
+ import {
6
+ normalizeLiveSession,
7
+ pathLabel,
8
+ readSynapseSnapshot,
9
+ resolveLakeRootDir,
10
+ shortHash,
11
+ } from "./lake-root.mjs";
7
12
 
8
13
  const SCHEMA_VERSION = "cto-lake.v1";
9
14
  const SYNAPSE_TIMEOUT_MS = 1500;
@@ -20,36 +25,6 @@ function readJson(filePath) {
20
25
  return JSON.parse(readFileSync(filePath, "utf8"));
21
26
  }
22
27
 
23
- function firstExisting(paths) {
24
- return paths.filter(Boolean).find((path) => existsSync(path)) || null;
25
- }
26
-
27
- function toIsoTime(value) {
28
- if (typeof value === "string" && value.trim()) return value;
29
- if (typeof value === "number" && Number.isFinite(value)) {
30
- return new Date(value).toISOString();
31
- }
32
- return null;
33
- }
34
-
35
- // Non-cryptographic djb2-style hash used only to produce stable redacted
36
- // labels for local paths. Do not use this as a trust boundary or secret token.
37
- function shortHash(value) {
38
- const str = String(value ?? "");
39
- let h = 5381;
40
- for (let i = 0; i < str.length; i++) {
41
- h = (h * 33) ^ str.charCodeAt(i);
42
- }
43
- return (h >>> 0).toString(36);
44
- }
45
-
46
- function pathLabel(value) {
47
- const str = String(value ?? "").replace(/[/\\]+$/u, "");
48
- if (!str) return "";
49
- const segments = str.split(/[/\\]+/u);
50
- return segments[segments.length - 1] || "";
51
- }
52
-
53
28
  export function deriveRepoRootFromCwd(cwd) {
54
29
  if (typeof cwd !== "string" || !cwd) return "";
55
30
  if (cwd.includes("\\") || /^[A-Za-z]:/u.test(cwd)) return cwd;
@@ -98,54 +73,6 @@ function redactedCwdFields(cwd) {
98
73
  };
99
74
  }
100
75
 
101
- function normalizeLiveSession(session) {
102
- const cwd = typeof session?.cwd === "string" ? session.cwd : "";
103
- return {
104
- sessionId: String(session?.sessionId || ""),
105
- host: typeof session?.host === "string" ? session.host : "local",
106
- agent_id:
107
- typeof session?.agent_id === "string"
108
- ? session.agent_id
109
- : typeof session?.agentId === "string"
110
- ? session.agentId
111
- : null,
112
- phase:
113
- typeof session?.phase === "string"
114
- ? session.phase
115
- : typeof session?.status === "string"
116
- ? session.status
117
- : "active",
118
- started_at: toIsoTime(
119
- session?.started_at ?? session?.startedAt ?? session?.lastHeartbeat,
120
- ),
121
- ...redactedCwdFields(cwd),
122
- };
123
- }
124
-
125
- function normalizeActiveShard(shard) {
126
- return {
127
- shard_name:
128
- typeof shard?.shard_name === "string"
129
- ? shard.shard_name
130
- : typeof shard?.shardName === "string"
131
- ? shard.shardName
132
- : typeof shard?.name === "string"
133
- ? shard.name
134
- : typeof shard?.id === "string"
135
- ? shard.id
136
- : null,
137
- phase:
138
- typeof shard?.phase === "string"
139
- ? shard.phase
140
- : typeof shard?.status === "string"
141
- ? shard.status
142
- : "active",
143
- members: Array.isArray(shard?.members)
144
- ? shard.members.map((member) => String(member))
145
- : [],
146
- };
147
- }
148
-
149
76
  function normalizeSynapseOverlay(value) {
150
77
  const sessions = Array.isArray(value)
151
78
  ? value
@@ -154,17 +81,15 @@ function normalizeSynapseOverlay(value) {
154
81
  : Array.isArray(value?.live_sessions)
155
82
  ? value.live_sessions
156
83
  : [];
157
- const activeShards = Array.isArray(value?.active_shards)
158
- ? value.active_shards
159
- : Array.isArray(value?.activeShards)
160
- ? value.activeShards
161
- : [];
162
84
 
163
85
  return {
164
86
  live_sessions: sessions
165
- .map(normalizeLiveSession)
87
+ .map((session) => ({
88
+ ...normalizeLiveSession(session),
89
+ host: typeof session?.host === "string" ? session.host : "local",
90
+ ...redactedCwdFields(session?.cwd),
91
+ }))
166
92
  .filter((session) => session.sessionId),
167
- active_shards: activeShards.map(normalizeActiveShard),
168
93
  };
169
94
  }
170
95
 
@@ -182,27 +107,9 @@ function withTimeout(promise, timeoutMs = SYNAPSE_TIMEOUT_MS) {
182
107
  });
183
108
  }
184
109
 
185
- async function readSynapseWithRegistry(opts = {}) {
186
- const rootDir = opts.rootDir || process.cwd();
187
- const persistPath = firstExisting([
188
- opts.synapsePersistPath,
189
- join(rootDir, ".triflux", "synapse-registry.json"),
190
- join(rootDir, ".triflux", "synapse", "registry.json"),
191
- join(homedir(), ".claude", "cache", "tfx-hub", "synapse-sessions.json"),
192
- join(homedir(), ".claude", "cache", "tfx-hub", "synapse-registry.json"),
193
- ]);
194
- if (!persistPath) return { sessions: [], active_shards: [] };
195
-
196
- const { createSynapseRegistry } = await import(
197
- "../hub/team/synapse-registry.mjs"
198
- );
199
- const registry = createSynapseRegistry({ persistPath });
200
- return { sessions: registry.getActive(), active_shards: [] };
201
- }
202
-
203
110
  async function readSynapseOverlay(opts) {
204
111
  try {
205
- const reader = opts.synapseReader || readSynapseWithRegistry;
112
+ const reader = opts.synapseReader || readSynapseSnapshot;
206
113
  const raw = await withTimeout(
207
114
  reader({
208
115
  rootDir: opts.rootDir,
@@ -213,28 +120,10 @@ async function readSynapseOverlay(opts) {
213
120
  );
214
121
  return normalizeSynapseOverlay(raw);
215
122
  } catch {
216
- return { live_sessions: [], active_shards: [] };
123
+ return { live_sessions: [] };
217
124
  }
218
125
  }
219
126
 
220
- // active_shards 의 source 는 live_sessions 와 다르다: live_sessions 는 synapse
221
- // overlay(실시간 세션)에서, active_shards 는 collect 가 .triflux/swarm-locks.json 을
222
- // snapshot 한 current.json(sources.tfx_swarm.detail.shards)에서 온다. synapse
223
- // overlay 가 shard 를 채우는 경로가 생기면 그쪽을 우선하고, 없으면 snapshot 으로
224
- // fallback 한다(현재 overlay 의 active_shards 는 항상 비어 snapshot 을 쓴다).
225
- function deriveActiveShards(current, overlay) {
226
- // overlay(synapse)가 채운 shard 중 식별 가능한 것만 authoritative 로 본다.
227
- // normalizeSynapseOverlay 가 shard_name 을 null 로 정규화한 무효 행이 snapshot
228
- // fallback 을 가로채지 않도록 먼저 거른다.
229
- const overlayShards = Array.isArray(overlay?.active_shards)
230
- ? overlay.active_shards.filter((shard) => shard?.shard_name)
231
- : [];
232
- if (overlayShards.length > 0) return overlayShards;
233
- const shards = current?.sources?.tfx_swarm?.detail?.shards;
234
- if (!Array.isArray(shards)) return [];
235
- return shards.map(normalizeActiveShard).filter((shard) => shard.shard_name);
236
- }
237
-
238
127
  function deriveLiveSessionGroups(liveSessions) {
239
128
  const groups = new Map();
240
129
  for (const session of liveSessions || []) {
@@ -265,7 +154,7 @@ function projectStatus(current, overlay) {
265
154
  ledger_tail: Array.isArray(current?.ledger_tail) ? current.ledger_tail : [],
266
155
  live_sessions: liveSessions,
267
156
  live_session_groups: deriveLiveSessionGroups(liveSessions),
268
- active_shards: deriveActiveShards(current, overlay),
157
+ active_shards: [],
269
158
  hygiene: compactHygieneCounts(projectCtoHygiene({ current, overlay })),
270
159
  };
271
160
  }
@@ -300,13 +189,29 @@ function formatLedgerEntry(entry) {
300
189
  return `${ts} ${event}${summary}`;
301
190
  }
302
191
 
303
- function renderHumanStatus(status) {
192
+ function formatSnapshotAge(generatedAt, now) {
193
+ const ageMs = new Date(now).getTime() - Date.parse(generatedAt);
194
+ if (!Number.isFinite(ageMs)) return "unknown age";
195
+ if (ageMs < 0) return "in the future";
196
+ for (const [unit, ms] of [
197
+ ["d", 86400000],
198
+ ["h", 3600000],
199
+ ["m", 60000],
200
+ ["s", 1000],
201
+ ]) {
202
+ if (ageMs >= ms || unit === "s")
203
+ return `${Math.floor(ageMs / ms)}${unit} ago`;
204
+ }
205
+ }
206
+
207
+ function renderHumanStatus(status, now) {
304
208
  const { available, total } = countAvailableSources(status.sources);
305
209
  const activeGoals = countActiveGoals(status);
306
210
  const recent = Array.isArray(status.ledger_tail)
307
211
  ? status.ledger_tail.slice(-2)
308
212
  : [];
309
213
  const lines = [
214
+ `generated_at: ${status.generated_at || "unknown"} (${formatSnapshotAge(status.generated_at, now)})`,
310
215
  `repo: ${formatRepo(status.repo)}`,
311
216
  `sources: ${available}/${total} available`,
312
217
  `active: ${status.live_sessions.length} sessions, ${status.active_shards.length} shards, ${activeGoals} goals`,
@@ -347,7 +252,7 @@ export async function runStatus(args = [], opts = {}) {
347
252
  const status = projectStatus(current, overlay);
348
253
 
349
254
  if (jsonOut) writeJson(stdout, status);
350
- else stdout.write(renderHumanStatus(status));
255
+ else stdout.write(renderHumanStatus(status, opts.now ?? Date.now()));
351
256
 
352
257
  return status;
353
258
  }
package/hub/index.mjs CHANGED
@@ -11,11 +11,3 @@ export { createPipeServer, getPipePath } from './pipe.mjs';
11
11
  // Team
12
12
  export { orchestrate, decomposeTask, buildLeadPrompt, buildPrompt } from './team/orchestrator.mjs';
13
13
  export { createPsmuxSession, startCapture } from './team/psmux.mjs';
14
- export { createConductor } from './team/conductor.mjs';
15
-
16
- // Swarm
17
- export { createSwarmLocks } from './team/swarm-locks.mjs';
18
- export { parseShards, buildFileLeaseMap, buildMcpManifest, computeMergeOrder, planSwarm } from './team/swarm-planner.mjs';
19
- export { createSwarmHypervisor, SWARM_STATES } from './team/swarm-hypervisor.mjs';
20
- export { reconcile, buildRedundantIds, shouldRunRedundant } from './team/swarm-reconciler.mjs';
21
- export { ensureWorktree, prepareIntegrationBranch, rebaseShardOntoIntegration, pruneWorktree, fetchRemoteShard } from './team/worktree-lifecycle.mjs';
@@ -135,7 +135,7 @@ function attachAdaptiveRuleStore(store) {
135
135
  return statements.pruneRules.run(cutoff, minConfidence).changes;
136
136
  };
137
137
 
138
- // listAdaptiveRules: decayRules/getActiveAdaptiveRules에서 사용
138
+ // listAdaptiveRules: decayRules에서 사용
139
139
  const listStmt = store.db.prepare(
140
140
  "SELECT * FROM adaptive_rules WHERE project_slug = ? ORDER BY confidence DESC",
141
141
  );
@@ -23,7 +23,6 @@
23
23
  "test-engineer": "codex",
24
24
  "qa-tester": "codex",
25
25
  "codex": "codex",
26
- "gemini": "antigravity",
27
26
  "antigravity": "antigravity",
28
27
  "agy": "antigravity",
29
28
  "claude": "claude"
package/hub/team/ansi.mjs CHANGED
@@ -21,9 +21,6 @@ export function moveTo(row, col) {
21
21
  export function moveUp(n = 1) {
22
22
  return `${ESC}[${n}A`;
23
23
  }
24
- export function moveDown(n = 1) {
25
- return `${ESC}[${n}B`;
26
- }
27
24
 
28
25
  // ── 줄 제어 ──
29
26
  export const clearLine = `${ESC}[2K`;
@@ -47,7 +44,7 @@ export const FG = {
47
44
  gray: `${ESC}[90m`,
48
45
  // triflux 브랜드
49
46
  codex: `${ESC}[38;2;16;163;127m`, // #10a37f codex green
50
- gemini: `${ESC}[38;5;39m`, // blue
47
+ antigravity: `${ESC}[38;5;39m`, // blue
51
48
  claude: `${ESC}[38;2;232;112;64m`, // orange
52
49
  triflux: `${ESC}[38;5;214m`, // amber
53
50
  accent: `${ESC}[38;5;75m`, // light blue (Catppuccin blue)
@@ -439,40 +436,9 @@ export function progressBar(percent, width = 20, time) {
439
436
  return `${fillText}${emptyText}`;
440
437
  }
441
438
 
442
- // ── 애니메이션 진행률 바 (shimmer sweep) ──
443
- export function animatedProgressBar(percent, width = 20, tick = 0) {
444
- const ratio = Math.max(0, Math.min(100, percent)) / 100;
445
- const filled = Math.round(ratio * width);
446
- const empty = width - filled;
447
- if (filled === 0 || percent >= 100) return progressBar(percent, width);
448
- const baseClr = percent >= 50 ? MOCHA.partial : MOCHA.fail;
449
- const pos = tick % (filled + 3);
450
- let bar = "";
451
- for (let i = 0; i < filled; i++) {
452
- const d = Math.abs(i - pos);
453
- if (d === 0) bar += `${ESC}[97m█`;
454
- else if (d === 1) bar += `${baseClr}▓`;
455
- else bar += `${baseClr}█`;
456
- }
457
- return `${bar}${MOCHA.border}${"░".repeat(empty)}${RESET}`;
458
- }
459
-
460
- // ── 상태 아이콘 ──
461
439
  export const STATUS_ICON = {
462
440
  running: `${MOCHA.partial}⏳${RESET}`,
463
441
  completed: `${MOCHA.ok}✓${RESET}`,
464
442
  failed: `${MOCHA.fail}✗${RESET}`,
465
443
  pending: `${FG.gray}⏸${RESET}`,
466
444
  };
467
-
468
- export const CLI_ICON = {
469
- codex: `${FG.codex}⚪${RESET}`,
470
- gemini: `${FG.gemini}🔵${RESET}`,
471
- claude: `${FG.claude}🟠${RESET}`,
472
- };
473
-
474
- // ── 로딩 도트 (braille spinner) ──
475
- const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
476
- export function loadingDots(tick = 0, clr = MOCHA.thinking) {
477
- return `${clr}${BRAILLE_FRAMES[tick % BRAILLE_FRAMES.length]}${RESET}`;
478
- }
@@ -1,153 +1,17 @@
1
- // hub/team/backend.mjs — CLI 백엔드 추상화 레이어
2
- // 각 CLI(codex/antigravity/claude)의 명령 빌드 로직을 클래스로 캡슐화한다.
3
- // v7.2.2
4
- import { writeFileSync } from "node:fs";
5
- import { createRequire } from "node:module";
6
-
7
- import { buildExecArgs } from "@triflux/core/hub/codex-adapter.mjs";
8
1
  import { IS_WINDOWS } from "@triflux/core/hub/platform.mjs";
2
+ import { powershellSingleQuote, shellQuote } from "./terminal-opener.mjs";
9
3
 
10
- export function buildGeminiCommand(prompt, resultFile, { isWindows } = {}) {
11
- return buildAntigravityCommand(prompt, resultFile, { isWindows });
12
- }
13
-
14
- export function buildAntigravityCommand(
15
- prompt,
16
- resultFile,
17
- { isWindows } = {},
18
- ) {
19
- // Persist prompt to a sibling file so the shell never interpolates raw
20
- // prompt content. Without this the Unix branch let `$()`, backticks, `\\`,
21
- // and `"` reach the shell verbatim (P1: shell injection).
22
- const promptFile = `${resultFile}.prompt`;
23
- writeFileSync(promptFile, prompt);
24
- // agy 1.1.27 부터 --print 는 값(프롬프트)이 필수이고 stdin 프롬프트는 거부된다.
25
- // 파일 내용을 셸이 재해석하지 않는 형태(따옴표 안 명령 치환 / Get-Content 식)로 값에 넣는다.
26
- if (isWindows) {
27
- return `agy --dangerously-skip-permissions --print (Get-Content -Raw '${promptFile}') > '${resultFile}' 2>'${resultFile}.err'`;
28
- }
29
- return `agy --dangerously-skip-permissions --print "$(cat '${promptFile}')" > '${resultFile}' 2>'${resultFile}.err'`;
30
- }
31
-
32
- const _require = createRequire(import.meta.url);
33
-
34
- // ── 백엔드 클래스 ──────────────────────────────────────────────────────────
35
-
36
- export class CodexBackend {
37
- name() {
38
- return "codex";
39
- }
40
- command() {
41
- return "codex";
42
- }
43
-
44
- /**
45
- * @param {string} prompt — 프롬프트 (프롬프트 파일 경로가 아닌 PowerShell 표현식)
46
- * @param {string} resultFile — 결과 저장 경로
47
- * @param {object} [opts]
48
- * @returns {string} PowerShell 명령 (cls 제외)
49
- */
50
- buildArgs(prompt, resultFile, opts = {}) {
51
- return buildExecArgs({ prompt, resultFile, ...opts });
52
- }
53
-
54
- env() {
55
- return {};
56
- }
57
- }
58
-
59
- export class GeminiBackend {
60
- name() {
61
- return "gemini";
62
- }
63
- command() {
64
- return "agy";
65
- }
66
-
67
- buildArgs(prompt, resultFile, opts = {}) {
68
- return buildGeminiCommand(prompt, resultFile, { isWindows: IS_WINDOWS });
69
- }
70
-
71
- env() {
72
- return {};
4
+ class ClaudeBackend {
5
+ buildArgs(prompt, resultFile, { isWindows = IS_WINDOWS } = {}) {
6
+ const quote = isWindows ? powershellSingleQuote : shellQuote;
7
+ const args = ["--print", prompt, "--output-format", "text"];
8
+ return `claude ${args.map(quote).join(" ")} > ${quote(resultFile)} 2>&1`;
73
9
  }
74
10
  }
75
11
 
76
- export class ClaudeBackend {
77
- name() {
78
- return "claude";
79
- }
80
- command() {
81
- return "claude";
82
- }
83
-
84
- buildArgs(prompt, resultFile, opts = {}) {
85
- return `claude --print ${prompt} --output-format text > '${resultFile}' 2>&1`;
86
- }
12
+ const claude = new ClaudeBackend();
87
13
 
88
- env() {
89
- return {};
90
- }
91
- }
92
-
93
- export class AntigravityBackend {
94
- name() {
95
- return "antigravity";
96
- }
97
- command() {
98
- return "agy";
99
- }
100
-
101
- buildArgs(prompt, resultFile, opts = {}) {
102
- return buildAntigravityCommand(prompt, resultFile, {
103
- isWindows: IS_WINDOWS,
104
- ...opts,
105
- });
106
- }
107
-
108
- env() {
109
- return {};
110
- }
111
- }
112
-
113
- // ── 레지스트리 ─────────────────────────────────────────────────────────────
114
-
115
- /** @type {Map<string, CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend>} */
116
- const backends = new Map([
117
- ["codex", new CodexBackend()],
118
- ["gemini", new GeminiBackend()],
119
- ["claude", new ClaudeBackend()],
120
- ["antigravity", new AntigravityBackend()],
121
- ]);
122
-
123
- /**
124
- * 백엔드 이름으로 조회한다.
125
- * @param {string} name — "codex" | "gemini" | "claude" | "antigravity"
126
- * @returns {CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend}
127
- * @throws {Error} 등록되지 않은 이름
128
- */
129
14
  export function getBackend(name) {
130
- const b = backends.get(name);
131
- if (!b) throw new Error(`지원하지 않는 CLI: ${name}`);
132
- return b;
133
- }
134
-
135
- /**
136
- * 에이전트명 또는 CLI명을 Backend로 해석한다.
137
- * agent-map.json을 통해 에이전트명 → CLI명으로 변환 후 레지스트리에서 조회한다.
138
- * @param {string} agentOrCli — "executor", "codex", "designer" 등
139
- * @returns {CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend}
140
- */
141
- export function getBackendForAgent(agentOrCli) {
142
- const agentMap = _require("./agent-map.json");
143
- const cliName = agentMap[agentOrCli] || agentOrCli;
144
- return getBackend(cliName);
145
- }
146
-
147
- /**
148
- * 등록된 모든 백엔드를 반환한다.
149
- * @returns {Array<CodexBackend|GeminiBackend|ClaudeBackend|AntigravityBackend>}
150
- */
151
- export function listBackends() {
152
- return Array.from(backends.values());
15
+ if (name !== "claude") throw new Error(`지원하지 않는 CLI: ${name}`);
16
+ return claude;
153
17
  }
@@ -6,11 +6,7 @@ import {
6
6
  launchAttachInWindowsTerminal,
7
7
  wantsWtAttachFallback,
8
8
  } from "../services/attach-fallback.mjs";
9
- import {
10
- isNativeMode,
11
- isTeamAlive,
12
- isWtMode,
13
- } from "../services/runtime-mode.mjs";
9
+ import { isTeamAlive } from "../services/runtime-mode.mjs";
14
10
  import { loadTeamState } from "../services/state-store.mjs";
15
11
 
16
12
  export async function teamAttach(args = []) {
@@ -19,18 +15,6 @@ export async function teamAttach(args = []) {
19
15
  console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
20
16
  return;
21
17
  }
22
- if (isNativeMode(state)) {
23
- console.log(
24
- `\n ${DIM}in-process 모드는 별도 attach가 없습니다.${RESET}\n ${DIM}상태 확인: tfx multi status${RESET}\n`,
25
- );
26
- return;
27
- }
28
- if (isWtMode(state)) {
29
- console.log(
30
- `\n ${DIM}wt 모드는 attach 개념이 없습니다 (Windows Terminal pane가 독립 실행됨).${RESET}\n ${DIM}재실행/정리는: tfx multi stop${RESET}\n`,
31
- );
32
- return;
33
- }
34
18
 
35
19
  try {
36
20
  attachSession(state.sessionName);
@@ -1,18 +1,15 @@
1
1
  import { injectPrompt, sendKeys } from "../../pane.mjs";
2
- import { DIM, RESET, WHITE, YELLOW } from "../../shared.mjs";
3
- import { ok, warn } from "../render.mjs";
4
- import { publishLeadControl } from "../services/hub-client.mjs";
2
+ import { DIM, RESET, WHITE } from "../../shared.mjs";
3
+ import { ok } from "../render.mjs";
5
4
  import { resolveMember } from "../services/member-selector.mjs";
6
- import { nativeRequest } from "../services/native-control.mjs";
7
- import {
8
- isNativeMode,
9
- isTeamAlive,
10
- isWtMode,
11
- } from "../services/runtime-mode.mjs";
5
+ import { isTeamAlive } from "../services/runtime-mode.mjs";
12
6
  import { loadTeamState } from "../services/state-store.mjs";
13
7
 
14
8
  export async function teamControl(args = []) {
15
9
  const state = loadTeamState();
10
+ if (state?.teammateMode === "headless") {
11
+ throw new Error("headless 실행에는 지원하지 않는다");
12
+ }
16
13
  if (!state || !isTeamAlive(state)) {
17
14
  console.log(`\n ${DIM}활성 팀 세션 없음${RESET}\n`);
18
15
  return;
@@ -30,35 +27,12 @@ export async function teamControl(args = []) {
30
27
  );
31
28
  return;
32
29
  }
33
- if (isWtMode(state)) {
34
- console.log(
35
- `\n ${YELLOW}⚠${RESET} wt 모드는 Hub direct/control 주입 경로가 비활성입니다.\n ${DIM}수동 제어: 해당 pane에서 직접 명령/인터럽트를 수행하세요.${RESET}\n`,
36
- );
37
- return;
38
- }
39
-
40
- let directOk = false;
41
- if (isNativeMode(state)) {
42
- directOk = !!(
43
- await nativeRequest(state, "/control", {
44
- member: member.name,
45
- command,
46
- reason,
47
- })
48
- )?.ok;
49
- } else {
50
- injectPrompt(
51
- member.pane,
52
- `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`,
53
- );
54
- if (command === "interrupt") sendKeys(member.pane, "C-c");
55
- directOk = true;
56
- }
30
+ injectPrompt(
31
+ member.pane,
32
+ `[LEAD CONTROL] command=${command}${reason ? ` reason=${reason}` : ""}`,
33
+ );
34
+ if (command === "interrupt") sendKeys(member.pane, "C-c");
57
35
 
58
- const published = await publishLeadControl(state, member, command, reason);
59
- if (directOk && published)
60
- ok(`${member.name} 제어 전송 (${command}, direct + hub)`);
61
- else if (directOk) ok(`${member.name} 제어 전송 (${command}, direct only)`);
62
- else warn(`${member.name} 제어 전송 실패 (${command})`);
36
+ ok(`${member.name} 제어 전송 (${command})`);
63
37
  console.log("");
64
38
  }
@@ -2,18 +2,10 @@ import {
2
2
  capturePaneOutput,
3
3
  detectMultiplexer,
4
4
  getSessionAttachedCount,
5
- hasWindowsTerminal,
6
- hasWindowsTerminalSession,
7
5
  listSessions,
8
6
  } from "../../session.mjs";
9
7
  import { AMBER, BOLD, DIM, RESET } from "../../shared.mjs";
10
- import { formatCompletionSuffix } from "../render.mjs";
11
- import { getHubInfo, nativeGetStatus } from "../services/hub-client.mjs";
12
- import {
13
- isNativeMode,
14
- isTeamAlive,
15
- isWtMode,
16
- } from "../services/runtime-mode.mjs";
8
+ import { isTeamAlive } from "../services/runtime-mode.mjs";
17
9
  import { loadTeamState, TEAM_PROFILE } from "../services/state-store.mjs";
18
10
 
19
11
  export async function teamDebug(args = []) {
@@ -23,7 +15,6 @@ export async function teamDebug(args = []) {
23
15
  flagIndex === -1
24
16
  ? 20
25
17
  : Math.max(3, parseInt(args[flagIndex + 1] || "20", 10) || 20);
26
- const hub = await getHubInfo();
27
18
 
28
19
  console.log(`\n ${AMBER}${BOLD}⬡ Team Debug${RESET}\n`);
29
20
  console.log(` platform: ${process.platform}`);
@@ -32,8 +23,6 @@ export async function teamDebug(args = []) {
32
23
  ` tty: stdout=${!!process.stdout.isTTY}, stdin=${!!process.stdin.isTTY}`,
33
24
  );
34
25
  console.log(` mux: ${detectMultiplexer() || "none"}`);
35
- console.log(` hub-pid: ${hub ? `${hub.pid}` : "-"}`);
36
- console.log(` hub-url: ${hub?.url || "-"}`);
37
26
  const sessions = listSessions();
38
27
  console.log(` sessions: ${sessions.length ? sessions.join(", ") : "-"}`);
39
28
 
@@ -53,31 +42,6 @@ export async function teamDebug(args = []) {
53
42
  ` attached: ${getSessionAttachedCount(state.sessionName) ?? "-"}`,
54
43
  );
55
44
 
56
- if (isWtMode(state)) {
57
- console.log(`\n ${BOLD}wt-session${RESET}`);
58
- console.log(` window: ${state?.wt?.windowId ?? 0}`);
59
- console.log(` layout: ${state?.wt?.layout || state?.layout || "-"}`);
60
- console.log(
61
- ` panes: ${state?.wt?.paneCount ?? (state.members || []).length}`,
62
- );
63
- console.log(` wt.exe: ${hasWindowsTerminal() ? "yes" : "no"}`);
64
- console.log(` WT_SESSION:${hasWindowsTerminalSession() ? "yes" : "no"}`);
65
- console.log("");
66
- return;
67
- }
68
-
69
- if (isNativeMode(state)) {
70
- console.log(`\n ${BOLD}native-members${RESET}`);
71
- const members = (await nativeGetStatus(state))?.data?.members || [];
72
- if (!members.length) console.log(` ${DIM}(no data)${RESET}`);
73
- for (const member of members)
74
- console.log(
75
- ` - ${member.name}: ${member.status}${formatCompletionSuffix(member)}${member.lastPreview ? ` ${DIM}${member.lastPreview}${RESET}` : ""}`,
76
- );
77
- console.log("");
78
- return;
79
- }
80
-
81
45
  console.log(
82
46
  `\n ${BOLD}pane-tail${RESET} ${DIM}(last ${lines} lines)${RESET}`,
83
47
  );