@triflux/remote 10.1.0 → 10.26.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 (162) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2179 -709
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +78 -17
  9. package/hub/team/build-worker-prompt.mjs +73 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +1130 -0
  12. package/hub/team/claude-native-bridge.mjs +1011 -0
  13. package/hub/team/claude-session-projection.mjs +74 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +151 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +97 -13
  24. package/hub/team/cli/commands/start/start-headless.mjs +155 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/help.mjs +1 -1
  33. package/hub/team/cli/index.mjs +2 -2
  34. package/hub/team/cli/manifest.mjs +38 -8
  35. package/hub/team/cli/render.mjs +52 -30
  36. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  37. package/hub/team/cli/services/hub-client.mjs +44 -16
  38. package/hub/team/cli/services/member-selector.mjs +37 -30
  39. package/hub/team/cli/services/native-control.mjs +144 -116
  40. package/hub/team/cli/services/runtime-mode.mjs +61 -4
  41. package/hub/team/cli/services/state-store.mjs +66 -9
  42. package/hub/team/cli/services/task-model.mjs +40 -30
  43. package/hub/team/codex-review.mjs +521 -0
  44. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  45. package/hub/team/conductor-registry.mjs +121 -0
  46. package/hub/team/conductor.mjs +1379 -671
  47. package/hub/team/consensus-meta.mjs +176 -0
  48. package/hub/team/dashboard-anchor.mjs +4 -5
  49. package/hub/team/dashboard-layout.mjs +3 -1
  50. package/hub/team/dashboard-open.mjs +20 -118
  51. package/hub/team/dashboard.mjs +76 -28
  52. package/hub/team/event-log.mjs +18 -10
  53. package/hub/team/execution-mode.mjs +256 -0
  54. package/hub/team/extract-completion-payload.mjs +110 -0
  55. package/hub/team/git-preflight.mjs +286 -0
  56. package/hub/team/handoff.mjs +49 -18
  57. package/hub/team/headless-bridge-session.mjs +17 -0
  58. package/hub/team/headless.mjs +2213 -1156
  59. package/hub/team/health-probe.mjs +232 -69
  60. package/hub/team/interactive-native-launcher.mjs +106 -0
  61. package/hub/team/interactive-native-worker.mjs +80 -0
  62. package/hub/team/interactive-tui-transport.mjs +465 -0
  63. package/hub/team/launcher-template.mjs +49 -17
  64. package/hub/team/lead-control.mjs +91 -4
  65. package/hub/team/mcp-selector.mjs +145 -0
  66. package/hub/team/native-supervisor.mjs +90 -26
  67. package/hub/team/native.mjs +120 -42
  68. package/hub/team/nativeProxy.mjs +184 -113
  69. package/hub/team/notify.mjs +147 -78
  70. package/hub/team/orchestrator.mjs +173 -161
  71. package/hub/team/pane.mjs +43 -20
  72. package/hub/team/process-cleanup.mjs +69 -25
  73. package/hub/team/psmux.mjs +1022 -281
  74. package/hub/team/recovery-store.mjs +149 -0
  75. package/hub/team/remote-probe.mjs +68 -52
  76. package/hub/team/remote-session.mjs +131 -59
  77. package/hub/team/remote-watcher.mjs +61 -33
  78. package/hub/team/retry-state-machine.mjs +336 -0
  79. package/hub/team/routing.mjs +51 -25
  80. package/hub/team/runtime-strategy.mjs +200 -0
  81. package/hub/team/schemas/worker-completion.json +39 -0
  82. package/hub/team/sentinel-capture.mjs +127 -0
  83. package/hub/team/session-sync.mjs +153 -6
  84. package/hub/team/session.mjs +574 -611
  85. package/hub/team/shared.mjs +13 -13
  86. package/hub/team/staleState.mjs +72 -30
  87. package/hub/team/swarm-cli.mjs +390 -0
  88. package/hub/team/swarm-hypervisor.mjs +1544 -193
  89. package/hub/team/swarm-intent.mjs +216 -0
  90. package/hub/team/swarm-locks.mjs +140 -25
  91. package/hub/team/swarm-planner.mjs +121 -21
  92. package/hub/team/swarm-preflight.mjs +319 -0
  93. package/hub/team/swarm-reconciler.mjs +128 -23
  94. package/hub/team/synapse-cli.mjs +458 -0
  95. package/hub/team/synapse-http.mjs +60 -0
  96. package/hub/team/synapse-registry.mjs +329 -0
  97. package/hub/team/terminal-opener.mjs +216 -0
  98. package/hub/team/tui-core.mjs +292 -0
  99. package/hub/team/tui-lite.mjs +237 -148
  100. package/hub/team/tui-remote-adapter.mjs +18 -34
  101. package/hub/team/tui-synapse.mjs +223 -0
  102. package/hub/team/tui-viewer.mjs +108 -48
  103. package/hub/team/tui-widgets.mjs +262 -0
  104. package/hub/team/tui.mjs +931 -439
  105. package/hub/team/worker-completion-validator.mjs +84 -0
  106. package/hub/team/worker-sandbox.mjs +97 -0
  107. package/hub/team/worker-signal.mjs +263 -0
  108. package/hub/team/worker-signal.types.d.ts +52 -0
  109. package/hub/team/worktree-lifecycle.mjs +517 -63
  110. package/hub/team/wt-manager.mjs +297 -26
  111. package/hub/tools.mjs +477 -252
  112. package/hub/tray.mjs +411 -368
  113. package/hub/workers/antigravity-route-worker.mjs +169 -0
  114. package/hub/workers/claude-worker.mjs +162 -118
  115. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  116. package/hub/workers/codex-mcp.mjs +724 -507
  117. package/hub/workers/delegator-mcp.mjs +570 -499
  118. package/hub/workers/factory.mjs +103 -21
  119. package/hub/workers/gemini-worker.mjs +126 -284
  120. package/hub/workers/interface.mjs +24 -2
  121. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  122. package/hub/workers/worker-utils.mjs +23 -17
  123. package/package.json +4 -3
  124. package/scripts/lib/agent-json.mjs +27 -0
  125. package/scripts/lib/async.mjs +174 -0
  126. package/scripts/lib/claudemd-scanner.mjs +223 -0
  127. package/scripts/lib/cli-agy.mjs +62 -0
  128. package/scripts/lib/cli-claude.mjs +78 -0
  129. package/scripts/lib/cli-codex.mjs +199 -0
  130. package/scripts/lib/cli-gemini.mjs +37 -0
  131. package/scripts/lib/codex-recovery.sh +50 -0
  132. package/scripts/lib/context.mjs +67 -0
  133. package/scripts/lib/cross-review-utils.mjs +59 -0
  134. package/scripts/lib/doctor-env-checks.mjs +121 -0
  135. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  136. package/scripts/lib/env-probe.mjs +269 -0
  137. package/scripts/lib/env.mjs +14 -0
  138. package/scripts/lib/gemini-profiles.mjs +119 -0
  139. package/scripts/lib/handoff.mjs +187 -0
  140. package/scripts/lib/hook-utils.mjs +16 -0
  141. package/scripts/lib/hub.mjs +48 -0
  142. package/scripts/lib/keyword-rules.mjs +195 -0
  143. package/scripts/lib/logger.mjs +105 -0
  144. package/scripts/lib/mcp-filter.mjs +883 -0
  145. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  146. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  147. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  148. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  149. package/scripts/lib/mcp-health.mjs +773 -0
  150. package/scripts/lib/mcp-manifest.mjs +89 -0
  151. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  152. package/scripts/lib/pid.mjs +63 -0
  153. package/scripts/lib/process-utils.mjs +26 -0
  154. package/scripts/lib/psmux-info.mjs +224 -0
  155. package/scripts/lib/quota.mjs +47 -0
  156. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  157. package/scripts/lib/skill-state.mjs +220 -0
  158. package/scripts/lib/skill-template.mjs +375 -0
  159. package/scripts/lib/team.mjs +75 -0
  160. package/scripts/lib/timeout.mjs +66 -0
  161. package/scripts/lib/tmp.mjs +26 -0
  162. package/scripts/lib/toml.mjs +113 -0
