@triflux/remote 10.50.2 → 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 (97) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +0 -73
  3. package/cto/current.schema.json +0 -4
  4. package/cto/status.mjs +2 -50
  5. package/hub/index.mjs +0 -8
  6. package/hub/team/agent-map.json +0 -1
  7. package/hub/team/ansi.mjs +1 -35
  8. package/hub/team/backend.mjs +9 -145
  9. package/hub/team/cli/commands/attach.mjs +1 -17
  10. package/hub/team/cli/commands/control.mjs +12 -38
  11. package/hub/team/cli/commands/debug.mjs +1 -37
  12. package/hub/team/cli/commands/focus.mjs +5 -31
  13. package/hub/team/cli/commands/interrupt.mjs +6 -25
  14. package/hub/team/cli/commands/kill.mjs +1 -30
  15. package/hub/team/cli/commands/list.mjs +0 -16
  16. package/hub/team/cli/commands/send.mjs +6 -26
  17. package/hub/team/cli/commands/start/index.mjs +41 -133
  18. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  19. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  20. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  21. package/hub/team/cli/commands/status.mjs +2 -57
  22. package/hub/team/cli/commands/stop.mjs +14 -19
  23. package/hub/team/cli/help.mjs +4 -5
  24. package/hub/team/cli/manifest.mjs +5 -1
  25. package/hub/team/cli/render.mjs +0 -12
  26. package/hub/team/cli/services/hub-client.mjs +2 -88
  27. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  28. package/hub/team/handoff.mjs +3 -34
  29. package/hub/team/headless.mjs +13 -754
  30. package/hub/team/orchestrator.mjs +4 -30
  31. package/hub/team/pane.mjs +6 -28
  32. package/hub/team/psmux.mjs +0 -1
  33. package/hub/team/remote-session.mjs +1 -2
  34. package/hub/team/session.mjs +7 -91
  35. package/hub/team/swarm-intent.mjs +0 -198
  36. package/hub/team/tui-core.mjs +1 -1
  37. package/hub/team/tui-viewer.mjs +2 -2
  38. package/hub/team/tui.mjs +8 -269
  39. package/hub/team/wt-manager.mjs +2 -2
  40. package/hub/workers/factory.mjs +4 -37
  41. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  42. package/package.json +1 -1
  43. package/scripts/lib/agent-route-policy.mjs +162 -14
  44. package/scripts/lib/claudemd-scanner.mjs +2 -199
  45. package/scripts/lib/cli-claude.mjs +0 -2
  46. package/scripts/lib/context.mjs +3 -19
  47. package/scripts/lib/env-probe.mjs +5 -64
  48. package/scripts/lib/gemini-profiles.mjs +2 -69
  49. package/scripts/lib/machine-profile.mjs +3 -17
  50. package/scripts/lib/mcp-filter.mjs +109 -53
  51. package/scripts/lib/mcp-health.mjs +2 -2
  52. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  53. package/scripts/lib/psmux-info.mjs +11 -59
  54. package/scripts/lib/skill-template.mjs +0 -245
  55. package/hub/team/build-worker-prompt.mjs +0 -92
  56. package/hub/team/check-mcp-hub.mjs +0 -49
  57. package/hub/team/claude-native-bridge.mjs +0 -987
  58. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  59. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  60. package/hub/team/cli/services/native-control.mjs +0 -145
  61. package/hub/team/codex-review.mjs +0 -520
  62. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  63. package/hub/team/conductor.mjs +0 -1529
  64. package/hub/team/consensus-meta.mjs +0 -176
  65. package/hub/team/cto-pull.mjs +0 -31
  66. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  67. package/hub/team/dashboard.mjs +0 -322
  68. package/hub/team/event-log.mjs +0 -84
  69. package/hub/team/execution-mode.mjs +0 -324
  70. package/hub/team/extract-completion-payload.mjs +0 -110
  71. package/hub/team/headless-bridge-session.mjs +0 -17
  72. package/hub/team/health-probe.mjs +0 -435
  73. package/hub/team/interactive-native-launcher.mjs +0 -119
  74. package/hub/team/interactive-native-worker.mjs +0 -80
  75. package/hub/team/interactive-tui-transport.mjs +0 -479
  76. package/hub/team/launcher-template.mjs +0 -144
  77. package/hub/team/mcp-selector.mjs +0 -145
  78. package/hub/team/native-supervisor.mjs +0 -460
  79. package/hub/team/native.mjs +0 -727
  80. package/hub/team/recovery-store.mjs +0 -149
  81. package/hub/team/remote-probe.mjs +0 -292
  82. package/hub/team/remote-watcher.mjs +0 -506
  83. package/hub/team/runtime-strategy.mjs +0 -200
  84. package/hub/team/schemas/worker-completion.json +0 -39
  85. package/hub/team/sentinel-capture.mjs +0 -127
  86. package/hub/team/swarm-cli.mjs +0 -390
  87. package/hub/team/swarm-hypervisor.mjs +0 -2206
  88. package/hub/team/swarm-locks.mjs +0 -335
  89. package/hub/team/swarm-planner.mjs +0 -371
  90. package/hub/team/swarm-preflight.mjs +0 -366
  91. package/hub/team/swarm-reconciler.mjs +0 -242
  92. package/hub/team/tui-remote-adapter.mjs +0 -377
  93. package/hub/team/worker-completion-validator.mjs +0 -79
  94. package/hub/team/worktree-lifecycle.mjs +0 -639
  95. package/hub/team/wt-templates.json +0 -43
  96. package/hub/workers/antigravity-route-worker.mjs +0 -179
  97. package/scripts/lib/dynamic-route-cli.mjs +0 -107
