@triflux/remote 10.43.0 → 10.45.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/hygiene.mjs +1 -1
- package/hub/lib/cto-env.mjs +1 -0
- package/hub/pipe.mjs +10 -9
- package/hub/schema.sql +62 -2
- package/hub/server.mjs +276 -132
- package/hub/store.mjs +699 -41
- package/hub/team/backend.mjs +4 -2
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +24 -3
- package/hub/team/cli/services/runtime-mode.mjs +23 -9
- package/hub/team/conductor.mjs +101 -24
- package/hub/team/cto-auto-collect.mjs +2 -6
- package/hub/team/cto-pull.mjs +4 -0
- package/hub/team/dashboard-open.mjs +1 -0
- package/hub/team/execution-mode.mjs +50 -9
- package/hub/team/headless.mjs +539 -92
- package/hub/team/interactive-tui-transport.mjs +15 -1
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/launcher-template.mjs +17 -1
- package/hub/team/notify.mjs +31 -11
- package/hub/team/orchestrator.mjs +36 -9
- package/hub/team/pane.mjs +176 -14
- package/hub/team/psmux.mjs +79 -70
- package/hub/team/retry-state-machine.mjs +41 -13
- package/hub/team/session.mjs +57 -0
- package/hub/team/swarm-hypervisor.mjs +4 -2
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/terminal-opener.mjs +68 -13
- package/hub/team/tui-viewer.mjs +65 -14
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +22 -1
- package/hub/workers/codex-mcp.mjs +26 -5
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +367 -0
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/cli-codex.mjs +22 -160
- package/scripts/lib/codex-profile-config.mjs +48 -4
- package/scripts/lib/codex-recovery.sh +19 -0
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +24 -0
- package/scripts/lib/logger.mjs +60 -43
- package/scripts/lib/machine-profile.mjs +293 -0
- package/scripts/lib/mcp-health.mjs +44 -18
- package/cto/steward.mjs +0 -362
- package/hub/log-retention.mjs +0 -726
|
@@ -14,6 +14,7 @@ const STARTUP_PROMPT_MAX_ATTEMPTS = 30;
|
|
|
14
14
|
const STARTUP_PROMPT_POLL_INTERVAL_MS = 300;
|
|
15
15
|
const PIPE_TEMP_PREFIX = "triflux-tui-pipe-";
|
|
16
16
|
const PIPE_OUTPUT_FILE = "pane.out";
|
|
17
|
+
const CODEX_NO_APPROVAL_FLAG = "--dangerously-bypass-approvals-and-sandbox";
|
|
17
18
|
|
|
18
19
|
const CONTROL_SEQUENCES = [
|
|
19
20
|
["\x1b[A", "Up"],
|
|
@@ -41,6 +42,13 @@ export async function defaultRunTmux(args, options = {}) {
|
|
|
41
42
|
});
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
export function ensureInteractiveCodexPosture(launchCmd) {
|
|
46
|
+
const command = String(launchCmd || "").trim();
|
|
47
|
+
if (!/^codex(?:\s|$)/u.test(command)) return command;
|
|
48
|
+
if (command.includes(CODEX_NO_APPROVAL_FLAG)) return command;
|
|
49
|
+
return command.replace(/^codex/u, `codex ${CODEX_NO_APPROVAL_FLAG}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
export function createInteractiveTuiTransport({
|
|
45
53
|
runTmux = defaultRunTmux,
|
|
46
54
|
sessionName,
|
|
@@ -113,7 +121,13 @@ export function createInteractiveTuiTransport({
|
|
|
113
121
|
...(cwd ? ["-c", cwd] : []),
|
|
114
122
|
...buildTmuxEnvArgs(env),
|
|
115
123
|
]);
|
|
116
|
-
await tmux([
|
|
124
|
+
await tmux([
|
|
125
|
+
"send-keys",
|
|
126
|
+
"-t",
|
|
127
|
+
sessionName,
|
|
128
|
+
ensureInteractiveCodexPosture(launchCmd),
|
|
129
|
+
"Enter",
|
|
130
|
+
]);
|
|
117
131
|
await dismissStartupPrompts({ tmux, captureVisible, sessionName });
|
|
118
132
|
started = true;
|
|
119
133
|
await startOutputStream();
|
|
@@ -5,11 +5,11 @@ import { statSync } from "node:fs";
|
|
|
5
5
|
import { readdir, readlink } from "node:fs/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { isPidAlive } from "@triflux/core/hub/lib/process-utils.mjs";
|
|
8
|
-
import { killProcess } from "@triflux/core/hub/platform.mjs";
|
|
9
8
|
import {
|
|
10
9
|
resolveHardCeilingMs,
|
|
11
10
|
resolveStallInterventionMs as resolveStallThresholdMs,
|
|
12
11
|
} from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
12
|
+
import { killProcess } from "@triflux/core/hub/platform.mjs";
|
|
13
13
|
|
|
14
14
|
export { resolveHardCeilingMs, resolveStallThresholdMs };
|
|
15
15
|
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
// F4 해결: codex exec "prompt" 인라인 (파이프/리다이렉트 아님)
|
|
6
6
|
// F5 해결: 동일 입력 → 동일 args 배열 (런타임 분기 없음)
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
buildDisabledCliError,
|
|
10
|
+
normalizeCliName,
|
|
11
|
+
resolveCliPolicy,
|
|
12
|
+
} from "../../scripts/lib/machine-profile.mjs";
|
|
8
13
|
import { buildExecArgs as buildCodexArgs } from "@triflux/core/hub/codex-adapter.mjs";
|
|
9
14
|
|
|
10
15
|
function shellSingleQuote(value) {
|
|
@@ -13,7 +18,7 @@ function shellSingleQuote(value) {
|
|
|
13
18
|
|
|
14
19
|
function buildAntigravityArgs(opts = {}) {
|
|
15
20
|
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
16
|
-
const command = `
|
|
21
|
+
const command = `agy --dangerously-skip-permissions --print ${shellSingleQuote(prompt)}`;
|
|
17
22
|
if (opts.resultFile) {
|
|
18
23
|
return `${command} > ${shellSingleQuote(opts.resultFile)} 2>${shellSingleQuote(`${opts.resultFile}.err`)}`;
|
|
19
24
|
}
|
|
@@ -90,6 +95,17 @@ export function buildLauncher(opts) {
|
|
|
90
95
|
if (!agent) throw new Error("agent is required");
|
|
91
96
|
if (!prompt && prompt !== "") throw new Error("prompt is required");
|
|
92
97
|
|
|
98
|
+
// TFX_DISABLE_* SSOT 집행. 이 런처는 tfx-route.sh 를 거치지 않고 codex/agy
|
|
99
|
+
// 바이너리를 직접 실행하는 경로라서 같은 정책을 여기서 다시 확인한다. 정책
|
|
100
|
+
// 판독 시임은 opts.cliPolicy(계산된 정책)와 opts.policyEnv(env 객체)뿐이다.
|
|
101
|
+
// claude 처럼 정책 대상이 아닌 CLI 는 프로파일 파일을 읽지 않는다.
|
|
102
|
+
if (normalizeCliName(agent)) {
|
|
103
|
+
const cliPolicy =
|
|
104
|
+
opts.cliPolicy ?? resolveCliPolicy(opts.policyEnv ?? process.env);
|
|
105
|
+
const blockedCli = buildDisabledCliError(agent, cliPolicy);
|
|
106
|
+
if (blockedCli) throw blockedCli;
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
const adapter = getAdapter(agent);
|
|
94
110
|
|
|
95
111
|
const command = adapter.buildArgs({
|
package/hub/team/notify.mjs
CHANGED
|
@@ -54,12 +54,17 @@ function normalizeEvent(event, defaults = {}) {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function defaultChannelConfig(name, env) {
|
|
57
|
+
function defaultChannelConfig(name, env, platform = process.platform) {
|
|
58
58
|
switch (name) {
|
|
59
59
|
case "bell":
|
|
60
|
-
return { enabled: true };
|
|
60
|
+
return { enabled: envFlag(env?.TRIFLUX_NOTIFY_BELL, true) };
|
|
61
61
|
case "toast":
|
|
62
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
// macOS `osascript display notification` can surface Script Editor
|
|
64
|
+
// authorization/UI prompts. Keep it opt-in there; Windows keeps the
|
|
65
|
+
// historical toast default, and other platforms skip as unsupported.
|
|
66
|
+
enabled: envFlag(env?.TRIFLUX_NOTIFY_TOAST, platform !== "darwin"),
|
|
67
|
+
};
|
|
63
68
|
case "webhook": {
|
|
64
69
|
const url = String(env?.TRIFLUX_NOTIFY_WEBHOOK || "");
|
|
65
70
|
return { enabled: Boolean(url), url };
|
|
@@ -69,8 +74,13 @@ function defaultChannelConfig(name, env) {
|
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
function
|
|
73
|
-
|
|
77
|
+
function envFlag(value, fallback) {
|
|
78
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
79
|
+
return !["0", "false", "no", "off"].includes(String(value).toLowerCase());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeChannelConfig(name, value, env, platform) {
|
|
83
|
+
const base = defaultChannelConfig(name, env, platform);
|
|
74
84
|
const patch =
|
|
75
85
|
typeof value === "boolean"
|
|
76
86
|
? { enabled: value }
|
|
@@ -103,16 +113,21 @@ function normalizeChannelConfig(name, value, env) {
|
|
|
103
113
|
return freezeRecord(next);
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
function normalizeChannels(channels, env) {
|
|
116
|
+
function normalizeChannels(channels, env, platform) {
|
|
107
117
|
const source = channels && typeof channels === "object" ? channels : {};
|
|
108
118
|
const normalized = {};
|
|
109
119
|
for (const name of NOTIFY_CHANNELS) {
|
|
110
|
-
normalized[name] = normalizeChannelConfig(
|
|
120
|
+
normalized[name] = normalizeChannelConfig(
|
|
121
|
+
name,
|
|
122
|
+
source[name],
|
|
123
|
+
env,
|
|
124
|
+
platform,
|
|
125
|
+
);
|
|
111
126
|
}
|
|
112
127
|
return Object.freeze(normalized);
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
function updateChannelConfig(channels, channel, config, env) {
|
|
130
|
+
function updateChannelConfig(channels, channel, config, env, platform) {
|
|
116
131
|
if (!NOTIFY_CHANNELS.includes(channel)) {
|
|
117
132
|
throw new TypeError(`Unknown notify channel: ${channel}`);
|
|
118
133
|
}
|
|
@@ -126,6 +141,7 @@ function updateChannelConfig(channels, channel, config, env) {
|
|
|
126
141
|
...(typeof config === "boolean" ? { enabled: config } : config || {}),
|
|
127
142
|
},
|
|
128
143
|
env,
|
|
144
|
+
platform,
|
|
129
145
|
),
|
|
130
146
|
});
|
|
131
147
|
}
|
|
@@ -322,7 +338,7 @@ function createNotifierInstance(channels, deps) {
|
|
|
322
338
|
|
|
323
339
|
function setChannel(channel, config) {
|
|
324
340
|
return createNotifierInstance(
|
|
325
|
-
updateChannelConfig(channels, channel, config, deps.env),
|
|
341
|
+
updateChannelConfig(channels, channel, config, deps.env, deps.platform),
|
|
326
342
|
deps,
|
|
327
343
|
);
|
|
328
344
|
}
|
|
@@ -349,17 +365,21 @@ function createNotifierInstance(channels, deps) {
|
|
|
349
365
|
*/
|
|
350
366
|
export function createNotifier(opts = {}) {
|
|
351
367
|
const env = opts.env || process.env;
|
|
368
|
+
const platform = opts.platform || process.platform;
|
|
352
369
|
const deps = Object.freeze({
|
|
353
370
|
env,
|
|
354
371
|
stdout: opts.stdout || process.stdout,
|
|
355
372
|
execFile: opts.deps?.execFile || execFile,
|
|
356
373
|
fetch: opts.deps?.fetch || globalThis.fetch?.bind(globalThis),
|
|
357
|
-
platform
|
|
374
|
+
platform,
|
|
358
375
|
hostname: opts.hostname || os.hostname(),
|
|
359
376
|
powerShellCandidates: Object.freeze(
|
|
360
377
|
opts.deps?.powerShellCandidates || ["pwsh", "powershell.exe"],
|
|
361
378
|
),
|
|
362
379
|
});
|
|
363
380
|
|
|
364
|
-
return createNotifierInstance(
|
|
381
|
+
return createNotifierInstance(
|
|
382
|
+
normalizeChannels(opts.channels, env, platform),
|
|
383
|
+
deps,
|
|
384
|
+
);
|
|
365
385
|
}
|
|
@@ -126,7 +126,8 @@ export function buildPrompt(subtask, config) {
|
|
|
126
126
|
* @param {string} opts.hubUrl — Hub URL
|
|
127
127
|
* @param {{target:string, cli:string, task:string}|null} opts.lead
|
|
128
128
|
* @param {string} opts.teammateMode
|
|
129
|
-
* @
|
|
129
|
+
* @param {(failure: {target:string, cli:string, role:string, message:string}) => void} [opts.onInjectionFailure]
|
|
130
|
+
* @returns {Promise<Array<{target:string, cli:string, role:string, message:string}>>}
|
|
130
131
|
*/
|
|
131
132
|
export async function orchestrate(sessionName, assignments, opts = {}) {
|
|
132
133
|
const {
|
|
@@ -134,7 +135,24 @@ export async function orchestrate(sessionName, assignments, opts = {}) {
|
|
|
134
135
|
lead = null,
|
|
135
136
|
teammateMode = "tmux",
|
|
136
137
|
injectPrompt = defaultInjectPrompt,
|
|
138
|
+
onInjectionFailure,
|
|
137
139
|
} = opts;
|
|
140
|
+
const failures = [];
|
|
141
|
+
|
|
142
|
+
function recordInjectionFailure(target, cli, role, error) {
|
|
143
|
+
const failure = {
|
|
144
|
+
target,
|
|
145
|
+
cli: cli || "unknown",
|
|
146
|
+
role,
|
|
147
|
+
message: String(error?.message || error || "prompt submission failed"),
|
|
148
|
+
};
|
|
149
|
+
failures.push(failure);
|
|
150
|
+
try {
|
|
151
|
+
onInjectionFailure?.(failure);
|
|
152
|
+
} catch {
|
|
153
|
+
// reporting failure must not stop the remaining pane injections
|
|
154
|
+
}
|
|
155
|
+
}
|
|
138
156
|
|
|
139
157
|
const workers = assignments.map(({ target, cli, subtask }) => ({
|
|
140
158
|
target,
|
|
@@ -155,10 +173,14 @@ export async function orchestrate(sessionName, assignments, opts = {}) {
|
|
|
155
173
|
subtask: w.subtask,
|
|
156
174
|
})),
|
|
157
175
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
176
|
+
try {
|
|
177
|
+
injectPrompt(lead.target, leadPrompt, {
|
|
178
|
+
useFileRef: true,
|
|
179
|
+
cli: lead.cli,
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
recordInjectionFailure(lead.target, lead.cli, "lead", error);
|
|
183
|
+
}
|
|
162
184
|
await new Promise((r) => setTimeout(r, 100));
|
|
163
185
|
}
|
|
164
186
|
|
|
@@ -169,10 +191,15 @@ export async function orchestrate(sessionName, assignments, opts = {}) {
|
|
|
169
191
|
hubUrl,
|
|
170
192
|
sessionName,
|
|
171
193
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
194
|
+
try {
|
|
195
|
+
injectPrompt(worker.target, prompt, {
|
|
196
|
+
useFileRef: true,
|
|
197
|
+
cli: worker.cli,
|
|
198
|
+
});
|
|
199
|
+
} catch (error) {
|
|
200
|
+
recordInjectionFailure(worker.target, worker.cli, "worker", error);
|
|
201
|
+
}
|
|
176
202
|
await new Promise((r) => setTimeout(r, 100));
|
|
177
203
|
}
|
|
204
|
+
return failures;
|
|
178
205
|
}
|
package/hub/team/pane.mjs
CHANGED
|
@@ -62,7 +62,7 @@ export function buildCliCommand(cli, options = {}) {
|
|
|
62
62
|
if (trustMode) {
|
|
63
63
|
return buildExecArgs({});
|
|
64
64
|
}
|
|
65
|
-
return "codex";
|
|
65
|
+
return "codex --dangerously-bypass-approvals-and-sandbox";
|
|
66
66
|
case "gemini":
|
|
67
67
|
// interactive 모드 — MCP는 ~/.gemini/settings.json에 사전 등록
|
|
68
68
|
return "gemini";
|
|
@@ -105,6 +105,149 @@ export function shouldUseFileRef({ multiplexer, useFileRef, cli }) {
|
|
|
105
105
|
return multiplexer === "psmux" && useFileRef && cli !== "codex";
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** 동기 sleep — injectPrompt는 sync 경로라 setTimeout을 쓸 수 없다 */
|
|
109
|
+
function sleepMs(ms) {
|
|
110
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// paste 직후 Enter가 TUI paste-burst 감지에 개행으로 흡수되는 것을 피하는 정착 지연
|
|
114
|
+
const PASTE_SETTLE_MS = 350;
|
|
115
|
+
// 제출 확인 재시도 상한
|
|
116
|
+
const SUBMIT_RETRY_LIMIT = 3;
|
|
117
|
+
// TUI 준비 대기 상한 — 배너/composer 렌더 완료 전 주입은 키 입력이 통째로 유실될 수 있음
|
|
118
|
+
const COMPOSER_READY_TIMEOUT_MS = 8000;
|
|
119
|
+
|
|
120
|
+
function capturePaneText(target) {
|
|
121
|
+
try {
|
|
122
|
+
if (detectMultiplexer() === "psmux") {
|
|
123
|
+
return String(
|
|
124
|
+
psmuxExec(["capture-pane", "-t", target, "-p"], { encoding: "utf8" }) ??
|
|
125
|
+
"",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return String(muxExec(`capture-pane -t ${target} -p`) ?? "");
|
|
129
|
+
} catch {
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* TUI가 그리기를 마칠 때까지 대기: pane 내용이 비어 있지 않고 연속 두 번의
|
|
136
|
+
* capture가 동일(정적)해질 때까지 폴링한다. CLI 종류와 무관한 준비 신호이며
|
|
137
|
+
* 시간 초과 시 그냥 진행한다 (기존 고정-대기 동작으로 degrade).
|
|
138
|
+
*/
|
|
139
|
+
function waitForComposerReady(target) {
|
|
140
|
+
const deadline = Date.now() + COMPOSER_READY_TIMEOUT_MS;
|
|
141
|
+
let prev = null;
|
|
142
|
+
while (Date.now() < deadline) {
|
|
143
|
+
const text = capturePaneText(target);
|
|
144
|
+
if (text.trim() && prev !== null && text === prev) return;
|
|
145
|
+
prev = text;
|
|
146
|
+
sleepMs(400);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 주입한 프롬프트가 실제 제출됐는지 확인하고, 안 됐으면 Enter를 재전송한다.
|
|
152
|
+
*
|
|
153
|
+
* TUI는 연속 키 입력을 paste burst로 묶으므로 paste 직후의 Enter가 제출 대신
|
|
154
|
+
* composer 개행으로 흡수될 수 있다. transcript의 과거 prompt가 아니라 마지막
|
|
155
|
+
* composer line에서 현재 prompt가 사라졌을 때만 제출로 판정한다. Antigravity는
|
|
156
|
+
* capture 불가 또는 재시도 소진을 성공으로 숨기지 않고 오류로 반환한다.
|
|
157
|
+
*/
|
|
158
|
+
function isAntigravityCli(cli) {
|
|
159
|
+
return ["agy", "antigravity", "gemini"].includes(
|
|
160
|
+
String(cli || "").toLowerCase(),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function promptComposerNeedle(prompt) {
|
|
165
|
+
const firstContentLine = String(prompt || "")
|
|
166
|
+
.split(/\r?\n/u)
|
|
167
|
+
.map((line) => line.trim())
|
|
168
|
+
.find(Boolean);
|
|
169
|
+
return (firstContentLine || "").slice(0, 16);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isComposerPromptLine(line) {
|
|
173
|
+
return /^\s*(?:[│┃]\s*)?[›❯▶▸>]\s*/u.test(String(line || ""));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function composerPromptState(text, needle) {
|
|
177
|
+
const composerLines = String(text || "")
|
|
178
|
+
.split("\n")
|
|
179
|
+
.filter(isComposerPromptLine);
|
|
180
|
+
if (composerLines.length === 0) return "unknown";
|
|
181
|
+
return composerLines.at(-1).includes(needle) ? "present" : "absent";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function hasBusyMarkerOutsideComposer(text) {
|
|
185
|
+
return String(text || "")
|
|
186
|
+
.split("\n")
|
|
187
|
+
.some(
|
|
188
|
+
(line) =>
|
|
189
|
+
!isComposerPromptLine(line) &&
|
|
190
|
+
/esc to (?:interrupt|cancel)/iu.test(line),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* paste된 prompt가 실제 제출될 때까지 Enter를 bounded retry한다.
|
|
196
|
+
* Antigravity는 capture로 성공을 확인하지 못하면 조용히 idle 상태로 남기지 않고
|
|
197
|
+
* caller에 명확한 오류를 반환한다.
|
|
198
|
+
*/
|
|
199
|
+
export function submitPromptWithVerification({
|
|
200
|
+
cli,
|
|
201
|
+
prompt,
|
|
202
|
+
capture,
|
|
203
|
+
sendEnter,
|
|
204
|
+
sleep = sleepMs,
|
|
205
|
+
settleMs = PASTE_SETTLE_MS,
|
|
206
|
+
retryLimit = SUBMIT_RETRY_LIMIT,
|
|
207
|
+
} = {}) {
|
|
208
|
+
if (typeof capture !== "function" || typeof sendEnter !== "function") {
|
|
209
|
+
throw new TypeError("capture and sendEnter are required");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const strictVerification = isAntigravityCli(cli);
|
|
213
|
+
const needle = promptComposerNeedle(prompt);
|
|
214
|
+
sleep(settleMs);
|
|
215
|
+
|
|
216
|
+
if (!needle) {
|
|
217
|
+
sendEnter();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (let attempt = 1; attempt <= retryLimit; attempt++) {
|
|
222
|
+
sendEnter();
|
|
223
|
+
sleep(400 * attempt);
|
|
224
|
+
const text = String(capture() ?? "");
|
|
225
|
+
if (!text) {
|
|
226
|
+
if (strictVerification) continue;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (hasBusyMarkerOutsideComposer(text)) return;
|
|
230
|
+
const composerState = composerPromptState(text, needle);
|
|
231
|
+
if (composerState === "absent") return;
|
|
232
|
+
if (composerState === "unknown" && !strictVerification) return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (strictVerification) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Antigravity prompt submission failed after ${retryLimit} attempts: prompt remains in composer or submission could not be verified`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function confirmSubmit(target, prompt, cli, sendEnter) {
|
|
243
|
+
submitPromptWithVerification({
|
|
244
|
+
cli,
|
|
245
|
+
prompt,
|
|
246
|
+
capture: () => capturePaneText(target),
|
|
247
|
+
sendEnter,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
108
251
|
/**
|
|
109
252
|
* pane에 프롬프트 주입
|
|
110
253
|
* @param {string} target — 예: tfx-multi-abc:0.1
|
|
@@ -131,16 +274,29 @@ export function injectPrompt(
|
|
|
131
274
|
if (shouldUseFileRef({ multiplexer, useFileRef, cli })) {
|
|
132
275
|
writeFileSync(tmpFile, prompt, "utf8");
|
|
133
276
|
const filePath = tmpFile.replace(/\\/g, "/");
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
277
|
+
let cleanupScheduled = false;
|
|
278
|
+
try {
|
|
279
|
+
waitForComposerReady(target);
|
|
280
|
+
psmuxExec(["select-pane", "-t", target]);
|
|
281
|
+
psmuxExec(["send-keys", "-t", target, "-l", `@${filePath}`]);
|
|
282
|
+
confirmSubmit(target, `@${filePath}`, cli, () =>
|
|
283
|
+
psmuxExec(["send-keys", "-t", target, "Enter"]),
|
|
284
|
+
);
|
|
285
|
+
// TUI가 파일을 읽을 시간을 주고 정리
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
try {
|
|
288
|
+
unlinkSync(tmpFile);
|
|
289
|
+
} catch {}
|
|
290
|
+
}, 10000);
|
|
291
|
+
cleanupScheduled = true;
|
|
292
|
+
return;
|
|
293
|
+
} finally {
|
|
294
|
+
if (!cleanupScheduled) {
|
|
295
|
+
try {
|
|
296
|
+
unlinkSync(tmpFile);
|
|
297
|
+
} catch {}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
144
300
|
}
|
|
145
301
|
|
|
146
302
|
try {
|
|
@@ -148,17 +304,23 @@ export function injectPrompt(
|
|
|
148
304
|
|
|
149
305
|
if (detectMultiplexer() === "psmux") {
|
|
150
306
|
const sessionName = getPsmuxSessionName(target);
|
|
307
|
+
waitForComposerReady(target);
|
|
151
308
|
psmuxExec(["load-buffer", "-t", sessionName, toMuxPath(tmpFile)]);
|
|
152
309
|
psmuxExec(["select-pane", "-t", target]);
|
|
153
310
|
psmuxExec(["paste-buffer", "-t", target]);
|
|
154
|
-
|
|
311
|
+
confirmSubmit(target, prompt, cli, () =>
|
|
312
|
+
psmuxExec(["send-keys", "-t", target, "Enter"]),
|
|
313
|
+
);
|
|
155
314
|
return;
|
|
156
315
|
}
|
|
157
316
|
|
|
158
|
-
// tmux load-buffer → paste-buffer → Enter
|
|
317
|
+
// tmux load-buffer → paste-buffer → (정착 지연 + 제출 확인) Enter
|
|
318
|
+
waitForComposerReady(target);
|
|
159
319
|
muxExec(`load-buffer ${quoteArg(toMuxPath(tmpFile))}`);
|
|
160
320
|
muxExec(`paste-buffer -t ${target}`);
|
|
161
|
-
|
|
321
|
+
confirmSubmit(target, prompt, cli, () =>
|
|
322
|
+
muxExec(`send-keys -t ${target} Enter`),
|
|
323
|
+
);
|
|
162
324
|
} finally {
|
|
163
325
|
try {
|
|
164
326
|
unlinkSync(tmpFile);
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -676,41 +676,32 @@ export function createPsmuxSession(sessionName, opts = {}) {
|
|
|
676
676
|
newSessionArgs.push(PWSH_BIN, "-NoLogo", "-NoProfile");
|
|
677
677
|
const leadPane = psmuxExec(newSessionArgs);
|
|
678
678
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
679
|
+
try {
|
|
680
|
+
// split-window로 생성되는 pane도 동일 셸 사용 (Windows: PowerShell 강제)
|
|
681
|
+
if (PWSH_BIN && IS_WINDOWS) {
|
|
682
|
+
try {
|
|
683
|
+
psmuxExec([
|
|
684
|
+
"set-option",
|
|
685
|
+
"-t",
|
|
686
|
+
sessionName,
|
|
687
|
+
"default-command",
|
|
688
|
+
`${PWSH_BIN} -NoLogo -NoProfile`,
|
|
689
|
+
]);
|
|
690
|
+
} catch {
|
|
691
|
+
/* 미지원 시 무시 */
|
|
692
|
+
}
|
|
691
693
|
}
|
|
692
|
-
}
|
|
693
694
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
psmuxExec([
|
|
705
|
-
"split-window",
|
|
706
|
-
"-v",
|
|
707
|
-
"-P",
|
|
708
|
-
"-F",
|
|
709
|
-
"#{session_name}:#{window_index}.#{pane_index}",
|
|
710
|
-
"-t",
|
|
711
|
-
rightPane,
|
|
712
|
-
]);
|
|
713
|
-
if (limitedPaneCount >= 4) {
|
|
695
|
+
if (layout === "2x2" && limitedPaneCount >= 3) {
|
|
696
|
+
const rightPane = psmuxExec([
|
|
697
|
+
"split-window",
|
|
698
|
+
"-h",
|
|
699
|
+
"-P",
|
|
700
|
+
"-F",
|
|
701
|
+
"#{session_name}:#{window_index}.#{pane_index}",
|
|
702
|
+
"-t",
|
|
703
|
+
leadPane,
|
|
704
|
+
]);
|
|
714
705
|
psmuxExec([
|
|
715
706
|
"split-window",
|
|
716
707
|
"-v",
|
|
@@ -718,51 +709,69 @@ export function createPsmuxSession(sessionName, opts = {}) {
|
|
|
718
709
|
"-F",
|
|
719
710
|
"#{session_name}:#{window_index}.#{pane_index}",
|
|
720
711
|
"-t",
|
|
721
|
-
|
|
712
|
+
rightPane,
|
|
722
713
|
]);
|
|
714
|
+
if (limitedPaneCount >= 4) {
|
|
715
|
+
psmuxExec([
|
|
716
|
+
"split-window",
|
|
717
|
+
"-v",
|
|
718
|
+
"-P",
|
|
719
|
+
"-F",
|
|
720
|
+
"#{session_name}:#{window_index}.#{pane_index}",
|
|
721
|
+
"-t",
|
|
722
|
+
leadPane,
|
|
723
|
+
]);
|
|
724
|
+
}
|
|
725
|
+
psmuxExec(["select-layout", "-t", sessionTarget, "tiled"]);
|
|
726
|
+
} else if (layout === "1xN") {
|
|
727
|
+
for (let index = 1; index < limitedPaneCount; index += 1) {
|
|
728
|
+
psmuxExec(["split-window", "-h", "-t", sessionTarget]);
|
|
729
|
+
}
|
|
730
|
+
psmuxExec(["select-layout", "-t", sessionTarget, "even-horizontal"]);
|
|
731
|
+
} else {
|
|
732
|
+
for (let index = 1; index < limitedPaneCount; index += 1) {
|
|
733
|
+
psmuxExec(["split-window", "-v", "-t", sessionTarget]);
|
|
734
|
+
}
|
|
735
|
+
psmuxExec(["select-layout", "-t", sessionTarget, "even-vertical"]);
|
|
723
736
|
}
|
|
724
|
-
psmuxExec(["select-layout", "-t", sessionTarget, "tiled"]);
|
|
725
|
-
} else if (layout === "1xN") {
|
|
726
|
-
for (let index = 1; index < limitedPaneCount; index += 1) {
|
|
727
|
-
psmuxExec(["split-window", "-h", "-t", sessionTarget]);
|
|
728
|
-
}
|
|
729
|
-
psmuxExec(["select-layout", "-t", sessionTarget, "even-horizontal"]);
|
|
730
|
-
} else {
|
|
731
|
-
for (let index = 1; index < limitedPaneCount; index += 1) {
|
|
732
|
-
psmuxExec(["split-window", "-v", "-t", sessionTarget]);
|
|
733
|
-
}
|
|
734
|
-
psmuxExec(["select-layout", "-t", sessionTarget, "even-vertical"]);
|
|
735
|
-
}
|
|
736
737
|
|
|
737
|
-
|
|
738
|
+
psmuxExec(["select-pane", "-t", leadPane]);
|
|
738
739
|
|
|
739
|
-
|
|
740
|
-
|
|
740
|
+
const panes = collectSessionPanes(sessionName).slice(0, limitedPaneCount);
|
|
741
|
+
panes.forEach((pane, index) => {
|
|
742
|
+
try {
|
|
743
|
+
psmuxExec(["select-pane", "-t", pane, "-T", toPaneTitle(index)]);
|
|
744
|
+
} catch {
|
|
745
|
+
// tmux 2.6 미만: select-pane -T 미지원 — pane title 없이 계속 진행
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// CP949 등 non-UTF-8 codepage 환경에서 CLI stdout 깨짐 방지:
|
|
750
|
+
// pane 생성 직후 UTF-8 인코딩을 강제 설정한다.
|
|
751
|
+
// chcp 65001: 프로세스 코드 페이지를 UTF-8로 전환
|
|
752
|
+
// OutputEncoding: 네이티브 명령(gemini/codex) stdout 디코딩
|
|
753
|
+
// $OutputEncoding: PowerShell 파이프라인 기본 인코딩
|
|
754
|
+
if (IS_WINDOWS) {
|
|
755
|
+
const encodingInit = [
|
|
756
|
+
"chcp 65001 > $null",
|
|
757
|
+
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8",
|
|
758
|
+
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
759
|
+
"$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
|
|
760
|
+
].join("; ");
|
|
761
|
+
panes.forEach((paneId) => {
|
|
762
|
+
sendLiteralToPane(paneId, encodingInit, true);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return { sessionName, panes };
|
|
767
|
+
} catch (error) {
|
|
741
768
|
try {
|
|
742
|
-
|
|
769
|
+
killPsmuxSession(sessionName);
|
|
743
770
|
} catch {
|
|
744
|
-
|
|
771
|
+
/* rollback is best-effort; preserve the initialization error */
|
|
745
772
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
// CP949 등 non-UTF-8 codepage 환경에서 CLI stdout 깨짐 방지:
|
|
749
|
-
// pane 생성 직후 UTF-8 인코딩을 강제 설정한다.
|
|
750
|
-
// chcp 65001: 프로세스 코드 페이지를 UTF-8로 전환
|
|
751
|
-
// OutputEncoding: 네이티브 명령(gemini/codex) stdout 디코딩
|
|
752
|
-
// $OutputEncoding: PowerShell 파이프라인 기본 인코딩
|
|
753
|
-
if (IS_WINDOWS) {
|
|
754
|
-
const encodingInit = [
|
|
755
|
-
"chcp 65001 > $null",
|
|
756
|
-
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8",
|
|
757
|
-
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
|
|
758
|
-
"$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
|
|
759
|
-
].join("; ");
|
|
760
|
-
panes.forEach((paneId) => {
|
|
761
|
-
sendLiteralToPane(paneId, encodingInit, true);
|
|
762
|
-
});
|
|
773
|
+
throw error;
|
|
763
774
|
}
|
|
764
|
-
|
|
765
|
-
return { sessionName, panes };
|
|
766
775
|
}
|
|
767
776
|
|
|
768
777
|
/**
|