@@ -0,0 +1,199 @@
1
+ // cli-codex.mjs — Codex CLI adapter (Phase 0 PoC).
2
+ //
3
+ // 책임: agent 이름과 입력 파라미터를 받아 Codex 실행 계획(profile/effort/timeout/runMode)을
4
+ // 산출한다. 실제 spawn 은 Phase 1 에서 이관된다 (현재는 tfx-route.sh 가 수행).
5
+ //
6
+ // 진입점: tfx-route.mjs → selectAdapter("codex") → plan({...}).
7
+ // 출처: scripts/tfx-route.sh route_agent() L1001-L1079 case 문 미러.
8
+
9
+ export const id = "codex";
10
+ export const cliType = "codex";
11
+ export const command = "codex";
12
+
13
+ const AGENT_PROFILES = {
14
+ executor: {
15
+ profile: "gpt55_high",
16
+ timeoutSec: 1080,
17
+ runMode: "fg",
18
+ opusOversight: false,
19
+ mcpHint: "implement",
20
+ },
21
+ codex: {
22
+ profile: "gpt55_high",
23
+ timeoutSec: 1080,
24
+ runMode: "fg",
25
+ opusOversight: false,
26
+ mcpHint: "implement",
27
+ },
28
+ "build-fixer": {
29
+ profile: "gpt55_low",
30
+ timeoutSec: 540,
31
+ runMode: "fg",
32
+ opusOversight: false,
33
+ mcpHint: "implement",
34
+ },
35
+ cleanup: {
36
+ profile: "gpt55_med",
37
+ timeoutSec: 540,
38
+ runMode: "fg",
39
+ opusOversight: false,
40
+ mcpHint: "implement",
41
+ },
42
+ deslop: {
43
+ profile: "gpt55_med",
44
+ timeoutSec: 540,
45
+ runMode: "fg",
46
+ opusOversight: false,
47
+ mcpHint: "implement",
48
+ },
49
+ debugger: {
50
+ profile: "gpt55_xhigh",
51
+ timeoutSec: 900,
52
+ runMode: "bg",
53
+ opusOversight: false,
54
+ mcpHint: "implement",
55
+ },
56
+ "deep-executor": {
57
+ profile: "gpt55_xhigh",
58
+ timeoutSec: 3600,
59
+ runMode: "bg",
60
+ opusOversight: true,
61
+ mcpHint: "implement",
62
+ },
63
+ architect: {
64
+ profile: "gpt55_xhigh",
65
+ timeoutSec: 3600,
66
+ runMode: "bg",
67
+ opusOversight: true,
68
+ mcpHint: "analyze",
69
+ },
70
+ critic: {
71
+ profile: "gpt55_xhigh",
72
+ timeoutSec: 3600,
73
+ runMode: "bg",
74
+ opusOversight: true,
75
+ mcpHint: "analyze",
76
+ },
77
+ planner: {
78
+ profile: "gpt55_xhigh",
79
+ timeoutSec: 3600,
80
+ runMode: "fg",
81
+ opusOversight: true,
82
+ mcpHint: "analyze",
83
+ },
84
+ analyst: {
85
+ profile: "gpt55_xhigh",
86
+ timeoutSec: 3600,
87
+ runMode: "fg",
88
+ opusOversight: true,
89
+ mcpHint: "analyze",
90
+ },
91
+ "code-reviewer": {
92
+ profile: "gpt55_high",
93
+ timeoutSec: 1800,
94
+ runMode: "bg",
95
+ opusOversight: false,
96
+ mcpHint: "review",
97
+ subcommand: "review",
98
+ },
99
+ "quality-reviewer": {
100
+ profile: "gpt55_high",
101
+ timeoutSec: 1800,
102
+ runMode: "bg",
103
+ opusOversight: false,
104
+ mcpHint: "review",
105
+ subcommand: "review",
106
+ },
107
+ "security-reviewer": {
108
+ profile: "gpt55_xhigh",
109
+ timeoutSec: 1800,
110
+ runMode: "bg",
111
+ opusOversight: true,
112
+ mcpHint: "review",
113
+ subcommand: "review",
114
+ },
115
+ scientist: {
116
+ profile: "gpt55_high",
117
+ timeoutSec: 1440,
118
+ runMode: "bg",
119
+ opusOversight: false,
120
+ mcpHint: "analyze",
121
+ },
122
+ "document-specialist": {
123
+ profile: "gpt55_high",
124
+ timeoutSec: 1440,
125
+ runMode: "bg",
126
+ opusOversight: false,
127
+ mcpHint: "analyze",
128
+ },
129
+ "scientist-deep": {
130
+ profile: "gpt55_xhigh",
131
+ timeoutSec: 3600,
132
+ runMode: "bg",
133
+ opusOversight: false,
134
+ mcpHint: "analyze",
135
+ },
136
+ verifier: {
137
+ profile: "gpt55_high",
138
+ timeoutSec: 1200,
139
+ runMode: "fg",
140
+ opusOversight: false,
141
+ mcpHint: "review",
142
+ subcommand: "review",
143
+ },
144
+ "test-engineer": {
145
+ profile: "gpt55_high",
146
+ timeoutSec: 1200,
147
+ runMode: "bg",
148
+ opusOversight: false,
149
+ mcpHint: "implement",
150
+ },
151
+ "qa-tester": {
152
+ profile: "gpt55_high",
153
+ timeoutSec: 1200,
154
+ runMode: "bg",
155
+ opusOversight: false,
156
+ mcpHint: "review",
157
+ subcommand: "review",
158
+ },
159
+ spark: {
160
+ profile: "gpt55_low",
161
+ timeoutSec: 180,
162
+ runMode: "fg",
163
+ opusOversight: false,
164
+ mcpHint: "implement",
165
+ },
166
+ };
167
+
168
+ const DEFAULT_PROFILE = AGENT_PROFILES.executor;
169
+
170
+ export function plan({
171
+ agent,
172
+ prompt = "",
173
+ mcpProfile = "auto",
174
+ timeoutSec,
175
+ contextFile,
176
+ } = {}) {
177
+ if (!agent) {
178
+ throw new Error("[cli-codex] agent required");
179
+ }
180
+ const cfg = AGENT_PROFILES[agent] ?? DEFAULT_PROFILE;
181
+ const effectiveTimeoutSec =
182
+ Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : cfg.timeoutSec;
183
+ return {
184
+ command,
185
+ subcommand: cfg.subcommand ?? "exec",
186
+ profile: cfg.profile,
187
+ effort: cfg.profile,
188
+ timeoutMs: effectiveTimeoutSec * 1000,
189
+ runMode: cfg.runMode,
190
+ opusOversight: cfg.opusOversight,
191
+ mcpProfile: mcpProfile === "auto" ? cfg.mcpHint : mcpProfile,
192
+ promptLength: prompt.length,
193
+ contextFile: contextFile || null,
194
+ };
195
+ }
196
+
197
+ export function describe() {
198
+ return { id, cliType, command, agents: Object.keys(AGENT_PROFILES) };
199
+ }
@@ -0,0 +1,37 @@
1
+ // cli-gemini.mjs — legacy Gemini adapter alias.
2
+ // The public "gemini" label remains accepted, but the executable plan is AGY.
3
+
4
+ import * as agyAdapter from "./cli-agy.mjs";
5
+
6
+ export const id = "gemini";
7
+ export const cliType = "antigravity";
8
+ export const command = "agy";
9
+
10
+ export function plan({
11
+ agent,
12
+ prompt = "",
13
+ mcpProfile = "auto",
14
+ timeoutSec,
15
+ contextFile,
16
+ } = {}) {
17
+ if (!agent) {
18
+ throw new Error("[cli-gemini] agent required");
19
+ }
20
+ return agyAdapter.plan({
21
+ agent: agent === "gemini" ? "antigravity" : agent,
22
+ prompt,
23
+ mcpProfile,
24
+ timeoutSec,
25
+ contextFile,
26
+ });
27
+ }
28
+
29
+ export function describe() {
30
+ return {
31
+ id,
32
+ cliType,
33
+ command,
34
+ aliases: ["gemini"],
35
+ agents: ["gemini", "designer", "writer", "antigravity", "agy"],
36
+ };
37
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # codex-recovery.sh — codex exec stdout 4계단 recovery helper.
3
+ # 사용: STDOUT_LOG / STDERR_LOG env 설정 후 `source` + `recover_codex_stdout`.
4
+ #
5
+ # 1차 codex marker → stderr 의 마지막 "codex" 라인 이후 본문 회수
6
+ # 2차 node parser → MCP/header/sandbox 로그 제외, 응답 부분만 추출
7
+ # 3차 stderr tail → "tokens used" 직전까지 tail buffer 회수
8
+ # 4차 FALLBACK_FAILED → 모두 실패 시 marker + stderr_log 경로 출력
9
+
10
+ recover_codex_stdout() {
11
+ if [[ -s "$STDOUT_LOG" || ! -s "$STDERR_LOG" ]]; then
12
+ return 0
13
+ fi
14
+
15
+ # 1차: codex marker
16
+ sed 's/\r$//' "$STDERR_LOG" \
17
+ | awk '/^codex$/{found=NR;content=""} found && NR>found{content=content RS $0} END{if(content) print substr(content,2)}' \
18
+ > "$STDOUT_LOG"
19
+
20
+ # 2차: node parser (MCP/header/sandbox 로그 제외)
21
+ if [[ ! -s "$STDOUT_LOG" ]]; then
22
+ node -e '
23
+ const fs=require("fs"),lines=fs.readFileSync(process.argv[1],"utf-8").split(/\r?\n/);
24
+ const skip=/^(mcp[: ]|OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|tokens used|EXIT:|exec$|"[A-Z]:|succeeded in |\s*$)/;
25
+ const out=lines.filter(l=>!skip.test(l));
26
+ if(out.length) fs.writeFileSync(process.argv[2],out.join("\n"));
27
+ ' -- "$STDERR_LOG" "$STDOUT_LOG" 2>/dev/null || true
28
+ fi
29
+
30
+ # 3차: stderr tail before "tokens used"
31
+ if [[ ! -s "$STDOUT_LOG" ]]; then
32
+ sed 's/\r$//' "$STDERR_LOG" \
33
+ | awk '
34
+ /^tokens used/ { exit }
35
+ { buf[NR]=$0 }
36
+ END {
37
+ start=NR-200; if (start<1) start=1
38
+ for (i=start; i<=NR; i++) if (i in buf) print buf[i]
39
+ }' \
40
+ > "$STDOUT_LOG"
41
+ fi
42
+
43
+ if [[ -s "$STDOUT_LOG" ]]; then
44
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr에서 응답 복구 ($(wc -c < "$STDOUT_LOG" | tr -d ' ') bytes)" >&2
45
+ else
46
+ # 4차: FALLBACK_FAILED
47
+ echo "[tfx-route] FALLBACK_FAILED stderr_log=$STDERR_LOG" >&2
48
+ echo "[tfx-route] 경고: codex stdout 비어있음, stderr 복구도 실패. 위 stderr_log 경로에서 raw codex 출력 확인 가능." >&2
49
+ fi
50
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 요청별 로그 컨텍스트 전파 (AsyncLocalStorage 기반).
3
+ *
4
+ * Hub HTTP 서버의 요청마다 correlationId를 자동 할당하여,
5
+ * 하나의 요청에서 발생한 모든 로그를 추적할 수 있다.
6
+ *
7
+ * 사용법:
8
+ * import { getLogger, getCorrelationId, withRequestContext } from './lib/context.mjs';
9
+ *
10
+ * // 미들웨어에서 컨텍스트 생성
11
+ * withRequestContext({ method: 'POST', path: '/bridge/result' }, () => {
12
+ * const log = getLogger();
13
+ * log.info({ agentId }, 'bridge.result_received');
14
+ * });
15
+ *
16
+ * // 내부 함수에서 자동 상관 ID
17
+ * function processResult() {
18
+ * const log = getLogger();
19
+ * log.info('result.processed'); // correlationId 자동 포함
20
+ * }
21
+ */
22
+ import { AsyncLocalStorage } from "node:async_hooks";
23
+ import { randomUUID } from "node:crypto";
24
+
25
+ import { logger } from "./logger.mjs";
26
+
27
+ /** @type {AsyncLocalStorage<{logger: import('pino').Logger, correlationId: string}>} */
28
+ export const asyncLocalStorage = new AsyncLocalStorage();
29
+
30
+ /**
31
+ * 현재 요청 컨텍스트의 로거를 반환한다.
32
+ * 요청 컨텍스트 밖에서 호출하면 기본 로거를 반환한다.
33
+ *
34
+ * @returns {import('pino').Logger}
35
+ */
36
+ export function getLogger() {
37
+ return asyncLocalStorage.getStore()?.logger || logger;
38
+ }
39
+
40
+ /**
41
+ * 현재 요청의 상관 ID를 반환한다.
42
+ *
43
+ * @returns {string|undefined}
44
+ */
45
+ export function getCorrelationId() {
46
+ return asyncLocalStorage.getStore()?.correlationId;
47
+ }
48
+
49
+ /**
50
+ * 요청 컨텍스트를 생성하고 콜백을 실행한다.
51
+ *
52
+ * @param {object} context — 컨텍스트 필드 (method, path 등)
53
+ * @param {string} [context.correlationId] — 외부에서 전달된 상관 ID (없으면 자동 생성)
54
+ * @param {function} callback — 컨텍스트 내에서 실행할 함수
55
+ * @returns {*}
56
+ */
57
+ export function withRequestContext(context, callback) {
58
+ const correlationId = context.correlationId || randomUUID();
59
+ const { correlationId: _, ...rest } = context;
60
+
61
+ const store = {
62
+ correlationId,
63
+ logger: logger.child({ correlationId, ...rest }),
64
+ };
65
+
66
+ return asyncLocalStorage.run(store, callback);
67
+ }
@@ -0,0 +1,59 @@
1
+ import { join } from "node:path";
2
+
3
+ export const SESSION_TTL_SEC = 30 * 60;
4
+ export const STATE_REL_PATH = join(".triflux", "state", "cross-review.json");
5
+
6
+ export function readStdin() {
7
+ return new Promise((resolve) => {
8
+ let raw = "";
9
+ process.stdin.setEncoding("utf8");
10
+ process.stdin.on("data", (chunk) => {
11
+ raw += chunk;
12
+ });
13
+ process.stdin.on("end", () => resolve(raw));
14
+ process.stdin.on("error", () => resolve(""));
15
+ });
16
+ }
17
+
18
+ export function parseJson(raw) {
19
+ try {
20
+ return JSON.parse(raw);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ export function nowSec() {
27
+ return Math.floor(Date.now() / 1000);
28
+ }
29
+
30
+ export function resolveBaseDir(payload) {
31
+ if (typeof payload?.cwd === "string" && payload.cwd.trim())
32
+ return payload.cwd;
33
+ if (typeof payload?.directory === "string" && payload.directory.trim())
34
+ return payload.directory;
35
+ return process.cwd();
36
+ }
37
+
38
+ export function shouldTrackPath(filePath) {
39
+ if (typeof filePath !== "string" || !filePath.trim()) return false;
40
+
41
+ const lower = filePath.toLowerCase();
42
+ if (
43
+ lower.startsWith(".triflux/") ||
44
+ lower.startsWith(".omc/") ||
45
+ lower.startsWith(".claude/")
46
+ )
47
+ return false;
48
+ if (lower === "package-lock.json" || lower.endsWith("/package-lock.json"))
49
+ return false;
50
+ if (/\.(md|lock|yml|yaml)$/i.test(lower)) return false;
51
+ return true;
52
+ }
53
+
54
+ export function expectedReviewer(author) {
55
+ if (author === "claude") return "codex";
56
+ if (author === "codex") return "claude";
57
+ if (author === "gemini") return "claude";
58
+ return "";
59
+ }
@@ -0,0 +1,121 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ export function commandExists(name) {
4
+ try {
5
+ execFileSync("sh", ["-c", 'command -v "$1" >/dev/null 2>&1', "sh", name], {
6
+ stdio: "ignore",
7
+ timeout: 2000,
8
+ });
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ export function inspectMacTimeoutDependency({
16
+ platform = process.platform,
17
+ commandExists: exists = commandExists,
18
+ } = {}) {
19
+ if (platform !== "darwin") {
20
+ return { ok: true, status: "skipped", platform };
21
+ }
22
+
23
+ if (exists("gtimeout")) {
24
+ return { ok: true, status: "ok", platform, provider: "gtimeout" };
25
+ }
26
+ if (exists("timeout")) {
27
+ return { ok: true, status: "ok", platform, provider: "timeout" };
28
+ }
29
+
30
+ return {
31
+ ok: false,
32
+ status: "missing",
33
+ platform,
34
+ fix: "brew install coreutils",
35
+ };
36
+ }
37
+
38
+ export function addPluginRootFallbackToCommand(command, pluginRoot) {
39
+ if (typeof command !== "string") return command;
40
+ const fallbackRoot = String(pluginRoot || "").replace(/\\/g, "/");
41
+ if (!fallbackRoot) return command;
42
+ return command
43
+ .replaceAll("${PLUGIN_ROOT}", `\${PLUGIN_ROOT:-${fallbackRoot}}`)
44
+ .replaceAll(
45
+ "${CLAUDE_PLUGIN_ROOT}",
46
+ `\${CLAUDE_PLUGIN_ROOT:-${fallbackRoot}}`,
47
+ )
48
+ .replace(
49
+ /(^|[\s"'])\/(hooks|scripts)\//g,
50
+ `$1\${PLUGIN_ROOT:-${fallbackRoot}}/$2/`,
51
+ );
52
+ }
53
+
54
+ function hasBarePluginRootReference(command) {
55
+ if (typeof command !== "string") return false;
56
+ return (
57
+ command.includes("${PLUGIN_ROOT}/") ||
58
+ command.includes("${CLAUDE_PLUGIN_ROOT}/")
59
+ );
60
+ }
61
+
62
+ function hasCollapsedRootHookPath(command) {
63
+ if (typeof command !== "string") return false;
64
+ return /(^|[\s"'])\/(?:hooks|scripts)\//.test(command);
65
+ }
66
+
67
+ export function findPluginRootHookIssues(settings) {
68
+ const hooksByEvent =
69
+ settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
70
+ const issues = [];
71
+
72
+ for (const [event, entries] of Object.entries(hooksByEvent)) {
73
+ if (!Array.isArray(entries)) continue;
74
+ for (const entry of entries) {
75
+ const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
76
+ for (const hook of hooks) {
77
+ const command = hook?.command;
78
+ if (
79
+ !hasBarePluginRootReference(command) &&
80
+ !hasCollapsedRootHookPath(command)
81
+ ) {
82
+ continue;
83
+ }
84
+ issues.push({
85
+ event,
86
+ matcher: entry?.matcher || "*",
87
+ command,
88
+ reason: hasCollapsedRootHookPath(command)
89
+ ? "collapsed-root-path"
90
+ : "missing-plugin-root-fallback",
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ return issues;
97
+ }
98
+
99
+ export function applyPluginRootHookFallbacks(settings, { pluginRoot } = {}) {
100
+ if (!settings?.hooks || typeof settings.hooks !== "object") {
101
+ return { changed: false, count: 0, settings };
102
+ }
103
+
104
+ let count = 0;
105
+ for (const entries of Object.values(settings.hooks)) {
106
+ if (!Array.isArray(entries)) continue;
107
+ for (const entry of entries) {
108
+ const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
109
+ for (const hook of hooks) {
110
+ if (typeof hook?.command !== "string") continue;
111
+ const next = addPluginRootFallbackToCommand(hook.command, pluginRoot);
112
+ if (next !== hook.command) {
113
+ hook.command = next;
114
+ count++;
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ return { changed: count > 0, count, settings };
121
+ }
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ // scripts/lib/dynamic-route-cli.mjs
3
+ //
4
+ // Phase 1 Wire-up 3 helper — tfx-route.sh 등 sh 경로에서 dynamic routing
5
+ // decision 을 받아 단일 CLI 문자열 (또는 JSON) 으로 출력한다.
6
+ //
7
+ // 사용:
8
+ // node scripts/lib/dynamic-route-cli.mjs --agent-hint codex
9
+ // → "codex"
10
+ // node scripts/lib/dynamic-route-cli.mjs --agent-hint codex --json
11
+ // → {"scenario":"fallback","mode":"codex-default","shards":[{"cli":"codex",...}]}
12
+ // node scripts/lib/dynamic-route-cli.mjs --agent-hint codex --field scenario
13
+ // → "fallback"
14
+ //
15
+ // 정책:
16
+ // - opt-in env TRIFLUX_DYNAMIC_ROUTING=1|true 미설정 시 즉시 silent exit 0
17
+ // (override 없음 = stdout empty, sh caller 가 graceful no-op).
18
+ // - error 발생 시 stderr 에 한 줄 + exit 0 (sh caller 가 fallback path 진행).
19
+
20
+ import { createDynamicRouter } from "@triflux/core/hub/dynamic-routing-engine.mjs";
21
+
22
+ function parseArgs(argv) {
23
+ const args = {
24
+ taskId: `tfx-route-${process.pid}-${Date.now()}`,
25
+ agentHint: "codex",
26
+ teamSize: 1,
27
+ json: false,
28
+ field: null,
29
+ };
30
+ for (let i = 2; i < argv.length; i++) {
31
+ const v = argv[i];
32
+ switch (v) {
33
+ case "--task-id":
34
+ args.taskId = argv[++i] || args.taskId;
35
+ break;
36
+ case "--agent-hint":
37
+ args.agentHint = argv[++i] || args.agentHint;
38
+ break;
39
+ case "--team-size":
40
+ args.teamSize = Number(argv[++i]) || args.teamSize;
41
+ break;
42
+ case "--json":
43
+ args.json = true;
44
+ break;
45
+ case "--field":
46
+ args.field = argv[++i] || null;
47
+ break;
48
+ case "--help":
49
+ case "-h":
50
+ process.stdout.write(
51
+ "Usage: dynamic-route-cli.mjs [--task-id ID] [--agent-hint CLI] [--team-size N] [--json|--field FIELD]\n",
52
+ );
53
+ process.exit(0);
54
+ break;
55
+ }
56
+ }
57
+ return args;
58
+ }
59
+
60
+ async function main() {
61
+ const args = parseArgs(process.argv);
62
+
63
+ // opt-in env 미설정 시 silent no-op (sh caller graceful path)
64
+ const flag = process.env.TRIFLUX_DYNAMIC_ROUTING;
65
+ if (flag !== "1" && flag !== "true") return;
66
+
67
+ let decision;
68
+ try {
69
+ const router = createDynamicRouter();
70
+ decision = await router.routeRequest({
71
+ task_id: args.taskId,
72
+ team_size: args.teamSize,
73
+ agent_hint: args.agentHint,
74
+ });
75
+ } catch (err) {
76
+ process.stderr.write(
77
+ `[dynamic-route-cli] error: ${err?.message || String(err)}\n`,
78
+ );
79
+ return;
80
+ }
81
+
82
+ if (args.json) {
83
+ process.stdout.write(JSON.stringify(decision));
84
+ return;
85
+ }
86
+
87
+ if (args.field) {
88
+ const value = decision?.[args.field];
89
+ if (value !== undefined && value !== null) {
90
+ process.stdout.write(String(value));
91
+ }
92
+ return;
93
+ }
94
+
95
+ // 기본 — shards[0].cli 만 stdout 으로 (sh caller 가 즉시 비교)
96
+ const cli = decision?.shards?.[0]?.cli;
97
+ if (cli) {
98
+ process.stdout.write(String(cli));
99
+ }
100
+ }
101
+
102
+ main().catch((err) => {
103
+ process.stderr.write(
104
+ `[dynamic-route-cli] fatal: ${err?.message || String(err)}\n`,
105
+ );
106
+ process.exit(0); // sh caller 가 정상 fallback 하도록 always 0
107
+ });