@@ -44,13 +44,12 @@ export function decomposeTask(taskDescription, agentCount) {
44
44
  * @param {string} taskDescription
45
45
  * @param {object} config
46
46
  * @param {string} config.agentId
47
- * @param {string} config.hubUrl
48
47
  * @param {string} config.teammateMode
49
48
  * @param {Array<{agentId:string, cli:string, subtask:string}>} config.workers
50
49
  * @returns {string}
51
50
  */
52
51
  export function buildLeadPrompt(taskDescription, config) {
53
- const { agentId, repoRoot, teammateMode = "tmux", workers = [] } = config;
52
+ const { agentId, teammateMode = "tmux", workers = [] } = config;
54
53
 
55
54
  const roster =
56
55
  workers
@@ -59,11 +58,6 @@ export function buildLeadPrompt(taskDescription, config) {
59
58
 
60
59
  const workerIds = workers.map((w) => w.agentId).join(", ");
61
60
 
62
- // TODO: Require repoRoot once all callers pass absolute repository roots.
63
- const bridgePath = repoRoot
64
- ? `node ${String(repoRoot).replace(/\/+$/, "")}/hub/bridge.mjs`
65
- : "node hub/bridge.mjs";
66
-
67
61
  return `리드 에이전트: ${agentId}
68
62
 
69
63
  목표: ${taskDescription}
@@ -74,12 +68,7 @@ ${roster}
74
68
 
75
69
  규칙:
76
70
  - 가능한 짧고 핵심만 지시/요약(토큰 절약)
77
- - 워커 제어:
78
- ${bridgePath} result --agent ${agentId} --topic lead.control
79
- - 워커 결과 수집:
80
- ${bridgePath} context --agent ${agentId} --max 20
81
- - 최종 결과는 topic="task.result"를 모아 통합
82
- - 모든 워커의 task.result 수신 후 결과를 통합하고 종료하라. 워커가 무응답이면 상태를 보고하고 중단하라.
71
+ - 모든 워커의 결과와 출력 파일을 확인하고 통합한다. 무응답 워커는 상태를 보고하고 중단한다.
83
72
  - 증거(커밋/테스트/파일) 없는 완료 주장은 통합하지 말고 해당 워커에 redo 를 지시하라.
84
73
 
85
74
  워커 ID: ${workerIds || "(없음)"}
@@ -92,28 +81,17 @@ ${roster}
92
81
  * @param {object} config
93
82
  * @param {string} config.cli — codex/gemini/claude
94
83
  * @param {string} config.agentId — 에이전트 식별자
95
- * @param {string} config.hubUrl — Hub URL
96
84
  * @returns {string}
97
85
  */
98
86
  export function buildPrompt(subtask, config) {
99
- const { cli, agentId, hubUrl } = config;
100
-
101
- const _hubBase = hubUrl.replace("/mcp", "");
102
-
103
- const bridgePath = "node hub/bridge.mjs";
87
+ const { cli, agentId } = config;
104
88
 
105
89
  return `워커: ${agentId} (${cli})
106
90
  작업: ${subtask}
107
91
 
108
92
  필수 규칙:
109
93
  1) 간결하게 작업(불필요한 장문 설명 금지)
110
- 2) 시작 즉시 등록:
111
- ${bridgePath} register --agent ${agentId} --cli ${cli} --topics lead.control,task.result
112
- 3) 주기적으로 수신함 확인:
113
- ${bridgePath} context --agent ${agentId} --max 10
114
- 4) lead.control 수신 시 즉시 반응 (interrupt/stop/pause/resume)
115
- 5) 완료 시 결과 발행:
116
- ${bridgePath} result --agent ${agentId} --topic task.result --file <출력파일>
94
+ 2) 완료 시 변경 파일, 검증 결과와 출력 파일 경로를 보고한다
117
95
 
