@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.
- package/cto/brief.mjs +0 -12
- package/cto/collect.mjs +16 -247
- package/cto/current.schema.json +4 -20
- package/cto/events.mjs +13 -294
- package/cto/hygiene.mjs +26 -346
- package/cto/lake-root.mjs +114 -16
- package/cto/status.mjs +33 -128
- package/hub/index.mjs +0 -8
- package/hub/store-adapter.mjs +1 -1
- package/hub/team/agent-map.json +0 -1
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +1 -37
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +41 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +0 -3
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/hub-client.mjs +2 -88
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +14 -755
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +16 -403
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session.mjs +8 -119
- package/hub/team/swarm-intent.mjs +0 -198
- package/hub/team/terminal-opener.mjs +11 -35
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +8 -269
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +4 -37
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
- package/package.json +1 -1
- 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/skill-template.mjs +0 -271
- package/cto/hygiene-actions.mjs +0 -355
- package/cto/hygiene-notify.mjs +0 -170
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/gemini-worker.mjs +0 -217
- 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
package/hub/team/event-log.mjs
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// hub/team/event-log.mjs — JSONL 블랙박스 리코더
|
|
2
|
-
// Conductor 세션 lifecycle 이벤트를 JSONL 파일에 기록한다.
|
|
3
|
-
// 기존 hub/server.mjs의 batch-events.jsonl(MCP 이벤트)과 독립. 공존.
|
|
4
|
-
|
|
5
|
-
import { createWriteStream, mkdirSync } from "node:fs";
|
|
6
|
-
import { dirname } from "node:path";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* JSONL event log 팩토리.
|
|
10
|
-
* @param {string} filePath — 로그 파일 경로 (.jsonl)
|
|
11
|
-
* @param {object} [opts]
|
|
12
|
-
* @param {string} [opts.sessionId] — 모든 이벤트에 자동 삽입할 세션 ID
|
|
13
|
-
* @returns {{ append, flush, close, filePath }}
|
|
14
|
-
*/
|
|
15
|
-
export function createEventLog(filePath, opts = {}) {
|
|
16
|
-
const { sessionId } = opts;
|
|
17
|
-
|
|
18
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
19
|
-
const stream = createWriteStream(filePath, { flags: "a" });
|
|
20
|
-
|
|
21
|
-
let closed = false;
|
|
22
|
-
let pending = 0;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 이벤트를 JSONL 한 줄로 기록.
|
|
26
|
-
* @param {string} event — 이벤트 타입 (spawn, health, kill, stateChange, ...)
|
|
27
|
-
* @param {object} [data] — 이벤트 페이로드
|
|
28
|
-
*/
|
|
29
|
-
function append(event, data = {}) {
|
|
30
|
-
if (closed) return;
|
|
31
|
-
const entry = {
|
|
32
|
-
ts: new Date().toISOString(),
|
|
33
|
-
...(sessionId ? { session: sessionId } : {}),
|
|
34
|
-
event,
|
|
35
|
-
...data,
|
|
36
|
-
};
|
|
37
|
-
pending += 1;
|
|
38
|
-
stream.write(JSON.stringify(entry) + "\n", () => {
|
|
39
|
-
pending -= 1;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 버퍼된 이벤트를 디스크에 flush.
|
|
45
|
-
* @returns {Promise<void>}
|
|
46
|
-
*/
|
|
47
|
-
function flush() {
|
|
48
|
-
if (closed) return Promise.resolve();
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
stream.once("error", reject);
|
|
51
|
-
stream.write("", () => {
|
|
52
|
-
stream.removeListener("error", reject);
|
|
53
|
-
resolve();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 스트림 종료. flush 후 close.
|
|
60
|
-
* @returns {Promise<void>}
|
|
61
|
-
*/
|
|
62
|
-
function close() {
|
|
63
|
-
if (closed) return Promise.resolve();
|
|
64
|
-
closed = true;
|
|
65
|
-
return new Promise((resolve) => {
|
|
66
|
-
stream.end(() => resolve());
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return Object.freeze({
|
|
71
|
-
append,
|
|
72
|
-
flush,
|
|
73
|
-
close,
|
|
74
|
-
get filePath() {
|
|
75
|
-
return filePath;
|
|
76
|
-
},
|
|
77
|
-
get pending() {
|
|
78
|
-
return pending;
|
|
79
|
-
},
|
|
80
|
-
get closed() {
|
|
81
|
-
return closed;
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
}
|
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
// hub/team/execution-mode.mjs — headless vs interactive execution mode selection
|
|
2
|
-
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import { dirname, resolve as pathResolve } from "node:path";
|
|
5
|
-
import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
|
|
6
|
-
import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
|
|
7
|
-
import {
|
|
8
|
-
resolveGeminiModel,
|
|
9
|
-
resolveGeminiProfileForPurpose,
|
|
10
|
-
} from "../../scripts/lib/gemini-profiles.mjs";
|
|
11
|
-
import {
|
|
12
|
-
buildDisabledCliError,
|
|
13
|
-
normalizeCliName,
|
|
14
|
-
resolveCliPolicy,
|
|
15
|
-
} from "../../scripts/lib/machine-profile.mjs";
|
|
16
|
-
import { whichCommand } from "@triflux/core/hub/platform.mjs";
|
|
17
|
-
|
|
18
|
-
const WIN32_EXT_PRECEDENCE = [".cmd", ".exe", ".bat", ".ps1"];
|
|
19
|
-
|
|
20
|
-
// #128: parse npm-cmd-shim wrapper to extract the underlying .js entry point.
|
|
21
|
-
// npm shim wrappers end with: "%_prog%" "%dp0%\node_modules\...\<cli>.js" %*
|
|
22
|
-
// %* is cmd batch arg pass-through which mangles multi-line / fenced prompts.
|
|
23
|
-
// Bypass cmd entirely by spawning node + the .js directly.
|
|
24
|
-
export function unwrapCmdToJsScript(cmdPath, opts = {}) {
|
|
25
|
-
const readFile = opts.readFile || readFileSync;
|
|
26
|
-
const existsFn = opts.existsSyncFn || existsSync;
|
|
27
|
-
try {
|
|
28
|
-
const content = readFile(cmdPath, "utf8");
|
|
29
|
-
const match = content.match(/"%dp0%[\\/]([^"]+\.[cm]?js)"/i);
|
|
30
|
-
if (!match) return null;
|
|
31
|
-
const dir = dirname(cmdPath);
|
|
32
|
-
const jsPath = pathResolve(dir, match[1].replace(/\\/g, "/"));
|
|
33
|
-
return existsFn(jsPath) ? jsPath : null;
|
|
34
|
-
} catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const MODES = Object.freeze({
|
|
40
|
-
HEADLESS: "headless",
|
|
41
|
-
INTERACTIVE: "interactive",
|
|
42
|
-
AUTO: "auto",
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
function asPrompt(prompt) {
|
|
46
|
-
return typeof prompt === "string" ? prompt : "";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function pushFlag(args, flag, value) {
|
|
50
|
-
if (typeof value === "string" && value.length > 0) {
|
|
51
|
-
args.push(flag, value);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// #116-C: codex headless 의 argv-inline prompt 가 macOS oh-my-codex hook
|
|
56
|
-
// SessionStart Failed 회귀를 유발 (codex CLI v0.130.0). cli-adapter-base 의
|
|
57
|
-
// buildExecCommand 가 shell command 형태에서 동일 fix 를 적용했고
|
|
58
|
-
// (TFX_CODEX_STDIN_PROMPT=0 opt-out), 본 함수의 spawn-args 형태에서도 같은
|
|
59
|
-
// 정신으로 prompt 를 stdin transport 로 분리한다. 동일 env var 를 재사용해
|
|
60
|
-
// 사용자가 한 곳만 끄면 양쪽 path 모두 비활성.
|
|
61
|
-
export function resolveStdinPromptMode(explicit, envSource) {
|
|
62
|
-
if (typeof explicit === "boolean") return explicit;
|
|
63
|
-
const env = (envSource || process.env).TFX_CODEX_STDIN_PROMPT;
|
|
64
|
-
if (env === "0" || env === "false") return false;
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function normalizeCli(cli) {
|
|
69
|
-
const value = String(cli || "codex").toLowerCase();
|
|
70
|
-
if (value === "gemini" || value === "antigravity" || value === "agy") {
|
|
71
|
-
return "antigravity";
|
|
72
|
-
}
|
|
73
|
-
return value;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function resolveCliExecutable(cli, opts = {}) {
|
|
77
|
-
const name = String(cli || "codex");
|
|
78
|
-
const resolveCommand = opts.resolveCommand || whichCommand;
|
|
79
|
-
const resolved = resolveCommand(name) || name;
|
|
80
|
-
|
|
81
|
-
// Windows: Node spawn({ shell: false }) calls CreateProcess directly and does NOT
|
|
82
|
-
// search PATHEXT. npm-installed CLIs (codex, gemini) live at `<npm>/codex` (Git Bash
|
|
83
|
-
// shell script) alongside `<npm>/codex.cmd` (Windows batch wrapper). whichCommand
|
|
84
|
-
// returns the extensionless path, which Windows cannot execute → ENOENT "The system
|
|
85
|
-
// cannot find the file specified." Append the correct extension when the resolved
|
|
86
|
-
// path has none.
|
|
87
|
-
const platform = opts.platform || process.platform;
|
|
88
|
-
if (platform === "win32" && resolved) {
|
|
89
|
-
const hasExt = /\.[^\\/.]+$/.test(resolved);
|
|
90
|
-
if (!hasExt) {
|
|
91
|
-
const existsFn = opts.existsSyncFn || existsSync;
|
|
92
|
-
for (const ext of WIN32_EXT_PRECEDENCE) {
|
|
93
|
-
const candidate = `${resolved}${ext}`;
|
|
94
|
-
try {
|
|
95
|
-
if (existsFn(candidate)) return candidate;
|
|
96
|
-
} catch {
|
|
97
|
-
// ignore stat failures, try next extension
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return resolved;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function buildSpawnSpecForMode(mode, opts = {}) {
|
|
107
|
-
const cli = normalizeCli(opts.cli || "codex");
|
|
108
|
-
|
|
109
|
-
// TFX_DISABLE_* SSOT 집행. route 를 우회하는 spawn 경로도 tfx-route.sh 와 같은
|
|
110
|
-
// 정책을 따른다. 정책 판독은 런타임 process.env 와 machine profile 만 쓰고,
|
|
111
|
-
// 자식에게 넘길 세션 env(opts.env)는 쓰지 않는다. 주입 시임은 opts.cliPolicy
|
|
112
|
-
// (계산된 정책)와 opts.policyEnv(env 객체) 둘뿐이다.
|
|
113
|
-
// claude 처럼 정책 대상이 아닌 CLI 는 프로파일 파일을 읽지 않는다.
|
|
114
|
-
if (normalizeCliName(cli)) {
|
|
115
|
-
const cliPolicy =
|
|
116
|
-
opts.cliPolicy ?? resolveCliPolicy(opts.policyEnv ?? process.env);
|
|
117
|
-
const blockedCli = buildDisabledCliError(cli, cliPolicy);
|
|
118
|
-
if (blockedCli) throw blockedCli;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const prompt = asPrompt(opts.prompt);
|
|
122
|
-
const commandName = cli === "antigravity" ? "agy" : cli;
|
|
123
|
-
const resolvedCommand = resolveCliExecutable(commandName, opts);
|
|
124
|
-
const platform = opts.platform || process.platform;
|
|
125
|
-
|
|
126
|
-
// Node v20.12+ (CVE-2024-27980) rejects spawn of .cmd/.bat files with shell:false
|
|
127
|
-
// (EINVAL). npm-installed Windows wrappers (e.g. codex.cmd) hit this.
|
|
128
|
-
//
|
|
129
|
-
// #128 (BUG-A): the cmd /c wrapper used to be the workaround, but cmd batch
|
|
130
|
-
// %* arg pass-through mangles multi-line and fenced (```bash) prompts before
|
|
131
|
-
// they reach the underlying .js. We now unwrap the .cmd to the node script
|
|
132
|
-
// it launches and spawn `node <script>` directly, bypassing cmd entirely.
|
|
133
|
-
// Falls back to cmd /c if the .cmd cannot be parsed.
|
|
134
|
-
const needsCmdWrap =
|
|
135
|
-
platform === "win32" && /\.(cmd|bat)$/i.test(resolvedCommand);
|
|
136
|
-
const unwrappedJs = needsCmdWrap
|
|
137
|
-
? (opts.unwrapCmdFn || unwrapCmdToJsScript)(resolvedCommand)
|
|
138
|
-
: null;
|
|
139
|
-
const wrap = (args) => {
|
|
140
|
-
if (unwrappedJs) {
|
|
141
|
-
const nodeBin = opts.nodeExecPath || process.execPath;
|
|
142
|
-
return { command: nodeBin, args: [unwrappedJs, ...args] };
|
|
143
|
-
}
|
|
144
|
-
if (needsCmdWrap) {
|
|
145
|
-
return { command: "cmd", args: ["/c", resolvedCommand, ...args] };
|
|
146
|
-
}
|
|
147
|
-
return { command: resolvedCommand, args };
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
if (cli === "antigravity") {
|
|
151
|
-
// agy 1.1.27: --print 는 값(프롬프트)을 받는 마지막 인자다. stdin 프롬프트는
|
|
152
|
-
// "empty prompt" 로 거부되므로 argv 로 넘긴다.
|
|
153
|
-
// effort 는 역할별 SSOT(scripts/lib/gemini-profiles.mjs). 명시 model 이 우선한다.
|
|
154
|
-
const model =
|
|
155
|
-
typeof opts.model === "string" && opts.model.trim()
|
|
156
|
-
? resolveGeminiModel(opts.model.trim(), {
|
|
157
|
-
profilesPath: opts.geminiProfilesPath,
|
|
158
|
-
})
|
|
159
|
-
: resolveGeminiModel(
|
|
160
|
-
resolveGeminiProfileForPurpose(opts.role || opts.agent),
|
|
161
|
-
{ profilesPath: opts.geminiProfilesPath },
|
|
162
|
-
);
|
|
163
|
-
const args = ["--dangerously-skip-permissions"];
|
|
164
|
-
if (model) args.push("--model", model);
|
|
165
|
-
args.push("--print", prompt);
|
|
166
|
-
return {
|
|
167
|
-
...wrap(args),
|
|
168
|
-
useExec: true,
|
|
169
|
-
shell: false,
|
|
170
|
-
stdinPrompt: false,
|
|
171
|
-
prompt,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (mode === MODES.INTERACTIVE || mode === MODES.AUTO) {
|
|
176
|
-
return { ...wrap([]), useExec: false, shell: false };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (cli === "claude") {
|
|
180
|
-
const args = [];
|
|
181
|
-
pushFlag(args, "--model", opts.model);
|
|
182
|
-
args.push("-p", prompt);
|
|
183
|
-
return { ...wrap(args), useExec: true, shell: false };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const args = [];
|
|
187
|
-
args.push(
|
|
188
|
-
"exec",
|
|
189
|
-
"--dangerously-bypass-approvals-and-sandbox",
|
|
190
|
-
"--skip-git-repo-check",
|
|
191
|
-
"--color",
|
|
192
|
-
"never",
|
|
193
|
-
);
|
|
194
|
-
// Select the effort profile via `-c` config overrides instead of
|
|
195
|
-
// `--profile <name>`: codex 0.134+ rejects `--profile X` whenever config.toml
|
|
196
|
-
// still contains an inline [profiles.X] table (which codex re-injects on
|
|
197
|
-
// config rewrite). These go straight to spawn argv (shell: false), so push
|
|
198
|
-
// the raw TOML-scalar overrides without shell-quoting.
|
|
199
|
-
const codexHome = opts.env?.CODEX_HOME;
|
|
200
|
-
const codexProfile = resolveNestedCodexAgentProfile(opts.role || "executor", {
|
|
201
|
-
profileOverride: opts.profile ?? "auto",
|
|
202
|
-
globalProfile: opts.env?.TFX_CODEX_PROFILE ?? process.env.TFX_CODEX_PROFILE,
|
|
203
|
-
codexHome,
|
|
204
|
-
});
|
|
205
|
-
for (const override of codexProfileConfigOverrides(codexProfile, {
|
|
206
|
-
codexHome,
|
|
207
|
-
disallowUltra: true,
|
|
208
|
-
enforceCanonicalProfile: true,
|
|
209
|
-
})) {
|
|
210
|
-
args.push("-c", override);
|
|
211
|
-
}
|
|
212
|
-
if (Array.isArray(opts.mcpServers)) {
|
|
213
|
-
const excludedMcpServers = new Set(
|
|
214
|
-
(Array.isArray(opts.excludeMcpServers) ? opts.excludeMcpServers : [])
|
|
215
|
-
.map((server) => String(server ?? "").trim())
|
|
216
|
-
.filter(Boolean),
|
|
217
|
-
);
|
|
218
|
-
for (const rawServer of opts.mcpServers) {
|
|
219
|
-
const server = String(rawServer ?? "").trim();
|
|
220
|
-
if (!server) continue;
|
|
221
|
-
if (excludedMcpServers.has(server)) {
|
|
222
|
-
const message = `Skipping MCP server '${server}' because Codex preflight did not find an enabled config entry.`;
|
|
223
|
-
if (typeof opts.onWarning === "function") {
|
|
224
|
-
opts.onWarning(message, { type: "mcp_server_skipped", server });
|
|
225
|
-
}
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
args.push("-c", `mcp_servers.${server}.enabled=true`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// #116-C: codex headless 의 argv-inline prompt → macOS oh-my-codex hook
|
|
233
|
-
// SessionStart Failed 회귀 (codex v0.130.0). default 가 stdin transport.
|
|
234
|
-
// conductor.respawnSession 이 spec.prompt 를 child.stdin.write 로 주입한다.
|
|
235
|
-
const useStdin =
|
|
236
|
-
resolveStdinPromptMode(opts.stdinPrompt, opts.env) && prompt.length > 0;
|
|
237
|
-
if (useStdin) {
|
|
238
|
-
return {
|
|
239
|
-
...wrap(args),
|
|
240
|
-
useExec: true,
|
|
241
|
-
shell: false,
|
|
242
|
-
stdinPrompt: true,
|
|
243
|
-
prompt,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
args.push(prompt);
|
|
248
|
-
return { ...wrap(args), useExec: true, shell: false };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* @param {{
|
|
253
|
-
* cli: "codex"|"gemini"|"antigravity"|"agy"|"claude",
|
|
254
|
-
* taskType?: "implement"|"review"|"research"|"test"|"analyze",
|
|
255
|
-
* needsInput?: boolean,
|
|
256
|
-
* estimatedDuration?: number,
|
|
257
|
-
* hasHub?: boolean,
|
|
258
|
-
* }} opts
|
|
259
|
-
* @returns {{ mode: string, reason: string }}
|
|
260
|
-
*/
|
|
261
|
-
export function selectExecutionMode(opts = {}) {
|
|
262
|
-
const {
|
|
263
|
-
cli: rawCli = "codex",
|
|
264
|
-
taskType = "research",
|
|
265
|
-
needsInput = false,
|
|
266
|
-
estimatedDuration = 0,
|
|
267
|
-
hasHub = false,
|
|
268
|
-
} = opts;
|
|
269
|
-
|
|
270
|
-
const cli = normalizeCli(rawCli);
|
|
271
|
-
|
|
272
|
-
if (!hasHub) {
|
|
273
|
-
return {
|
|
274
|
-
mode: MODES.HEADLESS,
|
|
275
|
-
reason: "interactive mode requires hub; falling back to headless",
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (cli === "antigravity") {
|
|
280
|
-
return {
|
|
281
|
-
mode: MODES.HEADLESS,
|
|
282
|
-
reason: "antigravity CLI uses headless stdin print mode",
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (taskType === "implement" && !needsInput) {
|
|
287
|
-
return {
|
|
288
|
-
mode: MODES.HEADLESS,
|
|
289
|
-
reason: "implementation without expected input fits headless execution",
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (taskType === "review" || taskType === "analyze") {
|
|
294
|
-
return {
|
|
295
|
-
mode: MODES.HEADLESS,
|
|
296
|
-
reason: "review and analyze tasks default to headless execution",
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (needsInput === true) {
|
|
301
|
-
return {
|
|
302
|
-
mode: MODES.INTERACTIVE,
|
|
303
|
-
reason: "task may require operator input during execution",
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (estimatedDuration > 300) {
|
|
308
|
-
return {
|
|
309
|
-
mode: MODES.INTERACTIVE,
|
|
310
|
-
reason: "long-running work benefits from monitored interactive mode",
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
mode: MODES.HEADLESS,
|
|
316
|
-
reason: "defaulting to headless execution",
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// buildCommandForMode 는 caller 가 없어 제거됨 (v10.20.x). argv-inline 프롬프트와
|
|
321
|
-
// `-s danger-full-access` flag 가 PR #252 의 stdin redirect 패턴과 정합하지
|
|
322
|
-
// 않아 회귀 위험이 있었다. 실제 헤드리스 spawn 은 buildSpawnSpecForMode
|
|
323
|
-
// (conductor.mjs:544) 가 담당하며, 셸 명령 형태가 필요한 경우는
|
|
324
|
-
// hub/cli-adapter-base.mjs:buildExecCommand 를 사용한다.
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
// hub/team/extract-completion-payload.mjs — worker stdout tail → completion JSON
|
|
2
|
-
//
|
|
3
|
-
// Two-tier extraction:
|
|
4
|
-
// 1) Sentinel-framed (preferred, #125): worker emits the payload between
|
|
5
|
-
// `<<<TFX_COMPLETION_BEGIN>>>` and `<<<TFX_COMPLETION_END>>>` markers
|
|
6
|
-
// that each occupy their own line. If a standalone-line BEGIN is present
|
|
7
|
-
// but END is missing, this is a deterministic truncation signal and we
|
|
8
|
-
// return null instead of falling back. Substring matches inside log
|
|
9
|
-
// lines (e.g. `[debug] saw <<<TFX_COMPLETION_BEGIN>>> earlier`) do NOT
|
|
10
|
-
// trigger Tier 1 (Codex R1 LOW finding).
|
|
11
|
-
// 2) Brace-scan fallback (legacy, #115): for workers that haven't adopted
|
|
12
|
-
// the sentinel protocol yet (or for unit fixtures), scan the tail for
|
|
13
|
-
// the latest `{...}` pair that parses as a JSON object.
|
|
14
|
-
//
|
|
15
|
-
// The hypervisor's F7 guard (validateWorkerCompletion) decides whether the
|
|
16
|
-
// parsed payload is semantically valid — this layer only handles extraction.
|
|
17
|
-
|
|
18
|
-
import { SENTINEL_BEGIN, SENTINEL_END } from "./sentinel-capture.mjs";
|
|
19
|
-
|
|
20
|
-
function isLineEdgeChar(ch) {
|
|
21
|
-
return ch === "\n" || ch === "\r";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function findStandaloneMarkerLast(text, marker) {
|
|
25
|
-
let from = 0;
|
|
26
|
-
let last = -1;
|
|
27
|
-
while (from <= text.length - marker.length) {
|
|
28
|
-
const idx = text.indexOf(marker, from);
|
|
29
|
-
if (idx === -1) return last;
|
|
30
|
-
const before = idx === 0 || isLineEdgeChar(text[idx - 1]);
|
|
31
|
-
const afterIdx = idx + marker.length;
|
|
32
|
-
const after = afterIdx === text.length || isLineEdgeChar(text[afterIdx]);
|
|
33
|
-
if (before && after) last = idx;
|
|
34
|
-
from = idx + 1;
|
|
35
|
-
}
|
|
36
|
-
return last;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function findStandaloneMarkerFrom(text, marker, fromIdx) {
|
|
40
|
-
let from = fromIdx;
|
|
41
|
-
while (from <= text.length - marker.length) {
|
|
42
|
-
const idx = text.indexOf(marker, from);
|
|
43
|
-
if (idx === -1) return -1;
|
|
44
|
-
const before = idx === 0 || isLineEdgeChar(text[idx - 1]);
|
|
45
|
-
const afterIdx = idx + marker.length;
|
|
46
|
-
const after = afterIdx === text.length || isLineEdgeChar(text[afterIdx]);
|
|
47
|
-
if (before && after) return idx;
|
|
48
|
-
from = idx + 1;
|
|
49
|
-
}
|
|
50
|
-
return -1;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {string} tail raw stdout (sentinel snapshot or 16 KiB sliding tail)
|
|
55
|
-
* @returns {{payload: object} | null}
|
|
56
|
-
*/
|
|
57
|
-
export function extractCompletionPayload(tail) {
|
|
58
|
-
if (typeof tail !== "string" || tail.length === 0) return null;
|
|
59
|
-
|
|
60
|
-
// Tier 1: sentinel-framed. Use last standalone-line BEGIN so a worker that
|
|
61
|
-
// emitted multiple payloads (retry, debug log) yields the most recent one.
|
|
62
|
-
const beginIdx = findStandaloneMarkerLast(tail, SENTINEL_BEGIN);
|
|
63
|
-
if (beginIdx !== -1) {
|
|
64
|
-
const innerStart = beginIdx + SENTINEL_BEGIN.length;
|
|
65
|
-
const endIdx = findStandaloneMarkerFrom(tail, SENTINEL_END, innerStart);
|
|
66
|
-
if (endIdx === -1) return null; // BEGIN without END → truncation/incomplete
|
|
67
|
-
const inner = tail.slice(innerStart, endIdx).trim();
|
|
68
|
-
try {
|
|
69
|
-
const parsed = JSON.parse(inner);
|
|
70
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
71
|
-
return { payload: parsed };
|
|
72
|
-
}
|
|
73
|
-
} catch {
|
|
74
|
-
// Sentinel-framed but invalid JSON — do not fall back to brace scan,
|
|
75
|
-
// because the worker explicitly tried (and failed) to use the protocol.
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Tier 2: brace-scan fallback. Iterate from the latest `}` backward; for
|
|
81
|
-
// each close, try every `{` that sits before it (nearest first, widening
|
|
82
|
-
// outward). Tolerates trailing noise containing `}` (`noise } more`) and
|
|
83
|
-
// dangling `{` (`{ broken`) after the real payload.
|
|
84
|
-
const closes = [];
|
|
85
|
-
const opens = [];
|
|
86
|
-
for (let i = 0; i < tail.length; i += 1) {
|
|
87
|
-
const ch = tail[i];
|
|
88
|
-
if (ch === "}") closes.push(i);
|
|
89
|
-
else if (ch === "{") opens.push(i);
|
|
90
|
-
}
|
|
91
|
-
if (closes.length === 0 || opens.length === 0) return null;
|
|
92
|
-
|
|
93
|
-
for (let c = closes.length - 1; c >= 0; c -= 1) {
|
|
94
|
-
const closeIdx = closes[c];
|
|
95
|
-
for (let o = opens.length - 1; o >= 0; o -= 1) {
|
|
96
|
-
const openIdx = opens[o];
|
|
97
|
-
if (openIdx >= closeIdx) continue;
|
|
98
|
-
const candidate = tail.slice(openIdx, closeIdx + 1);
|
|
99
|
-
try {
|
|
100
|
-
const parsed = JSON.parse(candidate);
|
|
101
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
102
|
-
return { payload: parsed };
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
// try an earlier `{` for this close, then an earlier `}`.
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
export async function writeBridgeSession(sessionId, socketPath, baseDir) {
|
|
5
|
-
const sessionsDir = path.join(baseDir, "sessions");
|
|
6
|
-
await fs.mkdir(sessionsDir, { recursive: true });
|
|
7
|
-
|
|
8
|
-
const sessionData = {
|
|
9
|
-
session_id: sessionId,
|
|
10
|
-
messagingSock: socketPath,
|
|
11
|
-
status: "RUNNING",
|
|
12
|
-
created_at: new Date().toISOString(),
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const filePath = path.join(sessionsDir, `${sessionId}.json`);
|
|
16
|
-
await fs.writeFile(filePath, JSON.stringify(sessionData, null, 2), "utf8");
|
|
17
|
-
}
|