triflux 10.44.0 → 10.46.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/README.md +1 -1
- package/adapters/codex/skills/tfx-harness/SKILL.md +45 -9
- package/bin/tfx-live.mjs +1298 -100
- package/bin/triflux.mjs +9 -6
- package/config/routing-policy.json +1 -1
- package/hooks/codex-session-hook.mjs +67 -1
- package/hooks/cto-north-star-brief.mjs +3 -16
- package/hooks/error-context.mjs +3 -6
- package/hooks/hook-manager.mjs +1 -1
- package/hooks/hook-registry.json +0 -11
- package/hooks/keyword-rules.json +8 -4
- package/hooks/permission-safe-allow.mjs +36 -2
- package/hooks/session-start-fast.mjs +50 -1
- package/hooks/session-start-lake.mjs +3 -16
- package/hub/bridge.mjs +111 -5
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/gemini-adapter.mjs +29 -11
- package/hub/lib/memory-store.mjs +66 -30
- package/hub/lib/tfx-route-args.mjs +41 -4
- package/hub/pipe.mjs +10 -15
- package/hub/reflexion.mjs +0 -57
- package/hub/router.mjs +202 -24
- package/hub/schema.sql +1 -0
- package/hub/server.mjs +172 -93
- package/hub/store.mjs +106 -33
- 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/codex-review.mjs +2 -3
- package/hub/team/conductor.mjs +80 -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 +38 -2
- package/hub/team/headless.mjs +493 -82
- package/hub/team/interactive-tui-transport.mjs +15 -1
- package/hub/team/launcher-template.mjs +19 -2
- 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 +47 -16
- package/hub/team/session.mjs +57 -0
- package/hub/team/terminal-opener.mjs +68 -13
- package/hub/team/tui-viewer.mjs +65 -14
- package/hub/team/uds-orchestrator.mjs +384 -83
- package/hub/tools.mjs +6 -0
- package/hub/workers/codex-mcp.mjs +35 -6
- package/hud/cli-policy.mjs +69 -0
- package/hud/constants.mjs +20 -17
- package/hud/context-monitor.mjs +31 -21
- package/hud/hud-qos-status.mjs +81 -91
- package/hud/providers/claude.mjs +150 -84
- package/hud/providers/codex.mjs +10 -1
- package/hud/renderers.mjs +21 -59
- package/hud/utils.mjs +1 -15
- package/package.json +1 -1
- package/scripts/__tests__/keyword-detector.test.mjs +89 -1
- package/scripts/__tests__/release-governance.test.mjs +329 -12
- 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__/skill-surface.test.mjs +6 -4
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +7 -5
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +27 -21
- package/scripts/claude-login-detect.mjs +107 -25
- package/scripts/ensure-agy-hooks.mjs +31 -1
- package/scripts/keyword-detector.mjs +11 -1
- package/scripts/lib/agent-route-policy.mjs +47 -39
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/codex-profile-config.mjs +45 -13
- package/scripts/lib/codex-recovery.sh +19 -0
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +12 -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/scripts/notion-read.mjs +2 -1
- package/scripts/release/check-packages-mirror.mjs +53 -1
- package/scripts/release/lib.mjs +1 -2
- package/scripts/release/prepare.mjs +24 -9
- 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/remote-spawn.mjs +830 -32
- package/scripts/session-stale-cleanup.mjs +0 -1
- package/scripts/setup.mjs +487 -150
- package/scripts/sync-hub-mcp-settings.mjs +11 -3
- package/scripts/test-lock.mjs +15 -1
- package/scripts/tfx-gate-activate.mjs +4 -4
- package/scripts/tfx-route-post.mjs +90 -13
- package/scripts/tfx-route.sh +520 -141
- package/skills/merge-worktree/SKILL.md +3 -3
- package/skills/star-prompt/skill.json +1 -1
- package/skills/tfx-analysis/SKILL.md +8 -9
- package/skills/tfx-analysis/skill.json +1 -10
- package/skills/tfx-auto/SKILL.md +208 -50
- package/skills/tfx-auto/skill.json +2 -23
- package/skills/tfx-doctor/skill.json +0 -3
- package/skills/tfx-find/skill.json +0 -7
- package/skills/tfx-forge/SKILL.md +2 -2
- package/skills/tfx-forge/skill.json +0 -7
- package/skills/tfx-goal-clarify/skill.json +5 -0
- package/skills/tfx-harness/SKILL.md +26 -3
- package/skills/tfx-harness/skill.json +5 -0
- package/skills/tfx-hooks/SKILL.md +0 -2
- package/skills/tfx-hooks/skill.json +0 -3
- package/skills/tfx-hub/SKILL.md +1 -6
- package/skills/tfx-hub/skill.json +0 -3
- package/skills/tfx-index/skill.json +0 -6
- package/skills/tfx-interview/SKILL.md +0 -2
- package/skills/tfx-live/SKILL.md +131 -5
- package/skills/tfx-live/skill.json +0 -5
- package/skills/tfx-plan/SKILL.md +7 -8
- package/skills/tfx-plan/skill.json +1 -12
- package/skills/tfx-profile/SKILL.md +15 -13
- package/skills/tfx-profile/skill.json +0 -3
- package/skills/tfx-prune/SKILL.md +4 -6
- package/skills/tfx-qa/SKILL.md +7 -7
- package/skills/tfx-qa/skill.json +1 -11
- package/skills/tfx-ralph/SKILL.md +8 -11
- package/skills/tfx-remote/SKILL.md +2 -2
- package/skills/tfx-remote/skill.json +0 -3
- package/skills/tfx-research/SKILL.md +2 -2
- package/skills/tfx-research/skill.json +1 -16
- package/skills/tfx-review/SKILL.md +11 -11
- package/skills/tfx-review/skill.json +1 -11
- package/skills/tfx-setup/SKILL.md +5 -6
- package/skills/tfx-setup/skill.json +0 -3
- package/skills/tfx-ship/SKILL.md +43 -36
- package/skills/tfx-ship/skill.json +1 -9
- package/skills/tfx-wt/skill.json +0 -7
- package/tui/codex-profile.mjs +3 -3
- package/tui/gemini-profile.mjs +11 -24
- package/tui/monitor.mjs +1 -0
- package/tui/setup.mjs +7 -6
- package/scripts/headless-guard-fast.sh +0 -29
- package/scripts/headless-guard.mjs +0 -523
- package/skills/tfx-autopilot/SKILL.md +0 -51
- package/skills/tfx-autopilot/skill.json +0 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +0 -75
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/eval_metadata.json +0 -42
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +0 -39
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/analysis.md +0 -87
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/classification.md +0 -35
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/commands.sh +0 -275
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/routing.md +0 -56
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +0 -39
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/analysis.md +0 -92
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/classification.md +0 -71
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/commands.sh +0 -264
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/routing.md +0 -113
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/eval_metadata.json +0 -32
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +0 -29
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/analysis.md +0 -96
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/classification.md +0 -38
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/commands.sh +0 -151
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/routing.md +0 -51
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +0 -29
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/analysis.md +0 -127
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/classification.md +0 -57
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/commands.sh +0 -129
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/routing.md +0 -84
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/eval_metadata.json +0 -27
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +0 -24
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/analysis.md +0 -98
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/classification.md +0 -65
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/commands.sh +0 -123
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/routing.md +0 -66
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +0 -20
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/analysis.md +0 -88
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/classification.md +0 -40
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/commands.sh +0 -130
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/routing.md +0 -61
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/timing.json +0 -5
- package/skills/tfx-consensus/SKILL.md +0 -56
- package/skills/tfx-consensus/skill.json +0 -8
- package/skills/tfx-debate/SKILL.md +0 -60
- package/skills/tfx-debate/skill.json +0 -14
- package/skills/tfx-fullcycle/SKILL.md +0 -47
- package/skills/tfx-fullcycle/skill.json +0 -13
- package/skills/tfx-multi/SKILL.md +0 -45
- package/skills/tfx-multi/references/agent-wrapper-rules.md +0 -94
- package/skills/tfx-multi/references/thorough-pipeline.md +0 -105
- package/skills/tfx-multi/skill.json +0 -10
- package/skills/tfx-panel/SKILL.md +0 -60
- package/skills/tfx-panel/skill.json +0 -14
- package/skills/tfx-persist/SKILL.md +0 -63
- package/skills/tfx-persist/skill.json +0 -14
- package/skills/tfx-psmux-rules/SKILL.md +0 -41
- package/skills/tfx-psmux-rules/skill.json +0 -10
- package/skills/tfx-remote-setup/SKILL.md +0 -42
- package/skills/tfx-remote-setup/skill.json +0 -10
- package/skills/tfx-remote-spawn/SKILL.md +0 -52
- package/skills/tfx-remote-spawn/skill.json +0 -10
- package/skills/tfx-swarm/SKILL.md +0 -49
- package/skills/tfx-swarm/skill.json +0 -13
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 { realpathSync } 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,
|
|
@@ -23,6 +24,9 @@ const DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
|
23
24
|
const DEFAULT_READY_TIMEOUT_MS = 30_000;
|
|
24
25
|
const DEFAULT_ANSWER_TIMEOUT_MS = 60_000;
|
|
25
26
|
const FALLBACK_QUIET_POLLS = 3;
|
|
27
|
+
const PROMPT_SUBMIT_MAX_ATTEMPTS = 3;
|
|
28
|
+
const PROMPT_SUBMIT_RETRY_DELAY_MS = 100;
|
|
29
|
+
const PROMPT_COMPOSER_NEEDLE_LENGTH = 30;
|
|
26
30
|
// Capture the complete pane history so a long TUI response does not lose its
|
|
27
31
|
// leading lines. runTmux's MAX_BUFFER remains the memory/output ceiling.
|
|
28
32
|
const CAPTURE_START = "-";
|
|
@@ -35,7 +39,8 @@ const BRIDGE_TIMEOUT_BUFFER_MS = 15_000;
|
|
|
35
39
|
// spill the JSON to a temp file and pass --payload-file instead.
|
|
36
40
|
const PAYLOAD_FILE_THRESHOLD = 96 * 1024;
|
|
37
41
|
const VALID_TRANSPORTS = ["tmux", "uds", "auto"];
|
|
38
|
-
const BOOLEAN_FLAGS = new Set(["json"]);
|
|
42
|
+
const BOOLEAN_FLAGS = new Set(["json", "attach-a", "attach-b"]);
|
|
43
|
+
const PEER_HOP_DONE_MARKER = "<<<TFX_PEER_HOP_DONE>>>";
|
|
39
44
|
// uds-fallback diagnostics land here, written async so a failed daemon attach
|
|
40
45
|
// never blocks the tmux fallback path (see writeUdsBugReport / doAskAuto).
|
|
41
46
|
const BUG_REPORT_DIR =
|
|
@@ -45,17 +50,20 @@ const BUG_REPORT_DIR =
|
|
|
45
50
|
function usage() {
|
|
46
51
|
return [
|
|
47
52
|
"Usage:",
|
|
48
|
-
" tfx-live start --session NAME [--cli codex|claude] [--cwd DIR] [--remote HOST] [--resume ID] [--resume-last 1] [--ready-timeout 30] [--poll-interval 1500]",
|
|
49
|
-
" tfx-live ask --session NAME --prompt TEXT [--cli codex|claude] [--timeout 60] [--remote HOST] [--settle 1500] [--poll-interval 1500]",
|
|
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]",
|
|
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]",
|
|
50
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]",
|
|
51
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.",
|
|
52
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]",
|
|
53
59
|
" tfx-live stop --session NAME [--cli codex|claude] [--remote HOST]",
|
|
54
60
|
" tfx-live probe [--short SHORT] [--session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 10]",
|
|
61
|
+
" tfx-live list-sessions --cli codex [--transport tmux|uds] [--codex-socket PATH|default] [--cwd DIR] [--remote HOST (tmux)]",
|
|
55
62
|
" tfx-live converse --session NAME --prompts-file PATH [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500]",
|
|
56
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]",
|
|
57
|
-
" tfx-live peer [--cli-a codex] [--cli-b claude] [--session-a
|
|
58
|
-
" 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.",
|
|
59
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.",
|
|
60
68
|
" tfx-live cto-hygiene-notify --root DIR --state-file PATH [--json]",
|
|
61
69
|
" One-shot CTO hygiene dry-run notification: sends only when actionable hygiene state changes; no polling or apply/steward lock.",
|
|
@@ -197,7 +205,7 @@ function buildRemoteLiveArgv(verb, opts) {
|
|
|
197
205
|
"tfx-live",
|
|
198
206
|
verb,
|
|
199
207
|
"--cli",
|
|
200
|
-
"claude",
|
|
208
|
+
opts.cli ?? "claude",
|
|
201
209
|
"--transport",
|
|
202
210
|
opts.transport ?? "uds",
|
|
203
211
|
];
|
|
@@ -205,6 +213,13 @@ function buildRemoteLiveArgv(verb, opts) {
|
|
|
205
213
|
if (opts.sessionId) args.push("--session-id", opts.sessionId);
|
|
206
214
|
if (opts.session) args.push("--session", opts.session);
|
|
207
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));
|
|
208
223
|
if (verb === "ask") args.push("--prompt", opts.prompt ?? "");
|
|
209
224
|
args.push("--timeout", timeoutSeconds(opts.timeoutMs));
|
|
210
225
|
if (verb === "ask" && opts.settleMs) {
|
|
@@ -311,8 +326,21 @@ async function callRemoteLive(verb, opts, deps = {}) {
|
|
|
311
326
|
const execRemote = deps.sshExec ?? execFileAsync;
|
|
312
327
|
const env = await probeRemoteEnv(host);
|
|
313
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;
|
|
314
337
|
const execOptions = {
|
|
315
|
-
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,
|
|
316
344
|
maxBuffer: MAX_BUFFER,
|
|
317
345
|
};
|
|
318
346
|
|
|
@@ -353,6 +381,292 @@ async function runTmux(remote, tmuxArgs, options = {}) {
|
|
|
353
381
|
}
|
|
354
382
|
}
|
|
355
383
|
|
|
384
|
+
function normalizeCwd(value) {
|
|
385
|
+
if (!value) return null;
|
|
386
|
+
const resolved = pathResolve(value);
|
|
387
|
+
try {
|
|
388
|
+
return realpathSync(resolved);
|
|
389
|
+
} catch {
|
|
390
|
+
return resolved;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
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
|
+
|
|
425
|
+
function isCodexTmuxPane(currentCommand, startCommand) {
|
|
426
|
+
const current = String(currentCommand ?? "").trim();
|
|
427
|
+
const currentBase = current.split("/").at(-1);
|
|
428
|
+
if (currentBase === "codex") {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const started = String(startCommand ?? "").trim();
|
|
433
|
+
const directStart = started
|
|
434
|
+
.replace(/^exec\s+/, "")
|
|
435
|
+
.replace(/^['"]|['"]$/g, "");
|
|
436
|
+
if (/^(?:\S*\/)?codex(?:\s|$)/.test(directStart)) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
/\bomx_codex_pid\b/.test(started) &&
|
|
442
|
+
/(?:^|[\s'""])(?:[^'" \t]+\/)?codex(?:[\s'"]|$)/.test(started)
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
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) {
|
|
542
|
+
const cwdFilter = normalizeCwd(cwd);
|
|
543
|
+
const sessions = new Map();
|
|
544
|
+
|
|
545
|
+
for (const line of String(stdout).split(/\r?\n/)) {
|
|
546
|
+
if (!line) continue;
|
|
547
|
+
const [
|
|
548
|
+
session,
|
|
549
|
+
createdRaw,
|
|
550
|
+
attachedRaw,
|
|
551
|
+
windowIndex,
|
|
552
|
+
paneIndex,
|
|
553
|
+
paneCwd,
|
|
554
|
+
currentCommand,
|
|
555
|
+
startCommand,
|
|
556
|
+
] = line.split("\t");
|
|
557
|
+
if (
|
|
558
|
+
!session ||
|
|
559
|
+
!(codexTargets
|
|
560
|
+
? codexTargets.has(`${session}:${windowIndex}.${paneIndex}`)
|
|
561
|
+
: isCodexTmuxPane(currentCommand, startCommand))
|
|
562
|
+
) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const normalizedPaneCwd = normalizeCwd(paneCwd);
|
|
567
|
+
if (cwdFilter && normalizedPaneCwd !== cwdFilter) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const startedAtEpoch = Number.parseInt(createdRaw, 10);
|
|
572
|
+
const startedAt = Number.isSafeInteger(startedAtEpoch)
|
|
573
|
+
? new Date(startedAtEpoch * 1000).toISOString()
|
|
574
|
+
: null;
|
|
575
|
+
const existing = sessions.get(session);
|
|
576
|
+
const pane = {
|
|
577
|
+
target: `${session}:${windowIndex}.${paneIndex}`,
|
|
578
|
+
cwd: normalizedPaneCwd,
|
|
579
|
+
command: currentCommand,
|
|
580
|
+
};
|
|
581
|
+
if (existing) {
|
|
582
|
+
if (!existing.cwd && normalizedPaneCwd) {
|
|
583
|
+
existing.cwd = normalizedPaneCwd;
|
|
584
|
+
}
|
|
585
|
+
existing.panes.push(pane);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
sessions.set(session, {
|
|
590
|
+
session,
|
|
591
|
+
startedAt,
|
|
592
|
+
startedAtEpoch: Number.isSafeInteger(startedAtEpoch)
|
|
593
|
+
? startedAtEpoch
|
|
594
|
+
: null,
|
|
595
|
+
cwd: normalizedPaneCwd,
|
|
596
|
+
attached: Number.parseInt(attachedRaw, 10) > 0,
|
|
597
|
+
target: pane.target,
|
|
598
|
+
panes: [pane],
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return [...sessions.values()].sort((left, right) =>
|
|
603
|
+
left.session.localeCompare(right.session),
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function discoverCodexTmuxSessions(
|
|
608
|
+
{ cwd = null, remote = null } = {},
|
|
609
|
+
deps = {},
|
|
610
|
+
) {
|
|
611
|
+
const format = [
|
|
612
|
+
"#{session_name}",
|
|
613
|
+
"#{session_created}",
|
|
614
|
+
"#{session_attached}",
|
|
615
|
+
"#{window_index}",
|
|
616
|
+
"#{pane_index}",
|
|
617
|
+
"#{pane_current_path}",
|
|
618
|
+
"#{pane_current_command}",
|
|
619
|
+
"#{pane_start_command}",
|
|
620
|
+
"#{pane_tty}",
|
|
621
|
+
"#{pane_pid}",
|
|
622
|
+
].join("\t");
|
|
623
|
+
|
|
624
|
+
try {
|
|
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
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
ok: true,
|
|
654
|
+
cli: "codex",
|
|
655
|
+
cwd: normalizeCwd(cwd),
|
|
656
|
+
sessions: parseCodexTmuxSessions(stdout, cwd, codexTargets),
|
|
657
|
+
};
|
|
658
|
+
} catch (error) {
|
|
659
|
+
return {
|
|
660
|
+
ok: false,
|
|
661
|
+
cli: "codex",
|
|
662
|
+
cwd: normalizeCwd(cwd),
|
|
663
|
+
reason: "tmux-unavailable",
|
|
664
|
+
error: error.message,
|
|
665
|
+
sessions: [],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
356
670
|
function sleep(ms) {
|
|
357
671
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
358
672
|
}
|
|
@@ -383,6 +697,30 @@ function isBusyByEsc(text) {
|
|
|
383
697
|
return String(text).includes("esc to interrupt");
|
|
384
698
|
}
|
|
385
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
|
+
|
|
386
724
|
function hasCodexComposerPrompt(text) {
|
|
387
725
|
return /^\s*[›❯▶▸>]\s*/m.test(text);
|
|
388
726
|
}
|
|
@@ -418,7 +756,11 @@ function isClaudeResponseLine(line) {
|
|
|
418
756
|
}
|
|
419
757
|
|
|
420
758
|
function isCodexReadyCapture(text) {
|
|
421
|
-
return
|
|
759
|
+
return (
|
|
760
|
+
!isBusyByEsc(text) &&
|
|
761
|
+
!isCodexLoadingCapture(text) &&
|
|
762
|
+
hasCodexComposerPrompt(text)
|
|
763
|
+
);
|
|
422
764
|
}
|
|
423
765
|
|
|
424
766
|
function hasClaudeLoginGuard(text) {
|
|
@@ -461,23 +803,9 @@ function isWarningLine(line) {
|
|
|
461
803
|
return /^\s*(?:\u26A0|\(warning\))/i.test(line);
|
|
462
804
|
}
|
|
463
805
|
|
|
464
|
-
function hasAssistantResponseLine(adapter, text) {
|
|
465
|
-
return String(text)
|
|
466
|
-
.split("\n")
|
|
467
|
-
.some((line) => adapter.isResponseLine(line));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
806
|
function promptLineIndex(lines, prompt) {
|
|
471
|
-
if (!prompt) {
|
|
472
|
-
return -1;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
807
|
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
476
|
-
|
|
477
|
-
if (
|
|
478
|
-
(hasCodexComposerPrompt(line) || hasClaudeComposerPrompt(line)) &&
|
|
479
|
-
line.includes(prompt)
|
|
480
|
-
) {
|
|
808
|
+
if (composerLineMatchesPrompt(lines[index], prompt)) {
|
|
481
809
|
return index;
|
|
482
810
|
}
|
|
483
811
|
}
|
|
@@ -489,6 +817,31 @@ function isComposerPromptLine(line) {
|
|
|
489
817
|
return hasCodexComposerPrompt(line) || hasClaudeComposerPrompt(line);
|
|
490
818
|
}
|
|
491
819
|
|
|
820
|
+
function promptComposerNeedle(prompt) {
|
|
821
|
+
const firstContentLine = String(prompt)
|
|
822
|
+
.split(/\r?\n/)
|
|
823
|
+
.map((line) => line.trim())
|
|
824
|
+
.find(Boolean);
|
|
825
|
+
return (firstContentLine ?? "").slice(0, PROMPT_COMPOSER_NEEDLE_LENGTH);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function composerLineMatchesPrompt(line, prompt) {
|
|
829
|
+
const needle = promptComposerNeedle(prompt);
|
|
830
|
+
return (
|
|
831
|
+
Boolean(needle) &&
|
|
832
|
+
isComposerPromptLine(line) &&
|
|
833
|
+
String(line).includes(needle)
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function composerShowsPrompt(text, prompt) {
|
|
838
|
+
const activeComposerLine = String(text)
|
|
839
|
+
.split("\n")
|
|
840
|
+
.filter(isComposerPromptLine)
|
|
841
|
+
.at(-1);
|
|
842
|
+
return composerLineMatchesPrompt(activeComposerLine, prompt);
|
|
843
|
+
}
|
|
844
|
+
|
|
492
845
|
function nextComposerPromptIndex(lines, startIndex) {
|
|
493
846
|
for (
|
|
494
847
|
let index = Math.max(0, startIndex + 1);
|
|
@@ -515,16 +868,6 @@ function responseLineIndexForPrompt(adapter, lines, promptIndex) {
|
|
|
515
868
|
return -1;
|
|
516
869
|
}
|
|
517
870
|
|
|
518
|
-
function hasAssistantResponseAfterPrompt(adapter, text, prompt) {
|
|
519
|
-
const lines = String(text).split("\n");
|
|
520
|
-
const promptIndex = promptLineIndex(lines, prompt);
|
|
521
|
-
if (promptIndex === -1) {
|
|
522
|
-
return hasAssistantResponseLine(adapter, text);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return responseLineIndexForPrompt(adapter, lines, promptIndex) !== -1;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
871
|
function extractAssistantResponse(adapter, text, prompt = null) {
|
|
529
872
|
const lines = String(text)
|
|
530
873
|
.split("\n")
|
|
@@ -560,7 +903,58 @@ function extractAssistantResponse(adapter, text, prompt = null) {
|
|
|
560
903
|
}
|
|
561
904
|
}
|
|
562
905
|
|
|
563
|
-
return response
|
|
906
|
+
return trimResponseLines(adapter, response);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function extractResponseSinceMarker({ beforeRaw, raw, doneMarker, adapter }) {
|
|
910
|
+
if (!doneMarker) {
|
|
911
|
+
return "";
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const beforeLines = String(beforeRaw)
|
|
915
|
+
.split("\n")
|
|
916
|
+
.map((line) => line.replace(/\s+$/g, ""));
|
|
917
|
+
const rawLines = String(raw)
|
|
918
|
+
.split("\n")
|
|
919
|
+
.map((line) => line.replace(/\s+$/g, ""));
|
|
920
|
+
let sharedPrefixLength = 0;
|
|
921
|
+
while (
|
|
922
|
+
sharedPrefixLength < beforeLines.length &&
|
|
923
|
+
sharedPrefixLength < rawLines.length &&
|
|
924
|
+
beforeLines[sharedPrefixLength] === rawLines[sharedPrefixLength]
|
|
925
|
+
) {
|
|
926
|
+
sharedPrefixLength += 1;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const newLines =
|
|
930
|
+
sharedPrefixLength > 0 ? rawLines.slice(sharedPrefixLength) : rawLines;
|
|
931
|
+
const markerIndex = newLines.findIndex((line) => line.trim() === doneMarker);
|
|
932
|
+
if (markerIndex === -1) {
|
|
933
|
+
return "";
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const candidateLines = newLines.slice(0, markerIndex);
|
|
937
|
+
const responseStart = candidateLines.findIndex((line) =>
|
|
938
|
+
adapter.isResponseLine(line),
|
|
939
|
+
);
|
|
940
|
+
const responseLines =
|
|
941
|
+
responseStart === -1 ? candidateLines : candidateLines.slice(responseStart);
|
|
942
|
+
|
|
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
|
+
);
|
|
564
958
|
}
|
|
565
959
|
|
|
566
960
|
function normalizeClaudeTaskText(text) {
|
|
@@ -685,19 +1079,35 @@ function hasClaudeCompletedTaskListResponse(text, options = {}) {
|
|
|
685
1079
|
return extractClaudeCompletedTaskListResponse(text, options).length > 0;
|
|
686
1080
|
}
|
|
687
1081
|
|
|
688
|
-
function
|
|
1082
|
+
function hasTmuxAssistantResponseAfterCurrentPrompt(
|
|
689
1083
|
adapter,
|
|
690
1084
|
text,
|
|
691
1085
|
prompt,
|
|
692
1086
|
beforeText,
|
|
693
1087
|
) {
|
|
1088
|
+
if (adapter.cli === "claude") {
|
|
1089
|
+
return hasClaudeCompletedTaskListResponse(text, {
|
|
1090
|
+
beforeText,
|
|
1091
|
+
prompt,
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const lines = String(text).split("\n");
|
|
1096
|
+
const promptIndex = promptLineIndex(lines, prompt);
|
|
1097
|
+
if (promptIndex === -1) {
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const beforePromptCount = String(beforeText)
|
|
1102
|
+
.split("\n")
|
|
1103
|
+
.filter((line) => composerLineMatchesPrompt(line, prompt)).length;
|
|
1104
|
+
const currentPromptCount = lines.filter((line) =>
|
|
1105
|
+
composerLineMatchesPrompt(line, prompt),
|
|
1106
|
+
).length;
|
|
1107
|
+
|
|
694
1108
|
return (
|
|
695
|
-
|
|
696
|
-
(adapter
|
|
697
|
-
hasClaudeCompletedTaskListResponse(text, {
|
|
698
|
-
beforeText,
|
|
699
|
-
prompt,
|
|
700
|
-
}))
|
|
1109
|
+
currentPromptCount > beforePromptCount &&
|
|
1110
|
+
responseLineIndexForPrompt(adapter, lines, promptIndex) !== -1
|
|
701
1111
|
);
|
|
702
1112
|
}
|
|
703
1113
|
|
|
@@ -912,9 +1322,21 @@ const ADAPTERS = {
|
|
|
912
1322
|
codex: {
|
|
913
1323
|
cli: "codex",
|
|
914
1324
|
launchArgv: ["codex"],
|
|
915
|
-
launchKeys: ["codex", "Enter"],
|
|
916
|
-
resumeById: (id) =>
|
|
917
|
-
|
|
1325
|
+
launchKeys: ["codex --dangerously-bypass-approvals-and-sandbox", "Enter"],
|
|
1326
|
+
resumeById: (id) =>
|
|
1327
|
+
shellQuote([
|
|
1328
|
+
"codex",
|
|
1329
|
+
"resume",
|
|
1330
|
+
id,
|
|
1331
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
1332
|
+
]),
|
|
1333
|
+
resumeLast: () =>
|
|
1334
|
+
shellQuote([
|
|
1335
|
+
"codex",
|
|
1336
|
+
"resume",
|
|
1337
|
+
"--last",
|
|
1338
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
1339
|
+
]),
|
|
918
1340
|
composerGlyph: "›",
|
|
919
1341
|
isReady: isCodexReadyCapture,
|
|
920
1342
|
contextPct: parseCodexContextPct,
|
|
@@ -996,6 +1418,39 @@ const ADAPTERS = {
|
|
|
996
1418
|
},
|
|
997
1419
|
};
|
|
998
1420
|
|
|
1421
|
+
function buildLaunchKeys(adapter, { model, effort } = {}) {
|
|
1422
|
+
const hasModel = model !== undefined;
|
|
1423
|
+
const hasEffort = effort !== undefined;
|
|
1424
|
+
if (!hasModel && !hasEffort) {
|
|
1425
|
+
return adapter.launchKeys;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const overrideArgs = [];
|
|
1429
|
+
if (adapter.cli === "codex") {
|
|
1430
|
+
if (hasModel) {
|
|
1431
|
+
overrideArgs.push("-c", `model=${JSON.stringify(String(model))}`);
|
|
1432
|
+
}
|
|
1433
|
+
if (hasEffort) {
|
|
1434
|
+
overrideArgs.push(
|
|
1435
|
+
"-c",
|
|
1436
|
+
`model_reasoning_effort=${JSON.stringify(String(effort))}`,
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
} else if (adapter.cli === "claude") {
|
|
1440
|
+
if (hasModel) {
|
|
1441
|
+
overrideArgs.push("--model", model);
|
|
1442
|
+
}
|
|
1443
|
+
if (hasEffort) {
|
|
1444
|
+
overrideArgs.push("--effort", effort);
|
|
1445
|
+
}
|
|
1446
|
+
} else {
|
|
1447
|
+
throw new Error(`Unsupported adapter: ${adapter.cli}`);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const [command, ...remainingKeys] = adapter.launchKeys;
|
|
1451
|
+
return [`${command} ${shellQuote(overrideArgs)}`, ...remainingKeys];
|
|
1452
|
+
}
|
|
1453
|
+
|
|
999
1454
|
async function dismissStartupScreens(adapter, remote, session) {
|
|
1000
1455
|
let raw = "";
|
|
1001
1456
|
let dismissed = false;
|
|
@@ -1184,7 +1639,11 @@ async function probeDaemon(bridgePath, ref, timeoutMs) {
|
|
|
1184
1639
|
async function hasTmuxSession(adapter, opts) {
|
|
1185
1640
|
if (!opts.session) return false;
|
|
1186
1641
|
try {
|
|
1187
|
-
await runTmux(opts.remote, [
|
|
1642
|
+
await runTmux(opts.remote, [
|
|
1643
|
+
"has-session",
|
|
1644
|
+
"-t",
|
|
1645
|
+
splitTmuxTarget(opts.session).session,
|
|
1646
|
+
]);
|
|
1188
1647
|
return true;
|
|
1189
1648
|
} catch {
|
|
1190
1649
|
return false;
|
|
@@ -1258,14 +1717,18 @@ async function doStart(adapter, opts) {
|
|
|
1258
1717
|
cwd,
|
|
1259
1718
|
resume,
|
|
1260
1719
|
resumeLast,
|
|
1720
|
+
model,
|
|
1721
|
+
effort,
|
|
1261
1722
|
readyTimeoutMs,
|
|
1262
1723
|
pollIntervalMs,
|
|
1263
1724
|
} = opts;
|
|
1725
|
+
if (session.includes(":"))
|
|
1726
|
+
throw new Error("start --session accepts only a session name without ':'");
|
|
1264
1727
|
const launchKeys = resume
|
|
1265
1728
|
? [adapter.resumeById(resume), "Enter"]
|
|
1266
1729
|
: resumeLast
|
|
1267
1730
|
? [adapter.resumeLast(), "Enter"]
|
|
1268
|
-
: adapter
|
|
1731
|
+
: buildLaunchKeys(adapter, { model, effort });
|
|
1269
1732
|
const resumed = Boolean(resume || resumeLast);
|
|
1270
1733
|
const resumeTarget = resume ?? (resumeLast ? "last" : null);
|
|
1271
1734
|
|
|
@@ -1276,6 +1739,16 @@ async function doStart(adapter, opts) {
|
|
|
1276
1739
|
session,
|
|
1277
1740
|
...(cwd ? ["-c", cwd] : []),
|
|
1278
1741
|
]);
|
|
1742
|
+
// Default tmux history-limit (2000) is far too small for long tool-heavy
|
|
1743
|
+
// CLI sessions (file reads, test runs) — the actual response can scroll
|
|
1744
|
+
// out of scrollback before capturePane() runs, producing an empty extract.
|
|
1745
|
+
await runTmux(remote, [
|
|
1746
|
+
"set-option",
|
|
1747
|
+
"-t",
|
|
1748
|
+
session,
|
|
1749
|
+
"history-limit",
|
|
1750
|
+
"100000",
|
|
1751
|
+
]);
|
|
1279
1752
|
await runTmux(remote, ["send-keys", "-t", session, ...launchKeys]);
|
|
1280
1753
|
await dismissStartupScreens(adapter, remote, session);
|
|
1281
1754
|
|
|
@@ -1297,14 +1770,89 @@ async function doStart(adapter, opts) {
|
|
|
1297
1770
|
});
|
|
1298
1771
|
}
|
|
1299
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
|
+
|
|
1300
1803
|
async function doAskViaTmux(adapter, opts) {
|
|
1301
|
-
const {
|
|
1804
|
+
const {
|
|
1805
|
+
session,
|
|
1806
|
+
prompt,
|
|
1807
|
+
remote,
|
|
1808
|
+
timeoutMs,
|
|
1809
|
+
settleMs,
|
|
1810
|
+
pollIntervalMs,
|
|
1811
|
+
doneMarker,
|
|
1812
|
+
ifBusy = "wait",
|
|
1813
|
+
busyTimeoutMs = timeoutMs,
|
|
1814
|
+
} = opts;
|
|
1815
|
+
const busyWaitedMs = await waitForTmuxIdle(adapter, {
|
|
1816
|
+
session,
|
|
1817
|
+
remote,
|
|
1818
|
+
pollIntervalMs,
|
|
1819
|
+
busyTimeoutMs,
|
|
1820
|
+
ifBusy,
|
|
1821
|
+
});
|
|
1302
1822
|
const beforeRaw = await capturePane(remote, session);
|
|
1303
1823
|
const contextPctBefore = adapter.contextPct(beforeRaw);
|
|
1304
1824
|
|
|
1305
|
-
|
|
1825
|
+
const bufferName = tmuxBufferName(session);
|
|
1826
|
+
await runTmux(remote, ["set-buffer", "-b", bufferName, "--", prompt]);
|
|
1827
|
+
await runTmux(remote, [
|
|
1828
|
+
"paste-buffer",
|
|
1829
|
+
"-b",
|
|
1830
|
+
bufferName,
|
|
1831
|
+
"-d",
|
|
1832
|
+
"-t",
|
|
1833
|
+
session,
|
|
1834
|
+
]);
|
|
1306
1835
|
await sleep(settleMs);
|
|
1307
|
-
|
|
1836
|
+
|
|
1837
|
+
let promptSubmitted = !doneMarker;
|
|
1838
|
+
if (doneMarker) {
|
|
1839
|
+
for (let attempt = 1; attempt <= PROMPT_SUBMIT_MAX_ATTEMPTS; attempt += 1) {
|
|
1840
|
+
await runTmux(remote, ["send-keys", "-t", session, "Enter"]);
|
|
1841
|
+
await sleep(PROMPT_SUBMIT_RETRY_DELAY_MS);
|
|
1842
|
+
const submissionVisible = await captureVisible(remote, session);
|
|
1843
|
+
if (!composerShowsPrompt(submissionVisible, prompt)) {
|
|
1844
|
+
promptSubmitted = true;
|
|
1845
|
+
break;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (!promptSubmitted) {
|
|
1849
|
+
throw new Error(
|
|
1850
|
+
`Prompt did not submit after ${PROMPT_SUBMIT_MAX_ATTEMPTS} attempts`,
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
} else {
|
|
1854
|
+
await runTmux(remote, ["send-keys", "-t", session, "Enter"]);
|
|
1855
|
+
}
|
|
1308
1856
|
|
|
1309
1857
|
const startedAt = Date.now();
|
|
1310
1858
|
let raw = "";
|
|
@@ -1316,18 +1864,36 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
1316
1864
|
let done = false;
|
|
1317
1865
|
|
|
1318
1866
|
while (Date.now() - startedAt < timeoutMs) {
|
|
1319
|
-
visible =
|
|
1867
|
+
visible = doneMarker
|
|
1868
|
+
? await capturePane(remote, session)
|
|
1869
|
+
: await captureVisible(remote, session);
|
|
1320
1870
|
contextPctAfter = adapter.contextPct(visible);
|
|
1321
1871
|
|
|
1322
1872
|
quietPolls = visible === previousVisible ? quietPolls + 1 : 0;
|
|
1323
1873
|
previousVisible = visible;
|
|
1324
1874
|
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1875
|
+
const markerDone =
|
|
1876
|
+
promptSubmitted &&
|
|
1877
|
+
Boolean(
|
|
1878
|
+
extractResponseSinceMarker({
|
|
1879
|
+
beforeRaw,
|
|
1880
|
+
raw: visible,
|
|
1881
|
+
doneMarker,
|
|
1882
|
+
adapter,
|
|
1883
|
+
}),
|
|
1884
|
+
);
|
|
1885
|
+
const fallbackDone =
|
|
1886
|
+
!doneMarker &&
|
|
1327
1887
|
!adapter.isBusy(visible) &&
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1888
|
+
adapter.isReady(visible) &&
|
|
1889
|
+
quietPolls >= FALLBACK_QUIET_POLLS &&
|
|
1890
|
+
hasTmuxAssistantResponseAfterCurrentPrompt(
|
|
1891
|
+
adapter,
|
|
1892
|
+
await capturePane(remote, session),
|
|
1893
|
+
prompt,
|
|
1894
|
+
beforeRaw,
|
|
1895
|
+
);
|
|
1896
|
+
if (markerDone || fallbackDone) {
|
|
1331
1897
|
done = true;
|
|
1332
1898
|
break;
|
|
1333
1899
|
}
|
|
@@ -1336,7 +1902,20 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
1336
1902
|
}
|
|
1337
1903
|
|
|
1338
1904
|
raw = await capturePane(remote, session);
|
|
1339
|
-
|
|
1905
|
+
const markerResponse = promptSubmitted
|
|
1906
|
+
? extractResponseSinceMarker({
|
|
1907
|
+
beforeRaw,
|
|
1908
|
+
raw,
|
|
1909
|
+
doneMarker,
|
|
1910
|
+
adapter,
|
|
1911
|
+
})
|
|
1912
|
+
: "";
|
|
1913
|
+
if (markerResponse) {
|
|
1914
|
+
response = markerResponse;
|
|
1915
|
+
done = true;
|
|
1916
|
+
} else {
|
|
1917
|
+
response = extractAssistantResponse(adapter, raw, prompt);
|
|
1918
|
+
}
|
|
1340
1919
|
if (!response && adapter.cli === "claude") {
|
|
1341
1920
|
response = extractClaudeCompletedTaskListResponse(raw, {
|
|
1342
1921
|
beforeText: beforeRaw,
|
|
@@ -1353,6 +1932,8 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
1353
1932
|
session,
|
|
1354
1933
|
remote: remote ?? null,
|
|
1355
1934
|
transport: "tmux",
|
|
1935
|
+
ifBusy,
|
|
1936
|
+
busyWaitedMs,
|
|
1356
1937
|
response,
|
|
1357
1938
|
contextPctBefore,
|
|
1358
1939
|
contextPctAfter,
|
|
@@ -1368,6 +1949,24 @@ async function doAsk(adapter, opts) {
|
|
|
1368
1949
|
return doAskViaTmux(adapter, opts);
|
|
1369
1950
|
}
|
|
1370
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
|
+
|
|
1371
1970
|
// uds/auto attach a live background Claude daemon, not a tmux TUI.
|
|
1372
1971
|
if (adapter.cli !== "claude") {
|
|
1373
1972
|
throw new Error(
|
|
@@ -1397,7 +1996,11 @@ async function doAsk(adapter, opts) {
|
|
|
1397
1996
|
}
|
|
1398
1997
|
|
|
1399
1998
|
async function doAskViaRemoteLive(adapter, opts, deps = {}) {
|
|
1400
|
-
const result = await callRemoteLive(
|
|
1999
|
+
const result = await callRemoteLive(
|
|
2000
|
+
"ask",
|
|
2001
|
+
{ ...opts, cli: adapter.cli },
|
|
2002
|
+
deps,
|
|
2003
|
+
);
|
|
1401
2004
|
return {
|
|
1402
2005
|
...result,
|
|
1403
2006
|
cli: result.cli ?? adapter.cli,
|
|
@@ -1465,7 +2068,11 @@ function daemonProbeTargetAttachable(probe, opts) {
|
|
|
1465
2068
|
async function ensureTmuxSession(adapter, opts) {
|
|
1466
2069
|
if (!opts.session) return false;
|
|
1467
2070
|
try {
|
|
1468
|
-
await runTmux(opts.remote, [
|
|
2071
|
+
await runTmux(opts.remote, [
|
|
2072
|
+
"has-session",
|
|
2073
|
+
"-t",
|
|
2074
|
+
splitTmuxTarget(opts.session).session,
|
|
2075
|
+
]);
|
|
1469
2076
|
return false;
|
|
1470
2077
|
} catch {
|
|
1471
2078
|
await doStart(adapter, {
|
|
@@ -1644,6 +2251,10 @@ async function doAskAuto(adapter, opts) {
|
|
|
1644
2251
|
|
|
1645
2252
|
async function doStop(adapter, opts) {
|
|
1646
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
|
+
);
|
|
1647
2258
|
await runTmux(remote, ["kill-session", "-t", session]);
|
|
1648
2259
|
return {
|
|
1649
2260
|
cli: adapter.cli,
|
|
@@ -1761,12 +2372,17 @@ async function doInterrupt(adapter, opts) {
|
|
|
1761
2372
|
}
|
|
1762
2373
|
|
|
1763
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 ':'");
|
|
1764
2378
|
return {
|
|
1765
|
-
session
|
|
2379
|
+
session,
|
|
1766
2380
|
remote: flags.remote,
|
|
1767
2381
|
cwd: flags.cwd,
|
|
1768
2382
|
resume: flags.resume,
|
|
1769
2383
|
resumeLast: Object.hasOwn(flags, "resume-last"),
|
|
2384
|
+
model: flags.model,
|
|
2385
|
+
effort: flags.effort,
|
|
1770
2386
|
readyTimeoutMs: secondsFlag(
|
|
1771
2387
|
flags,
|
|
1772
2388
|
"ready-timeout",
|
|
@@ -1804,7 +2420,10 @@ function askOpts(flags, adapter) {
|
|
|
1804
2420
|
const short = flags.short;
|
|
1805
2421
|
const sessionId = flags["session-id"];
|
|
1806
2422
|
const transport = transportFlag(flags, adapter, short, sessionId);
|
|
1807
|
-
|
|
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) {
|
|
1808
2427
|
throw new Error(
|
|
1809
2428
|
`--transport ${transport} requires --short or --session-id`,
|
|
1810
2429
|
);
|
|
@@ -1813,15 +2432,37 @@ function askOpts(flags, adapter) {
|
|
|
1813
2432
|
// tmux needs a tmux session name; uds needs a daemon ref (short/sessionId);
|
|
1814
2433
|
// auto can take both (daemon ref to probe, tmux session for fallback).
|
|
1815
2434
|
session:
|
|
1816
|
-
transport === "tmux"
|
|
2435
|
+
transport === "tmux"
|
|
2436
|
+
? splitTmuxTarget(requireFlag(flags, "session")).target
|
|
2437
|
+
: flags.session,
|
|
1817
2438
|
short,
|
|
1818
2439
|
sessionId,
|
|
1819
2440
|
configDir: flags["config-dir"],
|
|
1820
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
|
+
),
|
|
1821
2455
|
bridgePath: resolveBridgePath(flags),
|
|
1822
2456
|
prompt: requireFlag(flags, "prompt"),
|
|
1823
2457
|
remote: flags.remote,
|
|
1824
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,
|
|
1825
2466
|
settleMs: msFlag(flags, "settle", DEFAULT_SETTLE_MS),
|
|
1826
2467
|
pollIntervalMs: msFlag(flags, "poll-interval", DEFAULT_POLL_INTERVAL_MS),
|
|
1827
2468
|
};
|
|
@@ -1838,9 +2479,15 @@ function interruptOpts(flags, adapter) {
|
|
|
1838
2479
|
const short = flags.short;
|
|
1839
2480
|
const sessionId = flags["session-id"];
|
|
1840
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
|
+
);
|
|
1841
2486
|
return {
|
|
1842
2487
|
session:
|
|
1843
|
-
transport === "tmux"
|
|
2488
|
+
transport === "tmux"
|
|
2489
|
+
? splitTmuxTarget(requireFlag(flags, "session")).target
|
|
2490
|
+
: flags.session,
|
|
1844
2491
|
short,
|
|
1845
2492
|
sessionId,
|
|
1846
2493
|
configDir: flags["config-dir"],
|
|
@@ -1887,11 +2534,43 @@ async function probe(flags) {
|
|
|
1887
2534
|
);
|
|
1888
2535
|
}
|
|
1889
2536
|
|
|
1890
|
-
function
|
|
2537
|
+
async function listSessions(flags) {
|
|
2538
|
+
const adapter = selectAdapter(flags);
|
|
2539
|
+
if (adapter.cli !== "codex") {
|
|
2540
|
+
throw new Error("list-sessions currently supports only --cli codex");
|
|
2541
|
+
}
|
|
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
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
function startOptsForSession(flags, session, side) {
|
|
1891
2564
|
return {
|
|
1892
2565
|
session,
|
|
1893
2566
|
remote: flags.remote,
|
|
1894
2567
|
cwd: flags.cwd,
|
|
2568
|
+
...(side
|
|
2569
|
+
? {
|
|
2570
|
+
model: flags[`model-${side}`],
|
|
2571
|
+
effort: flags[`effort-${side}`],
|
|
2572
|
+
}
|
|
2573
|
+
: {}),
|
|
1895
2574
|
readyTimeoutMs: secondsFlag(
|
|
1896
2575
|
flags,
|
|
1897
2576
|
"ready-timeout",
|
|
@@ -1909,6 +2588,12 @@ function askOptsForSession(flags, session, prompt) {
|
|
|
1909
2588
|
timeoutMs: secondsFlag(flags, "timeout", DEFAULT_ANSWER_TIMEOUT_MS),
|
|
1910
2589
|
settleMs: msFlag(flags, "settle", DEFAULT_SETTLE_MS),
|
|
1911
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
|
+
),
|
|
1912
2597
|
};
|
|
1913
2598
|
}
|
|
1914
2599
|
|
|
@@ -1929,9 +2614,12 @@ async function readPromptsFile(path) {
|
|
|
1929
2614
|
|
|
1930
2615
|
async function stopAfterLifecycle(stopSpecs, primaryError = null) {
|
|
1931
2616
|
let stopError = null;
|
|
1932
|
-
for (const
|
|
2617
|
+
for (const spec of stopSpecs) {
|
|
1933
2618
|
try {
|
|
1934
|
-
|
|
2619
|
+
if (!spec.stopped) {
|
|
2620
|
+
const result = await doStop(spec.adapter, spec.opts);
|
|
2621
|
+
spec.stopped = result.stopped === true;
|
|
2622
|
+
}
|
|
1935
2623
|
} catch (error) {
|
|
1936
2624
|
stopError ??= error;
|
|
1937
2625
|
}
|
|
@@ -2054,17 +2742,279 @@ function peerMode(flags) {
|
|
|
2054
2742
|
}
|
|
2055
2743
|
|
|
2056
2744
|
function peerPrompt(mode, hopIndex, previous) {
|
|
2745
|
+
let prompt;
|
|
2057
2746
|
if (mode === "counting") {
|
|
2058
2747
|
if (hopIndex === 0) {
|
|
2059
|
-
|
|
2748
|
+
prompt = "Reply with only the integer 1.";
|
|
2749
|
+
} else {
|
|
2750
|
+
prompt = `The current integer is ${previous}. Add 1 to it and reply with ONLY the resulting integer, nothing else.`;
|
|
2751
|
+
}
|
|
2752
|
+
} else if (hopIndex === 0) {
|
|
2753
|
+
prompt = previous;
|
|
2754
|
+
} else {
|
|
2755
|
+
prompt = `The other party said: ${previous}\nReply briefly in 1-2 sentences and ask one short follow-up question.`;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
return [
|
|
2759
|
+
prompt,
|
|
2760
|
+
`After you have completely finished your response, print a new line containing only this token: \`${PEER_HOP_DONE_MARKER}\`. Do not put any other characters on that line.`,
|
|
2761
|
+
].join("\n");
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
function buildPeerClosurePrompt(previous) {
|
|
2765
|
+
return [
|
|
2766
|
+
"This is the final closure turn. Based on the discussion so far, declare whether a final agreement was reached.",
|
|
2767
|
+
"First reply with exactly one JSON object and no markdown fences:",
|
|
2768
|
+
'{"agreement_status":"complete|partial","unresolved_questions":[],"needs_more_rounds":false,"summary":"short final agreement"}',
|
|
2769
|
+
"Use agreement_status=complete only when no unresolved question remains and no more rounds are needed.",
|
|
2770
|
+
`The other party's latest response was: ${previous ?? "(none)"}`,
|
|
2771
|
+
`After the JSON is complete, print a new line containing only this token: \`${PEER_HOP_DONE_MARKER}\`. Do not put any other characters on that line.`,
|
|
2772
|
+
].join("\n");
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
function isCompleteDeclaration(value) {
|
|
2776
|
+
return ["complete", "completed", "agreed", "합의완료", "완료"].includes(
|
|
2777
|
+
String(value ?? "")
|
|
2778
|
+
.trim()
|
|
2779
|
+
.toLowerCase(),
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
function unstructuredPeerClosure() {
|
|
2784
|
+
return {
|
|
2785
|
+
structured: false,
|
|
2786
|
+
declaredComplete: false,
|
|
2787
|
+
unresolvedQuestions: [],
|
|
2788
|
+
needsMoreRounds: null,
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function parsePeerClosure(response) {
|
|
2793
|
+
const text = String(response ?? "").trim();
|
|
2794
|
+
const fenced = text.match(/```(?:json)?\s*({[\s\S]*})\s*```/i)?.[1];
|
|
2795
|
+
const firstBrace = text.indexOf("{");
|
|
2796
|
+
const lastBrace = text.lastIndexOf("}");
|
|
2797
|
+
const objectText =
|
|
2798
|
+
fenced ??
|
|
2799
|
+
(firstBrace >= 0 && lastBrace > firstBrace
|
|
2800
|
+
? text.slice(firstBrace, lastBrace + 1)
|
|
2801
|
+
: null);
|
|
2802
|
+
|
|
2803
|
+
if (objectText) {
|
|
2804
|
+
try {
|
|
2805
|
+
// Terminal word-wrap can inject a raw newline + indent mid-token in
|
|
2806
|
+
// compact JSON (e.g. "f\n alse" for "false") since there's often no
|
|
2807
|
+
// space nearby to wrap at. Try the raw text first, then retry with
|
|
2808
|
+
// wrap artifacts collapsed before giving up as unstructured.
|
|
2809
|
+
let parsed;
|
|
2810
|
+
try {
|
|
2811
|
+
parsed = JSON.parse(objectText);
|
|
2812
|
+
} catch {
|
|
2813
|
+
parsed = JSON.parse(objectText.replace(/\n[ \t]*/g, ""));
|
|
2814
|
+
}
|
|
2815
|
+
const unresolvedQuestions = parsed.unresolved_questions;
|
|
2816
|
+
const needsMoreRounds = parsed.needs_more_rounds;
|
|
2817
|
+
const structured =
|
|
2818
|
+
Object.hasOwn(parsed, "agreement_status") &&
|
|
2819
|
+
Array.isArray(unresolvedQuestions) &&
|
|
2820
|
+
typeof needsMoreRounds === "boolean";
|
|
2821
|
+
if (structured) {
|
|
2822
|
+
return {
|
|
2823
|
+
structured: true,
|
|
2824
|
+
declaredComplete: isCompleteDeclaration(parsed.agreement_status),
|
|
2825
|
+
unresolvedQuestions: unresolvedQuestions.map(String),
|
|
2826
|
+
needsMoreRounds,
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
return unstructuredPeerClosure();
|
|
2830
|
+
} catch {
|
|
2831
|
+
return unstructuredPeerClosure();
|
|
2060
2832
|
}
|
|
2061
|
-
return `The current integer is ${previous}. Add 1 to it and reply with ONLY the resulting integer, nothing else.`;
|
|
2062
2833
|
}
|
|
2063
2834
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2835
|
+
const status = text.match(
|
|
2836
|
+
/(?:agreement_status|agreement status|합의상태|합의 상태)\s*[:=]\s*["']?([^\n,"'}]+)/i,
|
|
2837
|
+
)?.[1];
|
|
2838
|
+
const unresolved = text.match(
|
|
2839
|
+
/(?:unresolved_questions|unresolved questions|미해결 쟁점)\s*[:=]\s*([^\n]+)/i,
|
|
2840
|
+
)?.[1];
|
|
2841
|
+
const needsMore = text.match(
|
|
2842
|
+
/(?:needs_more_rounds|needs more rounds|추가 라운드 필요)\s*[:=]\s*(true|false)/i,
|
|
2843
|
+
)?.[1];
|
|
2844
|
+
const structured =
|
|
2845
|
+
status !== undefined && unresolved !== undefined && needsMore !== undefined;
|
|
2846
|
+
const unresolvedQuestions = /^(?:\[\s*\]|none|없음)$/i.test(
|
|
2847
|
+
String(unresolved ?? "").trim(),
|
|
2848
|
+
)
|
|
2849
|
+
? []
|
|
2850
|
+
: String(unresolved ?? "")
|
|
2851
|
+
.replace(/^\[|\]$/g, "")
|
|
2852
|
+
.split(/[,;|]/)
|
|
2853
|
+
.map((item) => item.trim())
|
|
2854
|
+
.filter(Boolean);
|
|
2855
|
+
|
|
2856
|
+
return {
|
|
2857
|
+
structured,
|
|
2858
|
+
declaredComplete: isCompleteDeclaration(status),
|
|
2859
|
+
unresolvedQuestions,
|
|
2860
|
+
needsMoreRounds:
|
|
2861
|
+
needsMore === undefined ? null : needsMore.toLowerCase() === "true",
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
function derivePeerStatus({ hopsCompleted, closure, exitReason = null }) {
|
|
2866
|
+
if (hopsCompleted < 1) return "failed";
|
|
2867
|
+
if (exitReason) return "partial";
|
|
2868
|
+
if (
|
|
2869
|
+
closure?.structured === true &&
|
|
2870
|
+
closure.declaredComplete === true &&
|
|
2871
|
+
closure.needsMoreRounds === false &&
|
|
2872
|
+
closure.unresolvedQuestions.length === 0
|
|
2873
|
+
) {
|
|
2874
|
+
return "complete";
|
|
2066
2875
|
}
|
|
2067
|
-
return
|
|
2876
|
+
return "partial";
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
function classifyPeerExitReason(error) {
|
|
2880
|
+
const fingerprint = [error?.name, error?.code, error?.message]
|
|
2881
|
+
.filter(Boolean)
|
|
2882
|
+
.join(" ")
|
|
2883
|
+
.toLowerCase();
|
|
2884
|
+
return /timeout|timed out|etimedout|aborterror/.test(fingerprint)
|
|
2885
|
+
? "timeout"
|
|
2886
|
+
: "transport_error";
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
function peerSignalExit(signal) {
|
|
2890
|
+
return signal === "SIGTERM"
|
|
2891
|
+
? { exitReason: "terminated", exitCode: 143 }
|
|
2892
|
+
: { exitReason: "user_interrupt", exitCode: 130 };
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
function createPeerSignalController({
|
|
2896
|
+
output,
|
|
2897
|
+
persistTranscript,
|
|
2898
|
+
persistStatus,
|
|
2899
|
+
stopSessions,
|
|
2900
|
+
printOutput = printJson,
|
|
2901
|
+
exitProcess = (code) => process.exit(code),
|
|
2902
|
+
timeoutMs = 3000,
|
|
2903
|
+
now = Date.now,
|
|
2904
|
+
setTimer = setTimeout,
|
|
2905
|
+
clearTimer = clearTimeout,
|
|
2906
|
+
}) {
|
|
2907
|
+
let signalCount = 0;
|
|
2908
|
+
let settled = false;
|
|
2909
|
+
|
|
2910
|
+
const recordArtifactError = (kind, error) => {
|
|
2911
|
+
output.artifact_errors ??= [];
|
|
2912
|
+
output.artifact_errors.push(`${kind}: ${error.message}`);
|
|
2913
|
+
};
|
|
2914
|
+
|
|
2915
|
+
return {
|
|
2916
|
+
async handle(signal) {
|
|
2917
|
+
if (settled) return;
|
|
2918
|
+
signalCount += 1;
|
|
2919
|
+
const { exitReason, exitCode } = peerSignalExit(signal);
|
|
2920
|
+
if (signalCount > 1) {
|
|
2921
|
+
settled = true;
|
|
2922
|
+
exitProcess(exitCode);
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
const startedAt = now();
|
|
2927
|
+
output.status = "aborted";
|
|
2928
|
+
output.exit_reason = exitReason;
|
|
2929
|
+
output.hops_completed = output.hops.length;
|
|
2930
|
+
|
|
2931
|
+
try {
|
|
2932
|
+
persistTranscript(output);
|
|
2933
|
+
} catch (error) {
|
|
2934
|
+
recordArtifactError("transcript", error);
|
|
2935
|
+
}
|
|
2936
|
+
try {
|
|
2937
|
+
persistStatus(output);
|
|
2938
|
+
} catch (error) {
|
|
2939
|
+
recordArtifactError("status", error);
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
const remainingMs = Math.max(0, timeoutMs - (now() - startedAt));
|
|
2943
|
+
let timer;
|
|
2944
|
+
const graceExpired = new Promise((resolve) => {
|
|
2945
|
+
timer = setTimer(() => resolve("timeout"), remainingMs);
|
|
2946
|
+
});
|
|
2947
|
+
await Promise.race([
|
|
2948
|
+
Promise.resolve()
|
|
2949
|
+
.then(stopSessions)
|
|
2950
|
+
.then(
|
|
2951
|
+
() => "stopped",
|
|
2952
|
+
() => "stop_error",
|
|
2953
|
+
),
|
|
2954
|
+
graceExpired,
|
|
2955
|
+
]);
|
|
2956
|
+
clearTimer(timer);
|
|
2957
|
+
|
|
2958
|
+
if (settled) return;
|
|
2959
|
+
settled = true;
|
|
2960
|
+
printOutput(output);
|
|
2961
|
+
exitProcess(exitCode);
|
|
2962
|
+
},
|
|
2963
|
+
dispose() {
|
|
2964
|
+
settled = true;
|
|
2965
|
+
},
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
function peerArtifactPaths(sessionA, sessionB) {
|
|
2970
|
+
const preferredDir =
|
|
2971
|
+
process.env.TFX_LIVE_ARTIFACT_DIR ??
|
|
2972
|
+
pathJoin(homedir(), ".claude", "cache", "triflux", "tfx-live", "peer-runs");
|
|
2973
|
+
const fallbackDir = pathJoin(tmpdir(), "triflux-live");
|
|
2974
|
+
let artifactDir = preferredDir;
|
|
2975
|
+
try {
|
|
2976
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
2977
|
+
} catch {
|
|
2978
|
+
artifactDir = fallbackDir;
|
|
2979
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
2980
|
+
}
|
|
2981
|
+
const safe = (value) =>
|
|
2982
|
+
String(value)
|
|
2983
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
2984
|
+
.slice(0, 48);
|
|
2985
|
+
const runId = `peer-${safe(sessionA)}-${safe(sessionB)}-${Date.now()}-${process.pid}`;
|
|
2986
|
+
return {
|
|
2987
|
+
transcriptPath: pathJoin(artifactDir, `${runId}.transcript.json`),
|
|
2988
|
+
statusPath: pathJoin(artifactDir, `${runId}.status.json`),
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
function persistPeerTranscript(paths, output) {
|
|
2993
|
+
writeFileSync(
|
|
2994
|
+
paths.transcriptPath,
|
|
2995
|
+
`${JSON.stringify(
|
|
2996
|
+
{
|
|
2997
|
+
mode: output.mode,
|
|
2998
|
+
rounds: output.rounds,
|
|
2999
|
+
cliA: output.cliA,
|
|
3000
|
+
cliB: output.cliB,
|
|
3001
|
+
hops_completed: output.hops.length,
|
|
3002
|
+
hops: output.hops,
|
|
3003
|
+
},
|
|
3004
|
+
null,
|
|
3005
|
+
2,
|
|
3006
|
+
)}\n`,
|
|
3007
|
+
"utf8",
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
function persistPeerStatus(paths, output) {
|
|
3012
|
+
const { hops: _hops, ...status } = output;
|
|
3013
|
+
writeFileSync(
|
|
3014
|
+
paths.statusPath,
|
|
3015
|
+
`${JSON.stringify(status, null, 2)}\n`,
|
|
3016
|
+
"utf8",
|
|
3017
|
+
);
|
|
2068
3018
|
}
|
|
2069
3019
|
|
|
2070
3020
|
function sideFlag(flags, side, name, fallback = undefined) {
|
|
@@ -2089,7 +3039,11 @@ function peerSideTransport(flags, side, adapter) {
|
|
|
2089
3039
|
`--transport-${side} must be one of: ${VALID_TRANSPORTS.join(", ")}`,
|
|
2090
3040
|
);
|
|
2091
3041
|
}
|
|
2092
|
-
if (
|
|
3042
|
+
if (
|
|
3043
|
+
transport !== "tmux" &&
|
|
3044
|
+
adapter.cli !== "claude" &&
|
|
3045
|
+
!(adapter.cli === "codex" && transport === "uds")
|
|
3046
|
+
) {
|
|
2093
3047
|
throw new Error(
|
|
2094
3048
|
`--transport-${side} ${transport} is only supported when --cli-${side} is claude`,
|
|
2095
3049
|
);
|
|
@@ -2097,11 +3051,34 @@ function peerSideTransport(flags, side, adapter) {
|
|
|
2097
3051
|
return transport;
|
|
2098
3052
|
}
|
|
2099
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
|
+
|
|
2100
3068
|
function peerSideBaseOpts(flags, side, adapter, session) {
|
|
2101
3069
|
const transport = peerSideTransport(flags, side, adapter);
|
|
3070
|
+
const ifBusy = peerSideBusyPolicy(flags, side, adapter, transport);
|
|
2102
3071
|
const short = sideFlag(flags, side, "short");
|
|
2103
3072
|
const sessionId = sideFlag(flags, side, "session-id");
|
|
2104
|
-
|
|
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
|
+
) {
|
|
2105
3082
|
throw new Error(
|
|
2106
3083
|
`--transport-${side} ${transport} requires --short-${side} or --session-id-${side}`,
|
|
2107
3084
|
);
|
|
@@ -2110,13 +3087,38 @@ function peerSideBaseOpts(flags, side, adapter, session) {
|
|
|
2110
3087
|
session,
|
|
2111
3088
|
short,
|
|
2112
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,
|
|
2113
3101
|
transport,
|
|
2114
3102
|
bridgePath: sideFlag(flags, side, "bridge", resolveBridgePath(flags)),
|
|
2115
3103
|
remote: flags.remote,
|
|
2116
3104
|
cwd: flags.cwd,
|
|
2117
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,
|
|
2118
3114
|
settleMs: msFlag(flags, "settle", DEFAULT_SETTLE_MS),
|
|
2119
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
|
+
),
|
|
2120
3122
|
readyTimeoutMs: secondsFlag(
|
|
2121
3123
|
flags,
|
|
2122
3124
|
"ready-timeout",
|
|
@@ -2126,13 +3128,44 @@ function peerSideBaseOpts(flags, side, adapter, session) {
|
|
|
2126
3128
|
}
|
|
2127
3129
|
|
|
2128
3130
|
function askOptsFromPeerBase(base, prompt) {
|
|
2129
|
-
return { ...base, prompt };
|
|
3131
|
+
return { ...base, prompt, doneMarker: PEER_HOP_DONE_MARKER };
|
|
2130
3132
|
}
|
|
2131
3133
|
|
|
2132
3134
|
function shouldPrestartPeerSide(base) {
|
|
2133
3135
|
return base.transport === "tmux";
|
|
2134
3136
|
}
|
|
2135
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
|
+
|
|
2136
3169
|
function addStopSpecOnce(stopSpecs, adapter, opts) {
|
|
2137
3170
|
if (!opts.session) return;
|
|
2138
3171
|
const exists = stopSpecs.some(
|
|
@@ -2152,29 +3185,110 @@ function addStopSpecOnce(stopSpecs, adapter, opts) {
|
|
|
2152
3185
|
async function peer(flags) {
|
|
2153
3186
|
const adapterA = selectAdapterName(flags["cli-a"] ?? "codex", "cli-a");
|
|
2154
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
|
+
);
|
|
2155
3200
|
const sessionA = flags["session-a"] ?? "peerA";
|
|
2156
3201
|
const sessionB = flags["session-b"] ?? "peerB";
|
|
2157
3202
|
const baseA = peerSideBaseOpts(flags, "a", adapterA, sessionA);
|
|
2158
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";
|
|
2159
3208
|
const rounds = integerFlag(flags, "rounds", 4);
|
|
2160
3209
|
const mode = peerMode(flags);
|
|
2161
3210
|
const hops = [];
|
|
2162
3211
|
const numbers = [];
|
|
2163
3212
|
const stopSpecs = [];
|
|
3213
|
+
const artifactPaths = peerArtifactPaths(sessionA, sessionB);
|
|
3214
|
+
const output = {
|
|
3215
|
+
mode,
|
|
3216
|
+
rounds,
|
|
3217
|
+
cliA: adapterA.cli,
|
|
3218
|
+
cliB: adapterB.cli,
|
|
3219
|
+
transportA: baseA.transport,
|
|
3220
|
+
transportB: baseB.transport,
|
|
3221
|
+
attachedA,
|
|
3222
|
+
attachedB,
|
|
3223
|
+
stoppedA: false,
|
|
3224
|
+
stoppedB: false,
|
|
3225
|
+
hops,
|
|
3226
|
+
transcript_path: artifactPaths.transcriptPath,
|
|
3227
|
+
status_path: artifactPaths.statusPath,
|
|
3228
|
+
};
|
|
3229
|
+
if (mode === "counting") {
|
|
3230
|
+
output.numbers = numbers;
|
|
3231
|
+
}
|
|
2164
3232
|
let previous =
|
|
2165
3233
|
mode === "freeform"
|
|
2166
3234
|
? (flags.seed ??
|
|
2167
3235
|
"Introduce yourself in one sentence, then ask the other party one short question.")
|
|
2168
3236
|
: null;
|
|
2169
3237
|
let primaryError = null;
|
|
3238
|
+
let stopError = null;
|
|
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
|
+
};
|
|
3256
|
+
const signalController = createPeerSignalController({
|
|
3257
|
+
output,
|
|
3258
|
+
persistTranscript: (value) => persistPeerTranscript(artifactPaths, value),
|
|
3259
|
+
persistStatus: (value) => persistPeerStatus(artifactPaths, value),
|
|
3260
|
+
stopSessions: stopPeerSessions,
|
|
3261
|
+
});
|
|
3262
|
+
const onSigint = () => {
|
|
3263
|
+
void signalController.handle("SIGINT");
|
|
3264
|
+
};
|
|
3265
|
+
const onSigterm = () => {
|
|
3266
|
+
void signalController.handle("SIGTERM");
|
|
3267
|
+
};
|
|
3268
|
+
process.on("SIGINT", onSigint);
|
|
3269
|
+
process.on("SIGTERM", onSigterm);
|
|
2170
3270
|
|
|
2171
3271
|
try {
|
|
2172
|
-
|
|
2173
|
-
|
|
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)) {
|
|
3287
|
+
await doStart(adapterA, startOptsForSession(flags, sessionA, "a"));
|
|
2174
3288
|
addStopSpecOnce(stopSpecs, adapterA, baseA);
|
|
2175
3289
|
}
|
|
2176
|
-
if (shouldPrestartPeerSide(baseB)) {
|
|
2177
|
-
await doStart(adapterB, startOptsForSession(flags, sessionB));
|
|
3290
|
+
if (!attachedB && shouldPrestartPeerSide(baseB)) {
|
|
3291
|
+
await doStart(adapterB, startOptsForSession(flags, sessionB, "b"));
|
|
2178
3292
|
addStopSpecOnce(stopSpecs, adapterB, baseB);
|
|
2179
3293
|
}
|
|
2180
3294
|
|
|
@@ -2186,11 +3300,36 @@ async function peer(flags) {
|
|
|
2186
3300
|
const isA = hopIndex % 2 === 0;
|
|
2187
3301
|
const adapter = isA ? adapterA : adapterB;
|
|
2188
3302
|
const base = isA ? baseA : baseB;
|
|
2189
|
-
const
|
|
3303
|
+
const isClosure = hopIndex === rounds * 2 - 1;
|
|
3304
|
+
const sent = isClosure
|
|
3305
|
+
? buildPeerClosurePrompt(previous)
|
|
3306
|
+
: peerPrompt(mode, hopIndex, previous);
|
|
2190
3307
|
const result = await doAsk(adapter, askOptsFromPeerBase(base, sent));
|
|
2191
|
-
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)) {
|
|
2192
3319
|
addStopSpecOnce(stopSpecs, adapter, base);
|
|
2193
3320
|
}
|
|
3321
|
+
if (
|
|
3322
|
+
result.done !== true ||
|
|
3323
|
+
result.error ||
|
|
3324
|
+
!String(result.response ?? "").trim()
|
|
3325
|
+
) {
|
|
3326
|
+
throw new Error(
|
|
3327
|
+
(result.timedOut
|
|
3328
|
+
? `Peer hop timed out at hop ${hopIndex + 1}`
|
|
3329
|
+
: result.error) ??
|
|
3330
|
+
`Empty response from ${adapter.cli} at hop ${hopIndex + 1}`,
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
2194
3333
|
|
|
2195
3334
|
hops.push({
|
|
2196
3335
|
hop: hopIndex + 1,
|
|
@@ -2205,40 +3344,68 @@ async function peer(flags) {
|
|
|
2205
3344
|
aborted: result.aborted === true ? true : undefined,
|
|
2206
3345
|
reason: result.reason,
|
|
2207
3346
|
});
|
|
3347
|
+
if (isClosure) {
|
|
3348
|
+
closure = parsePeerClosure(result.response);
|
|
3349
|
+
}
|
|
2208
3350
|
|
|
2209
3351
|
if (mode === "counting") {
|
|
2210
3352
|
const parsed = parseFirstInteger(result.response);
|
|
2211
|
-
if (parsed === null) {
|
|
3353
|
+
if (parsed === null && !isClosure) {
|
|
2212
3354
|
throw new Error(
|
|
2213
3355
|
`Could not parse integer from ${adapter.cli} response at hop ${hopIndex + 1}`,
|
|
2214
3356
|
);
|
|
2215
3357
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
3358
|
+
if (parsed !== null) {
|
|
3359
|
+
numbers.push(parsed);
|
|
3360
|
+
previous = parsed;
|
|
3361
|
+
} else {
|
|
3362
|
+
previous = result.response;
|
|
3363
|
+
}
|
|
2218
3364
|
} else {
|
|
2219
3365
|
previous = result.response;
|
|
2220
3366
|
}
|
|
2221
3367
|
}
|
|
2222
3368
|
} catch (error) {
|
|
2223
3369
|
primaryError = error;
|
|
2224
|
-
} finally {
|
|
2225
|
-
await stopAfterLifecycle(stopSpecs, primaryError);
|
|
2226
3370
|
}
|
|
2227
3371
|
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
3372
|
+
try {
|
|
3373
|
+
await stopPeerSessions();
|
|
3374
|
+
} catch (error) {
|
|
3375
|
+
stopError = error;
|
|
3376
|
+
}
|
|
3377
|
+
signalController.dispose();
|
|
3378
|
+
process.off("SIGINT", onSigint);
|
|
3379
|
+
process.off("SIGTERM", onSigterm);
|
|
3380
|
+
|
|
3381
|
+
const terminalError = primaryError ?? stopError;
|
|
3382
|
+
const exitReason = terminalError
|
|
3383
|
+
? classifyPeerExitReason(terminalError)
|
|
3384
|
+
: null;
|
|
3385
|
+
output.hops_completed = hops.length;
|
|
3386
|
+
output.closure = closure;
|
|
3387
|
+
output.status = derivePeerStatus({
|
|
3388
|
+
hopsCompleted: hops.length,
|
|
3389
|
+
closure,
|
|
3390
|
+
exitReason,
|
|
3391
|
+
});
|
|
3392
|
+
if (exitReason) {
|
|
3393
|
+
output.exit_reason = exitReason;
|
|
3394
|
+
output.error = terminalError.message;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
try {
|
|
3398
|
+
persistPeerTranscript(artifactPaths, output);
|
|
3399
|
+
} catch (error) {
|
|
3400
|
+
output.artifact_errors ??= [];
|
|
3401
|
+
output.artifact_errors.push(`transcript: ${error.message}`);
|
|
3402
|
+
}
|
|
3403
|
+
try {
|
|
3404
|
+
persistPeerStatus(artifactPaths, output);
|
|
3405
|
+
} catch (error) {
|
|
3406
|
+
output.artifact_errors ??= [];
|
|
3407
|
+
output.artifact_errors.push(`status: ${error.message}`);
|
|
2239
3408
|
}
|
|
2240
|
-
output.stoppedA = true;
|
|
2241
|
-
output.stoppedB = true;
|
|
2242
3409
|
printJson(output);
|
|
2243
3410
|
}
|
|
2244
3411
|
|
|
@@ -2251,7 +3418,11 @@ async function orchestrate(flags) {
|
|
|
2251
3418
|
throw new Error(`--mode must be one of: ${ORCHESTRATION_MODES.join(", ")}`);
|
|
2252
3419
|
}
|
|
2253
3420
|
const task = requireFlag(flags, "task");
|
|
2254
|
-
|
|
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");
|
|
2255
3426
|
if (!CODEX_ORCH_TRANSPORTS.includes(codexTransport)) {
|
|
2256
3427
|
throw new Error(
|
|
2257
3428
|
`--codex-transport must be one of: ${CODEX_ORCH_TRANSPORTS.join(", ")}`,
|
|
@@ -2259,6 +3430,9 @@ async function orchestrate(flags) {
|
|
|
2259
3430
|
}
|
|
2260
3431
|
const cwd = flags.cwd ?? process.cwd();
|
|
2261
3432
|
const timeoutMs = secondsFlag(flags, "timeout", 120_000);
|
|
3433
|
+
const socketPath = flags["codex-socket"]
|
|
3434
|
+
? resolveCodexDaemonSocket(flags["codex-socket"])
|
|
3435
|
+
: undefined;
|
|
2262
3436
|
|
|
2263
3437
|
// Lazy import keeps the orchestration engine (and its hub/team deps) off the
|
|
2264
3438
|
// hot path for every other thin-CLI verb; only `orchestrate` pays the cost.
|
|
@@ -2276,7 +3450,11 @@ async function orchestrate(flags) {
|
|
|
2276
3450
|
const claude = createClaudeUdsEndpoint({ cwd, timeoutMs });
|
|
2277
3451
|
const codex =
|
|
2278
3452
|
codexTransport === "app-server-uds"
|
|
2279
|
-
? createCodexAppServerUdsEndpoint({
|
|
3453
|
+
? createCodexAppServerUdsEndpoint({
|
|
3454
|
+
cwd,
|
|
3455
|
+
timeoutMs,
|
|
3456
|
+
...(socketPath ? { socketPath, spawnServer: false } : {}),
|
|
3457
|
+
})
|
|
2280
3458
|
: createCodexExecEndpoint({ workdir: cwd, timeout: timeoutMs });
|
|
2281
3459
|
|
|
2282
3460
|
const result = await runUdsOrchestration({ mode, task, claude, codex });
|
|
@@ -2344,6 +3522,8 @@ async function main() {
|
|
|
2344
3522
|
await interrupt(flags);
|
|
2345
3523
|
} else if (command === "probe") {
|
|
2346
3524
|
await probe(flags);
|
|
3525
|
+
} else if (command === "list-sessions") {
|
|
3526
|
+
await listSessions(flags);
|
|
2347
3527
|
} else if (command === "converse") {
|
|
2348
3528
|
await converse(flags);
|
|
2349
3529
|
} else if (command === "goal-driven") {
|
|
@@ -2361,14 +3541,32 @@ async function main() {
|
|
|
2361
3541
|
|
|
2362
3542
|
export {
|
|
2363
3543
|
ADAPTERS,
|
|
3544
|
+
buildLaunchKeys,
|
|
3545
|
+
buildPeerClosurePrompt,
|
|
2364
3546
|
buildRemoteLiveCommand,
|
|
3547
|
+
buildTmuxCommand,
|
|
2365
3548
|
callRemoteLive,
|
|
3549
|
+
classifyPeerExitReason,
|
|
3550
|
+
createPeerSignalController,
|
|
2366
3551
|
ctoHygieneNotify,
|
|
3552
|
+
derivePeerStatus,
|
|
3553
|
+
discoverCodexTmuxSessions,
|
|
3554
|
+
doAskViaTmux,
|
|
2367
3555
|
extractAssistantResponse,
|
|
2368
3556
|
extractClaudeCompletedTaskListResponse,
|
|
3557
|
+
extractResponseSinceMarker,
|
|
2369
3558
|
hasClaudeCompletedTaskListResponse,
|
|
3559
|
+
inspectCodexTmuxPane,
|
|
3560
|
+
isCodexLoadingCapture,
|
|
3561
|
+
parseCodexTmuxSessions,
|
|
3562
|
+
parsePeerClosure,
|
|
2370
3563
|
parseRemoteLiveJson,
|
|
3564
|
+
peerSideBaseOpts,
|
|
2371
3565
|
resolveAskTransport,
|
|
3566
|
+
resolveCodexDaemonSocket,
|
|
3567
|
+
splitTmuxTarget,
|
|
3568
|
+
tmuxBufferName,
|
|
3569
|
+
verifyAttachedPeerSide,
|
|
2372
3570
|
};
|
|
2373
3571
|
|
|
2374
3572
|
function isMainModule() {
|