triflux 10.45.0 → 10.47.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/bin/tfx-live.mjs +576 -53
- package/config/routing-policy.json +1 -1
- package/hooks/session-start-fast.mjs +6 -0
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/lib/cto-env.mjs +2 -3
- package/hub/server.mjs +2 -51
- package/hub/team/recovery-store.mjs +1 -1
- package/hub/team/retry-state-machine.mjs +9 -1
- package/hub/team/synapse-registry.mjs +14 -11
- package/hub/team/uds-orchestrator.mjs +384 -83
- package/hub/team/worktree-lifecycle.mjs +8 -48
- package/hub/tray-lifecycle.mjs +1 -1
- package/hub/workers/codex-mcp.mjs +1 -1
- package/hud/providers/codex.mjs +91 -8
- package/hud/providers/cto.mjs +3 -0
- package/package.json +1 -1
- package/scripts/__tests__/release-governance.test.mjs +235 -11
- package/scripts/__tests__/release-publish-ancestor.test.mjs +67 -0
- package/scripts/__tests__/release-verify-wait.test.mjs +169 -0
- package/scripts/__tests__/resolve-auto-release.test.mjs +216 -0
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +4 -4
- package/scripts/lib/agent-route-policy.mjs +18 -17
- package/scripts/lib/codex-profile-config.mjs +19 -5
- package/scripts/release/publish.mjs +16 -1
- package/scripts/release/resolve-auto-release.mjs +136 -0
- package/scripts/release/verify.mjs +74 -25
- package/scripts/setup.mjs +20 -7
- package/scripts/tfx-route.sh +27 -24
- package/skills/tfx-auto/SKILL.md +5 -4
- package/skills/tfx-live/SKILL.md +112 -7
- package/skills/tfx-profile/SKILL.md +9 -7
- package/skills/tfx-setup/SKILL.md +1 -1
- package/skills/tfx-ship/SKILL.md +43 -36
- package/tui/codex-profile.mjs +2 -3
- package/tui/setup.mjs +4 -3
- package/hub/team/git-preflight.mjs +0 -286
package/bin/tfx-live.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { join as pathJoin, resolve as pathResolve } from "node:path";
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import { runHygiene } from "../cto/hygiene.mjs";
|
|
10
10
|
import { notifyCtoHygieneOnce } from "../cto/hygiene-notify.mjs";
|
|
11
|
+
import { resolveHardCeilingMs } from "../hub/lib/worker-lifecycle.mjs";
|
|
11
12
|
import { createNotifier } from "../hub/team/notify.mjs";
|
|
12
13
|
import {
|
|
13
14
|
escapePwshSingleQuoted as escapeRemotePwshSingleQuoted,
|
|
@@ -38,7 +39,7 @@ const BRIDGE_TIMEOUT_BUFFER_MS = 15_000;
|
|
|
38
39
|
// spill the JSON to a temp file and pass --payload-file instead.
|
|
39
40
|
const PAYLOAD_FILE_THRESHOLD = 96 * 1024;
|
|
40
41
|
const VALID_TRANSPORTS = ["tmux", "uds", "auto"];
|
|
41
|
-
const BOOLEAN_FLAGS = new Set(["json"]);
|
|
42
|
+
const BOOLEAN_FLAGS = new Set(["json", "attach-a", "attach-b"]);
|
|
42
43
|
const PEER_HOP_DONE_MARKER = "<<<TFX_PEER_HOP_DONE>>>";
|
|
43
44
|
// uds-fallback diagnostics land here, written async so a failed daemon attach
|
|
44
45
|
// never blocks the tmux fallback path (see writeUdsBugReport / doAskAuto).
|
|
@@ -50,17 +51,19 @@ function usage() {
|
|
|
50
51
|
return [
|
|
51
52
|
"Usage:",
|
|
52
53
|
" tfx-live start --session NAME [--cli codex|claude] [--model ID] [--effort TIER] [--cwd DIR] [--remote HOST] [--resume ID] [--resume-last 1] [--ready-timeout 30] [--poll-interval 1500]",
|
|
53
|
-
" tfx-live ask --session NAME --prompt TEXT [--cli codex|claude] [--timeout 60] [--remote HOST] [--settle 1500] [--poll-interval 1500]",
|
|
54
|
+
" tfx-live ask --session NAME[:WINDOW.PANE] --prompt TEXT [--cli codex|claude] [--if-busy wait|fail|interrupt] [--busy-timeout 60] [--timeout 60] [--remote HOST] [--settle 1500] [--poll-interval 1500]",
|
|
55
|
+
" tfx-live ask --cli codex --transport uds --thread ID|auto --prompt TEXT [--codex-socket PATH|default] [--cwd DIR] [--if-busy wait|fail|steer] [--busy-timeout 60] [--timeout 60] [--max-turn SECONDS]",
|
|
54
56
|
" tfx-live ask --transport uds|auto (--short SHORT | --session-id ID) --prompt TEXT [--config-dir DIR] [--bridge ABS] [--session NAME (auto fallback)] [--timeout 60]",
|
|
55
57
|
" transport: auto is the default for Claude when --short/--session-id is present; otherwise tmux. bridge path: --bridge > $TFX_BRIDGE > $TFX_REPO_ROOT/hub/bridge.mjs > bundled Triflux hub/bridge.mjs.",
|
|
56
58
|
" tfx-live interrupt --session NAME [--cli codex|claude] [--transport tmux|uds|auto] [--short SHORT | --session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 5]",
|
|
57
59
|
" tfx-live stop --session NAME [--cli codex|claude] [--remote HOST]",
|
|
58
60
|
" tfx-live probe [--short SHORT] [--session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 10]",
|
|
59
|
-
" tfx-live list-sessions --cli codex [--cwd DIR]",
|
|
61
|
+
" tfx-live list-sessions --cli codex [--transport tmux|uds] [--codex-socket PATH|default] [--cwd DIR] [--remote HOST (tmux)]",
|
|
60
62
|
" tfx-live converse --session NAME --prompts-file PATH [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500]",
|
|
61
63
|
" tfx-live goal-driven --session NAME --goal TEXT [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500] [--max-rounds 8] [--done-token DONE]",
|
|
62
|
-
" tfx-live peer [--cli-a codex] [--cli-b claude] [--model-a ID] [--model-b ID] [--effort-a TIER] [--effort-b TIER] [--session-a
|
|
63
|
-
" tfx-live orchestrate --task TEXT [--mode peer|codex-led|claude-led] [--codex-transport exec|app-server-uds] [--cwd DIR] [--timeout 120]",
|
|
64
|
+
" tfx-live peer [--cli-a codex] [--cli-b claude] [--model-a ID] [--model-b ID] [--effort-a TIER] [--effort-b TIER] [--session-a NAME[:WINDOW.PANE]] [--session-b NAME[:WINDOW.PANE]] [--attach-a] [--attach-b] [--transport-a tmux|uds|auto] [--transport-b tmux|uds|auto] [--short-a SHORT] [--short-b SHORT] [--session-id-a ID] [--session-id-b ID] [--thread-a ID|auto] [--thread-b ID|auto] [--codex-socket-a PATH|default] [--codex-socket-b PATH|default] [--bridge ABS] [--remote HOST] [--cwd DIR] [--if-busy wait|fail] [--if-busy-a POLICY] [--if-busy-b POLICY] [--busy-timeout 60] [--max-turn SECONDS] [--rounds 4] [--mode counting|freeform] [--seed TEXT] [--timeout 60]",
|
|
65
|
+
" tfx-live orchestrate --task TEXT [--mode peer|codex-led|claude-led] [--codex-transport exec|app-server-uds] [--codex-socket PATH|default] [--cwd DIR] [--timeout 120]",
|
|
66
|
+
" Codex TUI may use a shared app-server daemon. UDS ask resumes the thread to receive answer events; sending to an active thread with --if-busy steer merges input into that turn.",
|
|
64
67
|
" Runs the Claude(UDS)+Codex orchestration engine. --codex-transport app-server-uds drives a real `codex app-server` over WebSocket-over-UDS (experimental); default exec keeps the codex stdio one-shot path.",
|
|
65
68
|
" tfx-live cto-hygiene-notify --root DIR --state-file PATH [--json]",
|
|
66
69
|
" One-shot CTO hygiene dry-run notification: sends only when actionable hygiene state changes; no polling or apply/steward lock.",
|
|
@@ -202,7 +205,7 @@ function buildRemoteLiveArgv(verb, opts) {
|
|
|
202
205
|
"tfx-live",
|
|
203
206
|
verb,
|
|
204
207
|
"--cli",
|
|
205
|
-
"claude",
|
|
208
|
+
opts.cli ?? "claude",
|
|
206
209
|
"--transport",
|
|
207
210
|
opts.transport ?? "uds",
|
|
208
211
|
];
|
|
@@ -210,6 +213,13 @@ function buildRemoteLiveArgv(verb, opts) {
|
|
|
210
213
|
if (opts.sessionId) args.push("--session-id", opts.sessionId);
|
|
211
214
|
if (opts.session) args.push("--session", opts.session);
|
|
212
215
|
if (opts.configDir) args.push("--config-dir", opts.configDir);
|
|
216
|
+
if (opts.threadId) args.push("--thread", opts.threadId);
|
|
217
|
+
if (opts.codexSocket) args.push("--codex-socket", opts.codexSocket);
|
|
218
|
+
if (opts.cwd) args.push("--cwd", opts.cwd);
|
|
219
|
+
if (opts.ifBusy) args.push("--if-busy", opts.ifBusy);
|
|
220
|
+
if (opts.busyTimeoutMs)
|
|
221
|
+
args.push("--busy-timeout", String(opts.busyTimeoutMs / 1000));
|
|
222
|
+
if (opts.maxTurnMs) args.push("--max-turn", String(opts.maxTurnMs / 1000));
|
|
213
223
|
if (verb === "ask") args.push("--prompt", opts.prompt ?? "");
|
|
214
224
|
args.push("--timeout", timeoutSeconds(opts.timeoutMs));
|
|
215
225
|
if (verb === "ask" && opts.settleMs) {
|
|
@@ -316,8 +326,21 @@ async function callRemoteLive(verb, opts, deps = {}) {
|
|
|
316
326
|
const execRemote = deps.sshExec ?? execFileAsync;
|
|
317
327
|
const env = await probeRemoteEnv(host);
|
|
318
328
|
const plan = buildRemoteLiveCommand(host, verb, opts, env);
|
|
329
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_ANSWER_TIMEOUT_MS;
|
|
330
|
+
const codexUds =
|
|
331
|
+
verb === "ask" &&
|
|
332
|
+
opts.cli === "codex" &&
|
|
333
|
+
(opts.transport ?? "uds") === "uds";
|
|
334
|
+
const maxTurnMs = codexUds
|
|
335
|
+
? (opts.maxTurnMs ?? Math.min(resolveHardCeilingMs(), 15 * 60_000))
|
|
336
|
+
: 0;
|
|
319
337
|
const execOptions = {
|
|
320
|
-
timeout:
|
|
338
|
+
timeout:
|
|
339
|
+
(verb === "ask" ? (opts.busyTimeoutMs ?? timeoutMs) : 0) +
|
|
340
|
+
// Codex checks its hard ceiling on a timeoutMs tick.
|
|
341
|
+
maxTurnMs +
|
|
342
|
+
timeoutMs +
|
|
343
|
+
30_000,
|
|
321
344
|
maxBuffer: MAX_BUFFER,
|
|
322
345
|
};
|
|
323
346
|
|
|
@@ -368,6 +391,37 @@ function normalizeCwd(value) {
|
|
|
368
391
|
}
|
|
369
392
|
}
|
|
370
393
|
|
|
394
|
+
function splitTmuxTarget(value) {
|
|
395
|
+
const target = String(value ?? "");
|
|
396
|
+
const session = target.split(":", 1)[0];
|
|
397
|
+
if (!session)
|
|
398
|
+
throw new Error("tmux target requires a session name before ':'");
|
|
399
|
+
return { session, target };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function tmuxBufferName(target) {
|
|
403
|
+
return `tfx-live-prompt-${target.replace(/[^A-Za-z0-9_-]/g, "_")}-${Date.now()}`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function resolveCodexDaemonSocket(value = "default") {
|
|
407
|
+
const socketPath =
|
|
408
|
+
value === "default"
|
|
409
|
+
? pathJoin(
|
|
410
|
+
process.env.CODEX_HOME ?? pathJoin(homedir(), ".codex"),
|
|
411
|
+
"app-server-control",
|
|
412
|
+
"app-server-control.sock",
|
|
413
|
+
)
|
|
414
|
+
: pathResolve(value);
|
|
415
|
+
try {
|
|
416
|
+
if (statSync(socketPath).isSocket()) return socketPath;
|
|
417
|
+
} catch {
|
|
418
|
+
/* handled below */
|
|
419
|
+
}
|
|
420
|
+
throw new Error(
|
|
421
|
+
`Codex daemon socket unavailable: ${socketPath}. Start it with codex app-server daemon start`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
371
425
|
function isCodexTmuxPane(currentCommand, startCommand) {
|
|
372
426
|
const current = String(currentCommand ?? "").trim();
|
|
373
427
|
const currentBase = current.split("/").at(-1);
|
|
@@ -389,7 +443,102 @@ function isCodexTmuxPane(currentCommand, startCommand) {
|
|
|
389
443
|
);
|
|
390
444
|
}
|
|
391
445
|
|
|
392
|
-
function
|
|
446
|
+
function isCodexProcessCommand(command) {
|
|
447
|
+
const argv = (command.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map((arg) =>
|
|
448
|
+
arg.replace(/^["']|["']$/g, ""),
|
|
449
|
+
);
|
|
450
|
+
const isCodexPath = (arg) => /^(?:.*\/)?codex(?:\.js)?$/.test(arg ?? "");
|
|
451
|
+
const executable = argv.shift();
|
|
452
|
+
if (isCodexPath(executable)) return true;
|
|
453
|
+
if (!/^(?:.*\/)?node(?:js)?$/.test(executable ?? "")) return false;
|
|
454
|
+
const valueOptions = new Set([
|
|
455
|
+
"-r",
|
|
456
|
+
"--require",
|
|
457
|
+
"--import",
|
|
458
|
+
"--loader",
|
|
459
|
+
"--experimental-loader",
|
|
460
|
+
"--icu-data-dir",
|
|
461
|
+
]);
|
|
462
|
+
const booleanOptions = new Set([
|
|
463
|
+
"--no-warnings",
|
|
464
|
+
"--enable-source-maps",
|
|
465
|
+
"--preserve-symlinks",
|
|
466
|
+
"--preserve-symlinks-main",
|
|
467
|
+
]);
|
|
468
|
+
while (argv.length) {
|
|
469
|
+
const arg = argv.shift();
|
|
470
|
+
if (arg === "--") return isCodexPath(argv[0]);
|
|
471
|
+
if (/^(?:-[ep]|--eval(?:=|$)|--print(?:=|$))/.test(arg)) return false;
|
|
472
|
+
if (!arg.startsWith("-")) return isCodexPath(arg);
|
|
473
|
+
if (valueOptions.has(arg.split("=", 1)[0])) {
|
|
474
|
+
if (!arg.includes("=")) argv.shift();
|
|
475
|
+
} else if (!booleanOptions.has(arg)) {
|
|
476
|
+
// Unknown Node options may consume the next token as a value.
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function inspectCodexTmuxPane(pane, deps = {}) {
|
|
484
|
+
if (isCodexTmuxPane(pane.currentCommand, pane.startCommand))
|
|
485
|
+
return { isCodex: true };
|
|
486
|
+
if (!pane.panePid && !pane.paneTty) return { isCodex: false };
|
|
487
|
+
const args = ["-ax", "-o", "pid=,ppid=,tty=,args="];
|
|
488
|
+
const command = pane.remote ? "ssh" : "ps";
|
|
489
|
+
const commandArgs = pane.remote
|
|
490
|
+
? [
|
|
491
|
+
"-o",
|
|
492
|
+
"BatchMode=yes",
|
|
493
|
+
"-o",
|
|
494
|
+
"ConnectTimeout=10",
|
|
495
|
+
pane.remote,
|
|
496
|
+
shellQuote(["ps", ...args]),
|
|
497
|
+
]
|
|
498
|
+
: args;
|
|
499
|
+
let stdout;
|
|
500
|
+
try {
|
|
501
|
+
({ stdout } = await (deps.psExec ?? execFileAsync)(command, commandArgs, {
|
|
502
|
+
timeout: 15_000,
|
|
503
|
+
maxBuffer: MAX_BUFFER,
|
|
504
|
+
}));
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return {
|
|
507
|
+
isCodex: false,
|
|
508
|
+
preflightWarning: `Codex pane process inspection unavailable: ${error.message}`,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const processes = String(stdout)
|
|
512
|
+
.split(/\r?\n/)
|
|
513
|
+
.flatMap((line) => {
|
|
514
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
|
|
515
|
+
return match
|
|
516
|
+
? [{ pid: match[1], ppid: match[2], tty: match[3], command: match[4] }]
|
|
517
|
+
: [];
|
|
518
|
+
});
|
|
519
|
+
const descendants = new Set([String(pane.panePid)]);
|
|
520
|
+
let added;
|
|
521
|
+
do {
|
|
522
|
+
added = false;
|
|
523
|
+
for (const process of processes) {
|
|
524
|
+
if (descendants.has(process.ppid) && !descendants.has(process.pid)) {
|
|
525
|
+
descendants.add(process.pid);
|
|
526
|
+
added = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} while (added);
|
|
530
|
+
const tty = String(pane.paneTty ?? "").replace(/^\/dev\//, "");
|
|
531
|
+
return {
|
|
532
|
+
isCodex: processes.some(
|
|
533
|
+
(process) =>
|
|
534
|
+
(descendants.has(process.pid) ||
|
|
535
|
+
(tty && tty !== "?" && tty !== "??" && process.tty === tty)) &&
|
|
536
|
+
isCodexProcessCommand(process.command),
|
|
537
|
+
),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function parseCodexTmuxSessions(stdout, cwd = null, codexTargets = null) {
|
|
393
542
|
const cwdFilter = normalizeCwd(cwd);
|
|
394
543
|
const sessions = new Map();
|
|
395
544
|
|
|
@@ -399,12 +548,18 @@ function parseCodexTmuxSessions(stdout, cwd = null) {
|
|
|
399
548
|
session,
|
|
400
549
|
createdRaw,
|
|
401
550
|
attachedRaw,
|
|
551
|
+
windowIndex,
|
|
552
|
+
paneIndex,
|
|
402
553
|
paneCwd,
|
|
403
554
|
currentCommand,
|
|
404
|
-
|
|
555
|
+
startCommand,
|
|
405
556
|
] = line.split("\t");
|
|
406
|
-
|
|
407
|
-
|
|
557
|
+
if (
|
|
558
|
+
!session ||
|
|
559
|
+
!(codexTargets
|
|
560
|
+
? codexTargets.has(`${session}:${windowIndex}.${paneIndex}`)
|
|
561
|
+
: isCodexTmuxPane(currentCommand, startCommand))
|
|
562
|
+
) {
|
|
408
563
|
continue;
|
|
409
564
|
}
|
|
410
565
|
|
|
@@ -418,10 +573,16 @@ function parseCodexTmuxSessions(stdout, cwd = null) {
|
|
|
418
573
|
? new Date(startedAtEpoch * 1000).toISOString()
|
|
419
574
|
: null;
|
|
420
575
|
const existing = sessions.get(session);
|
|
576
|
+
const pane = {
|
|
577
|
+
target: `${session}:${windowIndex}.${paneIndex}`,
|
|
578
|
+
cwd: normalizedPaneCwd,
|
|
579
|
+
command: currentCommand,
|
|
580
|
+
};
|
|
421
581
|
if (existing) {
|
|
422
582
|
if (!existing.cwd && normalizedPaneCwd) {
|
|
423
583
|
existing.cwd = normalizedPaneCwd;
|
|
424
584
|
}
|
|
585
|
+
existing.panes.push(pane);
|
|
425
586
|
continue;
|
|
426
587
|
}
|
|
427
588
|
|
|
@@ -433,6 +594,8 @@ function parseCodexTmuxSessions(stdout, cwd = null) {
|
|
|
433
594
|
: null,
|
|
434
595
|
cwd: normalizedPaneCwd,
|
|
435
596
|
attached: Number.parseInt(attachedRaw, 10) > 0,
|
|
597
|
+
target: pane.target,
|
|
598
|
+
panes: [pane],
|
|
436
599
|
});
|
|
437
600
|
}
|
|
438
601
|
|
|
@@ -441,23 +604,56 @@ function parseCodexTmuxSessions(stdout, cwd = null) {
|
|
|
441
604
|
);
|
|
442
605
|
}
|
|
443
606
|
|
|
444
|
-
async function discoverCodexTmuxSessions(
|
|
607
|
+
async function discoverCodexTmuxSessions(
|
|
608
|
+
{ cwd = null, remote = null } = {},
|
|
609
|
+
deps = {},
|
|
610
|
+
) {
|
|
445
611
|
const format = [
|
|
446
612
|
"#{session_name}",
|
|
447
613
|
"#{session_created}",
|
|
448
614
|
"#{session_attached}",
|
|
615
|
+
"#{window_index}",
|
|
616
|
+
"#{pane_index}",
|
|
449
617
|
"#{pane_current_path}",
|
|
450
618
|
"#{pane_current_command}",
|
|
451
619
|
"#{pane_start_command}",
|
|
620
|
+
"#{pane_tty}",
|
|
621
|
+
"#{pane_pid}",
|
|
452
622
|
].join("\t");
|
|
453
623
|
|
|
454
624
|
try {
|
|
455
|
-
const { stdout } = await runTmux(
|
|
625
|
+
const { stdout } = await (deps.runTmux ?? runTmux)(remote, [
|
|
626
|
+
"list-panes",
|
|
627
|
+
"-a",
|
|
628
|
+
"-F",
|
|
629
|
+
format,
|
|
630
|
+
]);
|
|
631
|
+
const codexTargets = new Set();
|
|
632
|
+
for (const line of String(stdout).split(/\r?\n/)) {
|
|
633
|
+
if (!line) continue;
|
|
634
|
+
const [
|
|
635
|
+
session,
|
|
636
|
+
,
|
|
637
|
+
,
|
|
638
|
+
window,
|
|
639
|
+
pane,
|
|
640
|
+
,
|
|
641
|
+
currentCommand,
|
|
642
|
+
startCommand,
|
|
643
|
+
paneTty,
|
|
644
|
+
panePid,
|
|
645
|
+
] = line.split("\t");
|
|
646
|
+
const result = await inspectCodexTmuxPane(
|
|
647
|
+
{ currentCommand, startCommand, paneTty, panePid, remote },
|
|
648
|
+
deps,
|
|
649
|
+
);
|
|
650
|
+
if (result.isCodex) codexTargets.add(`${session}:${window}.${pane}`);
|
|
651
|
+
}
|
|
456
652
|
return {
|
|
457
653
|
ok: true,
|
|
458
654
|
cli: "codex",
|
|
459
655
|
cwd: normalizeCwd(cwd),
|
|
460
|
-
sessions: parseCodexTmuxSessions(stdout, cwd),
|
|
656
|
+
sessions: parseCodexTmuxSessions(stdout, cwd, codexTargets),
|
|
461
657
|
};
|
|
462
658
|
} catch (error) {
|
|
463
659
|
return {
|
|
@@ -501,6 +697,30 @@ function isBusyByEsc(text) {
|
|
|
501
697
|
return String(text).includes("esc to interrupt");
|
|
502
698
|
}
|
|
503
699
|
|
|
700
|
+
function isCodexLoadingCapture(text) {
|
|
701
|
+
const headers = [
|
|
702
|
+
...stripAnsi(text).matchAll(/^\s*(?:│\s*)?model:\s*([^\n]*)/gim),
|
|
703
|
+
];
|
|
704
|
+
return /^loading\b/i.test(headers.at(-1)?.[1] ?? "");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function isCodexTimeLine(line) {
|
|
708
|
+
return (
|
|
709
|
+
/^\s*\d{1,2}:\d{2}(?:\s?[AP]M)?\s*$/.test(line) ||
|
|
710
|
+
/^\s*(?:오전|오후)\s?\d{1,2}:\d{2}\s*$/.test(line)
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function trimResponseLines(adapter, lines) {
|
|
715
|
+
while (
|
|
716
|
+
lines.length &&
|
|
717
|
+
(!lines.at(-1).trim() ||
|
|
718
|
+
(adapter.cli === "codex" && isCodexTimeLine(lines.at(-1))))
|
|
719
|
+
)
|
|
720
|
+
lines.pop();
|
|
721
|
+
return lines.join("\n").trim();
|
|
722
|
+
}
|
|
723
|
+
|
|
504
724
|
function hasCodexComposerPrompt(text) {
|
|
505
725
|
return /^\s*[›❯▶▸>]\s*/m.test(text);
|
|
506
726
|
}
|
|
@@ -536,7 +756,11 @@ function isClaudeResponseLine(line) {
|
|
|
536
756
|
}
|
|
537
757
|
|
|
538
758
|
function isCodexReadyCapture(text) {
|
|
539
|
-
return
|
|
759
|
+
return (
|
|
760
|
+
!isBusyByEsc(text) &&
|
|
761
|
+
!isCodexLoadingCapture(text) &&
|
|
762
|
+
hasCodexComposerPrompt(text)
|
|
763
|
+
);
|
|
540
764
|
}
|
|
541
765
|
|
|
542
766
|
function hasClaudeLoginGuard(text) {
|
|
@@ -679,7 +903,7 @@ function extractAssistantResponse(adapter, text, prompt = null) {
|
|
|
679
903
|
}
|
|
680
904
|
}
|
|
681
905
|
|
|
682
|
-
return response
|
|
906
|
+
return trimResponseLines(adapter, response);
|
|
683
907
|
}
|
|
684
908
|
|
|
685
909
|
function extractResponseSinceMarker({ beforeRaw, raw, doneMarker, adapter }) {
|
|
@@ -716,20 +940,21 @@ function extractResponseSinceMarker({ beforeRaw, raw, doneMarker, adapter }) {
|
|
|
716
940
|
const responseLines =
|
|
717
941
|
responseStart === -1 ? candidateLines : candidateLines.slice(responseStart);
|
|
718
942
|
|
|
719
|
-
return
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
943
|
+
return trimResponseLines(
|
|
944
|
+
adapter,
|
|
945
|
+
responseLines
|
|
946
|
+
.filter(
|
|
947
|
+
(line) =>
|
|
948
|
+
!adapter.isChromeLine(line) &&
|
|
949
|
+
!isWarningLine(line) &&
|
|
950
|
+
!adapter.isStatusLine(line) &&
|
|
951
|
+
!hasCodexComposerPrompt(line) &&
|
|
952
|
+
!hasClaudeComposerPrompt(line),
|
|
953
|
+
)
|
|
954
|
+
.map((line) =>
|
|
955
|
+
adapter.isResponseLine(line) ? adapter.stripResponseMarker(line) : line,
|
|
956
|
+
),
|
|
957
|
+
);
|
|
733
958
|
}
|
|
734
959
|
|
|
735
960
|
function normalizeClaudeTaskText(text) {
|
|
@@ -1414,7 +1639,11 @@ async function probeDaemon(bridgePath, ref, timeoutMs) {
|
|
|
1414
1639
|
async function hasTmuxSession(adapter, opts) {
|
|
1415
1640
|
if (!opts.session) return false;
|
|
1416
1641
|
try {
|
|
1417
|
-
await runTmux(opts.remote, [
|
|
1642
|
+
await runTmux(opts.remote, [
|
|
1643
|
+
"has-session",
|
|
1644
|
+
"-t",
|
|
1645
|
+
splitTmuxTarget(opts.session).session,
|
|
1646
|
+
]);
|
|
1418
1647
|
return true;
|
|
1419
1648
|
} catch {
|
|
1420
1649
|
return false;
|
|
@@ -1493,6 +1722,8 @@ async function doStart(adapter, opts) {
|
|
|
1493
1722
|
readyTimeoutMs,
|
|
1494
1723
|
pollIntervalMs,
|
|
1495
1724
|
} = opts;
|
|
1725
|
+
if (session.includes(":"))
|
|
1726
|
+
throw new Error("start --session accepts only a session name without ':'");
|
|
1496
1727
|
const launchKeys = resume
|
|
1497
1728
|
? [adapter.resumeById(resume), "Enter"]
|
|
1498
1729
|
: resumeLast
|
|
@@ -1539,6 +1770,36 @@ async function doStart(adapter, opts) {
|
|
|
1539
1770
|
});
|
|
1540
1771
|
}
|
|
1541
1772
|
|
|
1773
|
+
async function waitForTmuxIdle(adapter, opts) {
|
|
1774
|
+
const { session, remote, pollIntervalMs, busyTimeoutMs, ifBusy } = opts;
|
|
1775
|
+
if (!["wait", "fail", "interrupt"].includes(ifBusy)) {
|
|
1776
|
+
throw new Error("--if-busy must be wait, fail, or interrupt for tmux");
|
|
1777
|
+
}
|
|
1778
|
+
const startedAt = Date.now();
|
|
1779
|
+
let interrupted = false;
|
|
1780
|
+
while (true) {
|
|
1781
|
+
const visible = await captureVisible(remote, session);
|
|
1782
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1783
|
+
const busy =
|
|
1784
|
+
isBusyByEsc(visible) ||
|
|
1785
|
+
(adapter.cli === "codex" && isCodexLoadingCapture(visible));
|
|
1786
|
+
if (!busy) return elapsedMs;
|
|
1787
|
+
if (ifBusy === "fail") throw new Error("target busy (--if-busy fail)");
|
|
1788
|
+
if (elapsedMs >= busyTimeoutMs) {
|
|
1789
|
+
throw new Error(
|
|
1790
|
+
`target busy after ${Math.ceil(busyTimeoutMs / 1000)}s (--if-busy ${ifBusy})`,
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
if (ifBusy === "interrupt" && !interrupted) {
|
|
1794
|
+
await runTmux(remote, ["send-keys", "-t", session, "Escape"]);
|
|
1795
|
+
interrupted = true;
|
|
1796
|
+
}
|
|
1797
|
+
await sleep(
|
|
1798
|
+
Math.min(pollIntervalMs, Math.max(1, busyTimeoutMs - elapsedMs)),
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1542
1803
|
async function doAskViaTmux(adapter, opts) {
|
|
1543
1804
|
const {
|
|
1544
1805
|
session,
|
|
@@ -1548,11 +1809,20 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
1548
1809
|
settleMs,
|
|
1549
1810
|
pollIntervalMs,
|
|
1550
1811
|
doneMarker,
|
|
1812
|
+
ifBusy = "wait",
|
|
1813
|
+
busyTimeoutMs = timeoutMs,
|
|
1551
1814
|
} = opts;
|
|
1815
|
+
const busyWaitedMs = await waitForTmuxIdle(adapter, {
|
|
1816
|
+
session,
|
|
1817
|
+
remote,
|
|
1818
|
+
pollIntervalMs,
|
|
1819
|
+
busyTimeoutMs,
|
|
1820
|
+
ifBusy,
|
|
1821
|
+
});
|
|
1552
1822
|
const beforeRaw = await capturePane(remote, session);
|
|
1553
1823
|
const contextPctBefore = adapter.contextPct(beforeRaw);
|
|
1554
1824
|
|
|
1555
|
-
const bufferName =
|
|
1825
|
+
const bufferName = tmuxBufferName(session);
|
|
1556
1826
|
await runTmux(remote, ["set-buffer", "-b", bufferName, "--", prompt]);
|
|
1557
1827
|
await runTmux(remote, [
|
|
1558
1828
|
"paste-buffer",
|
|
@@ -1662,6 +1932,8 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
1662
1932
|
session,
|
|
1663
1933
|
remote: remote ?? null,
|
|
1664
1934
|
transport: "tmux",
|
|
1935
|
+
ifBusy,
|
|
1936
|
+
busyWaitedMs,
|
|
1665
1937
|
response,
|
|
1666
1938
|
contextPctBefore,
|
|
1667
1939
|
contextPctAfter,
|
|
@@ -1677,6 +1949,24 @@ async function doAsk(adapter, opts) {
|
|
|
1677
1949
|
return doAskViaTmux(adapter, opts);
|
|
1678
1950
|
}
|
|
1679
1951
|
|
|
1952
|
+
if (adapter.cli === "codex" && transport === "uds") {
|
|
1953
|
+
if (opts.remote) return doAskViaRemoteLive(adapter, opts);
|
|
1954
|
+
const { askCodexAppServerThread } = await import(
|
|
1955
|
+
"../hub/team/uds-orchestrator.mjs"
|
|
1956
|
+
);
|
|
1957
|
+
return askCodexAppServerThread({
|
|
1958
|
+
socketPath: opts.socketPath,
|
|
1959
|
+
threadId: opts.threadId,
|
|
1960
|
+
cwd: opts.cwd,
|
|
1961
|
+
prompt: opts.prompt,
|
|
1962
|
+
timeoutMs: opts.timeoutMs,
|
|
1963
|
+
maxTurnMs: opts.maxTurnMs,
|
|
1964
|
+
ifBusy: opts.ifBusy,
|
|
1965
|
+
busyTimeoutMs: opts.busyTimeoutMs,
|
|
1966
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1680
1970
|
// uds/auto attach a live background Claude daemon, not a tmux TUI.
|
|
1681
1971
|
if (adapter.cli !== "claude") {
|
|
1682
1972
|
throw new Error(
|
|
@@ -1706,7 +1996,11 @@ async function doAsk(adapter, opts) {
|
|
|
1706
1996
|
}
|
|
1707
1997
|
|
|
1708
1998
|
async function doAskViaRemoteLive(adapter, opts, deps = {}) {
|
|
1709
|
-
const result = await callRemoteLive(
|
|
1999
|
+
const result = await callRemoteLive(
|
|
2000
|
+
"ask",
|
|
2001
|
+
{ ...opts, cli: adapter.cli },
|
|
2002
|
+
deps,
|
|
2003
|
+
);
|
|
1710
2004
|
return {
|
|
1711
2005
|
...result,
|
|
1712
2006
|
cli: result.cli ?? adapter.cli,
|
|
@@ -1774,7 +2068,11 @@ function daemonProbeTargetAttachable(probe, opts) {
|
|
|
1774
2068
|
async function ensureTmuxSession(adapter, opts) {
|
|
1775
2069
|
if (!opts.session) return false;
|
|
1776
2070
|
try {
|
|
1777
|
-
await runTmux(opts.remote, [
|
|
2071
|
+
await runTmux(opts.remote, [
|
|
2072
|
+
"has-session",
|
|
2073
|
+
"-t",
|
|
2074
|
+
splitTmuxTarget(opts.session).session,
|
|
2075
|
+
]);
|
|
1778
2076
|
return false;
|
|
1779
2077
|
} catch {
|
|
1780
2078
|
await doStart(adapter, {
|
|
@@ -1953,6 +2251,10 @@ async function doAskAuto(adapter, opts) {
|
|
|
1953
2251
|
|
|
1954
2252
|
async function doStop(adapter, opts) {
|
|
1955
2253
|
const { session, remote } = opts;
|
|
2254
|
+
if (/[:.]|^[%@]\d+$/.test(session))
|
|
2255
|
+
throw new Error(
|
|
2256
|
+
"stop --session accepts only a session name; pass the session name without a pane target",
|
|
2257
|
+
);
|
|
1956
2258
|
await runTmux(remote, ["kill-session", "-t", session]);
|
|
1957
2259
|
return {
|
|
1958
2260
|
cli: adapter.cli,
|
|
@@ -2070,8 +2372,11 @@ async function doInterrupt(adapter, opts) {
|
|
|
2070
2372
|
}
|
|
2071
2373
|
|
|
2072
2374
|
function startOpts(flags) {
|
|
2375
|
+
const session = requireFlag(flags, "session");
|
|
2376
|
+
if (session.includes(":"))
|
|
2377
|
+
throw new Error("start --session accepts only a session name without ':'");
|
|
2073
2378
|
return {
|
|
2074
|
-
session
|
|
2379
|
+
session,
|
|
2075
2380
|
remote: flags.remote,
|
|
2076
2381
|
cwd: flags.cwd,
|
|
2077
2382
|
resume: flags.resume,
|
|
@@ -2115,7 +2420,10 @@ function askOpts(flags, adapter) {
|
|
|
2115
2420
|
const short = flags.short;
|
|
2116
2421
|
const sessionId = flags["session-id"];
|
|
2117
2422
|
const transport = transportFlag(flags, adapter, short, sessionId);
|
|
2118
|
-
|
|
2423
|
+
const codexUds = adapter.cli === "codex" && transport === "uds";
|
|
2424
|
+
if (adapter.cli === "codex" && transport === "auto")
|
|
2425
|
+
throw new Error("--transport auto is only supported with --cli claude");
|
|
2426
|
+
if (transport !== "tmux" && !codexUds && !short && !sessionId) {
|
|
2119
2427
|
throw new Error(
|
|
2120
2428
|
`--transport ${transport} requires --short or --session-id`,
|
|
2121
2429
|
);
|
|
@@ -2124,15 +2432,37 @@ function askOpts(flags, adapter) {
|
|
|
2124
2432
|
// tmux needs a tmux session name; uds needs a daemon ref (short/sessionId);
|
|
2125
2433
|
// auto can take both (daemon ref to probe, tmux session for fallback).
|
|
2126
2434
|
session:
|
|
2127
|
-
transport === "tmux"
|
|
2435
|
+
transport === "tmux"
|
|
2436
|
+
? splitTmuxTarget(requireFlag(flags, "session")).target
|
|
2437
|
+
: flags.session,
|
|
2128
2438
|
short,
|
|
2129
2439
|
sessionId,
|
|
2130
2440
|
configDir: flags["config-dir"],
|
|
2131
2441
|
transport,
|
|
2442
|
+
threadId: codexUds ? requireFlag(flags, "thread") : null,
|
|
2443
|
+
codexSocket: codexUds ? (flags["codex-socket"] ?? "default") : null,
|
|
2444
|
+
socketPath:
|
|
2445
|
+
codexUds && !flags.remote
|
|
2446
|
+
? resolveCodexDaemonSocket(flags["codex-socket"] ?? "default")
|
|
2447
|
+
: null,
|
|
2448
|
+
cwd: flags.cwd,
|
|
2449
|
+
ifBusy: flags["if-busy"] ?? "wait",
|
|
2450
|
+
busyTimeoutMs: secondsFlag(
|
|
2451
|
+
flags,
|
|
2452
|
+
"busy-timeout",
|
|
2453
|
+
secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS),
|
|
2454
|
+
),
|
|
2132
2455
|
bridgePath: resolveBridgePath(flags),
|
|
2133
2456
|
prompt: requireFlag(flags, "prompt"),
|
|
2134
2457
|
remote: flags.remote,
|
|
2135
2458
|
timeoutMs: secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS),
|
|
2459
|
+
maxTurnMs: codexUds
|
|
2460
|
+
? secondsFlag(
|
|
2461
|
+
flags,
|
|
2462
|
+
"max-turn",
|
|
2463
|
+
Math.min(resolveHardCeilingMs(), 15 * 60_000),
|
|
2464
|
+
)
|
|
2465
|
+
: undefined,
|
|
2136
2466
|
settleMs: msFlag(flags, "settle", DEFAULT_SETTLE_MS),
|
|
2137
2467
|
pollIntervalMs: msFlag(flags, "poll-interval", DEFAULT_POLL_INTERVAL_MS),
|
|
2138
2468
|
};
|
|
@@ -2149,9 +2479,15 @@ function interruptOpts(flags, adapter) {
|
|
|
2149
2479
|
const short = flags.short;
|
|
2150
2480
|
const sessionId = flags["session-id"];
|
|
2151
2481
|
const transport = transportFlag(flags, adapter, short, sessionId);
|
|
2482
|
+
if (adapter.cli === "codex" && transport === "uds")
|
|
2483
|
+
throw new Error(
|
|
2484
|
+
"active turnId unavailable; cannot interrupt Codex UDS thread",
|
|
2485
|
+
);
|
|
2152
2486
|
return {
|
|
2153
2487
|
session:
|
|
2154
|
-
transport === "tmux"
|
|
2488
|
+
transport === "tmux"
|
|
2489
|
+
? splitTmuxTarget(requireFlag(flags, "session")).target
|
|
2490
|
+
: flags.session,
|
|
2155
2491
|
short,
|
|
2156
2492
|
sessionId,
|
|
2157
2493
|
configDir: flags["config-dir"],
|
|
@@ -2203,7 +2539,25 @@ async function listSessions(flags) {
|
|
|
2203
2539
|
if (adapter.cli !== "codex") {
|
|
2204
2540
|
throw new Error("list-sessions currently supports only --cli codex");
|
|
2205
2541
|
}
|
|
2206
|
-
|
|
2542
|
+
if (flags.transport === "uds") {
|
|
2543
|
+
const { listCodexAppServerThreads } = await import(
|
|
2544
|
+
"../hub/team/uds-orchestrator.mjs"
|
|
2545
|
+
);
|
|
2546
|
+
printJson(
|
|
2547
|
+
await listCodexAppServerThreads({
|
|
2548
|
+
socketPath: resolveCodexDaemonSocket(
|
|
2549
|
+
flags["codex-socket"] ?? "default",
|
|
2550
|
+
),
|
|
2551
|
+
cwd: flags.cwd,
|
|
2552
|
+
}),
|
|
2553
|
+
);
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
if (flags.transport && flags.transport !== "tmux")
|
|
2557
|
+
throw new Error("list-sessions --transport must be tmux or uds");
|
|
2558
|
+
printJson(
|
|
2559
|
+
await discoverCodexTmuxSessions({ cwd: flags.cwd, remote: flags.remote }),
|
|
2560
|
+
);
|
|
2207
2561
|
}
|
|
2208
2562
|
|
|
2209
2563
|
function startOptsForSession(flags, session, side) {
|
|
@@ -2234,6 +2588,12 @@ function askOptsForSession(flags, session, prompt) {
|
|
|
2234
2588
|
timeoutMs: secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS),
|
|
2235
2589
|
settleMs: msFlag(flags, "settle", DEFAULT_SETTLE_MS),
|
|
2236
2590
|
pollIntervalMs: msFlag(flags, "poll-interval", DEFAULT_POLL_INTERVAL_MS),
|
|
2591
|
+
ifBusy: flags["if-busy"] ?? "wait",
|
|
2592
|
+
busyTimeoutMs: secondsFlag(
|
|
2593
|
+
flags,
|
|
2594
|
+
"busy-timeout",
|
|
2595
|
+
secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS),
|
|
2596
|
+
),
|
|
2237
2597
|
};
|
|
2238
2598
|
}
|
|
2239
2599
|
|
|
@@ -2254,9 +2614,12 @@ async function readPromptsFile(path) {
|
|
|
2254
2614
|
|
|
2255
2615
|
async function stopAfterLifecycle(stopSpecs, primaryError = null) {
|
|
2256
2616
|
let stopError = null;
|
|
2257
|
-
for (const
|
|
2617
|
+
for (const spec of stopSpecs) {
|
|
2258
2618
|
try {
|
|
2259
|
-
|
|
2619
|
+
if (!spec.stopped) {
|
|
2620
|
+
const result = await doStop(spec.adapter, spec.opts);
|
|
2621
|
+
spec.stopped = result.stopped === true;
|
|
2622
|
+
}
|
|
2260
2623
|
} catch (error) {
|
|
2261
2624
|
stopError ??= error;
|
|
2262
2625
|
}
|
|
@@ -2676,7 +3039,11 @@ function peerSideTransport(flags, side, adapter) {
|
|
|
2676
3039
|
`--transport-${side} must be one of: ${VALID_TRANSPORTS.join(", ")}`,
|
|
2677
3040
|
);
|
|
2678
3041
|
}
|
|
2679
|
-
if (
|
|
3042
|
+
if (
|
|
3043
|
+
transport !== "tmux" &&
|
|
3044
|
+
adapter.cli !== "claude" &&
|
|
3045
|
+
!(adapter.cli === "codex" && transport === "uds")
|
|
3046
|
+
) {
|
|
2680
3047
|
throw new Error(
|
|
2681
3048
|
`--transport-${side} ${transport} is only supported when --cli-${side} is claude`,
|
|
2682
3049
|
);
|
|
@@ -2684,11 +3051,34 @@ function peerSideTransport(flags, side, adapter) {
|
|
|
2684
3051
|
return transport;
|
|
2685
3052
|
}
|
|
2686
3053
|
|
|
3054
|
+
function peerSideBusyPolicy(flags, side, adapter, transport) {
|
|
3055
|
+
const allowed =
|
|
3056
|
+
adapter.cli === "codex" && transport === "uds"
|
|
3057
|
+
? ["wait", "fail", "steer"]
|
|
3058
|
+
: ["wait", "fail", "interrupt"];
|
|
3059
|
+
for (const name of ["if-busy", `if-busy-${side}`]) {
|
|
3060
|
+
if (flags[name] !== undefined && !allowed.includes(flags[name]))
|
|
3061
|
+
throw new Error(
|
|
3062
|
+
`--${name} must be ${allowed.join("|")} for side ${side} (${adapter.cli} ${transport})`,
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
return sideFlag(flags, side, "if-busy", "wait");
|
|
3066
|
+
}
|
|
3067
|
+
|
|
2687
3068
|
function peerSideBaseOpts(flags, side, adapter, session) {
|
|
2688
3069
|
const transport = peerSideTransport(flags, side, adapter);
|
|
3070
|
+
const ifBusy = peerSideBusyPolicy(flags, side, adapter, transport);
|
|
2689
3071
|
const short = sideFlag(flags, side, "short");
|
|
2690
3072
|
const sessionId = sideFlag(flags, side, "session-id");
|
|
2691
|
-
|
|
3073
|
+
const threadId = sideFlag(flags, side, "thread");
|
|
3074
|
+
if (transport === "uds" && adapter.cli === "codex" && !threadId)
|
|
3075
|
+
throw new Error(`--transport-${side} uds requires --thread-${side}`);
|
|
3076
|
+
if (
|
|
3077
|
+
transport !== "tmux" &&
|
|
3078
|
+
adapter.cli === "claude" &&
|
|
3079
|
+
!short &&
|
|
3080
|
+
!sessionId
|
|
3081
|
+
) {
|
|
2692
3082
|
throw new Error(
|
|
2693
3083
|
`--transport-${side} ${transport} requires --short-${side} or --session-id-${side}`,
|
|
2694
3084
|
);
|
|
@@ -2697,13 +3087,38 @@ function peerSideBaseOpts(flags, side, adapter, session) {
|
|
|
2697
3087
|
session,
|
|
2698
3088
|
short,
|
|
2699
3089
|
sessionId,
|
|
3090
|
+
threadId,
|
|
3091
|
+
codexSocket:
|
|
3092
|
+
adapter.cli === "codex" && transport === "uds"
|
|
3093
|
+
? sideFlag(flags, side, "codex-socket", "default")
|
|
3094
|
+
: null,
|
|
3095
|
+
socketPath:
|
|
3096
|
+
adapter.cli === "codex" && transport === "uds" && !flags.remote
|
|
3097
|
+
? resolveCodexDaemonSocket(
|
|
3098
|
+
sideFlag(flags, side, "codex-socket", "default"),
|
|
3099
|
+
)
|
|
3100
|
+
: null,
|
|
2700
3101
|
transport,
|
|
2701
3102
|
bridgePath: sideFlag(flags, side, "bridge", resolveBridgePath(flags)),
|
|
2702
3103
|
remote: flags.remote,
|
|
2703
3104
|
cwd: flags.cwd,
|
|
2704
3105
|
timeoutMs: secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS),
|
|
3106
|
+
maxTurnMs:
|
|
3107
|
+
adapter.cli === "codex" && transport === "uds"
|
|
3108
|
+
? secondsFlag(
|
|
3109
|
+
flags,
|
|
3110
|
+
"max-turn",
|
|
3111
|
+
Math.min(resolveHardCeilingMs(), 15 * 60_000),
|
|
3112
|
+
)
|
|
3113
|
+
: undefined,
|
|
2705
3114
|
settleMs: msFlag(flags, "settle", DEFAULT_SETTLE_MS),
|
|
2706
3115
|
pollIntervalMs: msFlag(flags, "poll-interval", DEFAULT_POLL_INTERVAL_MS),
|
|
3116
|
+
ifBusy,
|
|
3117
|
+
busyTimeoutMs: secondsFlag(
|
|
3118
|
+
flags,
|
|
3119
|
+
"busy-timeout",
|
|
3120
|
+
secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS),
|
|
3121
|
+
),
|
|
2707
3122
|
readyTimeoutMs: secondsFlag(
|
|
2708
3123
|
flags,
|
|
2709
3124
|
"ready-timeout",
|
|
@@ -2720,6 +3135,37 @@ function shouldPrestartPeerSide(base) {
|
|
|
2720
3135
|
return base.transport === "tmux";
|
|
2721
3136
|
}
|
|
2722
3137
|
|
|
3138
|
+
async function verifyAttachedPeerSide(adapter, base, deps = {}) {
|
|
3139
|
+
if (base.transport !== "tmux") return;
|
|
3140
|
+
const tmux = deps.runTmux ?? runTmux;
|
|
3141
|
+
const { session, target } = splitTmuxTarget(base.session);
|
|
3142
|
+
try {
|
|
3143
|
+
await tmux(base.remote, ["has-session", "-t", session]);
|
|
3144
|
+
} catch {
|
|
3145
|
+
throw new Error(`attached ${adapter.cli} session not found: ${session}`);
|
|
3146
|
+
}
|
|
3147
|
+
if (adapter.cli === "codex") {
|
|
3148
|
+
const { stdout } = await tmux(base.remote, [
|
|
3149
|
+
"display-message",
|
|
3150
|
+
"-p",
|
|
3151
|
+
"-t",
|
|
3152
|
+
target,
|
|
3153
|
+
"#{pane_current_command}\t#{pane_start_command}\t#{pane_tty}\t#{pane_pid}",
|
|
3154
|
+
]);
|
|
3155
|
+
const [currentCommand, startCommand, paneTty, panePid] = stdout
|
|
3156
|
+
.trimEnd()
|
|
3157
|
+
.split("\t");
|
|
3158
|
+
const result = await inspectCodexTmuxPane(
|
|
3159
|
+
{ currentCommand, startCommand, paneTty, panePid, remote: base.remote },
|
|
3160
|
+
deps,
|
|
3161
|
+
);
|
|
3162
|
+
if (result.preflightWarning) return result;
|
|
3163
|
+
if (!result.isCodex) {
|
|
3164
|
+
throw new Error(`attached target is not a Codex tmux pane: ${target}`);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
|
|
2723
3169
|
function addStopSpecOnce(stopSpecs, adapter, opts) {
|
|
2724
3170
|
if (!opts.session) return;
|
|
2725
3171
|
const exists = stopSpecs.some(
|
|
@@ -2739,10 +3185,26 @@ function addStopSpecOnce(stopSpecs, adapter, opts) {
|
|
|
2739
3185
|
async function peer(flags) {
|
|
2740
3186
|
const adapterA = selectAdapterName(flags["cli-a"] ?? "codex", "cli-a");
|
|
2741
3187
|
const adapterB = selectAdapterName(flags["cli-b"] ?? "claude", "cli-b");
|
|
3188
|
+
peerSideBusyPolicy(
|
|
3189
|
+
flags,
|
|
3190
|
+
"a",
|
|
3191
|
+
adapterA,
|
|
3192
|
+
peerSideTransport(flags, "a", adapterA),
|
|
3193
|
+
);
|
|
3194
|
+
peerSideBusyPolicy(
|
|
3195
|
+
flags,
|
|
3196
|
+
"b",
|
|
3197
|
+
adapterB,
|
|
3198
|
+
peerSideTransport(flags, "b", adapterB),
|
|
3199
|
+
);
|
|
2742
3200
|
const sessionA = flags["session-a"] ?? "peerA";
|
|
2743
3201
|
const sessionB = flags["session-b"] ?? "peerB";
|
|
2744
3202
|
const baseA = peerSideBaseOpts(flags, "a", adapterA, sessionA);
|
|
2745
3203
|
const baseB = peerSideBaseOpts(flags, "b", adapterB, sessionB);
|
|
3204
|
+
const attachedA =
|
|
3205
|
+
Object.hasOwn(flags, "attach-a") || baseA.transport === "uds";
|
|
3206
|
+
const attachedB =
|
|
3207
|
+
Object.hasOwn(flags, "attach-b") || baseB.transport === "uds";
|
|
2746
3208
|
const rounds = integerFlag(flags, "rounds", 4);
|
|
2747
3209
|
const mode = peerMode(flags);
|
|
2748
3210
|
const hops = [];
|
|
@@ -2756,6 +3218,10 @@ async function peer(flags) {
|
|
|
2756
3218
|
cliB: adapterB.cli,
|
|
2757
3219
|
transportA: baseA.transport,
|
|
2758
3220
|
transportB: baseB.transport,
|
|
3221
|
+
attachedA,
|
|
3222
|
+
attachedB,
|
|
3223
|
+
stoppedA: false,
|
|
3224
|
+
stoppedB: false,
|
|
2759
3225
|
hops,
|
|
2760
3226
|
transcript_path: artifactPaths.transcriptPath,
|
|
2761
3227
|
status_path: artifactPaths.statusPath,
|
|
@@ -2771,11 +3237,27 @@ async function peer(flags) {
|
|
|
2771
3237
|
let primaryError = null;
|
|
2772
3238
|
let stopError = null;
|
|
2773
3239
|
let closure = null;
|
|
3240
|
+
const stopPeerSessions = async () => {
|
|
3241
|
+
try {
|
|
3242
|
+
await stopAfterLifecycle(stopSpecs);
|
|
3243
|
+
} finally {
|
|
3244
|
+
output.stoppedA =
|
|
3245
|
+
!attachedA &&
|
|
3246
|
+
stopSpecs.some(
|
|
3247
|
+
(spec) => spec.opts.session === sessionA && spec.stopped,
|
|
3248
|
+
);
|
|
3249
|
+
output.stoppedB =
|
|
3250
|
+
!attachedB &&
|
|
3251
|
+
stopSpecs.some(
|
|
3252
|
+
(spec) => spec.opts.session === sessionB && spec.stopped,
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
};
|
|
2774
3256
|
const signalController = createPeerSignalController({
|
|
2775
3257
|
output,
|
|
2776
3258
|
persistTranscript: (value) => persistPeerTranscript(artifactPaths, value),
|
|
2777
3259
|
persistStatus: (value) => persistPeerStatus(artifactPaths, value),
|
|
2778
|
-
stopSessions:
|
|
3260
|
+
stopSessions: stopPeerSessions,
|
|
2779
3261
|
});
|
|
2780
3262
|
const onSigint = () => {
|
|
2781
3263
|
void signalController.handle("SIGINT");
|
|
@@ -2787,11 +3269,25 @@ async function peer(flags) {
|
|
|
2787
3269
|
process.on("SIGTERM", onSigterm);
|
|
2788
3270
|
|
|
2789
3271
|
try {
|
|
2790
|
-
|
|
3272
|
+
for (const [side, adapter, base, attached] of [
|
|
3273
|
+
["a", adapterA, baseA, attachedA],
|
|
3274
|
+
["b", adapterB, baseB, attachedB],
|
|
3275
|
+
]) {
|
|
3276
|
+
if (!attached) continue;
|
|
3277
|
+
const result = await verifyAttachedPeerSide(adapter, base);
|
|
3278
|
+
if (result?.preflightWarning) {
|
|
3279
|
+
output.preflightWarning ??= [];
|
|
3280
|
+
output.preflightWarning.push({
|
|
3281
|
+
side,
|
|
3282
|
+
message: result.preflightWarning,
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
if (!attachedA && shouldPrestartPeerSide(baseA)) {
|
|
2791
3287
|
await doStart(adapterA, startOptsForSession(flags, sessionA, "a"));
|
|
2792
3288
|
addStopSpecOnce(stopSpecs, adapterA, baseA);
|
|
2793
3289
|
}
|
|
2794
|
-
if (shouldPrestartPeerSide(baseB)) {
|
|
3290
|
+
if (!attachedB && shouldPrestartPeerSide(baseB)) {
|
|
2795
3291
|
await doStart(adapterB, startOptsForSession(flags, sessionB, "b"));
|
|
2796
3292
|
addStopSpecOnce(stopSpecs, adapterB, baseB);
|
|
2797
3293
|
}
|
|
@@ -2809,7 +3305,17 @@ async function peer(flags) {
|
|
|
2809
3305
|
? buildPeerClosurePrompt(previous)
|
|
2810
3306
|
: peerPrompt(mode, hopIndex, previous);
|
|
2811
3307
|
const result = await doAsk(adapter, askOptsFromPeerBase(base, sent));
|
|
2812
|
-
if (
|
|
3308
|
+
if (
|
|
3309
|
+
(result.transportSelected ?? result.transport ?? base.transport) ===
|
|
3310
|
+
"uds"
|
|
3311
|
+
) {
|
|
3312
|
+
result.response = String(result.response ?? "")
|
|
3313
|
+
.split(/\r?\n/)
|
|
3314
|
+
.filter((line) => line.trim() !== PEER_HOP_DONE_MARKER)
|
|
3315
|
+
.join("\n")
|
|
3316
|
+
.trim();
|
|
3317
|
+
}
|
|
3318
|
+
if (result.tmuxStartedOnDemand && !(isA ? attachedA : attachedB)) {
|
|
2813
3319
|
addStopSpecOnce(stopSpecs, adapter, base);
|
|
2814
3320
|
}
|
|
2815
3321
|
if (
|
|
@@ -2864,7 +3370,7 @@ async function peer(flags) {
|
|
|
2864
3370
|
}
|
|
2865
3371
|
|
|
2866
3372
|
try {
|
|
2867
|
-
await
|
|
3373
|
+
await stopPeerSessions();
|
|
2868
3374
|
} catch (error) {
|
|
2869
3375
|
stopError = error;
|
|
2870
3376
|
}
|
|
@@ -2887,8 +3393,6 @@ async function peer(flags) {
|
|
|
2887
3393
|
output.exit_reason = exitReason;
|
|
2888
3394
|
output.error = terminalError.message;
|
|
2889
3395
|
}
|
|
2890
|
-
output.stoppedA = stopError === null;
|
|
2891
|
-
output.stoppedB = stopError === null;
|
|
2892
3396
|
|
|
2893
3397
|
try {
|
|
2894
3398
|
persistPeerTranscript(artifactPaths, output);
|
|
@@ -2914,7 +3418,11 @@ async function orchestrate(flags) {
|
|
|
2914
3418
|
throw new Error(`--mode must be one of: ${ORCHESTRATION_MODES.join(", ")}`);
|
|
2915
3419
|
}
|
|
2916
3420
|
const task = requireFlag(flags, "task");
|
|
2917
|
-
|
|
3421
|
+
if (flags["codex-socket"] && flags["codex-transport"] === "exec")
|
|
3422
|
+
throw new Error("--codex-socket conflicts with --codex-transport exec");
|
|
3423
|
+
const codexTransport =
|
|
3424
|
+
flags["codex-transport"] ??
|
|
3425
|
+
(flags["codex-socket"] ? "app-server-uds" : "exec");
|
|
2918
3426
|
if (!CODEX_ORCH_TRANSPORTS.includes(codexTransport)) {
|
|
2919
3427
|
throw new Error(
|
|
2920
3428
|
`--codex-transport must be one of: ${CODEX_ORCH_TRANSPORTS.join(", ")}`,
|
|
@@ -2922,6 +3430,9 @@ async function orchestrate(flags) {
|
|
|
2922
3430
|
}
|
|
2923
3431
|
const cwd = flags.cwd ?? process.cwd();
|
|
2924
3432
|
const timeoutMs = secondsFlag(flags, "timeout", 120_000);
|
|
3433
|
+
const socketPath = flags["codex-socket"]
|
|
3434
|
+
? resolveCodexDaemonSocket(flags["codex-socket"])
|
|
3435
|
+
: undefined;
|
|
2925
3436
|
|
|
2926
3437
|
// Lazy import keeps the orchestration engine (and its hub/team deps) off the
|
|
2927
3438
|
// hot path for every other thin-CLI verb; only `orchestrate` pays the cost.
|
|
@@ -2939,7 +3450,11 @@ async function orchestrate(flags) {
|
|
|
2939
3450
|
const claude = createClaudeUdsEndpoint({ cwd, timeoutMs });
|
|
2940
3451
|
const codex =
|
|
2941
3452
|
codexTransport === "app-server-uds"
|
|
2942
|
-
? createCodexAppServerUdsEndpoint({
|
|
3453
|
+
? createCodexAppServerUdsEndpoint({
|
|
3454
|
+
cwd,
|
|
3455
|
+
timeoutMs,
|
|
3456
|
+
...(socketPath ? { socketPath, spawnServer: false } : {}),
|
|
3457
|
+
})
|
|
2943
3458
|
: createCodexExecEndpoint({ workdir: cwd, timeout: timeoutMs });
|
|
2944
3459
|
|
|
2945
3460
|
const result = await runUdsOrchestration({ mode, task, claude, codex });
|
|
@@ -3029,6 +3544,7 @@ export {
|
|
|
3029
3544
|
buildLaunchKeys,
|
|
3030
3545
|
buildPeerClosurePrompt,
|
|
3031
3546
|
buildRemoteLiveCommand,
|
|
3547
|
+
buildTmuxCommand,
|
|
3032
3548
|
callRemoteLive,
|
|
3033
3549
|
classifyPeerExitReason,
|
|
3034
3550
|
createPeerSignalController,
|
|
@@ -3040,10 +3556,17 @@ export {
|
|
|
3040
3556
|
extractClaudeCompletedTaskListResponse,
|
|
3041
3557
|
extractResponseSinceMarker,
|
|
3042
3558
|
hasClaudeCompletedTaskListResponse,
|
|
3559
|
+
inspectCodexTmuxPane,
|
|
3560
|
+
isCodexLoadingCapture,
|
|
3043
3561
|
parseCodexTmuxSessions,
|
|
3044
3562
|
parsePeerClosure,
|
|
3045
3563
|
parseRemoteLiveJson,
|
|
3564
|
+
peerSideBaseOpts,
|
|
3046
3565
|
resolveAskTransport,
|
|
3566
|
+
resolveCodexDaemonSocket,
|
|
3567
|
+
splitTmuxTarget,
|
|
3568
|
+
tmuxBufferName,
|
|
3569
|
+
verifyAttachedPeerSide,
|
|
3047
3570
|
};
|
|
3048
3571
|
|
|
3049
3572
|
function isMainModule() {
|