@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
@@ -1,727 +0,0 @@
1
- // hub/team/native.mjs — Claude Native Teams 래퍼
2
- // teammate 프롬프트 템플릿 + 팀 설정 빌더
3
- //
4
- // Claude Code 네이티브 Agent Teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)
5
- // 환경에서 teammate를 Codex/Antigravity CLI 래퍼로 구성하는 유틸리티.
6
- // SKILL.md가 인라인 프롬프트를 사용하므로, 이 모듈은 CLI(tfx multi --native)에서
7
- // 팀 설정을 프로그래밍적으로 생성할 때 사용한다.
8
-
9
- import * as fs from "node:fs/promises";
10
- import { createRequire } from "node:module";
11
- import os from "node:os";
12
- import path from "node:path";
13
-
14
- const ROUTE_SCRIPT = "~/.claude/scripts/tfx-route.sh";
15
- export const SLIM_WRAPPER_SUBAGENT_TYPE = "slim-wrapper";
16
- /** scout 역할 기본 설정 — read-only 탐색 전용 */
17
- export const SCOUT_ROLE_CONFIG = {
18
- cli: "codex",
19
- role: "scientist",
20
- mcp_profile: "analyze",
21
- maxIterations: 2,
22
- readOnly: true,
23
- };
24
- const ROUTE_LOG_RE = /\[tfx-route\]/i;
25
- const ROUTE_COMMAND_RE =
26
- /(?:^|[\s"'`])(?:bash\s+)?(?:[^"'`\s]*\/)?tfx-route\.sh\b/i;
27
- const ROUTE_PROMPT_RE = /tfx-route\.sh/i;
28
- const DIRECT_TOOL_BYPASS_RE = /\b(?:Read|Edit|Write)\s*\(/;
29
- const _require = createRequire(import.meta.url);
30
- const AGENT_TO_CLI = _require("./agent-map.json");
31
- const VALID_ROUTE_AGENTS = new Set(Object.keys(AGENT_TO_CLI));
32
-
33
- function inferWorkerIndex(agentName = "") {
34
- const match = /(\d+)(?!.*\d)/.exec(agentName);
35
- if (!match) return null;
36
- const index = Number(match[1]);
37
- return Number.isInteger(index) && index > 0 ? index : null;
38
- }
39
-
40
- function normalizeRouteMode(cli) {
41
- const normalized = String(cli || "").trim();
42
- if (["gemini", "antigravity", "agy"].includes(normalized)) {
43
- return "antigravity";
44
- }
45
- if (normalized === "codex") return "codex";
46
- return "";
47
- }
48
-
49
- function resolveRouteCli(cli) {
50
- const normalized = String(cli || "").trim();
51
- const mapped = AGENT_TO_CLI[normalized] || normalized;
52
- return mapped === "gemini" ? "antigravity" : mapped;
53
- }
54
-
55
- function resolveSlimWrapperRouteAgent(cli, role) {
56
- const routeRole = String(role || "").trim();
57
- if (
58
- routeRole &&
59
- VALID_ROUTE_AGENTS.has(routeRole) &&
60
- !["gemini", "antigravity", "agy"].includes(routeRole)
61
- ) {
62
- return routeRole;
63
- }
64
-
65
- const resolvedCli = resolveRouteCli(cli);
66
- return VALID_ROUTE_AGENTS.has(resolvedCli) ? resolvedCli : "codex";
67
- }
68
-
69
- function buildRouteEnvPrefix(agentName, workerIndex, searchTool, cli) {
70
- const effectiveWorkerIndex =
71
- Number.isInteger(workerIndex) && workerIndex > 0
72
- ? workerIndex
73
- : inferWorkerIndex(agentName);
74
- const routeMode = normalizeRouteMode(cli);
75
-
76
- let envPrefix = "";
77
- if (routeMode) envPrefix += ` TFX_CLI_MODE="${routeMode}"`;
78
- if (effectiveWorkerIndex)
79
- envPrefix += ` TFX_WORKER_INDEX="${effectiveWorkerIndex}"`;
80
- if (searchTool) envPrefix += ` TFX_SEARCH_TOOL="${searchTool}"`;
81
- return envPrefix;
82
- }
83
-
84
- /**
85
- * slim-wrapper 커스텀 subagent 사양.
86
- * Claude Code custom subagent(`.claude/agents/slim-wrapper.md`)와 짝을 이룬다.
87
- *
88
- * @param {'codex'|'gemini'} cli
89
- * @param {object} opts
90
- * @returns {{name:string, cli:string, subagent_type:string, prompt:string}}
91
- */
92
- export function buildSlimWrapperAgent(cli, opts = {}) {
93
- return {
94
- name: opts.agentName || `${cli}-wrapper`,
95
- cli,
96
- subagent_type: SLIM_WRAPPER_SUBAGENT_TYPE,
97
- prompt: buildSlimWrapperPrompt(cli, opts),
98
- };
99
- }
100
-
101
- /**
102
- * slim-wrapper 로그에서 tfx-route.sh 경유 흔적을 판정한다.
103
- * route stderr prefix(`[tfx-route]`) 또는 Bash command trace를 근거로 본다.
104
- *
105
- * @param {object} input
106
- * @param {string} [input.promptText]
107
- * @param {string} [input.stdoutText]
108
- * @param {string} [input.stderrText]
109
- * @returns {{
110
- * expectedRouteInvocation: boolean,
111
- * promptMentionsRoute: boolean,
112
- * sawRouteCommand: boolean,
113
- * sawRouteLog: boolean,
114
- * sawDirectToolBypass: boolean,
115
- * usedRoute: boolean,
116
- * abnormal: boolean,
117
- * reason: string|null,
118
- * slopDetected: boolean,
119
- * }}
120
- */
121
- export function verifySlimWrapperRouteExecution(input = {}) {
122
- const promptText = String(input.promptText || "");
123
- const stdoutText = String(input.stdoutText || "");
124
- const stderrText = String(input.stderrText || "");
125
- const combinedLogs = `${stdoutText}\n${stderrText}`;
126
- const promptMentionsRoute = ROUTE_PROMPT_RE.test(promptText);
127
- const sawRouteCommand = ROUTE_COMMAND_RE.test(combinedLogs);
128
- const sawRouteLog = ROUTE_LOG_RE.test(combinedLogs);
129
- const sawDirectToolBypass = DIRECT_TOOL_BYPASS_RE.test(stdoutText);
130
- const usedRoute = sawRouteCommand || sawRouteLog;
131
- const expectedRouteInvocation = promptMentionsRoute;
132
- const abnormal =
133
- expectedRouteInvocation && (sawDirectToolBypass || !usedRoute);
134
- const reason = !abnormal
135
- ? null
136
- : sawDirectToolBypass
137
- ? "direct_tool_bypass_detected"
138
- : "missing_tfx_route_evidence";
139
- const slopDetected = detectSlop(stdoutText);
140
-
141
- return {
142
- expectedRouteInvocation,
143
- promptMentionsRoute,
144
- sawRouteCommand,
145
- sawRouteLog,
146
- sawDirectToolBypass,
147
- usedRoute,
148
- abnormal,
149
- reason,
150
- slopDetected,
151
- };
152
- }
153
-
154
- /**
155
- * role/mcp_profile별 tfx-route.sh 기본 timeout (초)
156
- * analyze/review 프로필이나 설계·분석 역할은 더 긴 timeout을 부여한다.
157
- * @param {string} role — 워커 역할
158
- * @param {string} mcpProfile — MCP 프로필
159
- * @returns {number} timeout(초)
160
- */
161
- function getRouteTimeout(role, _mcpProfile) {
162
- // tfx-route.sh route_agent()의 DEFAULT_TIMEOUT 기반, 최소 1080초(18분) 보장.
163
- // Bash timeout = 이 값 + 60초 여유. 짧은 역할도 네트워크/스케줄 지연 대비.
164
- const TIMEOUTS = {
165
- "build-fixer": 1080,
166
- debugger: 1080,
167
- executor: 1080,
168
- "deep-executor": 3600,
169
- architect: 3600,
170
- planner: 3600,
171
- critic: 3600,
172
- analyst: 3600,
173
- scientist: 1800,
174
- "scientist-deep": 3600,
175
- "document-specialist": 1800,
176
- "code-reviewer": 1800,
177
- "security-reviewer": 1800,
178
- "quality-reviewer": 1800,
179
- verifier: 1800,
180
- designer: 1080,
181
- writer: 1080,
182
- explore: 1080,
183
- "test-engineer": 1080,
184
- "qa-tester": 1080,
185
- spark: 600,
186
- };
187
- return TIMEOUTS[role] || 1080;
188
- }
189
-
190
- /**
191
- * v3 슬림 래퍼 프롬프트 생성 (async 모드)
192
- * --async로 즉시 시작 → --job-wait로 내부 폴링 → --job-result로 결과 수집.
193
- * Claude Code Bash 도구 600초 제한을 우회하여 scientist(24분), scientist-deep(60분) 등
194
- * 장시간 워커를 안정적으로 실행한다.
195
- *
196
- * @param {'codex'|'gemini'} cli — CLI 타입
197
- * @param {object} opts
198
- * @param {string} opts.subtask — 서브태스크 설명
199
- * @param {string} [opts.role] — 역할 (executor, designer, reviewer 등)
200
- * @param {string} [opts.teamName] — 팀 이름
201
- * @param {string} [opts.taskId] — Hub task ID
202
- * @param {string} [opts.agentName] — 워커 표시 이름
203
- * @param {string} [opts.leadName] — 리드 수신자 이름
204
- * @param {string} [opts.mcp_profile] — MCP 프로필
205
- * @param {number} [opts.workerIndex] — 검색 힌트 회전에 사용할 워커 인덱스(1-based)
206
- * @param {string} [opts.searchTool] — 전용 검색 도구 힌트(brave-search|tavily|exa)
207
- * @param {number} [opts.bashTimeout] — (deprecated, async에서는 무시됨)
208
- * @param {number} [opts.maxIterations=3] — 피드백 루프 최대 반복 횟수
209
- * @returns {string} 슬림 래퍼 프롬프트
210
- */
211
- export function buildSlimWrapperPrompt(cli, opts = {}) {
212
- const {
213
- subtask,
214
- role = "executor",
215
- teamName = "tfx-multi",
216
- taskId = "",
217
- agentName = "",
218
- leadName = "team-lead",
219
- mcp_profile = "auto",
220
- workerIndex,
221
- searchTool = "",
222
- pipelinePhase = "",
223
- maxIterations = 3,
224
- } = opts;
225
-
226
- const routeAgent = resolveSlimWrapperRouteAgent(cli, role);
227
- const routeTimeoutSec = getRouteTimeout(routeAgent, mcp_profile);
228
- const escaped = subtask.replace(/'/g, "'\\''");
229
- const pipelineHint = pipelinePhase
230
- ? `\n파이프라인 단계: ${pipelinePhase}`
231
- : "";
232
- const routeEnvPrefix = buildRouteEnvPrefix(
233
- agentName,
234
- workerIndex,
235
- searchTool,
236
- cli,
237
- );
238
- const scoutConstraint =
239
- role === "scout" || role === "scientist"
240
- ? "\n이 워커는 scout(탐색 전용)이다. 코드를 수정하거나 파일을 생성하지 마라. 기존 코드를 읽고 분석하여 보고만 하라."
241
- : "";
242
-
243
- // Bash 도구 timeout (모두 600초 이내)
244
- const launchTimeoutMs = 15000; // Step 1: fork + job_id 반환
245
- const waitTimeoutMs = 570000; // Step 2: 내부 폴링 (540초 대기 + 여유)
246
- const resultTimeoutMs = 30000; // Step 3: 결과 읽기
247
-
248
- return `실행 프로토콜 (subagent_type="${SLIM_WRAPPER_SUBAGENT_TYPE}", async + feedback):
249
- MAX_ITERATIONS = ${maxIterations}
250
- ITERATION = 0${pipelineHint}
251
-
252
- Step 0 — 시작 보고 (턴 경계 생성):
253
- TaskUpdate(taskId: "${taskId}", status: "in_progress")
254
- SendMessage(type: "message", recipient: "${leadName}", content: "작업 시작: ${agentName}", summary: "task ${taskId} started")
255
-
256
- [HARD CONSTRAINT] 허용 도구: Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용한다.
257
- Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
258
- 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Antigravity에 위임하라.
259
- 이 규칙을 위반하면 작업 실패로 간주한다.${scoutConstraint}
260
-
261
- gemini/codex를 직접 호출하지 마라. 반드시 tfx-route.sh를 거쳐야 한다.
262
- 프롬프트를 파일로 저장하지 마라. tfx-route.sh가 인자로 받는다.
263
-
264
- Step 1 — Async 시작 (즉시 리턴, <1초):
265
- Bash(command: 'TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} --async "${routeAgent}" '"'"'${escaped}'"'"' ${mcp_profile} ${routeTimeoutSec}', timeout: ${launchTimeoutMs})
266
- → 출력 한 줄이 JOB_ID이다. 반드시 기억하라.
267
-
268
- Step 2 — 완료 대기 (내부 폴링, 최대 540초):
269
- Bash(command: 'bash ${ROUTE_SCRIPT} --job-wait JOB_ID 540', timeout: ${waitTimeoutMs})
270
- → 주기적 "waiting elapsed=Ns progress=NB" 출력 후 최종 상태:
271
- "done" → Step 3으로
272
- "timeout" 또는 "failed ..." → Step 4로 (실패 상태로)
273
- "still_running ..." → Step 2 반복 (같은 명령 재실행)
274
-
275
- Step 3 — 결과 수집:
276
- Bash(command: 'bash ${ROUTE_SCRIPT} --job-result JOB_ID', timeout: ${resultTimeoutMs})
277
- → RESULT에 저장.
278
-
279
- Step 4 — 결과 보고 (턴 경계 생성, TaskUpdate 하지 않음):
280
- "done"이면:
281
- SendMessage(type: "message", recipient: "${leadName}", content: "결과 (iteration ITERATION): ${agentName} 성공\\n{결과 요약}", summary: "task ${taskId} iteration ITERATION done")
282
- "timeout" 또는 "failed"이면:
283
- SendMessage(type: "message", recipient: "${leadName}", content: "결과 (iteration ITERATION): ${agentName} 실패\\n{에러 요약}", summary: "task ${taskId} iteration ITERATION failed")
284
- TFX_NEEDS_FALLBACK 출력 감지 시:
285
- → Step 6으로 즉시 이동 (fallback은 재실행 불가)
286
-
287
- Step 5 — 피드백 대기:
288
- SendMessage 후 너는 IDLE 상태가 된다. 리드의 응답을 기다려라.
289
- 수신 메시지에 따라:
290
- - "재실행:" 포함 → ITERATION++ → ITERATION < MAX_ITERATIONS이면 메시지의 지시를 반영하여 Step 1로. ITERATION >= MAX_ITERATIONS이면 Step 6으로 (반복 한도 초과)
291
- - "승인" 또는 기타 → Step 6으로
292
- - 메시지 없이 팀이 삭제되면 자동 종료 (처리 불필요)
293
-
294
- Step 6 — 최종 종료 (반드시 실행):
295
- TaskUpdate(taskId: "${taskId}", status: "completed", metadata: {result: "success"|"failed"|"fallback", iterations: ITERATION})
296
- SendMessage(type: "message", recipient: "${leadName}", content: "최종 완료: ${agentName} (ITERATION회 실행)", summary: "task ${taskId} final")
297
- → 종료. 이후 추가 도구 호출 금지.`;
298
- }
299
-
300
- /**
301
- * scout 파견용 프롬프트 생성
302
- * @param {object} opts
303
- * @param {string} opts.question — 탐색 질문
304
- * @param {string} [opts.scope] — 탐색 범위 힌트 (파일 패턴)
305
- * @param {string} [opts.teamName] — 팀 이름
306
- * @param {string} [opts.taskId] — 태스크 ID
307
- * @param {string} [opts.agentName] — 에이전트 이름
308
- * @param {string} [opts.leadName] — 리드 이름
309
- * @returns {string} slim wrapper 프롬프트
310
- */
311
- export function buildScoutDispatchPrompt(opts = {}) {
312
- const { question, scope = "", teamName, taskId, agentName, leadName } = opts;
313
- const subtask = scope ? `${question} 탐색 범위: ${scope}` : question;
314
- return buildSlimWrapperPrompt("codex", {
315
- subtask,
316
- role: "scientist",
317
- teamName,
318
- taskId,
319
- agentName,
320
- leadName,
321
- mcp_profile: "analyze",
322
- maxIterations: SCOUT_ROLE_CONFIG.maxIterations,
323
- });
324
- }
325
-
326
- /**
327
- * v3 하이브리드 래퍼 프롬프트 생성
328
- * psmux pane 기반 비동기 실행 + polling 패턴.
329
- * Agent가 idle 상태를 유지하여 인터럽트 수신이 가능하다.
330
- *
331
- * @param {'codex'|'gemini'} cli — CLI 타입
332
- * @param {object} opts
333
- * @param {string} opts.subtask — 서브태스크 설명
334
- * @param {string} [opts.role] — 역할
335
- * @param {string} [opts.teamName] — 팀 이름
336
- * @param {string} [opts.taskId] — Hub task ID
337
- * @param {string} [opts.agentName] — 워커 표시 이름
338
- * @param {string} [opts.leadName] — 리드 수신자 이름
339
- * @param {string} [opts.mcp_profile] — MCP 프로필
340
- * @param {number} [opts.workerIndex] — 검색 힌트 회전에 사용할 워커 인덱스(1-based)
341
- * @param {string} [opts.searchTool] — 전용 검색 도구 힌트(brave-search|tavily|exa)
342
- * @param {string} [opts.sessionName] — psmux 세션 이름
343
- * @param {string} [opts.pipelinePhase] — 파이프라인 단계
344
- * @param {string} [opts.psmuxPath] — psmux.mjs 경로
345
- * @returns {string} 하이브리드 래퍼 프롬프트
346
- */
347
- export function buildHybridWrapperPrompt(cli, opts = {}) {
348
- const {
349
- subtask,
350
- role = "executor",
351
- teamName = "tfx-multi",
352
- taskId = "",
353
- agentName = "",
354
- leadName = "team-lead",
355
- mcp_profile = "auto",
356
- workerIndex,
357
- searchTool = "",
358
- sessionName = teamName,
359
- pipelinePhase = "",
360
- psmuxPath = "hub/team/psmux.mjs",
361
- } = opts;
362
-
363
- const escaped = subtask.replace(/'/g, "'\\''");
364
- const pipelineHint = pipelinePhase
365
- ? `\n파이프라인 단계: ${pipelinePhase}`
366
- : "";
367
- const taskIdRef = taskId ? `taskId: "${taskId}"` : "";
368
- const taskIdArg = taskIdRef ? `${taskIdRef}, ` : "";
369
- const routeEnvPrefix = buildRouteEnvPrefix(
370
- agentName,
371
- workerIndex,
372
- searchTool,
373
- cli,
374
- );
375
- const routeAgent = resolveSlimWrapperRouteAgent(cli, role);
376
-
377
- const routeCmd = `TFX_TEAM_NAME="${teamName}" TFX_TEAM_TASK_ID="${taskId}" TFX_TEAM_AGENT_NAME="${agentName}" TFX_TEAM_LEAD_NAME="${leadName}"${routeEnvPrefix} bash ${ROUTE_SCRIPT} "${routeAgent}" '${escaped}' ${mcp_profile}`;
378
-
379
- return `하이브리드 psmux 워커 프로토콜:
380
-
381
- 1. TaskUpdate(${taskIdArg}status: in_progress) + SendMessage(to: ${leadName}, "작업 시작: ${agentName}")
382
-
383
- 2. pane 생성 (비동기 실행):
384
- Bash: node ${psmuxPath} spawn --session "${sessionName}" --name "${agentName}" --cmd "${routeCmd}"
385
-
386
- 3. 폴링 루프 (10초 간격, idle 유지 → 인터럽트 수신 가능):
387
- Bash: node ${psmuxPath} status --session "${sessionName}" --name "${agentName}"
388
- - status: "running" → 10초 대기 후 재확인
389
- - status: "exited" → 5단계로
390
-
391
- 4. 인터럽트 수신 시:
392
- Bash: node ${psmuxPath} kill --session "${sessionName}" --name "${agentName}"
393
- → SendMessage(to: ${leadName}, "인터럽트 수신, 방향 전환")
394
- → 새 지시에 따라 2단계부터 재실행
395
-
396
- 5. 완료 시:
397
- Bash: node ${psmuxPath} output --session "${sessionName}" --name "${agentName}" --lines 100
398
- → 결과를 TaskUpdate + SendMessage로 보고
399
- ${pipelineHint}
400
- [HARD CONSTRAINT] 너는 Bash, TaskUpdate, TaskGet, TaskList, SendMessage만 사용할 수 있다.
401
- Read, Edit, Write, Grep, Glob, Agent, WebSearch, WebFetch 등 다른 모든 도구 사용을 금지한다.
402
- 코드를 직접 읽거나 수정하면 안 된다. 반드시 아래 Bash 명령(tfx-route.sh)을 통해 Codex/Antigravity에 위임하라.
403
- 이 규칙을 위반하면 작업 실패로 간주한다.
404
-
405
- gemini/codex를 직접 호출하지 마라. psmux spawn이 tfx-route.sh를 통해 실행한다.
406
- 프롬프트를 파일로 저장하지 마라. psmux spawn --cmd 인자로 전달된다.
407
-
408
- 성공 → TaskUpdate(${taskIdArg}status: completed, metadata: {result: "success"}) + SendMessage(to: ${leadName}).
409
- 실패 → TaskUpdate(${taskIdArg}status: completed, metadata: {result: "failed", error: "에러 요약"}) + SendMessage(to: ${leadName}).
410
-
411
- 중요: TaskUpdate의 status는 "completed"만 사용. "failed"는 API 미지원.
412
- 실패 여부는 metadata.result로 구분. pane 실패 시에도 반드시 TaskUpdate + SendMessage 후 종료.`;
413
- }
414
-
415
- /**
416
- * tfx-route.sh가 남긴 로컬 결과 파일을 폴링해서 완료/대기 태스크를 분리한다.
417
- * SendMessage 전달 지연이 있더라도 Phase 4에서 파일 기반으로 완료를 빠르게 감지하기 위한 보조 경로다.
418
- *
419
- * @param {string} teamName
420
- * @param {string[]} expectedTaskIds
421
- * @returns {Promise<{completed:Array<{taskId:string,result:string,summary:string}>, pending:string[]}>}
422
- */
423
- export async function pollTeamResults(teamName, expectedTaskIds = []) {
424
- const normalizedTaskIds = Array.from(
425
- new Set(
426
- (Array.isArray(expectedTaskIds) ? expectedTaskIds : [])
427
- .map((taskId) => String(taskId || "").trim())
428
- .filter(Boolean),
429
- ),
430
- );
431
-
432
- if (!normalizedTaskIds.length) {
433
- return { completed: [], pending: [] };
434
- }
435
-
436
- const normalizedTeamName = String(teamName || "").trim();
437
- if (!normalizedTeamName) {
438
- return { completed: [], pending: normalizedTaskIds };
439
- }
440
-
441
- const resultDir = path.join(
442
- os.homedir(),
443
- ".claude",
444
- "tfx-results",
445
- normalizedTeamName,
446
- );
447
-
448
- let entries;
449
- try {
450
- entries = await fs.readdir(resultDir, { withFileTypes: true });
451
- } catch (error) {
452
- if (error && error.code === "ENOENT") {
453
- return { completed: [], pending: normalizedTaskIds };
454
- }
455
- throw error;
456
- }
457
-
458
- const availableFiles = new Set(
459
- entries.filter((entry) => entry.isFile()).map((entry) => entry.name),
460
- );
461
-
462
- const completedCandidates = await Promise.all(
463
- normalizedTaskIds.map(async (taskId) => {
464
- const fileName = `${taskId}.json`;
465
- if (!availableFiles.has(fileName)) return null;
466
-
467
- try {
468
- const raw = await fs.readFile(path.join(resultDir, fileName), "utf8");
469
- const parsed = JSON.parse(raw);
470
- return {
471
- taskId,
472
- result: typeof parsed?.result === "string" ? parsed.result : "failed",
473
- summary: typeof parsed?.summary === "string" ? parsed.summary : "",
474
- };
475
- } catch (error) {
476
- if (error && error.code === "ENOENT") return null;
477
- return {
478
- taskId,
479
- result: "failed",
480
- summary: "결과 파일 파싱 실패",
481
- };
482
- }
483
- }),
484
- );
485
-
486
- const completed = completedCandidates.filter(Boolean);
487
- const completedTaskIds = new Set(completed.map((item) => item.taskId));
488
- const pending = normalizedTaskIds.filter(
489
- (taskId) => !completedTaskIds.has(taskId),
490
- );
491
-
492
- return { completed, pending };
493
- }
494
-
495
- /**
496
- * 폴링 결과를 진행률 한 줄 요약으로 바꾼다.
497
- *
498
- * @param {{completed?:Array<{taskId:string,result:string}>, pending?:string[]}} pollResult
499
- * @returns {string}
500
- */
501
- export function formatPollReport(pollResult = {}) {
502
- const completed = Array.isArray(pollResult.completed)
503
- ? pollResult.completed
504
- : [];
505
- const pending = Array.isArray(pollResult.pending) ? pollResult.pending : [];
506
- const total = completed.length + pending.length;
507
-
508
- if (total === 0) return "0/0 완료";
509
-
510
- const detail = completed
511
- .map(({ taskId, result }) => `${taskId} ${result || "unknown"}`)
512
- .join(", ");
513
-
514
- return detail
515
- ? `${completed.length}/${total} 완료 (${detail})`
516
- : `${completed.length}/${total} 완료`;
517
- }
518
-
519
- // ── Anti-slop 필터링 ────────────────────────────────────────────
520
-
521
- /**
522
- * 문자열을 정규화: 소문자 변환 + 연속 공백을 단일 공백으로 + trim
523
- * @param {string} s
524
- * @returns {string}
525
- */
526
- function normalizeText(s) {
527
- return String(s || "")
528
- .toLowerCase()
529
- .replace(/\s+/g, " ")
530
- .trim();
531
- }
532
-
533
- /**
534
- * 두 문자열의 단어 집합 Jaccard 유사도를 계산한다.
535
- * @param {string} a — 정규화된 문자열
536
- * @param {string} b — 정규화된 문자열
537
- * @returns {number} 0.0–1.0
538
- */
539
- function jaccardSimilarity(a, b) {
540
- const setA = new Set(a.split(" ").filter(Boolean));
541
- const setB = new Set(b.split(" ").filter(Boolean));
542
- if (setA.size === 0 && setB.size === 0) return 1.0;
543
- if (setA.size === 0 || setB.size === 0) return 0.0;
544
- let intersection = 0;
545
- for (const w of setA) {
546
- if (setB.has(w)) intersection++;
547
- }
548
- return intersection / (setA.size + setB.size - intersection);
549
- }
550
-
551
- /**
552
- * findings 배열에서 중복을 제거한다.
553
- * 정규화 후 description의 Jaccard 유사도 >= 0.8이면 중복으로 판정.
554
- * 동일 file+line인 경우도 중복 후보로 취급.
555
- *
556
- * @param {Array<{description:string, file?:string, line?:number, severity?:string}>} findings
557
- * @returns {Array<{description:string, file?:string, line?:number, severity?:string, occurrences:number}>}
558
- */
559
- export function deduplicateFindings(findings) {
560
- if (!Array.isArray(findings) || findings.length === 0) return [];
561
-
562
- const groups = []; // [{canonical, items:[]}]
563
-
564
- for (const f of findings) {
565
- const norm = normalizeText(f.description);
566
- let merged = false;
567
- for (const g of groups) {
568
- if (jaccardSimilarity(norm, g.norm) >= 0.8) {
569
- g.items.push(f);
570
- merged = true;
571
- break;
572
- }
573
- }
574
- if (!merged) {
575
- groups.push({ norm, canonical: f, items: [f] });
576
- }
577
- }
578
-
579
- return groups.map((g) => ({
580
- description: g.canonical.description,
581
- ...(g.canonical.file != null ? { file: g.canonical.file } : {}),
582
- ...(g.canonical.line != null ? { line: g.canonical.line } : {}),
583
- ...(g.canonical.severity != null ? { severity: g.canonical.severity } : {}),
584
- occurrences: g.items.length,
585
- }));
586
- }
587
-
588
- /**
589
- * scout 보고서 원문을 핵심 발견 사항만 추출하여 압축한다.
590
- * 파일:라인 + 한줄 요약 형태로 변환하며 최대 ~500토큰(2000자) 이하로 제한.
591
- *
592
- * @param {string} rawReport — 자유형 텍스트
593
- * @returns {{findings: Array<{file:string, line:string, summary:string}>, summary:string, tokenEstimate:number}}
594
- */
595
- export function compressScoutReport(rawReport) {
596
- const MAX_CHARS = 2000;
597
- const text = String(rawReport || "");
598
-
599
- // 파일:라인 패턴 추출 (path/to/file.ext:123 형태 + 뒤따르는 설명)
600
- const fileLineRe =
601
- /([a-zA-Z0-9_./-]+\.[a-zA-Z]{1,10}):(\d+)\s*[:\-–—]?\s*(.+)/g;
602
- const findings = [];
603
- let match;
604
- while ((match = fileLineRe.exec(text)) !== null) {
605
- findings.push({
606
- file: match[1],
607
- line: match[2],
608
- summary: match[3].trim().slice(0, 120),
609
- });
610
- }
611
-
612
- // 문장 단위로 핵심 요약 구성
613
- const sentences = text
614
- .split(/[.\n]/)
615
- .map((s) => s.trim())
616
- .filter((s) => s.length > 10);
617
- let summary = sentences.slice(0, 5).join(". ");
618
-
619
- // 토큰 추정: ~4자 = 1토큰
620
- const estimateTokens = (s) => Math.ceil(s.length / 4);
621
-
622
- // findings를 먼저 계산하고, 남은 공간에 맞춰 summary를 자른다
623
- const findingsJson = JSON.stringify(findings);
624
- const findingsBudget = Math.min(
625
- findingsJson.length,
626
- Math.floor(MAX_CHARS * 0.3),
627
- );
628
-
629
- let trimmedFindings = findings;
630
- if (findingsJson.length > findingsBudget && findings.length > 0) {
631
- trimmedFindings = [];
632
- let used = 2; // []
633
- for (const f of findings) {
634
- const entryLen = JSON.stringify(f).length + 1;
635
- if (used + entryLen > findingsBudget) break;
636
- trimmedFindings.push(f);
637
- used += entryLen;
638
- }
639
- }
640
-
641
- const summaryBudget = MAX_CHARS - JSON.stringify(trimmedFindings).length;
642
- if (summary.length > summaryBudget) {
643
- summary = summary.slice(0, Math.max(0, summaryBudget - 3)) + "...";
644
- }
645
-
646
- return {
647
- findings: trimmedFindings,
648
- summary,
649
- tokenEstimate: estimateTokens(summary + JSON.stringify(trimmedFindings)),
650
- };
651
- }
652
-
653
- /**
654
- * 여러 scout 보고서의 발견 사항을 종합하여 가중 신뢰도를 계산한다.
655
- * 동일 발견이 여러 scout에서 보고되면 신뢰도가 높다.
656
- *
657
- * @param {Array<{agentName:string, findings:Array<{description:string}>}>} scoutReports
658
- * @returns {Array<{description:string, confidence:number, reporters:string[]}>}
659
- */
660
- export function weightedConsensus(scoutReports) {
661
- if (!Array.isArray(scoutReports) || scoutReports.length === 0) return [];
662
-
663
- const totalScouts = scoutReports.length;
664
- // {normDesc -> {description, reporters: Set}}
665
- const consensusMap = new Map();
666
-
667
- for (const report of scoutReports) {
668
- const agent = String(report.agentName || "unknown");
669
- const findings = Array.isArray(report.findings) ? report.findings : [];
670
- for (const f of findings) {
671
- const norm = normalizeText(f.description);
672
- let matched = false;
673
- for (const [key, entry] of consensusMap) {
674
- if (jaccardSimilarity(norm, key) >= 0.8) {
675
- entry.reporters.add(agent);
676
- matched = true;
677
- break;
678
- }
679
- }
680
- if (!matched) {
681
- consensusMap.set(norm, {
682
- description: f.description,
683
- reporters: new Set([agent]),
684
- });
685
- }
686
- }
687
- }
688
-
689
- return Array.from(consensusMap.values()).map((entry) => ({
690
- description: entry.description,
691
- confidence: Math.round((entry.reporters.size / totalScouts) * 100) / 100,
692
- reporters: Array.from(entry.reporters),
693
- }));
694
- }
695
-
696
- // ── Slop detection helper ───────────────────────────────────────
697
-
698
- const SLOP_REPEAT_THRESHOLD = 3;
699
-
700
- /**
701
- * 텍스트에서 동일 패턴이 SLOP_REPEAT_THRESHOLD회 이상 반복되는지 판정한다.
702
- * 줄 단위로 정규화하여 비교.
703
- * @param {string} text
704
- * @returns {boolean}
705
- */
706
- function detectSlop(text) {
707
- const lines = String(text || "")
708
- .split("\n")
709
- .map(normalizeText)
710
- .filter((l) => l.length > 15); // 너무 짧은 줄은 무시
711
- const counts = new Map();
712
- for (const line of lines) {
713
- counts.set(line, (counts.get(line) || 0) + 1);
714
- if (counts.get(line) >= SLOP_REPEAT_THRESHOLD) return true;
715
- }
716
- return false;
717
- }
718
-
719
- /**
720
- * 팀 이름 생성 (타임스탬프 기반)
721
- * @returns {string}
722
- */
723
- export function generateTeamName() {
724
- const ts = Date.now().toString(36).slice(-4);
725
- const rand = Math.random().toString(36).slice(2, 6);
726
- return `tfx-${ts}${rand}`;
727
- }