triflux 10.44.0 → 10.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/adapters/codex/skills/tfx-harness/SKILL.md +45 -9
- package/bin/tfx-live.mjs +752 -77
- package/bin/triflux.mjs +9 -6
- 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/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 +39 -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/tools.mjs +6 -0
- package/hub/workers/codex-mcp.mjs +34 -5
- 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 +94 -1
- package/scripts/__tests__/skill-surface.test.mjs +6 -4
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +6 -4
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +23 -17
- 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 +29 -22
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/codex-profile-config.mjs +26 -8
- 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/remote-spawn.mjs +830 -32
- package/scripts/session-stale-cleanup.mjs +0 -1
- package/scripts/setup.mjs +474 -143
- 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 +502 -126
- 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 +203 -46
- 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 +22 -1
- 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 +6 -6
- 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.json +1 -9
- package/skills/tfx-wt/skill.json +0 -7
- package/tui/codex-profile.mjs +2 -1
- package/tui/gemini-profile.mjs +11 -24
- package/tui/monitor.mjs +1 -0
- package/tui/setup.mjs +3 -3
- 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, 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";
|
|
@@ -23,6 +23,9 @@ const DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
|
23
23
|
const DEFAULT_READY_TIMEOUT_MS = 30_000;
|
|
24
24
|
const DEFAULT_ANSWER_TIMEOUT_MS = 60_000;
|
|
25
25
|
const FALLBACK_QUIET_POLLS = 3;
|
|
26
|
+
const PROMPT_SUBMIT_MAX_ATTEMPTS = 3;
|
|
27
|
+
const PROMPT_SUBMIT_RETRY_DELAY_MS = 100;
|
|
28
|
+
const PROMPT_COMPOSER_NEEDLE_LENGTH = 30;
|
|
26
29
|
// Capture the complete pane history so a long TUI response does not lose its
|
|
27
30
|
// leading lines. runTmux's MAX_BUFFER remains the memory/output ceiling.
|
|
28
31
|
const CAPTURE_START = "-";
|
|
@@ -36,6 +39,7 @@ const BRIDGE_TIMEOUT_BUFFER_MS = 15_000;
|
|
|
36
39
|
const PAYLOAD_FILE_THRESHOLD = 96 * 1024;
|
|
37
40
|
const VALID_TRANSPORTS = ["tmux", "uds", "auto"];
|
|
38
41
|
const BOOLEAN_FLAGS = new Set(["json"]);
|
|
42
|
+
const PEER_HOP_DONE_MARKER = "<<<TFX_PEER_HOP_DONE>>>";
|
|
39
43
|
// uds-fallback diagnostics land here, written async so a failed daemon attach
|
|
40
44
|
// never blocks the tmux fallback path (see writeUdsBugReport / doAskAuto).
|
|
41
45
|
const BUG_REPORT_DIR =
|
|
@@ -45,16 +49,17 @@ const BUG_REPORT_DIR =
|
|
|
45
49
|
function usage() {
|
|
46
50
|
return [
|
|
47
51
|
"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]",
|
|
52
|
+
" 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]",
|
|
49
53
|
" tfx-live ask --session NAME --prompt TEXT [--cli codex|claude] [--timeout 60] [--remote HOST] [--settle 1500] [--poll-interval 1500]",
|
|
50
54
|
" 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
55
|
" 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
56
|
" 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
57
|
" tfx-live stop --session NAME [--cli codex|claude] [--remote HOST]",
|
|
54
58
|
" tfx-live probe [--short SHORT] [--session-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 10]",
|
|
59
|
+
" tfx-live list-sessions --cli codex [--cwd DIR]",
|
|
55
60
|
" tfx-live converse --session NAME --prompts-file PATH [--cli codex|claude] [--remote HOST] [--cwd DIR] [--timeout 60] [--settle 1500]",
|
|
56
61
|
" 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 peerA] [--session-b peerB] [--transport-a tmux|uds|auto] [--transport-b tmux|uds|auto] [--short-a SHORT] [--short-b SHORT] [--session-id-a ID] [--session-id-b ID] [--bridge ABS] [--remote HOST] [--cwd DIR] [--rounds 4] [--mode counting|freeform] [--seed TEXT] [--timeout 60]",
|
|
62
|
+
" tfx-live peer [--cli-a codex] [--cli-b claude] [--model-a ID] [--model-b ID] [--effort-a TIER] [--effort-b TIER] [--session-a peerA] [--session-b peerB] [--transport-a tmux|uds|auto] [--transport-b tmux|uds|auto] [--short-a SHORT] [--short-b SHORT] [--session-id-a ID] [--session-id-b ID] [--bridge ABS] [--remote HOST] [--cwd DIR] [--rounds 4] [--mode counting|freeform] [--seed TEXT] [--timeout 60]",
|
|
58
63
|
" tfx-live orchestrate --task TEXT [--mode peer|codex-led|claude-led] [--codex-transport exec|app-server-uds] [--cwd DIR] [--timeout 120]",
|
|
59
64
|
" 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
65
|
" tfx-live cto-hygiene-notify --root DIR --state-file PATH [--json]",
|
|
@@ -353,6 +358,119 @@ async function runTmux(remote, tmuxArgs, options = {}) {
|
|
|
353
358
|
}
|
|
354
359
|
}
|
|
355
360
|
|
|
361
|
+
function normalizeCwd(value) {
|
|
362
|
+
if (!value) return null;
|
|
363
|
+
const resolved = pathResolve(value);
|
|
364
|
+
try {
|
|
365
|
+
return realpathSync(resolved);
|
|
366
|
+
} catch {
|
|
367
|
+
return resolved;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isCodexTmuxPane(currentCommand, startCommand) {
|
|
372
|
+
const current = String(currentCommand ?? "").trim();
|
|
373
|
+
const currentBase = current.split("/").at(-1);
|
|
374
|
+
if (currentBase === "codex") {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const started = String(startCommand ?? "").trim();
|
|
379
|
+
const directStart = started
|
|
380
|
+
.replace(/^exec\s+/, "")
|
|
381
|
+
.replace(/^['"]|['"]$/g, "");
|
|
382
|
+
if (/^(?:\S*\/)?codex(?:\s|$)/.test(directStart)) {
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
/\bomx_codex_pid\b/.test(started) &&
|
|
388
|
+
/(?:^|[\s'""])(?:[^'" \t]+\/)?codex(?:[\s'"]|$)/.test(started)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function parseCodexTmuxSessions(stdout, cwd = null) {
|
|
393
|
+
const cwdFilter = normalizeCwd(cwd);
|
|
394
|
+
const sessions = new Map();
|
|
395
|
+
|
|
396
|
+
for (const line of String(stdout).split(/\r?\n/)) {
|
|
397
|
+
if (!line) continue;
|
|
398
|
+
const [
|
|
399
|
+
session,
|
|
400
|
+
createdRaw,
|
|
401
|
+
attachedRaw,
|
|
402
|
+
paneCwd,
|
|
403
|
+
currentCommand,
|
|
404
|
+
...startParts
|
|
405
|
+
] = line.split("\t");
|
|
406
|
+
const startCommand = startParts.join("\t");
|
|
407
|
+
if (!session || !isCodexTmuxPane(currentCommand, startCommand)) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const normalizedPaneCwd = normalizeCwd(paneCwd);
|
|
412
|
+
if (cwdFilter && normalizedPaneCwd !== cwdFilter) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const startedAtEpoch = Number.parseInt(createdRaw, 10);
|
|
417
|
+
const startedAt = Number.isSafeInteger(startedAtEpoch)
|
|
418
|
+
? new Date(startedAtEpoch * 1000).toISOString()
|
|
419
|
+
: null;
|
|
420
|
+
const existing = sessions.get(session);
|
|
421
|
+
if (existing) {
|
|
422
|
+
if (!existing.cwd && normalizedPaneCwd) {
|
|
423
|
+
existing.cwd = normalizedPaneCwd;
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
sessions.set(session, {
|
|
429
|
+
session,
|
|
430
|
+
startedAt,
|
|
431
|
+
startedAtEpoch: Number.isSafeInteger(startedAtEpoch)
|
|
432
|
+
? startedAtEpoch
|
|
433
|
+
: null,
|
|
434
|
+
cwd: normalizedPaneCwd,
|
|
435
|
+
attached: Number.parseInt(attachedRaw, 10) > 0,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return [...sessions.values()].sort((left, right) =>
|
|
440
|
+
left.session.localeCompare(right.session),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function discoverCodexTmuxSessions({ cwd = null } = {}) {
|
|
445
|
+
const format = [
|
|
446
|
+
"#{session_name}",
|
|
447
|
+
"#{session_created}",
|
|
448
|
+
"#{session_attached}",
|
|
449
|
+
"#{pane_current_path}",
|
|
450
|
+
"#{pane_current_command}",
|
|
451
|
+
"#{pane_start_command}",
|
|
452
|
+
].join("\t");
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const { stdout } = await runTmux(null, ["list-panes", "-a", "-F", format]);
|
|
456
|
+
return {
|
|
457
|
+
ok: true,
|
|
458
|
+
cli: "codex",
|
|
459
|
+
cwd: normalizeCwd(cwd),
|
|
460
|
+
sessions: parseCodexTmuxSessions(stdout, cwd),
|
|
461
|
+
};
|
|
462
|
+
} catch (error) {
|
|
463
|
+
return {
|
|
464
|
+
ok: false,
|
|
465
|
+
cli: "codex",
|
|
466
|
+
cwd: normalizeCwd(cwd),
|
|
467
|
+
reason: "tmux-unavailable",
|
|
468
|
+
error: error.message,
|
|
469
|
+
sessions: [],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
356
474
|
function sleep(ms) {
|
|
357
475
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
358
476
|
}
|
|
@@ -461,23 +579,9 @@ function isWarningLine(line) {
|
|
|
461
579
|
return /^\s*(?:\u26A0|\(warning\))/i.test(line);
|
|
462
580
|
}
|
|
463
581
|
|
|
464
|
-
function hasAssistantResponseLine(adapter, text) {
|
|
465
|
-
return String(text)
|
|
466
|
-
.split("\n")
|
|
467
|
-
.some((line) => adapter.isResponseLine(line));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
582
|
function promptLineIndex(lines, prompt) {
|
|
471
|
-
if (!prompt) {
|
|
472
|
-
return -1;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
583
|
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
476
|
-
|
|
477
|
-
if (
|
|
478
|
-
(hasCodexComposerPrompt(line) || hasClaudeComposerPrompt(line)) &&
|
|
479
|
-
line.includes(prompt)
|
|
480
|
-
) {
|
|
584
|
+
if (composerLineMatchesPrompt(lines[index], prompt)) {
|
|
481
585
|
return index;
|
|
482
586
|
}
|
|
483
587
|
}
|
|
@@ -489,6 +593,31 @@ function isComposerPromptLine(line) {
|
|
|
489
593
|
return hasCodexComposerPrompt(line) || hasClaudeComposerPrompt(line);
|
|
490
594
|
}
|
|
491
595
|
|
|
596
|
+
function promptComposerNeedle(prompt) {
|
|
597
|
+
const firstContentLine = String(prompt)
|
|
598
|
+
.split(/\r?\n/)
|
|
599
|
+
.map((line) => line.trim())
|
|
600
|
+
.find(Boolean);
|
|
601
|
+
return (firstContentLine ?? "").slice(0, PROMPT_COMPOSER_NEEDLE_LENGTH);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function composerLineMatchesPrompt(line, prompt) {
|
|
605
|
+
const needle = promptComposerNeedle(prompt);
|
|
606
|
+
return (
|
|
607
|
+
Boolean(needle) &&
|
|
608
|
+
isComposerPromptLine(line) &&
|
|
609
|
+
String(line).includes(needle)
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function composerShowsPrompt(text, prompt) {
|
|
614
|
+
const activeComposerLine = String(text)
|
|
615
|
+
.split("\n")
|
|
616
|
+
.filter(isComposerPromptLine)
|
|
617
|
+
.at(-1);
|
|
618
|
+
return composerLineMatchesPrompt(activeComposerLine, prompt);
|
|
619
|
+
}
|
|
620
|
+
|
|
492
621
|
function nextComposerPromptIndex(lines, startIndex) {
|
|
493
622
|
for (
|
|
494
623
|
let index = Math.max(0, startIndex + 1);
|
|
@@ -515,16 +644,6 @@ function responseLineIndexForPrompt(adapter, lines, promptIndex) {
|
|
|
515
644
|
return -1;
|
|
516
645
|
}
|
|
517
646
|
|
|
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
647
|
function extractAssistantResponse(adapter, text, prompt = null) {
|
|
529
648
|
const lines = String(text)
|
|
530
649
|
.split("\n")
|
|
@@ -563,6 +682,56 @@ function extractAssistantResponse(adapter, text, prompt = null) {
|
|
|
563
682
|
return response.join("\n").trim();
|
|
564
683
|
}
|
|
565
684
|
|
|
685
|
+
function extractResponseSinceMarker({ beforeRaw, raw, doneMarker, adapter }) {
|
|
686
|
+
if (!doneMarker) {
|
|
687
|
+
return "";
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const beforeLines = String(beforeRaw)
|
|
691
|
+
.split("\n")
|
|
692
|
+
.map((line) => line.replace(/\s+$/g, ""));
|
|
693
|
+
const rawLines = String(raw)
|
|
694
|
+
.split("\n")
|
|
695
|
+
.map((line) => line.replace(/\s+$/g, ""));
|
|
696
|
+
let sharedPrefixLength = 0;
|
|
697
|
+
while (
|
|
698
|
+
sharedPrefixLength < beforeLines.length &&
|
|
699
|
+
sharedPrefixLength < rawLines.length &&
|
|
700
|
+
beforeLines[sharedPrefixLength] === rawLines[sharedPrefixLength]
|
|
701
|
+
) {
|
|
702
|
+
sharedPrefixLength += 1;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const newLines =
|
|
706
|
+
sharedPrefixLength > 0 ? rawLines.slice(sharedPrefixLength) : rawLines;
|
|
707
|
+
const markerIndex = newLines.findIndex((line) => line.trim() === doneMarker);
|
|
708
|
+
if (markerIndex === -1) {
|
|
709
|
+
return "";
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const candidateLines = newLines.slice(0, markerIndex);
|
|
713
|
+
const responseStart = candidateLines.findIndex((line) =>
|
|
714
|
+
adapter.isResponseLine(line),
|
|
715
|
+
);
|
|
716
|
+
const responseLines =
|
|
717
|
+
responseStart === -1 ? candidateLines : candidateLines.slice(responseStart);
|
|
718
|
+
|
|
719
|
+
return responseLines
|
|
720
|
+
.filter(
|
|
721
|
+
(line) =>
|
|
722
|
+
!adapter.isChromeLine(line) &&
|
|
723
|
+
!isWarningLine(line) &&
|
|
724
|
+
!adapter.isStatusLine(line) &&
|
|
725
|
+
!hasCodexComposerPrompt(line) &&
|
|
726
|
+
!hasClaudeComposerPrompt(line),
|
|
727
|
+
)
|
|
728
|
+
.map((line) =>
|
|
729
|
+
adapter.isResponseLine(line) ? adapter.stripResponseMarker(line) : line,
|
|
730
|
+
)
|
|
731
|
+
.join("\n")
|
|
732
|
+
.trim();
|
|
733
|
+
}
|
|
734
|
+
|
|
566
735
|
function normalizeClaudeTaskText(text) {
|
|
567
736
|
return String(text)
|
|
568
737
|
.replace(/[.…]+/g, " ")
|
|
@@ -685,19 +854,35 @@ function hasClaudeCompletedTaskListResponse(text, options = {}) {
|
|
|
685
854
|
return extractClaudeCompletedTaskListResponse(text, options).length > 0;
|
|
686
855
|
}
|
|
687
856
|
|
|
688
|
-
function
|
|
857
|
+
function hasTmuxAssistantResponseAfterCurrentPrompt(
|
|
689
858
|
adapter,
|
|
690
859
|
text,
|
|
691
860
|
prompt,
|
|
692
861
|
beforeText,
|
|
693
862
|
) {
|
|
863
|
+
if (adapter.cli === "claude") {
|
|
864
|
+
return hasClaudeCompletedTaskListResponse(text, {
|
|
865
|
+
beforeText,
|
|
866
|
+
prompt,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const lines = String(text).split("\n");
|
|
871
|
+
const promptIndex = promptLineIndex(lines, prompt);
|
|
872
|
+
if (promptIndex === -1) {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const beforePromptCount = String(beforeText)
|
|
877
|
+
.split("\n")
|
|
878
|
+
.filter((line) => composerLineMatchesPrompt(line, prompt)).length;
|
|
879
|
+
const currentPromptCount = lines.filter((line) =>
|
|
880
|
+
composerLineMatchesPrompt(line, prompt),
|
|
881
|
+
).length;
|
|
882
|
+
|
|
694
883
|
return (
|
|
695
|
-
|
|
696
|
-
(adapter
|
|
697
|
-
hasClaudeCompletedTaskListResponse(text, {
|
|
698
|
-
beforeText,
|
|
699
|
-
prompt,
|
|
700
|
-
}))
|
|
884
|
+
currentPromptCount > beforePromptCount &&
|
|
885
|
+
responseLineIndexForPrompt(adapter, lines, promptIndex) !== -1
|
|
701
886
|
);
|
|
702
887
|
}
|
|
703
888
|
|
|
@@ -912,9 +1097,21 @@ const ADAPTERS = {
|
|
|
912
1097
|
codex: {
|
|
913
1098
|
cli: "codex",
|
|
914
1099
|
launchArgv: ["codex"],
|
|
915
|
-
launchKeys: ["codex", "Enter"],
|
|
916
|
-
resumeById: (id) =>
|
|
917
|
-
|
|
1100
|
+
launchKeys: ["codex --dangerously-bypass-approvals-and-sandbox", "Enter"],
|
|
1101
|
+
resumeById: (id) =>
|
|
1102
|
+
shellQuote([
|
|
1103
|
+
"codex",
|
|
1104
|
+
"resume",
|
|
1105
|
+
id,
|
|
1106
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
1107
|
+
]),
|
|
1108
|
+
resumeLast: () =>
|
|
1109
|
+
shellQuote([
|
|
1110
|
+
"codex",
|
|
1111
|
+
"resume",
|
|
1112
|
+
"--last",
|
|
1113
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
1114
|
+
]),
|
|
918
1115
|
composerGlyph: "›",
|
|
919
1116
|
isReady: isCodexReadyCapture,
|
|
920
1117
|
contextPct: parseCodexContextPct,
|
|
@@ -996,6 +1193,39 @@ const ADAPTERS = {
|
|
|
996
1193
|
},
|
|
997
1194
|
};
|
|
998
1195
|
|
|
1196
|
+
function buildLaunchKeys(adapter, { model, effort } = {}) {
|
|
1197
|
+
const hasModel = model !== undefined;
|
|
1198
|
+
const hasEffort = effort !== undefined;
|
|
1199
|
+
if (!hasModel && !hasEffort) {
|
|
1200
|
+
return adapter.launchKeys;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const overrideArgs = [];
|
|
1204
|
+
if (adapter.cli === "codex") {
|
|
1205
|
+
if (hasModel) {
|
|
1206
|
+
overrideArgs.push("-c", `model=${JSON.stringify(String(model))}`);
|
|
1207
|
+
}
|
|
1208
|
+
if (hasEffort) {
|
|
1209
|
+
overrideArgs.push(
|
|
1210
|
+
"-c",
|
|
1211
|
+
`model_reasoning_effort=${JSON.stringify(String(effort))}`,
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
} else if (adapter.cli === "claude") {
|
|
1215
|
+
if (hasModel) {
|
|
1216
|
+
overrideArgs.push("--model", model);
|
|
1217
|
+
}
|
|
1218
|
+
if (hasEffort) {
|
|
1219
|
+
overrideArgs.push("--effort", effort);
|
|
1220
|
+
}
|
|
1221
|
+
} else {
|
|
1222
|
+
throw new Error(`Unsupported adapter: ${adapter.cli}`);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const [command, ...remainingKeys] = adapter.launchKeys;
|
|
1226
|
+
return [`${command} ${shellQuote(overrideArgs)}`, ...remainingKeys];
|
|
1227
|
+
}
|
|
1228
|
+
|
|
999
1229
|
async function dismissStartupScreens(adapter, remote, session) {
|
|
1000
1230
|
let raw = "";
|
|
1001
1231
|
let dismissed = false;
|
|
@@ -1258,6 +1488,8 @@ async function doStart(adapter, opts) {
|
|
|
1258
1488
|
cwd,
|
|
1259
1489
|
resume,
|
|
1260
1490
|
resumeLast,
|
|
1491
|
+
model,
|
|
1492
|
+
effort,
|
|
1261
1493
|
readyTimeoutMs,
|
|
1262
1494
|
pollIntervalMs,
|
|
1263
1495
|
} = opts;
|
|
@@ -1265,7 +1497,7 @@ async function doStart(adapter, opts) {
|
|
|
1265
1497
|
? [adapter.resumeById(resume), "Enter"]
|
|
1266
1498
|
: resumeLast
|
|
1267
1499
|
? [adapter.resumeLast(), "Enter"]
|
|
1268
|
-
: adapter
|
|
1500
|
+
: buildLaunchKeys(adapter, { model, effort });
|
|
1269
1501
|
const resumed = Boolean(resume || resumeLast);
|
|
1270
1502
|
const resumeTarget = resume ?? (resumeLast ? "last" : null);
|
|
1271
1503
|
|
|
@@ -1276,6 +1508,16 @@ async function doStart(adapter, opts) {
|
|
|
1276
1508
|
session,
|
|
1277
1509
|
...(cwd ? ["-c", cwd] : []),
|
|
1278
1510
|
]);
|
|
1511
|
+
// Default tmux history-limit (2000) is far too small for long tool-heavy
|
|
1512
|
+
// CLI sessions (file reads, test runs) — the actual response can scroll
|
|
1513
|
+
// out of scrollback before capturePane() runs, producing an empty extract.
|
|
1514
|
+
await runTmux(remote, [
|
|
1515
|
+
"set-option",
|
|
1516
|
+
"-t",
|
|
1517
|
+
session,
|
|
1518
|
+
"history-limit",
|
|
1519
|
+
"100000",
|
|
1520
|
+
]);
|
|
1279
1521
|
await runTmux(remote, ["send-keys", "-t", session, ...launchKeys]);
|
|
1280
1522
|
await dismissStartupScreens(adapter, remote, session);
|
|
1281
1523
|
|
|
@@ -1298,13 +1540,49 @@ async function doStart(adapter, opts) {
|
|
|
1298
1540
|
}
|
|
1299
1541
|
|
|
1300
1542
|
async function doAskViaTmux(adapter, opts) {
|
|
1301
|
-
const {
|
|
1543
|
+
const {
|
|
1544
|
+
session,
|
|
1545
|
+
prompt,
|
|
1546
|
+
remote,
|
|
1547
|
+
timeoutMs,
|
|
1548
|
+
settleMs,
|
|
1549
|
+
pollIntervalMs,
|
|
1550
|
+
doneMarker,
|
|
1551
|
+
} = opts;
|
|
1302
1552
|
const beforeRaw = await capturePane(remote, session);
|
|
1303
1553
|
const contextPctBefore = adapter.contextPct(beforeRaw);
|
|
1304
1554
|
|
|
1305
|
-
|
|
1555
|
+
const bufferName = `tfx-live-prompt-${session}-${Date.now()}`;
|
|
1556
|
+
await runTmux(remote, ["set-buffer", "-b", bufferName, "--", prompt]);
|
|
1557
|
+
await runTmux(remote, [
|
|
1558
|
+
"paste-buffer",
|
|
1559
|
+
"-b",
|
|
1560
|
+
bufferName,
|
|
1561
|
+
"-d",
|
|
1562
|
+
"-t",
|
|
1563
|
+
session,
|
|
1564
|
+
]);
|
|
1306
1565
|
await sleep(settleMs);
|
|
1307
|
-
|
|
1566
|
+
|
|
1567
|
+
let promptSubmitted = !doneMarker;
|
|
1568
|
+
if (doneMarker) {
|
|
1569
|
+
for (let attempt = 1; attempt <= PROMPT_SUBMIT_MAX_ATTEMPTS; attempt += 1) {
|
|
1570
|
+
await runTmux(remote, ["send-keys", "-t", session, "Enter"]);
|
|
1571
|
+
await sleep(PROMPT_SUBMIT_RETRY_DELAY_MS);
|
|
1572
|
+
const submissionVisible = await captureVisible(remote, session);
|
|
1573
|
+
if (!composerShowsPrompt(submissionVisible, prompt)) {
|
|
1574
|
+
promptSubmitted = true;
|
|
1575
|
+
break;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
if (!promptSubmitted) {
|
|
1579
|
+
throw new Error(
|
|
1580
|
+
`Prompt did not submit after ${PROMPT_SUBMIT_MAX_ATTEMPTS} attempts`,
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
} else {
|
|
1584
|
+
await runTmux(remote, ["send-keys", "-t", session, "Enter"]);
|
|
1585
|
+
}
|
|
1308
1586
|
|
|
1309
1587
|
const startedAt = Date.now();
|
|
1310
1588
|
let raw = "";
|
|
@@ -1316,18 +1594,36 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
1316
1594
|
let done = false;
|
|
1317
1595
|
|
|
1318
1596
|
while (Date.now() - startedAt < timeoutMs) {
|
|
1319
|
-
visible =
|
|
1597
|
+
visible = doneMarker
|
|
1598
|
+
? await capturePane(remote, session)
|
|
1599
|
+
: await captureVisible(remote, session);
|
|
1320
1600
|
contextPctAfter = adapter.contextPct(visible);
|
|
1321
1601
|
|
|
1322
1602
|
quietPolls = visible === previousVisible ? quietPolls + 1 : 0;
|
|
1323
1603
|
previousVisible = visible;
|
|
1324
1604
|
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1605
|
+
const markerDone =
|
|
1606
|
+
promptSubmitted &&
|
|
1607
|
+
Boolean(
|
|
1608
|
+
extractResponseSinceMarker({
|
|
1609
|
+
beforeRaw,
|
|
1610
|
+
raw: visible,
|
|
1611
|
+
doneMarker,
|
|
1612
|
+
adapter,
|
|
1613
|
+
}),
|
|
1614
|
+
);
|
|
1615
|
+
const fallbackDone =
|
|
1616
|
+
!doneMarker &&
|
|
1327
1617
|
!adapter.isBusy(visible) &&
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1618
|
+
adapter.isReady(visible) &&
|
|
1619
|
+
quietPolls >= FALLBACK_QUIET_POLLS &&
|
|
1620
|
+
hasTmuxAssistantResponseAfterCurrentPrompt(
|
|
1621
|
+
adapter,
|
|
1622
|
+
await capturePane(remote, session),
|
|
1623
|
+
prompt,
|
|
1624
|
+
beforeRaw,
|
|
1625
|
+
);
|
|
1626
|
+
if (markerDone || fallbackDone) {
|
|
1331
1627
|
done = true;
|
|
1332
1628
|
break;
|
|
1333
1629
|
}
|
|
@@ -1336,7 +1632,20 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
1336
1632
|
}
|
|
1337
1633
|
|
|
1338
1634
|
raw = await capturePane(remote, session);
|
|
1339
|
-
|
|
1635
|
+
const markerResponse = promptSubmitted
|
|
1636
|
+
? extractResponseSinceMarker({
|
|
1637
|
+
beforeRaw,
|
|
1638
|
+
raw,
|
|
1639
|
+
doneMarker,
|
|
1640
|
+
adapter,
|
|
1641
|
+
})
|
|
1642
|
+
: "";
|
|
1643
|
+
if (markerResponse) {
|
|
1644
|
+
response = markerResponse;
|
|
1645
|
+
done = true;
|
|
1646
|
+
} else {
|
|
1647
|
+
response = extractAssistantResponse(adapter, raw, prompt);
|
|
1648
|
+
}
|
|
1340
1649
|
if (!response && adapter.cli === "claude") {
|
|
1341
1650
|
response = extractClaudeCompletedTaskListResponse(raw, {
|
|
1342
1651
|
beforeText: beforeRaw,
|
|
@@ -1767,6 +2076,8 @@ function startOpts(flags) {
|
|
|
1767
2076
|
cwd: flags.cwd,
|
|
1768
2077
|
resume: flags.resume,
|
|
1769
2078
|
resumeLast: Object.hasOwn(flags, "resume-last"),
|
|
2079
|
+
model: flags.model,
|
|
2080
|
+
effort: flags.effort,
|
|
1770
2081
|
readyTimeoutMs: secondsFlag(
|
|
1771
2082
|
flags,
|
|
1772
2083
|
"ready-timeout",
|
|
@@ -1887,11 +2198,25 @@ async function probe(flags) {
|
|
|
1887
2198
|
);
|
|
1888
2199
|
}
|
|
1889
2200
|
|
|
1890
|
-
function
|
|
2201
|
+
async function listSessions(flags) {
|
|
2202
|
+
const adapter = selectAdapter(flags);
|
|
2203
|
+
if (adapter.cli !== "codex") {
|
|
2204
|
+
throw new Error("list-sessions currently supports only --cli codex");
|
|
2205
|
+
}
|
|
2206
|
+
printJson(await discoverCodexTmuxSessions({ cwd: flags.cwd }));
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
function startOptsForSession(flags, session, side) {
|
|
1891
2210
|
return {
|
|
1892
2211
|
session,
|
|
1893
2212
|
remote: flags.remote,
|
|
1894
2213
|
cwd: flags.cwd,
|
|
2214
|
+
...(side
|
|
2215
|
+
? {
|
|
2216
|
+
model: flags[`model-${side}`],
|
|
2217
|
+
effort: flags[`effort-${side}`],
|
|
2218
|
+
}
|
|
2219
|
+
: {}),
|
|
1895
2220
|
readyTimeoutMs: secondsFlag(
|
|
1896
2221
|
flags,
|
|
1897
2222
|
"ready-timeout",
|
|
@@ -2054,17 +2379,279 @@ function peerMode(flags) {
|
|
|
2054
2379
|
}
|
|
2055
2380
|
|
|
2056
2381
|
function peerPrompt(mode, hopIndex, previous) {
|
|
2382
|
+
let prompt;
|
|
2057
2383
|
if (mode === "counting") {
|
|
2058
2384
|
if (hopIndex === 0) {
|
|
2059
|
-
|
|
2385
|
+
prompt = "Reply with only the integer 1.";
|
|
2386
|
+
} else {
|
|
2387
|
+
prompt = `The current integer is ${previous}. Add 1 to it and reply with ONLY the resulting integer, nothing else.`;
|
|
2388
|
+
}
|
|
2389
|
+
} else if (hopIndex === 0) {
|
|
2390
|
+
prompt = previous;
|
|
2391
|
+
} else {
|
|
2392
|
+
prompt = `The other party said: ${previous}\nReply briefly in 1-2 sentences and ask one short follow-up question.`;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
return [
|
|
2396
|
+
prompt,
|
|
2397
|
+
`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.`,
|
|
2398
|
+
].join("\n");
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
function buildPeerClosurePrompt(previous) {
|
|
2402
|
+
return [
|
|
2403
|
+
"This is the final closure turn. Based on the discussion so far, declare whether a final agreement was reached.",
|
|
2404
|
+
"First reply with exactly one JSON object and no markdown fences:",
|
|
2405
|
+
'{"agreement_status":"complete|partial","unresolved_questions":[],"needs_more_rounds":false,"summary":"short final agreement"}',
|
|
2406
|
+
"Use agreement_status=complete only when no unresolved question remains and no more rounds are needed.",
|
|
2407
|
+
`The other party's latest response was: ${previous ?? "(none)"}`,
|
|
2408
|
+
`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.`,
|
|
2409
|
+
].join("\n");
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
function isCompleteDeclaration(value) {
|
|
2413
|
+
return ["complete", "completed", "agreed", "합의완료", "완료"].includes(
|
|
2414
|
+
String(value ?? "")
|
|
2415
|
+
.trim()
|
|
2416
|
+
.toLowerCase(),
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
function unstructuredPeerClosure() {
|
|
2421
|
+
return {
|
|
2422
|
+
structured: false,
|
|
2423
|
+
declaredComplete: false,
|
|
2424
|
+
unresolvedQuestions: [],
|
|
2425
|
+
needsMoreRounds: null,
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
function parsePeerClosure(response) {
|
|
2430
|
+
const text = String(response ?? "").trim();
|
|
2431
|
+
const fenced = text.match(/```(?:json)?\s*({[\s\S]*})\s*```/i)?.[1];
|
|
2432
|
+
const firstBrace = text.indexOf("{");
|
|
2433
|
+
const lastBrace = text.lastIndexOf("}");
|
|
2434
|
+
const objectText =
|
|
2435
|
+
fenced ??
|
|
2436
|
+
(firstBrace >= 0 && lastBrace > firstBrace
|
|
2437
|
+
? text.slice(firstBrace, lastBrace + 1)
|
|
2438
|
+
: null);
|
|
2439
|
+
|
|
2440
|
+
if (objectText) {
|
|
2441
|
+
try {
|
|
2442
|
+
// Terminal word-wrap can inject a raw newline + indent mid-token in
|
|
2443
|
+
// compact JSON (e.g. "f\n alse" for "false") since there's often no
|
|
2444
|
+
// space nearby to wrap at. Try the raw text first, then retry with
|
|
2445
|
+
// wrap artifacts collapsed before giving up as unstructured.
|
|
2446
|
+
let parsed;
|
|
2447
|
+
try {
|
|
2448
|
+
parsed = JSON.parse(objectText);
|
|
2449
|
+
} catch {
|
|
2450
|
+
parsed = JSON.parse(objectText.replace(/\n[ \t]*/g, ""));
|
|
2451
|
+
}
|
|
2452
|
+
const unresolvedQuestions = parsed.unresolved_questions;
|
|
2453
|
+
const needsMoreRounds = parsed.needs_more_rounds;
|
|
2454
|
+
const structured =
|
|
2455
|
+
Object.hasOwn(parsed, "agreement_status") &&
|
|
2456
|
+
Array.isArray(unresolvedQuestions) &&
|
|
2457
|
+
typeof needsMoreRounds === "boolean";
|
|
2458
|
+
if (structured) {
|
|
2459
|
+
return {
|
|
2460
|
+
structured: true,
|
|
2461
|
+
declaredComplete: isCompleteDeclaration(parsed.agreement_status),
|
|
2462
|
+
unresolvedQuestions: unresolvedQuestions.map(String),
|
|
2463
|
+
needsMoreRounds,
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
return unstructuredPeerClosure();
|
|
2467
|
+
} catch {
|
|
2468
|
+
return unstructuredPeerClosure();
|
|
2060
2469
|
}
|
|
2061
|
-
return `The current integer is ${previous}. Add 1 to it and reply with ONLY the resulting integer, nothing else.`;
|
|
2062
2470
|
}
|
|
2063
2471
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2472
|
+
const status = text.match(
|
|
2473
|
+
/(?:agreement_status|agreement status|합의상태|합의 상태)\s*[:=]\s*["']?([^\n,"'}]+)/i,
|
|
2474
|
+
)?.[1];
|
|
2475
|
+
const unresolved = text.match(
|
|
2476
|
+
/(?:unresolved_questions|unresolved questions|미해결 쟁점)\s*[:=]\s*([^\n]+)/i,
|
|
2477
|
+
)?.[1];
|
|
2478
|
+
const needsMore = text.match(
|
|
2479
|
+
/(?:needs_more_rounds|needs more rounds|추가 라운드 필요)\s*[:=]\s*(true|false)/i,
|
|
2480
|
+
)?.[1];
|
|
2481
|
+
const structured =
|
|
2482
|
+
status !== undefined && unresolved !== undefined && needsMore !== undefined;
|
|
2483
|
+
const unresolvedQuestions = /^(?:\[\s*\]|none|없음)$/i.test(
|
|
2484
|
+
String(unresolved ?? "").trim(),
|
|
2485
|
+
)
|
|
2486
|
+
? []
|
|
2487
|
+
: String(unresolved ?? "")
|
|
2488
|
+
.replace(/^\[|\]$/g, "")
|
|
2489
|
+
.split(/[,;|]/)
|
|
2490
|
+
.map((item) => item.trim())
|
|
2491
|
+
.filter(Boolean);
|
|
2492
|
+
|
|
2493
|
+
return {
|
|
2494
|
+
structured,
|
|
2495
|
+
declaredComplete: isCompleteDeclaration(status),
|
|
2496
|
+
unresolvedQuestions,
|
|
2497
|
+
needsMoreRounds:
|
|
2498
|
+
needsMore === undefined ? null : needsMore.toLowerCase() === "true",
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function derivePeerStatus({ hopsCompleted, closure, exitReason = null }) {
|
|
2503
|
+
if (hopsCompleted < 1) return "failed";
|
|
2504
|
+
if (exitReason) return "partial";
|
|
2505
|
+
if (
|
|
2506
|
+
closure?.structured === true &&
|
|
2507
|
+
closure.declaredComplete === true &&
|
|
2508
|
+
closure.needsMoreRounds === false &&
|
|
2509
|
+
closure.unresolvedQuestions.length === 0
|
|
2510
|
+
) {
|
|
2511
|
+
return "complete";
|
|
2066
2512
|
}
|
|
2067
|
-
return
|
|
2513
|
+
return "partial";
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function classifyPeerExitReason(error) {
|
|
2517
|
+
const fingerprint = [error?.name, error?.code, error?.message]
|
|
2518
|
+
.filter(Boolean)
|
|
2519
|
+
.join(" ")
|
|
2520
|
+
.toLowerCase();
|
|
2521
|
+
return /timeout|timed out|etimedout|aborterror/.test(fingerprint)
|
|
2522
|
+
? "timeout"
|
|
2523
|
+
: "transport_error";
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
function peerSignalExit(signal) {
|
|
2527
|
+
return signal === "SIGTERM"
|
|
2528
|
+
? { exitReason: "terminated", exitCode: 143 }
|
|
2529
|
+
: { exitReason: "user_interrupt", exitCode: 130 };
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
function createPeerSignalController({
|
|
2533
|
+
output,
|
|
2534
|
+
persistTranscript,
|
|
2535
|
+
persistStatus,
|
|
2536
|
+
stopSessions,
|
|
2537
|
+
printOutput = printJson,
|
|
2538
|
+
exitProcess = (code) => process.exit(code),
|
|
2539
|
+
timeoutMs = 3000,
|
|
2540
|
+
now = Date.now,
|
|
2541
|
+
setTimer = setTimeout,
|
|
2542
|
+
clearTimer = clearTimeout,
|
|
2543
|
+
}) {
|
|
2544
|
+
let signalCount = 0;
|
|
2545
|
+
let settled = false;
|
|
2546
|
+
|
|
2547
|
+
const recordArtifactError = (kind, error) => {
|
|
2548
|
+
output.artifact_errors ??= [];
|
|
2549
|
+
output.artifact_errors.push(`${kind}: ${error.message}`);
|
|
2550
|
+
};
|
|
2551
|
+
|
|
2552
|
+
return {
|
|
2553
|
+
async handle(signal) {
|
|
2554
|
+
if (settled) return;
|
|
2555
|
+
signalCount += 1;
|
|
2556
|
+
const { exitReason, exitCode } = peerSignalExit(signal);
|
|
2557
|
+
if (signalCount > 1) {
|
|
2558
|
+
settled = true;
|
|
2559
|
+
exitProcess(exitCode);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
const startedAt = now();
|
|
2564
|
+
output.status = "aborted";
|
|
2565
|
+
output.exit_reason = exitReason;
|
|
2566
|
+
output.hops_completed = output.hops.length;
|
|
2567
|
+
|
|
2568
|
+
try {
|
|
2569
|
+
persistTranscript(output);
|
|
2570
|
+
} catch (error) {
|
|
2571
|
+
recordArtifactError("transcript", error);
|
|
2572
|
+
}
|
|
2573
|
+
try {
|
|
2574
|
+
persistStatus(output);
|
|
2575
|
+
} catch (error) {
|
|
2576
|
+
recordArtifactError("status", error);
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
const remainingMs = Math.max(0, timeoutMs - (now() - startedAt));
|
|
2580
|
+
let timer;
|
|
2581
|
+
const graceExpired = new Promise((resolve) => {
|
|
2582
|
+
timer = setTimer(() => resolve("timeout"), remainingMs);
|
|
2583
|
+
});
|
|
2584
|
+
await Promise.race([
|
|
2585
|
+
Promise.resolve()
|
|
2586
|
+
.then(stopSessions)
|
|
2587
|
+
.then(
|
|
2588
|
+
() => "stopped",
|
|
2589
|
+
() => "stop_error",
|
|
2590
|
+
),
|
|
2591
|
+
graceExpired,
|
|
2592
|
+
]);
|
|
2593
|
+
clearTimer(timer);
|
|
2594
|
+
|
|
2595
|
+
if (settled) return;
|
|
2596
|
+
settled = true;
|
|
2597
|
+
printOutput(output);
|
|
2598
|
+
exitProcess(exitCode);
|
|
2599
|
+
},
|
|
2600
|
+
dispose() {
|
|
2601
|
+
settled = true;
|
|
2602
|
+
},
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
function peerArtifactPaths(sessionA, sessionB) {
|
|
2607
|
+
const preferredDir =
|
|
2608
|
+
process.env.TFX_LIVE_ARTIFACT_DIR ??
|
|
2609
|
+
pathJoin(homedir(), ".claude", "cache", "triflux", "tfx-live", "peer-runs");
|
|
2610
|
+
const fallbackDir = pathJoin(tmpdir(), "triflux-live");
|
|
2611
|
+
let artifactDir = preferredDir;
|
|
2612
|
+
try {
|
|
2613
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
2614
|
+
} catch {
|
|
2615
|
+
artifactDir = fallbackDir;
|
|
2616
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
2617
|
+
}
|
|
2618
|
+
const safe = (value) =>
|
|
2619
|
+
String(value)
|
|
2620
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
2621
|
+
.slice(0, 48);
|
|
2622
|
+
const runId = `peer-${safe(sessionA)}-${safe(sessionB)}-${Date.now()}-${process.pid}`;
|
|
2623
|
+
return {
|
|
2624
|
+
transcriptPath: pathJoin(artifactDir, `${runId}.transcript.json`),
|
|
2625
|
+
statusPath: pathJoin(artifactDir, `${runId}.status.json`),
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
function persistPeerTranscript(paths, output) {
|
|
2630
|
+
writeFileSync(
|
|
2631
|
+
paths.transcriptPath,
|
|
2632
|
+
`${JSON.stringify(
|
|
2633
|
+
{
|
|
2634
|
+
mode: output.mode,
|
|
2635
|
+
rounds: output.rounds,
|
|
2636
|
+
cliA: output.cliA,
|
|
2637
|
+
cliB: output.cliB,
|
|
2638
|
+
hops_completed: output.hops.length,
|
|
2639
|
+
hops: output.hops,
|
|
2640
|
+
},
|
|
2641
|
+
null,
|
|
2642
|
+
2,
|
|
2643
|
+
)}\n`,
|
|
2644
|
+
"utf8",
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
function persistPeerStatus(paths, output) {
|
|
2649
|
+
const { hops: _hops, ...status } = output;
|
|
2650
|
+
writeFileSync(
|
|
2651
|
+
paths.statusPath,
|
|
2652
|
+
`${JSON.stringify(status, null, 2)}\n`,
|
|
2653
|
+
"utf8",
|
|
2654
|
+
);
|
|
2068
2655
|
}
|
|
2069
2656
|
|
|
2070
2657
|
function sideFlag(flags, side, name, fallback = undefined) {
|
|
@@ -2126,7 +2713,7 @@ function peerSideBaseOpts(flags, side, adapter, session) {
|
|
|
2126
2713
|
}
|
|
2127
2714
|
|
|
2128
2715
|
function askOptsFromPeerBase(base, prompt) {
|
|
2129
|
-
return { ...base, prompt };
|
|
2716
|
+
return { ...base, prompt, doneMarker: PEER_HOP_DONE_MARKER };
|
|
2130
2717
|
}
|
|
2131
2718
|
|
|
2132
2719
|
function shouldPrestartPeerSide(base) {
|
|
@@ -2161,20 +2748,51 @@ async function peer(flags) {
|
|
|
2161
2748
|
const hops = [];
|
|
2162
2749
|
const numbers = [];
|
|
2163
2750
|
const stopSpecs = [];
|
|
2751
|
+
const artifactPaths = peerArtifactPaths(sessionA, sessionB);
|
|
2752
|
+
const output = {
|
|
2753
|
+
mode,
|
|
2754
|
+
rounds,
|
|
2755
|
+
cliA: adapterA.cli,
|
|
2756
|
+
cliB: adapterB.cli,
|
|
2757
|
+
transportA: baseA.transport,
|
|
2758
|
+
transportB: baseB.transport,
|
|
2759
|
+
hops,
|
|
2760
|
+
transcript_path: artifactPaths.transcriptPath,
|
|
2761
|
+
status_path: artifactPaths.statusPath,
|
|
2762
|
+
};
|
|
2763
|
+
if (mode === "counting") {
|
|
2764
|
+
output.numbers = numbers;
|
|
2765
|
+
}
|
|
2164
2766
|
let previous =
|
|
2165
2767
|
mode === "freeform"
|
|
2166
2768
|
? (flags.seed ??
|
|
2167
2769
|
"Introduce yourself in one sentence, then ask the other party one short question.")
|
|
2168
2770
|
: null;
|
|
2169
2771
|
let primaryError = null;
|
|
2772
|
+
let stopError = null;
|
|
2773
|
+
let closure = null;
|
|
2774
|
+
const signalController = createPeerSignalController({
|
|
2775
|
+
output,
|
|
2776
|
+
persistTranscript: (value) => persistPeerTranscript(artifactPaths, value),
|
|
2777
|
+
persistStatus: (value) => persistPeerStatus(artifactPaths, value),
|
|
2778
|
+
stopSessions: () => stopAfterLifecycle(stopSpecs),
|
|
2779
|
+
});
|
|
2780
|
+
const onSigint = () => {
|
|
2781
|
+
void signalController.handle("SIGINT");
|
|
2782
|
+
};
|
|
2783
|
+
const onSigterm = () => {
|
|
2784
|
+
void signalController.handle("SIGTERM");
|
|
2785
|
+
};
|
|
2786
|
+
process.on("SIGINT", onSigint);
|
|
2787
|
+
process.on("SIGTERM", onSigterm);
|
|
2170
2788
|
|
|
2171
2789
|
try {
|
|
2172
2790
|
if (shouldPrestartPeerSide(baseA)) {
|
|
2173
|
-
await doStart(adapterA, startOptsForSession(flags, sessionA));
|
|
2791
|
+
await doStart(adapterA, startOptsForSession(flags, sessionA, "a"));
|
|
2174
2792
|
addStopSpecOnce(stopSpecs, adapterA, baseA);
|
|
2175
2793
|
}
|
|
2176
2794
|
if (shouldPrestartPeerSide(baseB)) {
|
|
2177
|
-
await doStart(adapterB, startOptsForSession(flags, sessionB));
|
|
2795
|
+
await doStart(adapterB, startOptsForSession(flags, sessionB, "b"));
|
|
2178
2796
|
addStopSpecOnce(stopSpecs, adapterB, baseB);
|
|
2179
2797
|
}
|
|
2180
2798
|
|
|
@@ -2186,11 +2804,26 @@ async function peer(flags) {
|
|
|
2186
2804
|
const isA = hopIndex % 2 === 0;
|
|
2187
2805
|
const adapter = isA ? adapterA : adapterB;
|
|
2188
2806
|
const base = isA ? baseA : baseB;
|
|
2189
|
-
const
|
|
2807
|
+
const isClosure = hopIndex === rounds * 2 - 1;
|
|
2808
|
+
const sent = isClosure
|
|
2809
|
+
? buildPeerClosurePrompt(previous)
|
|
2810
|
+
: peerPrompt(mode, hopIndex, previous);
|
|
2190
2811
|
const result = await doAsk(adapter, askOptsFromPeerBase(base, sent));
|
|
2191
2812
|
if (result.tmuxStartedOnDemand) {
|
|
2192
2813
|
addStopSpecOnce(stopSpecs, adapter, base);
|
|
2193
2814
|
}
|
|
2815
|
+
if (
|
|
2816
|
+
result.done !== true ||
|
|
2817
|
+
result.error ||
|
|
2818
|
+
!String(result.response ?? "").trim()
|
|
2819
|
+
) {
|
|
2820
|
+
throw new Error(
|
|
2821
|
+
(result.timedOut
|
|
2822
|
+
? `Peer hop timed out at hop ${hopIndex + 1}`
|
|
2823
|
+
: result.error) ??
|
|
2824
|
+
`Empty response from ${adapter.cli} at hop ${hopIndex + 1}`,
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2194
2827
|
|
|
2195
2828
|
hops.push({
|
|
2196
2829
|
hop: hopIndex + 1,
|
|
@@ -2205,40 +2838,70 @@ async function peer(flags) {
|
|
|
2205
2838
|
aborted: result.aborted === true ? true : undefined,
|
|
2206
2839
|
reason: result.reason,
|
|
2207
2840
|
});
|
|
2841
|
+
if (isClosure) {
|
|
2842
|
+
closure = parsePeerClosure(result.response);
|
|
2843
|
+
}
|
|
2208
2844
|
|
|
2209
2845
|
if (mode === "counting") {
|
|
2210
2846
|
const parsed = parseFirstInteger(result.response);
|
|
2211
|
-
if (parsed === null) {
|
|
2847
|
+
if (parsed === null && !isClosure) {
|
|
2212
2848
|
throw new Error(
|
|
2213
2849
|
`Could not parse integer from ${adapter.cli} response at hop ${hopIndex + 1}`,
|
|
2214
2850
|
);
|
|
2215
2851
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2852
|
+
if (parsed !== null) {
|
|
2853
|
+
numbers.push(parsed);
|
|
2854
|
+
previous = parsed;
|
|
2855
|
+
} else {
|
|
2856
|
+
previous = result.response;
|
|
2857
|
+
}
|
|
2218
2858
|
} else {
|
|
2219
2859
|
previous = result.response;
|
|
2220
2860
|
}
|
|
2221
2861
|
}
|
|
2222
2862
|
} catch (error) {
|
|
2223
2863
|
primaryError = error;
|
|
2224
|
-
} finally {
|
|
2225
|
-
await stopAfterLifecycle(stopSpecs, primaryError);
|
|
2226
2864
|
}
|
|
2227
2865
|
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2866
|
+
try {
|
|
2867
|
+
await stopAfterLifecycle(stopSpecs);
|
|
2868
|
+
} catch (error) {
|
|
2869
|
+
stopError = error;
|
|
2870
|
+
}
|
|
2871
|
+
signalController.dispose();
|
|
2872
|
+
process.off("SIGINT", onSigint);
|
|
2873
|
+
process.off("SIGTERM", onSigterm);
|
|
2874
|
+
|
|
2875
|
+
const terminalError = primaryError ?? stopError;
|
|
2876
|
+
const exitReason = terminalError
|
|
2877
|
+
? classifyPeerExitReason(terminalError)
|
|
2878
|
+
: null;
|
|
2879
|
+
output.hops_completed = hops.length;
|
|
2880
|
+
output.closure = closure;
|
|
2881
|
+
output.status = derivePeerStatus({
|
|
2882
|
+
hopsCompleted: hops.length,
|
|
2883
|
+
closure,
|
|
2884
|
+
exitReason,
|
|
2885
|
+
});
|
|
2886
|
+
if (exitReason) {
|
|
2887
|
+
output.exit_reason = exitReason;
|
|
2888
|
+
output.error = terminalError.message;
|
|
2889
|
+
}
|
|
2890
|
+
output.stoppedA = stopError === null;
|
|
2891
|
+
output.stoppedB = stopError === null;
|
|
2892
|
+
|
|
2893
|
+
try {
|
|
2894
|
+
persistPeerTranscript(artifactPaths, output);
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
output.artifact_errors ??= [];
|
|
2897
|
+
output.artifact_errors.push(`transcript: ${error.message}`);
|
|
2898
|
+
}
|
|
2899
|
+
try {
|
|
2900
|
+
persistPeerStatus(artifactPaths, output);
|
|
2901
|
+
} catch (error) {
|
|
2902
|
+
output.artifact_errors ??= [];
|
|
2903
|
+
output.artifact_errors.push(`status: ${error.message}`);
|
|
2239
2904
|
}
|
|
2240
|
-
output.stoppedA = true;
|
|
2241
|
-
output.stoppedB = true;
|
|
2242
2905
|
printJson(output);
|
|
2243
2906
|
}
|
|
2244
2907
|
|
|
@@ -2344,6 +3007,8 @@ async function main() {
|
|
|
2344
3007
|
await interrupt(flags);
|
|
2345
3008
|
} else if (command === "probe") {
|
|
2346
3009
|
await probe(flags);
|
|
3010
|
+
} else if (command === "list-sessions") {
|
|
3011
|
+
await listSessions(flags);
|
|
2347
3012
|
} else if (command === "converse") {
|
|
2348
3013
|
await converse(flags);
|
|
2349
3014
|
} else if (command === "goal-driven") {
|
|
@@ -2361,12 +3026,22 @@ async function main() {
|
|
|
2361
3026
|
|
|
2362
3027
|
export {
|
|
2363
3028
|
ADAPTERS,
|
|
3029
|
+
buildLaunchKeys,
|
|
3030
|
+
buildPeerClosurePrompt,
|
|
2364
3031
|
buildRemoteLiveCommand,
|
|
2365
3032
|
callRemoteLive,
|
|
3033
|
+
classifyPeerExitReason,
|
|
3034
|
+
createPeerSignalController,
|
|
2366
3035
|
ctoHygieneNotify,
|
|
3036
|
+
derivePeerStatus,
|
|
3037
|
+
discoverCodexTmuxSessions,
|
|
3038
|
+
doAskViaTmux,
|
|
2367
3039
|
extractAssistantResponse,
|
|
2368
3040
|
extractClaudeCompletedTaskListResponse,
|
|
3041
|
+
extractResponseSinceMarker,
|
|
2369
3042
|
hasClaudeCompletedTaskListResponse,
|
|
3043
|
+
parseCodexTmuxSessions,
|
|
3044
|
+
parsePeerClosure,
|
|
2370
3045
|
parseRemoteLiveJson,
|
|
2371
3046
|
resolveAskTransport,
|
|
2372
3047
|
};
|