@triflux/core 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.
- package/hooks/agy-session-hook.mjs +11 -56
- package/hooks/codex-session-hook.mjs +11 -101
- package/hub/bridge.mjs +1 -79
- package/hub/cli-adapter-base.mjs +12 -90
- package/hub/codex-adapter.mjs +0 -1
- package/hub/codex-preflight.mjs +5 -28
- package/hub/index.mjs +3 -9
- package/hub/lib/cto-env.mjs +0 -23
- package/hub/lib/known-errors.json +2 -2
- package/hub/lib/process-utils.mjs +0 -123
- package/hub/lib/spawn-trace.mjs +3 -2
- package/hub/lib/timeout-defaults.mjs +0 -1
- package/hub/paths.mjs +0 -6
- package/hub/pipeline/index.mjs +0 -190
- package/hub/pipeline/transitions.mjs +0 -10
- package/hub/reflexion.mjs +1 -84
- package/hub/role-contract.mjs +0 -2
- package/hud/cli-policy.mjs +0 -26
- package/hud/colors.mjs +0 -13
- package/hud/constants.mjs +7 -153
- package/hud/context-monitor.mjs +3 -64
- package/hud/hud-qos-status.mjs +54 -264
- package/hud/providers/claude.mjs +4 -16
- package/hud/providers/gemini.mjs +83 -689
- package/hud/renderers.mjs +59 -367
- package/hud/terminal.mjs +1 -63
- package/hud/utils.mjs +2 -26
- package/package.json +1 -4
- package/scripts/lib/agent-route-policy.mjs +162 -14
- package/scripts/lib/claudemd-scanner.mjs +2 -199
- package/scripts/lib/cli-claude.mjs +0 -2
- package/scripts/lib/context.mjs +3 -19
- package/scripts/lib/env-probe.mjs +5 -64
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +25 -104
- package/scripts/lib/session-presence.mjs +1 -1
- package/scripts/lib/skill-template.mjs +0 -271
- package/hub/codex-compat.mjs +0 -12
- package/hub/dynamic-routing-engine.mjs +0 -511
- package/hub/fullcycle.mjs +0 -113
- package/hub/gemini-adapter.mjs +0 -174
- package/hub/intent.mjs +0 -321
- package/hub/lib/mcp-response-cache.mjs +0 -239
- package/hub/lib/path-utils.mjs +0 -165
- package/hub/lib/risk-tier.mjs +0 -53
- package/hub/lib/ssh-command.mjs +0 -231
- package/hub/lib/staged-file-detect.mjs +0 -43
- package/hub/lib/tfx-route-args.mjs +0 -353
- package/hub/lib/timeline-adapter.mjs +0 -190
- package/hub/pipeline/gates/confidence.mjs +0 -75
- package/hub/pipeline/gates/consensus.mjs +0 -106
- package/hub/pipeline/gates/index.mjs +0 -9
- package/hub/pipeline/gates/selfcheck.mjs +0 -120
- package/hub/quality/deslop.mjs +0 -274
- package/hub/research.mjs +0 -155
- package/hub/role-control-events.mjs +0 -113
- package/hub/routing/complexity.mjs +0 -269
- package/hub/routing/index.mjs +0 -122
- package/hub/routing/q-learning.mjs +0 -387
- package/hub/routing-snapshot.mjs +0 -261
- package/hub/token-mode.mjs +0 -228
- package/hud/mission-board.mjs +0 -56
- package/hud/providers/cto.mjs +0 -76
- package/mesh/index.mjs +0 -69
- package/mesh/mesh-budget.mjs +0 -137
- package/mesh/mesh-heartbeat.mjs +0 -100
- package/mesh/mesh-protocol.mjs +0 -96
- package/mesh/mesh-queue.mjs +0 -163
- package/mesh/mesh-registry.mjs +0 -78
- package/mesh/mesh-router.mjs +0 -77
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -2,23 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { argv, exit, stdin, stdout } from "node:process";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { drainPendingSynapse as defaultDrainPendingSynapse } from "../hub/team/synapse-http.mjs";
|
|
6
5
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
emitParticipantSessionStarted,
|
|
7
|
+
shouldSkipInteractiveRegistration,
|
|
9
8
|
} from "../scripts/lib/session-presence.mjs";
|
|
10
9
|
|
|
11
|
-
// hub-ensure is loaded lazily so the byte-identical packages/core mirror of this
|
|
12
|
-
// file loads cleanly. packages/core mirrors scripts/lib only (not scripts/*), so a
|
|
13
|
-
// static `../scripts/hub-ensure.mjs` import would make the core copy throw
|
|
14
|
-
// ERR_MODULE_NOT_FOUND at load time. Mirrors codex-session-hook.mjs's pattern.
|
|
15
|
-
async function defaultHubEnsureRun(stdinData) {
|
|
16
|
-
const { run } = await import(
|
|
17
|
-
new URL("../scripts/hub-ensure.mjs", import.meta.url).href
|
|
18
|
-
);
|
|
19
|
-
return run(stdinData);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
10
|
function parsePayload(stdinData) {
|
|
23
11
|
try {
|
|
24
12
|
const raw = typeof stdinData === "string" ? stdinData : "";
|
|
@@ -76,8 +64,7 @@ export function normalizeMode(argvMode, payload) {
|
|
|
76
64
|
if (Number.isFinite(invocationNum)) {
|
|
77
65
|
return invocationNum <= 1 ? "register" : "heartbeat";
|
|
78
66
|
}
|
|
79
|
-
//
|
|
80
|
-
// so it is the safe default for a first-contact payload.
|
|
67
|
+
// 첫 호출 정보가 없으면 시작 기록을 남긴다.
|
|
81
68
|
return "register";
|
|
82
69
|
}
|
|
83
70
|
|
|
@@ -108,23 +95,6 @@ async function runHookSideEffectsWithStdoutSuppressed(fn) {
|
|
|
108
95
|
}
|
|
109
96
|
}
|
|
110
97
|
|
|
111
|
-
/**
|
|
112
|
-
* Execute the observational Antigravity session hook.
|
|
113
|
-
*
|
|
114
|
-
* The hook always returns/writes an empty JSON object so hook stdout remains
|
|
115
|
-
* JSON-only and hook failures never block the user session.
|
|
116
|
-
*
|
|
117
|
-
* @param {string} stdinData
|
|
118
|
-
* @param {{
|
|
119
|
-
* argvMode?: string,
|
|
120
|
-
* writeStdout?: boolean,
|
|
121
|
-
* hubEnsureRun?: (stdinData: string) => Promise<unknown> | unknown,
|
|
122
|
-
* registerInteractiveSession?: (stdinData: string) => Promise<unknown> | unknown,
|
|
123
|
-
* heartbeatInteractiveSession?: (stdinData: string) => Promise<unknown> | unknown,
|
|
124
|
-
* drainPendingSynapse?: (timeoutMs?: number) => Promise<unknown> | unknown,
|
|
125
|
-
* }} [opts]
|
|
126
|
-
* @returns {Promise<string>}
|
|
127
|
-
*/
|
|
128
98
|
export async function runAgySessionHook(stdinData, opts = {}) {
|
|
129
99
|
const output = "{}\n";
|
|
130
100
|
const parsed = parsePayload(stdinData);
|
|
@@ -142,37 +112,22 @@ export async function runAgySessionHook(stdinData, opts = {}) {
|
|
|
142
112
|
}
|
|
143
113
|
|
|
144
114
|
const mode = normalizeMode(opts.argvMode ?? argv[2], parsed.payload);
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
opts.registerInteractiveSession || defaultRegisterInteractiveSession;
|
|
148
|
-
const heartbeatInteractiveSession =
|
|
149
|
-
opts.heartbeatInteractiveSession || defaultHeartbeatInteractiveSession;
|
|
150
|
-
const drainPendingSynapse =
|
|
151
|
-
opts.drainPendingSynapse || defaultDrainPendingSynapse;
|
|
115
|
+
const emitSessionStarted =
|
|
116
|
+
opts.emitSessionStarted || emitParticipantSessionStarted;
|
|
152
117
|
|
|
153
118
|
try {
|
|
154
119
|
await runHookSideEffectsWithStdoutSuppressed(async () => {
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
await Promise.resolve(registerInteractiveSession(sessionPayload));
|
|
161
|
-
} catch {}
|
|
162
|
-
try {
|
|
163
|
-
await drainPendingSynapse(1000);
|
|
164
|
-
} catch {}
|
|
165
|
-
} else if (mode === "heartbeat") {
|
|
166
|
-
try {
|
|
167
|
-
heartbeatInteractiveSession(sessionPayload);
|
|
168
|
-
} catch {}
|
|
120
|
+
if (
|
|
121
|
+
mode === "register" &&
|
|
122
|
+
!shouldSkipInteractiveRegistration(JSON.parse(sessionPayload), opts)
|
|
123
|
+
) {
|
|
169
124
|
try {
|
|
170
|
-
await
|
|
125
|
+
await emitSessionStarted(sessionPayload);
|
|
171
126
|
} catch {}
|
|
172
127
|
}
|
|
173
128
|
});
|
|
174
129
|
} catch {
|
|
175
|
-
//
|
|
130
|
+
// 로컬 기록 실패가 세션을 막지 않게 한다.
|
|
176
131
|
}
|
|
177
132
|
|
|
178
133
|
if (opts.writeStdout !== false) {
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
3
|
import { argv, exit, stdin, stdout } from "node:process";
|
|
5
|
-
import {
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
6
5
|
import { writeCodexSessionRecord } from "../hub/lib/codex-session-registry.mjs";
|
|
7
|
-
import { drainPendingSynapse as defaultDrainPendingSynapse } from "../hub/team/synapse-http.mjs";
|
|
8
6
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
emitParticipantSessionStarted,
|
|
8
|
+
shouldSkipInteractiveRegistration,
|
|
11
9
|
} from "../scripts/lib/session-presence.mjs";
|
|
12
10
|
|
|
13
|
-
// hub-ensure가 없는 core 미러도 불러올 수 있도록 지연 import한다.
|
|
14
|
-
async function defaultHubEnsureRun(stdinData) {
|
|
15
|
-
const { run } = await import(
|
|
16
|
-
new URL("../scripts/hub-ensure.mjs", import.meta.url).href
|
|
17
|
-
);
|
|
18
|
-
return run(stdinData);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
11
|
function parsePayload(stdinData) {
|
|
22
12
|
try {
|
|
23
13
|
const raw = typeof stdinData === "string" ? stdinData : "";
|
|
@@ -48,66 +38,6 @@ function normalizeMode(mode, payload) {
|
|
|
48
38
|
return "";
|
|
49
39
|
}
|
|
50
40
|
|
|
51
|
-
/**
|
|
52
|
-
* Start a detached bridge call so Codex/OMX presence reaches both the Synapse
|
|
53
|
-
* session registry and the hub agents table. This is intentionally independent
|
|
54
|
-
* of the legacy session-start work below: a missing or unavailable hub must
|
|
55
|
-
* never delay or fail SessionStart.
|
|
56
|
-
*/
|
|
57
|
-
export function launchCodexPresenceRegistration(payload, opts = {}) {
|
|
58
|
-
try {
|
|
59
|
-
const sessionId = String(payload?.session_id || "").trim();
|
|
60
|
-
if (!sessionId) return false;
|
|
61
|
-
|
|
62
|
-
const cwd =
|
|
63
|
-
typeof payload?.cwd === "string" && payload.cwd.trim()
|
|
64
|
-
? payload.cwd
|
|
65
|
-
: process.cwd();
|
|
66
|
-
const bridgePath =
|
|
67
|
-
opts.bridgePath ||
|
|
68
|
-
fileURLToPath(new URL("../hub/bridge.mjs", import.meta.url));
|
|
69
|
-
const spawnFn = opts.spawnFn || spawn;
|
|
70
|
-
const canonicalSessionId = String(process.env.OMX_SESSION_ID || "").trim();
|
|
71
|
-
const tmuxSession = String(
|
|
72
|
-
process.env.OMX_TMUX_SESSION_NAME || process.env.OMX_TMUX_SESSION || "",
|
|
73
|
-
).trim();
|
|
74
|
-
const tmuxPane = String(process.env.TMUX_PANE || "").trim();
|
|
75
|
-
const args = [
|
|
76
|
-
bridgePath,
|
|
77
|
-
"register",
|
|
78
|
-
"--session-id",
|
|
79
|
-
sessionId,
|
|
80
|
-
"--cwd",
|
|
81
|
-
cwd,
|
|
82
|
-
"--worktree-path",
|
|
83
|
-
cwd,
|
|
84
|
-
"--session-kind",
|
|
85
|
-
"interactive",
|
|
86
|
-
"--host",
|
|
87
|
-
"local",
|
|
88
|
-
"--codex-session-id",
|
|
89
|
-
sessionId,
|
|
90
|
-
];
|
|
91
|
-
if (canonicalSessionId && canonicalSessionId !== sessionId) {
|
|
92
|
-
args.push("--omx-session-id", canonicalSessionId);
|
|
93
|
-
}
|
|
94
|
-
if (tmuxSession) args.push("--tmux-session", tmuxSession);
|
|
95
|
-
if (tmuxPane) args.push("--tmux-pane", tmuxPane);
|
|
96
|
-
|
|
97
|
-
const child = spawnFn(process.execPath, args, {
|
|
98
|
-
detached: true,
|
|
99
|
-
stdio: "ignore",
|
|
100
|
-
windowsHide: true,
|
|
101
|
-
env: process.env,
|
|
102
|
-
});
|
|
103
|
-
child?.once?.("error", () => {});
|
|
104
|
-
child?.unref?.();
|
|
105
|
-
return true;
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
41
|
function swallowStdoutWrite(_chunk, encodingOrCallback, callback) {
|
|
112
42
|
const done =
|
|
113
43
|
typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
@@ -142,15 +72,8 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
|
142
72
|
? normalizeMode(opts.argvMode ?? argv[2], parsed.payload)
|
|
143
73
|
: "";
|
|
144
74
|
const writeSessionRecord = opts.writeSessionRecord || writeCodexSessionRecord;
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
opts.registerInteractiveSession || defaultRegisterInteractiveSession;
|
|
148
|
-
const heartbeatInteractiveSession =
|
|
149
|
-
opts.heartbeatInteractiveSession || defaultHeartbeatInteractiveSession;
|
|
150
|
-
const drainPendingSynapse =
|
|
151
|
-
opts.drainPendingSynapse || defaultDrainPendingSynapse;
|
|
152
|
-
const launchPresenceRegistration =
|
|
153
|
-
opts.launchPresenceRegistration || launchCodexPresenceRegistration;
|
|
75
|
+
const emitSessionStarted =
|
|
76
|
+
opts.emitSessionStarted || emitParticipantSessionStarted;
|
|
154
77
|
|
|
155
78
|
try {
|
|
156
79
|
await runHookSideEffectsWithStdoutSuppressed(async () => {
|
|
@@ -159,30 +82,17 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
|
|
|
159
82
|
writeSessionRecord(parsed.payload);
|
|
160
83
|
} catch {}
|
|
161
84
|
}
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
await hubEnsureRun(stdinData);
|
|
168
|
-
} catch {}
|
|
169
|
-
try {
|
|
170
|
-
await Promise.resolve(registerInteractiveSession(stdinData));
|
|
171
|
-
} catch {}
|
|
172
|
-
try {
|
|
173
|
-
await drainPendingSynapse(1000);
|
|
174
|
-
} catch {}
|
|
175
|
-
} else if (mode === "heartbeat") {
|
|
176
|
-
try {
|
|
177
|
-
heartbeatInteractiveSession(stdinData);
|
|
178
|
-
} catch {}
|
|
85
|
+
if (
|
|
86
|
+
mode === "register" &&
|
|
87
|
+
!shouldSkipInteractiveRegistration(parsed.payload, opts)
|
|
88
|
+
) {
|
|
179
89
|
try {
|
|
180
|
-
await
|
|
90
|
+
await emitSessionStarted(stdinData);
|
|
181
91
|
} catch {}
|
|
182
92
|
}
|
|
183
93
|
});
|
|
184
94
|
} catch {
|
|
185
|
-
//
|
|
95
|
+
// 로컬 기록 실패가 세션을 막지 않게 한다.
|
|
186
96
|
}
|
|
187
97
|
|
|
188
98
|
if (opts.writeStdout !== false) {
|
package/hub/bridge.mjs
CHANGED
|
@@ -5,15 +5,8 @@
|
|
|
5
5
|
// Named Pipe/Unix Socket 제어 채널을 우선 사용하고,
|
|
6
6
|
// 연결이 없을 때만 HTTP /bridge/* 엔드포인트로 내려간다.
|
|
7
7
|
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
8
|
import { randomUUID } from "node:crypto";
|
|
10
|
-
import {
|
|
11
|
-
existsSync,
|
|
12
|
-
mkdirSync,
|
|
13
|
-
openSync,
|
|
14
|
-
readFileSync,
|
|
15
|
-
writeFileSync,
|
|
16
|
-
} from "node:fs";
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
10
|
import net from "node:net";
|
|
18
11
|
import { homedir } from "node:os";
|
|
19
12
|
import { join } from "node:path";
|
|
@@ -515,56 +508,6 @@ function buildPublishBody(from, to, type, payload) {
|
|
|
515
508
|
};
|
|
516
509
|
}
|
|
517
510
|
|
|
518
|
-
// Hub 자동 재시작 (Pipe+HTTP 모두 실패 시 1회 시도, 최대 4초 대기)
|
|
519
|
-
async function tryRestartHub() {
|
|
520
|
-
const serverPath = join(PROJECT_ROOT, "hub", "server.mjs");
|
|
521
|
-
if (!existsSync(serverPath)) return false;
|
|
522
|
-
|
|
523
|
-
if (existsSync(HUB_PID_FILE)) {
|
|
524
|
-
try {
|
|
525
|
-
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
526
|
-
if (info.pid) {
|
|
527
|
-
try {
|
|
528
|
-
process.kill(info.pid, 0);
|
|
529
|
-
return false;
|
|
530
|
-
} catch (e) {
|
|
531
|
-
// still alive
|
|
532
|
-
if (e.code === "EPERM") return false;
|
|
533
|
-
} // alive, no permission
|
|
534
|
-
}
|
|
535
|
-
} catch {} // corrupt PID file, proceed with restart
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
try {
|
|
539
|
-
const logDir = join(process.cwd(), ".tfx", "logs");
|
|
540
|
-
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
541
|
-
const logFile = join(logDir, "hub-restart.log");
|
|
542
|
-
const logFd = openSync(logFile, "a");
|
|
543
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
544
|
-
detached: true,
|
|
545
|
-
stdio: ["ignore", "ignore", logFd],
|
|
546
|
-
windowsHide: true,
|
|
547
|
-
});
|
|
548
|
-
child.unref();
|
|
549
|
-
} catch {
|
|
550
|
-
return false;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
for (let i = 0; i < 8; i++) {
|
|
554
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
555
|
-
try {
|
|
556
|
-
const res = await fetch(`${getHubUrl()}/status`, {
|
|
557
|
-
signal: AbortSignal.timeout(1000),
|
|
558
|
-
});
|
|
559
|
-
if (res.ok) {
|
|
560
|
-
const data = await res.json();
|
|
561
|
-
if (data?.hub?.state === "healthy") return true;
|
|
562
|
-
}
|
|
563
|
-
} catch {}
|
|
564
|
-
}
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
511
|
async function requestHub(operation, body, timeoutMs = 3000, fallback = null) {
|
|
569
512
|
const viaPipe =
|
|
570
513
|
operation.transport === "command"
|
|
@@ -585,27 +528,6 @@ async function requestHub(operation, body, timeoutMs = 3000, fallback = null) {
|
|
|
585
528
|
return { transport: "http", result: viaHttp };
|
|
586
529
|
}
|
|
587
530
|
|
|
588
|
-
// Hub 재시작 시도 → Pipe/HTTP 재시도
|
|
589
|
-
if (await tryRestartHub()) {
|
|
590
|
-
const retryPipe =
|
|
591
|
-
operation.transport === "command"
|
|
592
|
-
? await pipeCommand(operation.action, body, timeoutMs)
|
|
593
|
-
: await pipeQuery(operation.action, body, timeoutMs);
|
|
594
|
-
if (retryPipe) {
|
|
595
|
-
return { transport: "pipe", result: retryPipe };
|
|
596
|
-
}
|
|
597
|
-
const retryHttp = operation.httpPath
|
|
598
|
-
? await requestJson(operation.httpPath, {
|
|
599
|
-
method: operation.httpMethod || "POST",
|
|
600
|
-
body: operation.httpMethod === "GET" ? undefined : body,
|
|
601
|
-
timeoutMs: Math.max(timeoutMs, 5000),
|
|
602
|
-
})
|
|
603
|
-
: null;
|
|
604
|
-
if (retryHttp) {
|
|
605
|
-
return { transport: "http", result: retryHttp };
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
531
|
if (!fallback) return null;
|
|
610
532
|
const viaFallback = await fallback();
|
|
611
533
|
if (!viaFallback) return null;
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// hub/cli-adapter-base.mjs — codex/gemini 공통 CLI adapter 인터페이스
|
|
2
2
|
// Phase 2: codex-adapter.mjs에서 추출한 재사용 가능 유틸리티
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
|
|
7
7
|
import { codexProfileConfigOverrides } from "../scripts/lib/codex-profile-config.mjs";
|
|
@@ -70,71 +70,6 @@ export function parseRetryAfterMs(text, provider) {
|
|
|
70
70
|
return FALLBACK_COOLDOWN_MS[provider] || 5 * 3600_000;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// ── Codex CLI compatibility ─────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
let _cachedVersion = null;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
|
|
79
|
-
* 파싱 실패 시 0 반환 (구버전으로 간주).
|
|
80
|
-
* @returns {number} 마이너 버전 (예: 0.117.0 → 117)
|
|
81
|
-
*/
|
|
82
|
-
export function getCodexVersion() {
|
|
83
|
-
if (_cachedVersion !== null) return _cachedVersion;
|
|
84
|
-
const override = Number(process.env.TFX_CODEX_VERSION_MINOR);
|
|
85
|
-
if (Number.isFinite(override) && override > 0) {
|
|
86
|
-
_cachedVersion = override;
|
|
87
|
-
return _cachedVersion;
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
const out = execSync("codex --version", {
|
|
91
|
-
encoding: "utf8",
|
|
92
|
-
timeout: 5000,
|
|
93
|
-
}).trim();
|
|
94
|
-
const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
95
|
-
_cachedVersion = match ? Number.parseInt(match[2], 10) : 0;
|
|
96
|
-
} catch {
|
|
97
|
-
// Command builders should remain stable in CI even when the real Codex
|
|
98
|
-
// CLI is absent. Runtime preflight still reports/install-gates Codex
|
|
99
|
-
// separately; this fallback only selects the modern argv shape.
|
|
100
|
-
_cachedVersion = 117;
|
|
101
|
-
}
|
|
102
|
-
return _cachedVersion;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 최소 마이너 버전 이상인지 확인.
|
|
107
|
-
* @param {number} minMinor
|
|
108
|
-
* @returns {boolean}
|
|
109
|
-
*/
|
|
110
|
-
export function gte(minMinor) {
|
|
111
|
-
return getCodexVersion() >= minMinor;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Codex CLI 기능별 분기 객체.
|
|
116
|
-
* 실측 기반 임계값: 0.114.0에서 exec/skip-git-repo-check/color 확인됨.
|
|
117
|
-
* --output-last-message는 0.114.0에 없음 (0.117+ 추정).
|
|
118
|
-
*/
|
|
119
|
-
export const FEATURES = {
|
|
120
|
-
/** exec 서브커맨드 사용 가능 여부 (0.110+ 이전부터 존재) */
|
|
121
|
-
get execSubcommand() {
|
|
122
|
-
return gte(110);
|
|
123
|
-
},
|
|
124
|
-
/** --output-last-message 플래그 지원 여부 (0.117+) */
|
|
125
|
-
get outputLastMessage() {
|
|
126
|
-
return gte(117);
|
|
127
|
-
},
|
|
128
|
-
/** --color <COLOR> 플래그 지원 여부 (exec와 동시 도입) */
|
|
129
|
-
get colorNever() {
|
|
130
|
-
return gte(110);
|
|
131
|
-
},
|
|
132
|
-
/** 플러그인 시스템 지원 여부 (향후 확장용) */
|
|
133
|
-
get pluginSystem() {
|
|
134
|
-
return gte(120);
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
|
|
138
73
|
// ── Shell utilities ─────────────────────────────────────────────
|
|
139
74
|
|
|
140
75
|
export function normalizePathForShell(value) {
|
|
@@ -182,7 +117,7 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
182
117
|
enforceCanonicalProfile,
|
|
183
118
|
} = opts;
|
|
184
119
|
|
|
185
|
-
const parts = ["codex"];
|
|
120
|
+
const parts = ["codex", "exec"];
|
|
186
121
|
// Select the effort profile via `-c` config overrides instead of
|
|
187
122
|
// `--profile <name>`. codex 0.134+ rejects `--profile X` whenever config.toml
|
|
188
123
|
// still contains an inline [profiles.X] table (and codex re-injects such
|
|
@@ -196,30 +131,17 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
196
131
|
})
|
|
197
132
|
: [];
|
|
198
133
|
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
for (const
|
|
208
|
-
parts.push("-c",
|
|
209
|
-
// NOTE: `codex exec`는 --cwd 플래그를 지원하지 않는다. Node spawn의 cwd
|
|
210
|
-
// 옵션으로 child process의 working directory를 제어한다 (conductor.mjs 참조).
|
|
211
|
-
// opts.cwd는 기록용으로만 받아두고 CLI command에는 반영하지 않는다.
|
|
212
|
-
// (이전 커밋에서 잘못 추가되어 shard 전체가 exit 2로 크래시한 회귀 #94 후속 이슈)
|
|
213
|
-
if (Array.isArray(mcpServers)) {
|
|
214
|
-
for (const server of mcpServers) {
|
|
215
|
-
parts.push("-c", `mcp_servers.${server}.enabled=true`);
|
|
216
|
-
}
|
|
134
|
+
if (sandboxBypass) parts.push("--dangerously-bypass-approvals-and-sandbox");
|
|
135
|
+
if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
|
|
136
|
+
if (resultFile) parts.push("--output-last-message", resultFile);
|
|
137
|
+
parts.push("--color", "never");
|
|
138
|
+
for (const override of profileOverrides)
|
|
139
|
+
parts.push("-c", shellQuote(override));
|
|
140
|
+
// `codex exec`는 --cwd를 받지 않아 child process의 cwd로 제어한다.
|
|
141
|
+
if (Array.isArray(mcpServers)) {
|
|
142
|
+
for (const server of mcpServers) {
|
|
143
|
+
parts.push("-c", `mcp_servers.${server}.enabled=true`);
|
|
217
144
|
}
|
|
218
|
-
} else {
|
|
219
|
-
parts.push("--dangerously-bypass-approvals-and-sandbox");
|
|
220
|
-
if (skipGitRepoCheck) parts.push("--skip-git-repo-check");
|
|
221
|
-
for (const override of profileOverrides)
|
|
222
|
-
parts.push("-c", shellQuote(override));
|
|
223
145
|
}
|
|
224
146
|
|
|
225
147
|
const useStdin = resolveStdinPromptMode(stdinPrompt);
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -126,7 +126,6 @@ export function buildExecArgs(opts = {}) {
|
|
|
126
126
|
cwd: opts.cwd,
|
|
127
127
|
// stdinPrompt 가 명시되면 buildExecCommand 로 forward.
|
|
128
128
|
// headless 워커 spawn (backend.mjs) 은 default (stdin on, macOS hook fix).
|
|
129
|
-
// launcher path (launcher-template.mjs) 는 결정론 위해 false 전달.
|
|
130
129
|
stdinPrompt: opts.stdinPrompt,
|
|
131
130
|
});
|
|
132
131
|
|
package/hub/codex-preflight.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import { join } from "node:path";
|
|
|
5
5
|
|
|
6
6
|
import { whichCommandAsync } from "./platform.mjs";
|
|
7
7
|
|
|
8
|
-
const MIN_RECOMMENDED_MINOR = 118;
|
|
9
8
|
let _cachedVersion = null;
|
|
10
9
|
|
|
11
10
|
function escapeRegExp(value) {
|
|
@@ -74,25 +73,11 @@ async function checkCodexInstalled() {
|
|
|
74
73
|
};
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
function checkCodexVersion() {
|
|
78
|
-
const version = getCodexVersion();
|
|
79
|
-
const warnings =
|
|
80
|
-
version >= MIN_RECOMMENDED_MINOR
|
|
81
|
-
? []
|
|
82
|
-
: [
|
|
83
|
-
`Codex CLI 0.${version}.x detected; 0.${MIN_RECOMMENDED_MINOR}.x or newer is recommended.`,
|
|
84
|
-
];
|
|
85
|
-
return { version, warnings };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
76
|
function checkApprovalMode(configText, opts = {}) {
|
|
89
|
-
const
|
|
90
|
-
const sandbox = readTomlString(configText, "sandbox");
|
|
77
|
+
const approvalPolicy = readTomlString(configText, "approval_policy");
|
|
91
78
|
const subcommand = opts.subcommand || "exec";
|
|
92
79
|
return {
|
|
93
|
-
needsBypass: subcommand === "exec" ||
|
|
94
|
-
approvalMode,
|
|
95
|
-
sandbox,
|
|
80
|
+
needsBypass: subcommand === "exec" || approvalPolicy !== "never",
|
|
96
81
|
};
|
|
97
82
|
}
|
|
98
83
|
|
|
@@ -154,7 +139,7 @@ async function checkMcpHealth(mcpServers, configText) {
|
|
|
154
139
|
/**
|
|
155
140
|
* Detects codex config drift between global (~/.codex/config.toml) and the
|
|
156
141
|
* target workdir. Covers:
|
|
157
|
-
* 1. workdir-local `.codex/config.toml` overriding global
|
|
142
|
+
* 1. workdir-local `.codex/config.toml` overriding global approval_policy/sandbox_mode
|
|
158
143
|
* 2. instruction-file asymmetry (AGENTS.md vs CLAUDE.md)
|
|
159
144
|
*
|
|
160
145
|
* @param {string} [workdir] absolute path to the invocation workdir
|
|
@@ -173,7 +158,7 @@ export function detectWorkdirDrift(workdir, globalConfigText = "") {
|
|
|
173
158
|
} catch {
|
|
174
159
|
localText = "";
|
|
175
160
|
}
|
|
176
|
-
const keys = ["
|
|
161
|
+
const keys = ["approval_policy", "sandbox_mode", "model"];
|
|
177
162
|
for (const key of keys) {
|
|
178
163
|
const local = readTomlString(localText, key);
|
|
179
164
|
const global = readTomlString(globalConfigText, key);
|
|
@@ -212,18 +197,10 @@ export async function runPreflight(opts = {}) {
|
|
|
212
197
|
}
|
|
213
198
|
|
|
214
199
|
const warnings = [...install.warnings];
|
|
215
|
-
const
|
|
216
|
-
warnings.push(...versionWarnings);
|
|
200
|
+
const version = getCodexVersion();
|
|
217
201
|
|
|
218
202
|
const configText = readConfigText(opts.configPath);
|
|
219
203
|
const approval = checkApprovalMode(configText, opts);
|
|
220
|
-
if (approval.approvalMode !== "full-auto") {
|
|
221
|
-
warnings.push(
|
|
222
|
-
`approval_mode is '${approval.approvalMode || "unset"}'; bypass flag will be used.`,
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
if (approval.sandbox)
|
|
226
|
-
warnings.push(`sandbox mode from config.toml: ${approval.sandbox}`);
|
|
227
204
|
|
|
228
205
|
const mcp = await checkMcpHealth(opts.mcpServers, configText);
|
|
229
206
|
warnings.push(...mcp.warnings);
|
package/hub/index.mjs
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// @triflux/core — barrel export (auto-generated by pack.mjs)
|
|
2
2
|
// Adapters
|
|
3
|
-
export { buildExecCommand, sleep,
|
|
3
|
+
export { buildExecCommand, sleep, CODEX_MCP_TRANSPORT_EXIT_CODE, normalizePathForShell, shellQuote } from './cli-adapter-base.mjs';
|
|
4
4
|
export { execute as executeCodex, buildExecArgs as buildCodexArgs, buildLaunchScript, getCircuitState as getCodexCircuit } from './codex-adapter.mjs';
|
|
5
|
-
export { execute as executeGemini, buildExecArgs as buildGeminiArgs, getCircuitState as getGeminiCircuit } from './gemini-adapter.mjs';
|
|
6
5
|
export { getCodexVersion, runPreflight } from './codex-preflight.mjs';
|
|
7
6
|
|
|
8
|
-
// Routing
|
|
7
|
+
// Routing
|
|
9
8
|
export { createRouter } from './router.mjs';
|
|
10
|
-
export { classifyIntent, quickClassify, INTENT_CATEGORIES, refineClassification } from './intent.mjs';
|
|
11
9
|
|
|
12
10
|
// State & Paths & Platform
|
|
13
11
|
export * from './paths.mjs';
|
|
@@ -18,12 +16,8 @@ export { createHitlManager } from './hitl.mjs';
|
|
|
18
16
|
export { createAssignCallbackServer } from './assign-callbacks.mjs';
|
|
19
17
|
export { getHubUrl, getHubPipePath, requestJson, post, connectPipe, parseArgs, parseJsonSafe } from './bridge.mjs';
|
|
20
18
|
|
|
21
|
-
// Fullcycle & Research
|
|
22
|
-
export { createFullcycleRunId, getFullcycleRunDir, ensureFullcycleRunDir, saveFullcycleArtifact, readFullcycleArtifact, writeFullcycleState, readFullcycleState, shouldStopQaLoop } from './fullcycle.mjs';
|
|
23
|
-
export { generateQueries, normalizeResults, buildReport, saveReport } from './research.mjs';
|
|
24
|
-
|
|
25
19
|
// Reflexion (adaptive error learning)
|
|
26
|
-
export { normalizeError,
|
|
20
|
+
export { normalizeError, adaptiveRuleFromError } from './reflexion.mjs';
|
|
27
21
|
|
|
28
22
|
// Neural Memory (Lake 1.5)
|
|
29
23
|
export { createAdaptiveEngine } from './adaptive.mjs';
|
package/hub/lib/cto-env.mjs
CHANGED
|
@@ -64,30 +64,7 @@ export function isCtoDisabled() {
|
|
|
64
64
|
return isExplicitlyOff("TFX_CTO");
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export function isCtoManagerEnabled() {
|
|
68
|
-
if (isCtoDisabled()) return false;
|
|
69
|
-
return isExplicitlyOn("TFX_CTO_MANAGER");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function getCtoHygieneApplyMode() {
|
|
73
|
-
if (isExplicitlyOff("TFX_CTO")) return "off";
|
|
74
|
-
return normalizeEnvValue("TFX_CTO_HYGIENE_APPLY") === "archive"
|
|
75
|
-
? "archive"
|
|
76
|
-
: "off";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
67
|
export function isCtoRetentionEnabled() {
|
|
80
68
|
if (isExplicitlyOff("TFX_CTO")) return false;
|
|
81
69
|
return isExplicitlyOn("TFX_CTO_RETENTION");
|
|
82
70
|
}
|
|
83
|
-
|
|
84
|
-
export function getCtoMode() {
|
|
85
|
-
return normalizeEnvValue("TFX_CTO_MODE") === "bridge" ? "bridge" : "bounded";
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function getCtoMaxTokens() {
|
|
89
|
-
const raw = normalizeEnvValue("TFX_CTO_MAX_TOKENS");
|
|
90
|
-
if (!raw) return 0;
|
|
91
|
-
const value = Number(raw);
|
|
92
|
-
return Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
93
|
-
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"root_cause": "원격 호스트가 PowerShell인데 bash 문법(2>/dev/null)을 사용",
|
|
10
10
|
"dna_factor": "remote.os",
|
|
11
11
|
"rule_template": "원격 호스트 {host}는 PowerShell 기본 셸. 2>/dev/null 대신 2>$null, && 대신 ;, $(cmd) 대신 $(cmd) PowerShell 구문 사용.",
|
|
12
|
-
"fix": "
|
|
12
|
+
"fix": "PowerShell 호스트에서는 2>/dev/null 대신 2>$null 사용",
|
|
13
13
|
"confidence_base": 0.95,
|
|
14
14
|
"severity": "high"
|
|
15
15
|
},
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"root_cause": "원격 호스트가 PowerShell인데 bash의 && 연결자를 사용",
|
|
21
21
|
"dna_factor": "remote.os",
|
|
22
22
|
"rule_template": "원격 호스트 {host}는 PowerShell 기본 셸. && 대신 ; 사용.",
|
|
23
|
-
"fix": "
|
|
23
|
+
"fix": "PowerShell 호스트에서는 && 대신 ; 사용",
|
|
24
24
|
"confidence_base": 0.95,
|
|
25
25
|
"severity": "high"
|
|
26
26
|
},
|