118
96
  지금 작업을 시작하라.`;
119
97
  }
@@ -123,7 +101,6 @@ export function buildPrompt(subtask, config) {
123
101
  * @param {string} sessionName — tmux 세션 이름
124
102
  * @param {Array<{target: string, cli: string, subtask: string}>} assignments
125
103
  * @param {object} opts
126
- * @param {string} opts.hubUrl — Hub URL
127
104
  * @param {{target:string, cli:string, task:string}|null} opts.lead
128
105
  * @param {string} opts.teammateMode
129
106
  * @param {(failure: {target:string, cli:string, role:string, message:string}) => void} [opts.onInjectionFailure]
@@ -131,7 +108,6 @@ export function buildPrompt(subtask, config) {
131
108
  */
132
109
  export async function orchestrate(sessionName, assignments, opts = {}) {
133
110
  const {
134
- hubUrl = "http://127.0.0.1:27888/mcp",
135
111
  lead = null,
136
112
  teammateMode = "tmux",
137
113
  injectPrompt = defaultInjectPrompt,
@@ -165,7 +141,6 @@ export async function orchestrate(sessionName, assignments, opts = {}) {
165
141
  const leadAgentId = `${lead.cli || "claude"}-${lead.target.split(".").pop()}`;
166
142
  const leadPrompt = buildLeadPrompt(lead.task || "팀 작업 조율", {
167
143
  agentId: leadAgentId,
168
- hubUrl,
169
144
  teammateMode,
170
145
  workers: workers.map((w) => ({
171
146
  agentId: w.agentId,
@@ -188,7 +163,6 @@ export async function orchestrate(sessionName, assignments, opts = {}) {
188
163
  const prompt = buildPrompt(worker.subtask, {
189
164
  cli: worker.cli,
190
165
  agentId: worker.agentId,
191
- hubUrl,
192
166
  sessionName,
193
167
  });
194
168
  try {
package/hub/team/pane.mjs CHANGED
@@ -3,7 +3,6 @@
3
3
  import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { buildExecArgs } from "@triflux/core/hub/codex-adapter.mjs";
7
6
  import { psmuxExec } from "./psmux.mjs";
8
7
  import { detectMultiplexer, tmuxExec } from "./session.mjs";
9
8
 
@@ -49,23 +48,13 @@ function muxExec(args, opts = {}) {
49
48
 
50
49
  /**
51
50
  * CLI 에이전트 시작 커맨드 생성
52
- * @param {'codex'|'gemini'|'claude'} cli
53
- * @param {{ trustMode?: boolean }} [options]
51
+ * @param {'codex'|'antigravity'|'claude'} cli
54
52
  * @returns {string} 실행할 셸 커맨드
55
53
  */
56
- export function buildCliCommand(cli, options = {}) {
57
- const { trustMode = false } = options;
58
-
54
+ export function buildCliCommand(cli) {
59
55
  switch (cli) {
60
56
  case "codex":
61
- // trust 모드에서는 exec 서브커맨드 기반 명령을 사용
62
- if (trustMode) {
63
- return buildExecArgs({});
64
- }
65
57
  return "codex --dangerously-bypass-approvals-and-sandbox";
66
- case "gemini":
67
- // interactive 모드 — MCP는 ~/.gemini/settings.json에 사전 등록
68
- return "gemini";
69
58
  case "claude":
70
59
  // interactive 모드
71
60
  return "claude";
@@ -87,17 +76,8 @@ export function startCliInPane(target, command) {
87
76
  /**
88
77
  * psmux `@file` 참조 주입이 가능한 CLI인지 판정한다.
89
78
  *
90
- * Codex TUI는 `@` 입력이 있으면 sync_file_search_popup 경로로 FileSearchPopup을
91
- * 활성화한다 (call-site `codex-rs/tui/src/bottom_pane/chat_composer.rs:3082`;
92
- * 본체 L3271-3313 에서 `ActivePopup::File(popup)` 분기; 빈 query 상태는
93
- * `file_search_popup.rs:54-63` 의 `set_empty_prompt()` 가 정의). 팝업이 떠 있는
94
- * 동안의 Enter 처리는 같은 파일 composer의 Enter handler (`chat_composer.rs:1585-1645`
95
- * 기준) 에서 popup 선택 path insert 또는 dismiss 로 분기하므로 prompt submit 으로
96
- * 전달되지 않는다. Gemini CLI의 `@path`는 공식 client-side file-content inject
97
- * 이므로 유지. Codex TUI에서 `@path` 선택은 path 문자열만 textarea에 insert할
98
- * 뿐 Gemini처럼 파일 내용을 inject하지 않으며, 별도 file-injection slash
99
- * command 도 존재하지 않음 (slash_commands / prompt_args / skill_popup /
100
- * command_popup 소스 전수 grep 결과 0건, 2026-04-19 검증 — absence-based).
79
+ * Codex TUI의 @path는 파일 검색 팝업을 열어 Enter 제출을 가로막는다.
80
+ * Gemini CLI의 파일 내용 주입은 유지한다.
101
81
  *
102
82
  * @param {{ multiplexer: string, useFileRef: boolean, cli: string|null }} args
103
83
  */
@@ -156,9 +136,7 @@ function waitForComposerReady(target) {
156
136
  * capture 불가 또는 재시도 소진을 성공으로 숨기지 않고 오류로 반환한다.
157
137
  */
158
138
  function isAntigravityCli(cli) {
159
- return ["agy", "antigravity", "gemini"].includes(
160
- String(cli || "").toLowerCase(),
161
- );
139
+ return ["agy", "antigravity"].includes(String(cli || "").toLowerCase());
162
140
  }
163
141
 
164
142
  function promptComposerNeedle(prompt) {
@@ -254,7 +232,7 @@ function confirmSubmit(target, prompt, cli, sendEnter) {
254
232
  * @param {string} prompt — 주입할 텍스트
255
233
  * @param {object} [opts]
256
234
  * @param {boolean} [opts.useFileRef] — true면 TUI용 @file 참조 방식 요청 (psmux 전용). Codex에서는 자동으로 paste-buffer 경로로 fallback.
257
- * @param {'codex'|'gemini'|'claude'|null} [opts.cli] — 대상 CLI. Codex일 때 @ intercept를 회피하기 위해 paste-buffer 경로를 강제한다.
235
+ * @param {'codex'|'antigravity'|'claude'|null} [opts.cli]: 대상 CLI. Codex일 때 @ intercept를 회피하기 위해 paste-buffer 경로를 강제한다.
258
236
  */
259
237
  export function injectPrompt(
260
238
  target,
@@ -1623,7 +1623,6 @@ export async function waitForPattern(
1623
1623
 
1624
1624
  /**
1625
1625
  * 완료 토큰이 찍힐 때까지 대기하고 exit code를 파싱한다.
1626
- * NOTE: 주 채널은 headless.waitForCompletionWithStallDetect이며, 본 함수는 fallback 채널이다.
1627
1626
  * @param {string} sessionName
1628
1627
  * @param {string} paneNameOrTarget
1629
1628
  * @param {string} token
@@ -1,5 +1,4 @@
1
- // hub/team/remote-session.mjs — Remote session primitives for swarm integration
2
- // Extracted from scripts/remote-spawn.mjs for reuse by swarm-hypervisor.
1
+ // Remote session primitives.
3
2
  // Pure functions + SSH operations. No psmux, no WT, no CLI arg parsing.
4
3
 
5
4
  import { execFileSync } from "node:child_process";
@@ -1,5 +1,4 @@
1
- // hub/team/session.mjs — tmux/psmux/wt 세션 생명주기 관리
2
- // 의존성: child_process (Node.js 내장)만 사용
1
+ // tmux/psmux 세션 생명주기 관리
3
2
  import { execSync, spawnSync } from "node:child_process";
4
3
  import { resolveGitBashExecutable } from "@triflux/core/hub/lib/bash-path.mjs";
5
4
  import { getEnvironment } from "@triflux/core/hub/lib/env-detect.mjs";
@@ -16,7 +15,6 @@ import {
16
15
  psmuxExec,
17
16
  psmuxSessionExists,
18
17
  } from "./psmux.mjs";
19
- import { createWtManager } from "./wt-manager.mjs";
20
18
 
21
19
  /** Windows Terminal 실행 파일 존재 여부 */
22
20
  export function hasWindowsTerminal() {
@@ -193,77 +191,6 @@ export function resolveAttachCommand(sessionName, opts = {}) {
193
191
  };
194
192
  }
195
193
 
196
- /**
197
- * Windows Terminal pane 포커스 이동
198
- * @param {number} paneIndex - createWtSession()에서 생성한 pane 인덱스(0 기반)
199
- * @param {object} opts
200
- * @param {'1xN'|'Nx1'} opts.layout
201
- * @returns {boolean}
202
- */
203
- export function focusWtPane(paneIndex, opts = {}) {
204
- // wt() 제거로 인해 일시 비활성화 (탭 기반 전환 필요)
205
- return false;
206
- }
207
-
208
- /**
209
- * Windows Terminal에서 생성한 팀 pane 정리
210
- * @param {object} opts
211
- * @param {'1xN'|'Nx1'} opts.layout
212
- * @param {number} opts.paneCount
213
- * @returns {number} 닫힌 pane 수 (best-effort)
214
- */
215
- export function closeWtSession(opts = {}) {
216
- // wt() 제거로 인해 일시 비활성화 (탭 기반 전환 필요)
217
- return 0;
218
- }
219
-
220
- /**
221
- * Windows Terminal 독립 모드 세션 생성 (비동기)
222
- * @param {string} sessionName
223
- * @param {object} opts
224
- * @param {'1xN'|'Nx1'} opts.layout
225
- * @param {Array<{title:string,command:string,cwd?:string}>} opts.paneCommands
226
- * @returns {Promise<{ sessionName: string, panes: string[], titles: string[], layout: '1xN'|'Nx1', paneCount: number, anchorPane: string }>}
227
- */
228
- export async function createWtSession(sessionName, opts = {}) {
229
- const { layout = "1xN", paneCommands = [] } = opts;
230
-
231
- if (!hasWindowsTerminalSession()) {
232
- throw new Error("WT_SESSION 미감지");
233
- }
234
- if (!getEnvironment().terminal.hasWt) {
235
- throw new Error("wt.exe 미발견");
236
- }
237
- if (!Array.isArray(paneCommands) || paneCommands.length === 0) {
238
- throw new Error("paneCommands가 비어 있음");
239
- }
240
-
241
- const wtManager = createWtManager();
242
- const panes = [];
243
- const titles = [];
244
-
245
- for (let i = 0; i < paneCommands.length; i++) {
246
- const pane = paneCommands[i] || {};
247
- const title = pane.title || `${sessionName}-${i + 1}`;
248
- const command = String(pane.command || "").trim();
249
- const cwd = pane.cwd || process.cwd();
250
- if (!command) continue;
251
-
252
- await wtManager.createTab({ title, command, cwd });
253
- panes.push(`wt:${i}`);
254
- titles.push(title);
255
- }
256
-
257
- return {
258
- sessionName,
259
- panes,
260
- titles,
261
- layout: layout === "Nx1" ? "Nx1" : "1xN",
262
- paneCount: panes.length,
263
- anchorPane: "wt:anchor",
264
- };
265
- }
266
-
267
194
  /**
268
195
  * tmux 세션 생성 + 레이아웃 분할
269
196
  * @param {string} sessionName — 세션 이름
@@ -375,23 +302,12 @@ export function configureTeammateKeybindings(sessionName, opts = {}) {
375
302
  ? `'select-pane -t :.- \\; resize-pane -Z'`
376
303
  : `'select-pane -t :.-'`;
377
304
 
378
- if (inProcess) {
379
- // 단일 뷰(zoom) 상태에서 팀메이트 순환
380
- tmux(
381
- `bind-key -T root -n S-Down if-shell -F '${cond}' ${bindNext} 'send-keys S-Down'`,
382
- );
383
- tmux(
384
- `bind-key -T root -n S-Up if-shell -F '${cond}' ${bindPrev} 'send-keys S-Up'`,
385
- );
386
- } else {
387
- // 분할 뷰에서 팀메이트 순환
388
- tmux(
389
- `bind-key -T root -n S-Down if-shell -F '${cond}' ${bindNext} 'send-keys S-Down'`,
390
- );
391
- tmux(
392
- `bind-key -T root -n S-Up if-shell -F '${cond}' ${bindPrev} 'send-keys S-Up'`,
393
- );
394
- }
305
+ tmux(
306
+ `bind-key -T root -n S-Down if-shell -F '${cond}' ${bindNext} 'send-keys S-Down'`,
307
+ );
308
+ tmux(
309
+ `bind-key -T root -n S-Up if-shell -F '${cond}' ${bindPrev} 'send-keys S-Up'`,
310
+ );
395
311
 
396
312
  // 대체 키: 일부 환경에서 S-Up이 누락될 때 사용
397
313
  tmux(
@@ -1,127 +1,5 @@
1
- // hub/team/swarm-intent.mjs — X-Intent helper utilities for semantic merge awareness
2
-
3
1
  const INTENT_TRAILER_REGEX = /^X-Intent:\s*(.+)$/m;
4
2
 
5
- function normalizeText(value) {
6
- return typeof value === "string" ? value.trim() : "";
7
- }
8
-
9
- function normalizeTouches(touches) {
10
- if (!Array.isArray(touches)) return null;
11
- return touches
12
- .filter((item) => typeof item === "string")
13
- .map((item) => item.trim().replace(/\\/g, "/"))
14
- .filter(Boolean);
15
- }
16
-
17
- function normalizeIntent(intentObj) {
18
- if (!intentObj || typeof intentObj !== "object" || Array.isArray(intentObj)) {
19
- return null;
20
- }
21
-
22
- const scope = normalizeText(intentObj.scope);
23
- const action = normalizeText(intentObj.action);
24
- const reason = normalizeText(intentObj.reason);
25
- const touches = normalizeTouches(intentObj.touches);
26
-
27
- if (!scope || !action || !reason || !touches) {
28
- return null;
29
- }
30
-
31
- return {
32
- scope,
33
- action,
34
- reason,
35
- touches,
36
- invariant: normalizeText(intentObj.invariant),
37
- conflictsWith: normalizeText(intentObj.conflictsWith),
38
- };
39
- }
40
-
41
- function fallbackIntent(llmResponse) {
42
- const responseText = String(llmResponse ?? "");
43
- return {
44
- scope: "unknown",
45
- action: "unknown",
46
- reason: responseText.slice(0, 100),
47
- touches: [],
48
- invariant: "",
49
- conflictsWith: "",
50
- };
51
- }
52
-
53
- function includesEitherWay(a, b) {
54
- if (!a || !b) return false;
55
- return a.includes(b) || b.includes(a);
56
- }
57
-
58
- function hasConflict(intentA, intentB) {
59
- const conflictsWith = normalizeText(intentA?.conflictsWith).toLowerCase();
60
- const scope = normalizeText(intentB?.scope).toLowerCase();
61
- const action = normalizeText(intentB?.action).toLowerCase();
62
-
63
- if (!conflictsWith) return false;
64
- return (
65
- includesEitherWay(conflictsWith, scope) ||
66
- includesEitherWay(conflictsWith, action)
67
- );
68
- }
69
-
70
- function getTouchesOverlap(intentA, intentB) {
71
- const touchesA = new Set(
72
- (normalizeTouches(intentA?.touches) ?? []).map((item) =>
73
- item.toLowerCase(),
74
- ),
75
- );
76
- const touchesB = (normalizeTouches(intentB?.touches) ?? []).map((item) =>
77
- item.toLowerCase(),
78
- );
79
- return touchesB.filter((item) => touchesA.has(item));
80
- }
81
-
82
- /**
83
- * Build prompts for an LLM to generate an X-Intent JSON object.
84
- * @param {string[]} changedFiles
85
- * @param {string} commitDiff
86
- * @param {string} taskContext
87
- * @returns {{ systemPrompt: string, userPrompt: string }}
88
- */
89
- export function generateIntentPrompt(changedFiles, commitDiff, taskContext) {
90
- const files = Array.isArray(changedFiles)
91
- ? changedFiles.filter((filePath) => typeof filePath === "string")
92
- : [];
93
- const diffSnippet = String(commitDiff ?? "").slice(0, 4000);
94
- const context = String(taskContext ?? "").trim();
95
-
96
- return {
97
- systemPrompt:
98
- "You generate commit intent metadata for semantic merge awareness. Return only valid JSON with keys: scope, action, reason, touches, invariant, conflictsWith.",
99
- userPrompt: [
100
- "Create an intent object from the following commit context.",
101
- "- scope/action/reason must be concise strings.",
102
- "- touches must be an array of touched file paths.",
103
- "- invariant/conflictsWith should be strings (empty if unknown).",
104
- "- Return JSON only (no markdown).",
105
- "",
106
- `Task context:\n${context || "(none provided)"}`,
107
- "",
108
- `Changed files (${files.length}):\n${files.join("\n") || "(none)"}`,
109
- "",
110
- "Commit diff (first 4000 chars):",
111
- diffSnippet || "(empty)",
112
- ].join("\n"),
113
- };
114
- }
115
-
116
- /**
117
- * Format an intent object into an X-Intent commit trailer.
118
- * @param {object} intentObj
119
- * @returns {string}
120
- */
121
- export function formatIntentTrailer(intentObj) {
122
- return `X-Intent: ${JSON.stringify(intentObj)}`;
123
- }
124
-
125
3
  /**
126
4
  * Parse an X-Intent trailer from a commit message.
127
5
  * @param {string} commitMessage
@@ -138,79 +16,3 @@ export function parseIntentTrailer(commitMessage) {
138
16
  return null;
139
17
  }
140
18
  }
141
-
142
- /**
143
- * Classify the relation between two intents.
144
- * @param {object} intentA
145
- * @param {object} intentB
146
- * @returns {{ relation: "complementary" | "complementary-risky" | "contradictory" | "independent", reason: string }}
147
- */
148
- export function classifyIntentPair(intentA, intentB) {
149
- if (hasConflict(intentA, intentB) || hasConflict(intentB, intentA)) {
150
- return {
151
- relation: "contradictory",
152
- reason:
153
- "conflictsWith on one intent matches the other intent scope/action",
154
- };
155
- }
156
-
157
- const scopeA = normalizeText(intentA?.scope).toLowerCase();
158
- const scopeB = normalizeText(intentB?.scope).toLowerCase();
159
-
160
- if (scopeA && scopeB && scopeA === scopeB) {
161
- const overlap = getTouchesOverlap(intentA, intentB);
162
- if (overlap.length > 0) {
163
- return {
164
- relation: "complementary-risky",
165
- reason: `same scope with overlapping touched files: ${overlap.join(", ")}`,
166
- };
167
- }
168
-
169
- return {
170
- relation: "complementary",
171
- reason:
172
- "same scope with no conflicting intent and no touched-file overlap",
173
- };
174
- }
175
-
176
- return {
177
- relation: "independent",
178
- reason: "different scopes and no conflictsWith match detected",
179
- };
180
- }
181
-
182
- /**
183
- * Build a validated intent object from raw LLM response text.
184
- * @param {string} llmResponse
185
- * @returns {{ scope: string, action: string, reason: string, touches: string[], invariant: string, conflictsWith: string }}
186
- */
187
- export function buildIntentFromLLMResponse(llmResponse) {
188
- const responseText = String(llmResponse ?? "").trim();
189
- const candidates = [];
190
-
191
- const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)```/gi;
192
- for (const match of responseText.matchAll(codeBlockRegex)) {
193
- if (match[1]) candidates.push(match[1].trim());
194
- }
195
-
196
- candidates.push(responseText);
197
-
198
- const firstBrace = responseText.indexOf("{");
199
- const lastBrace = responseText.lastIndexOf("}");
200
- if (firstBrace >= 0 && lastBrace > firstBrace) {
201
- candidates.push(responseText.slice(firstBrace, lastBrace + 1));
202
- }
203
-
204
- for (const candidate of candidates) {
205
- if (!candidate) continue;
206
- try {
207
- const parsed = JSON.parse(candidate);
208
- const normalized = normalizeIntent(parsed);
209
- if (normalized) return normalized;
210
- } catch {
211
- // ignore parse failures and continue trying other candidates
212
- }
213
- }
214
-
215
- return fallbackIntent(responseText);
216
- }
@@ -195,7 +195,7 @@ export function normalizeWorkerState(existing = {}, state = {}, opts = {}) {
195
195
 
196
196
  // ── 색상 헬퍼 ─────────────────────────────────────────────────────────────
197
197
  export function cliColor(cli) {
198
- if (cli === "gemini") return FG.gemini;
198
+ if (cli === "antigravity" || cli === "agy") return FG.antigravity;
199
199
  if (cli === "claude") return FG.claude;
200
200
  if (cli === "codex") return FG.codex;
201
201
  return FG.white;
@@ -358,8 +358,8 @@ function ingest() {
358
358
 
359
359
  // CLI 타입 감지
360
360
  let cli = "codex";
361
- if (pane.title.includes("gemini") || pane.title.includes("🔵"))
362
- cli = "gemini";
361
+ if (pane.title.includes("antigravity") || pane.title.includes("🔵"))
362
+ cli = "antigravity";
363
363
  else if (pane.title.includes("claude") || pane.title.includes("🟠"))
364
364
  cli = "claude";
365
365
  ws.title = pane.title;