triflux 10.61.2 → 10.63.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/.claude/rules/tfx-routing.md +14 -9
- package/README.ko.md +11 -9
- package/README.md +11 -8
- package/adapters/codex/skills/tfx-live/SKILL.md +30 -5
- package/bin/tfx-live.mjs +591 -83
- package/bin/triflux.mjs +9 -2
- package/hub/team/claude-transcript.mjs +27 -0
- package/hub/team/retry-state-machine.mjs +16 -24
- package/hub/team/session-context.mjs +13 -2
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +40 -40
- package/scripts/lib/codex-profile-config.mjs +44 -33
- package/scripts/lib/gemini-profiles.mjs +112 -126
- package/scripts/lib/model-family-resolver.mjs +335 -0
- package/scripts/setup.mjs +21 -47
- package/scripts/tfx-route.sh +29 -98
- package/skills/tfx-auto/SKILL.md +1 -1
- package/skills/tfx-lead/SKILL.md +5 -5
- package/skills/tfx-live/SKILL.md +30 -5
- package/skills/tfx-setup/SKILL.md +1 -1
package/bin/tfx-live.mjs
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
5
11
|
import {
|
|
6
12
|
chmod,
|
|
7
13
|
mkdir,
|
|
@@ -53,6 +59,7 @@ import {
|
|
|
53
59
|
modelContext,
|
|
54
60
|
readCodexContext,
|
|
55
61
|
} from "../hub/team/session-context.mjs";
|
|
62
|
+
import { JsonRpcWsUdsClient } from "../hub/workers/lib/jsonrpc-ws-uds.mjs";
|
|
56
63
|
|
|
57
64
|
const execFileAsync = promisify(execFile);
|
|
58
65
|
|
|
@@ -101,7 +108,10 @@ function usage(command) {
|
|
|
101
108
|
" 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]",
|
|
102
109
|
" tfx-live ask --transport uds|auto (--short SHORT | --session-id ID) --prompt TEXT [--config-dir DIR] [--bridge ABS] [--session NAME (auto fallback)] [--timeout 60]",
|
|
103
110
|
" ask options: --no-wait --no-relay-tag --warn-context-pct N --max-context-pct N (0 disables; Claude 60/90, Codex 15/22).",
|
|
104
|
-
" tfx-live compact --
|
|
111
|
+
" tfx-live compact --session NAME [--cli claude|codex] [--instructions TEXT] [--then PROMPT] [--no-wait] [--if-busy fail|wait] [--timeout 60]",
|
|
112
|
+
" tfx-live goal set --session NAME [--cli claude|codex] --condition TEXT [--budget N]",
|
|
113
|
+
" tfx-live goal status --session NAME [--cli claude|codex]",
|
|
114
|
+
" tfx-live goal clear --session NAME [--cli claude|codex]",
|
|
105
115
|
" tfx-live wait --cli claude (--short SHORT | --session-id ID | --session NAME[:WINDOW.PANE]) [--request-id ID] [--config-dir DIR] [--bridge ABS] [--timeout 60] [--poll-interval 1500]",
|
|
106
116
|
" tfx-live wait --cli codex (--session NAME[:WINDOW.PANE] | --thread UUID) --request-id ID [--timeout 60] [--poll-interval 1500]",
|
|
107
117
|
" tfx-live wait --cli agy --session NAME[:WINDOW.PANE] [--request-id ID] [--timeout 60] [--poll-interval 1500]",
|
|
@@ -141,6 +151,7 @@ function parseCli(argv) {
|
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
const flags = {};
|
|
154
|
+
const action = command === "goal" ? rest.shift() : undefined;
|
|
144
155
|
for (let index = 0; index < rest.length; index += 1) {
|
|
145
156
|
const arg = rest[index];
|
|
146
157
|
if (!arg.startsWith("--")) {
|
|
@@ -165,7 +176,7 @@ function parseCli(argv) {
|
|
|
165
176
|
index += 1;
|
|
166
177
|
}
|
|
167
178
|
|
|
168
|
-
return { command, flags };
|
|
179
|
+
return { command, flags, action };
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
function requireFlag(flags, name) {
|
|
@@ -2930,7 +2941,10 @@ async function renameAgyConversation(session, name, timeoutMs, pollIntervalMs) {
|
|
|
2930
2941
|
return false;
|
|
2931
2942
|
}
|
|
2932
2943
|
|
|
2933
|
-
async function waitForTmuxIdle(adapter, opts) {
|
|
2944
|
+
async function waitForTmuxIdle(adapter, opts, deps = {}) {
|
|
2945
|
+
const capture = deps.captureVisible ?? captureVisible;
|
|
2946
|
+
const tmux = deps.runTmux ?? runTmux;
|
|
2947
|
+
const pause = deps.sleep ?? sleep;
|
|
2934
2948
|
const { session, remote, pollIntervalMs, busyTimeoutMs, ifBusy } = opts;
|
|
2935
2949
|
if (
|
|
2936
2950
|
![
|
|
@@ -2947,7 +2961,7 @@ async function waitForTmuxIdle(adapter, opts) {
|
|
|
2947
2961
|
const startedAt = Date.now();
|
|
2948
2962
|
let interrupted = false;
|
|
2949
2963
|
while (true) {
|
|
2950
|
-
const visible = await
|
|
2964
|
+
const visible = await capture(remote, session);
|
|
2951
2965
|
const elapsedMs = Date.now() - startedAt;
|
|
2952
2966
|
const busy =
|
|
2953
2967
|
adapter.isBusy(visible) ||
|
|
@@ -2960,16 +2974,20 @@ async function waitForTmuxIdle(adapter, opts) {
|
|
|
2960
2974
|
);
|
|
2961
2975
|
}
|
|
2962
2976
|
if (ifBusy === "interrupt" && !interrupted) {
|
|
2963
|
-
await
|
|
2977
|
+
await tmux(remote, ["send-keys", "-t", session, "Escape"]);
|
|
2964
2978
|
interrupted = true;
|
|
2965
2979
|
}
|
|
2966
|
-
await
|
|
2980
|
+
await pause(
|
|
2967
2981
|
Math.min(pollIntervalMs, Math.max(1, busyTimeoutMs - elapsedMs)),
|
|
2968
2982
|
);
|
|
2969
2983
|
}
|
|
2970
2984
|
}
|
|
2971
2985
|
|
|
2972
|
-
async function doAskViaTmux(adapter, opts) {
|
|
2986
|
+
async function doAskViaTmux(adapter, opts, deps = {}) {
|
|
2987
|
+
const tmux = deps.runTmux ?? runTmux;
|
|
2988
|
+
const capture = deps.captureVisible ?? captureVisible;
|
|
2989
|
+
const captureHistory = deps.capturePane ?? capturePane;
|
|
2990
|
+
const pause = deps.sleep ?? sleep;
|
|
2973
2991
|
const {
|
|
2974
2992
|
session,
|
|
2975
2993
|
prompt,
|
|
@@ -2982,17 +3000,22 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
2982
3000
|
busyTimeoutMs = timeoutMs,
|
|
2983
3001
|
} = opts;
|
|
2984
3002
|
const slashCommand = adapter.cli === "agy" && isSlashCommand(prompt);
|
|
2985
|
-
const busyWaitedMs =
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
3003
|
+
const busyWaitedMs = opts.skipBusyCheck
|
|
3004
|
+
? 0
|
|
3005
|
+
: await waitForTmuxIdle(
|
|
3006
|
+
adapter,
|
|
3007
|
+
{
|
|
3008
|
+
session,
|
|
3009
|
+
remote,
|
|
3010
|
+
pollIntervalMs,
|
|
3011
|
+
busyTimeoutMs,
|
|
3012
|
+
ifBusy: slashCommand ? "queue" : ifBusy,
|
|
3013
|
+
},
|
|
3014
|
+
deps,
|
|
3015
|
+
);
|
|
2992
3016
|
const queued =
|
|
2993
|
-
adapter.cli === "agy" &&
|
|
2994
|
-
|
|
2995
|
-
const beforeRaw = await capturePane(remote, session);
|
|
3017
|
+
adapter.cli === "agy" && adapter.isBusy(await capture(remote, session));
|
|
3018
|
+
const beforeRaw = await captureHistory(remote, session);
|
|
2996
3019
|
const contextPctBefore = adapter.contextPct(beforeRaw);
|
|
2997
3020
|
const context =
|
|
2998
3021
|
adapter.cli === "agy"
|
|
@@ -3015,8 +3038,8 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
3015
3038
|
};
|
|
3016
3039
|
|
|
3017
3040
|
const bufferName = tmuxBufferName(session);
|
|
3018
|
-
await
|
|
3019
|
-
await
|
|
3041
|
+
await tmux(remote, ["set-buffer", "-b", bufferName, "--", prompt]);
|
|
3042
|
+
await tmux(remote, [
|
|
3020
3043
|
"paste-buffer",
|
|
3021
3044
|
"-b",
|
|
3022
3045
|
bufferName,
|
|
@@ -3025,31 +3048,42 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
3025
3048
|
"-t",
|
|
3026
3049
|
session,
|
|
3027
3050
|
]);
|
|
3028
|
-
await
|
|
3051
|
+
await pause(settleMs);
|
|
3029
3052
|
|
|
3030
|
-
let promptSubmitted =
|
|
3053
|
+
let promptSubmitted =
|
|
3054
|
+
adapter.cli === "agy" || opts.confirmSubmission ? false : !doneMarker;
|
|
3031
3055
|
if (doneMarker || opts.noWait || adapter.cli === "agy") {
|
|
3032
|
-
for (
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3056
|
+
for (
|
|
3057
|
+
let attempt = 1;
|
|
3058
|
+
attempt <= (opts.submitAttempts ?? PROMPT_SUBMIT_MAX_ATTEMPTS);
|
|
3059
|
+
attempt += 1
|
|
3060
|
+
) {
|
|
3061
|
+
await tmux(remote, ["send-keys", "-t", session, "Enter"]);
|
|
3062
|
+
await pause(PROMPT_SUBMIT_RETRY_DELAY_MS);
|
|
3063
|
+
const submissionVisible = await capture(remote, session);
|
|
3064
|
+
if (
|
|
3065
|
+
opts.confirmSubmission
|
|
3066
|
+
? await opts.confirmSubmission(submissionVisible)
|
|
3067
|
+
: !composerShowsPrompt(submissionVisible, prompt, adapter.cli)
|
|
3068
|
+
) {
|
|
3037
3069
|
promptSubmitted = true;
|
|
3038
3070
|
break;
|
|
3039
3071
|
}
|
|
3040
3072
|
}
|
|
3041
3073
|
if (!promptSubmitted) {
|
|
3042
3074
|
throw new Error(
|
|
3043
|
-
|
|
3075
|
+
opts.confirmSubmission
|
|
3076
|
+
? "compact submission could not be confirmed (it may still have started, or /compact may remain in the composer); --then was not sent"
|
|
3077
|
+
: `Prompt did not submit after ${PROMPT_SUBMIT_MAX_ATTEMPTS} attempts`,
|
|
3044
3078
|
);
|
|
3045
3079
|
}
|
|
3046
3080
|
} else {
|
|
3047
|
-
await
|
|
3081
|
+
await tmux(remote, ["send-keys", "-t", session, "Enter"]);
|
|
3048
3082
|
}
|
|
3049
3083
|
|
|
3050
3084
|
let nameApplied;
|
|
3051
3085
|
if (adapter.cli === "agy" && !slashCommand) {
|
|
3052
|
-
const { stdout } = await
|
|
3086
|
+
const { stdout } = await tmux(null, [
|
|
3053
3087
|
"show-options",
|
|
3054
3088
|
"-qv",
|
|
3055
3089
|
"-t",
|
|
@@ -3082,7 +3116,7 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
3082
3116
|
pollIntervalMs,
|
|
3083
3117
|
);
|
|
3084
3118
|
if (nameApplied)
|
|
3085
|
-
await
|
|
3119
|
+
await tmux(null, [
|
|
3086
3120
|
"set-option",
|
|
3087
3121
|
"-u",
|
|
3088
3122
|
"-t",
|
|
@@ -3091,7 +3125,7 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
3091
3125
|
]);
|
|
3092
3126
|
break;
|
|
3093
3127
|
}
|
|
3094
|
-
await
|
|
3128
|
+
await pause(Math.min(pollIntervalMs, 200));
|
|
3095
3129
|
}
|
|
3096
3130
|
} catch {
|
|
3097
3131
|
nameApplied = false;
|
|
@@ -3149,8 +3183,8 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
3149
3183
|
|
|
3150
3184
|
while (Date.now() - startedAt < timeoutMs) {
|
|
3151
3185
|
visible = doneMarker
|
|
3152
|
-
? await
|
|
3153
|
-
: await
|
|
3186
|
+
? await captureHistory(remote, session)
|
|
3187
|
+
: await capture(remote, session);
|
|
3154
3188
|
contextPctAfter = adapter.contextPct(visible);
|
|
3155
3189
|
|
|
3156
3190
|
quietPolls = visible === previousVisible ? quietPolls + 1 : 0;
|
|
@@ -3173,7 +3207,7 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
3173
3207
|
quietPolls >= FALLBACK_QUIET_POLLS &&
|
|
3174
3208
|
hasTmuxAssistantResponseAfterCurrentPrompt(
|
|
3175
3209
|
adapter,
|
|
3176
|
-
await
|
|
3210
|
+
await captureHistory(remote, session),
|
|
3177
3211
|
prompt,
|
|
3178
3212
|
beforeRaw,
|
|
3179
3213
|
);
|
|
@@ -3182,10 +3216,10 @@ async function doAskViaTmux(adapter, opts) {
|
|
|
3182
3216
|
break;
|
|
3183
3217
|
}
|
|
3184
3218
|
|
|
3185
|
-
await
|
|
3219
|
+
await pause(pollIntervalMs);
|
|
3186
3220
|
}
|
|
3187
3221
|
|
|
3188
|
-
raw = await
|
|
3222
|
+
raw = await captureHistory(remote, session);
|
|
3189
3223
|
const markerResponse = promptSubmitted
|
|
3190
3224
|
? extractResponseSinceMarker({
|
|
3191
3225
|
beforeRaw,
|
|
@@ -3233,10 +3267,8 @@ async function doAsk(adapter, opts) {
|
|
|
3233
3267
|
const requestId = opts.requestId ?? randomBytes(6).toString("hex");
|
|
3234
3268
|
const tag = `[tfx-live req=${requestId}]`;
|
|
3235
3269
|
// Codex 는 접힌 발신자 표시가 없어 첫 줄 머리말로 보낸 세션을 드러낸다.
|
|
3236
|
-
// 슬래시 명령은 첫 글자가 / 여야 실행되므로 표식을 붙이지 않는다.
|
|
3237
|
-
const bare =
|
|
3238
|
-
opts.noRelayTag ||
|
|
3239
|
-
(["codex", "agy"].includes(adapter.cli) && isSlashCommand(opts.prompt));
|
|
3270
|
+
// 슬래시 명령은 CLI 와 상관없이 첫 글자가 / 여야 실행되므로 표식을 붙이지 않는다.
|
|
3271
|
+
const bare = opts.noRelayTag || isSlashCommand(opts.prompt);
|
|
3240
3272
|
const from =
|
|
3241
3273
|
adapter.cli === "codex" && !bare
|
|
3242
3274
|
? (opts.from ?? (await resolveSenderName()))
|
|
@@ -3248,7 +3280,15 @@ async function doAsk(adapter, opts) {
|
|
|
3248
3280
|
: `${header}\n${opts.prompt}`;
|
|
3249
3281
|
try {
|
|
3250
3282
|
return {
|
|
3251
|
-
...(await dispatchAsk(adapter, {
|
|
3283
|
+
...(await dispatchAsk(adapter, {
|
|
3284
|
+
...opts,
|
|
3285
|
+
prompt,
|
|
3286
|
+
requestId,
|
|
3287
|
+
noRelayTag: bare,
|
|
3288
|
+
noWait:
|
|
3289
|
+
opts.noWait ||
|
|
3290
|
+
(adapter.cli === "claude" && CLAUDE_SILENT_SLASH.test(opts.prompt)),
|
|
3291
|
+
})),
|
|
3252
3292
|
requestId,
|
|
3253
3293
|
...(opts.transportReason
|
|
3254
3294
|
? { transportReason: opts.transportReason }
|
|
@@ -3395,11 +3435,15 @@ async function resolveCodexTmuxThread(session, deps = {}) {
|
|
|
3395
3435
|
|
|
3396
3436
|
async function doAskViaQueue(adapter, opts, deps = {}) {
|
|
3397
3437
|
// queue 를 못 쓰면 tmux 붙여넣기로 보내고 그 이유를 결과에 남긴다.
|
|
3398
|
-
const fallback = async (reason) =>
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3438
|
+
const fallback = async (reason) => {
|
|
3439
|
+
if (opts.queueOnly)
|
|
3440
|
+
throw new Error(`compact --then requires Codex queue: ${reason}`);
|
|
3441
|
+
return {
|
|
3442
|
+
...(await doAskViaTmux(adapter, { ...opts, transport: "tmux" })),
|
|
3443
|
+
transportRequested: "queue",
|
|
3444
|
+
fallbackReason: reason,
|
|
3445
|
+
};
|
|
3446
|
+
};
|
|
3403
3447
|
if (opts.remote) return fallback("remote-host");
|
|
3404
3448
|
const thread = opts.threadId
|
|
3405
3449
|
? { threadId: opts.threadId, threadSource: "flag" }
|
|
@@ -3980,9 +4024,15 @@ function transportFlag(flags, adapter, short, sessionId) {
|
|
|
3980
4024
|
|
|
3981
4025
|
// 절대 경로로 시작하는 프롬프트는 슬래시 명령이 아니다.
|
|
3982
4026
|
function isSlashCommand(prompt) {
|
|
3983
|
-
|
|
4027
|
+
const word = String(prompt ?? "").match(/^\/[a-z][\w-]*(?=\s|$)/i)?.[0];
|
|
4028
|
+
// `/tmp 를 확인해` 처럼 이 기기에 실제로 있는 경로로 시작하는 문장은 명령이 아니다.
|
|
4029
|
+
return Boolean(word) && !existsSync(word);
|
|
3984
4030
|
}
|
|
3985
4031
|
|
|
4032
|
+
// 응답 턴을 만들지 않는 Claude 내장 명령. 스킬 명령은 응답을 만드므로 기다린다.
|
|
4033
|
+
const CLAUDE_SILENT_SLASH =
|
|
4034
|
+
/^\/(?:rename|clear|compact|model|effort|color)(?:\s|$)/i;
|
|
4035
|
+
|
|
3986
4036
|
function askOpts(flags, adapter) {
|
|
3987
4037
|
const short = flags.short;
|
|
3988
4038
|
const sessionId = flags["session-id"];
|
|
@@ -4351,70 +4401,521 @@ async function tmuxContext(adapter, opts) {
|
|
|
4351
4401
|
}
|
|
4352
4402
|
}
|
|
4353
4403
|
|
|
4354
|
-
async function
|
|
4355
|
-
|
|
4404
|
+
async function doCompact(flags, deps = {}) {
|
|
4405
|
+
const cli = flags.cli ?? "claude";
|
|
4406
|
+
if (cli === "agy")
|
|
4356
4407
|
throw new Error("agy has no compact; use /clear or a handoff");
|
|
4357
4408
|
if (
|
|
4358
|
-
|
|
4409
|
+
!["claude", "codex"].includes(cli) ||
|
|
4359
4410
|
flags.remote ||
|
|
4360
4411
|
flags.short ||
|
|
4361
4412
|
flags["session-id"] ||
|
|
4362
4413
|
(flags.transport && flags.transport !== "tmux")
|
|
4363
4414
|
)
|
|
4364
4415
|
throw new Error(
|
|
4365
|
-
"compact supports local --cli claude --session NAME only
|
|
4416
|
+
"compact supports local --cli claude|codex --session NAME only",
|
|
4366
4417
|
);
|
|
4418
|
+
if (cli === "codex" && flags.instructions)
|
|
4419
|
+
throw new Error(
|
|
4420
|
+
"Codex /compact does not accept --instructions; use --then for follow-up instructions",
|
|
4421
|
+
);
|
|
4422
|
+
const thenPrompt = flags.then?.trim();
|
|
4423
|
+
if (flags.then !== undefined && !thenPrompt)
|
|
4424
|
+
throw new Error("--then must not be blank");
|
|
4425
|
+
if (isSlashCommand(thenPrompt))
|
|
4426
|
+
throw new Error("--then accepts text instructions, not slash commands");
|
|
4367
4427
|
const ifBusy = flags["if-busy"] ?? "fail";
|
|
4368
4428
|
if (!["fail", "wait"].includes(ifBusy))
|
|
4369
4429
|
throw new Error("compact --if-busy must be fail or wait");
|
|
4430
|
+
const adapter = ADAPTERS[cli];
|
|
4370
4431
|
const opts = askOpts(
|
|
4371
4432
|
{
|
|
4372
4433
|
...flags,
|
|
4373
|
-
cli
|
|
4434
|
+
cli,
|
|
4374
4435
|
transport: "tmux",
|
|
4375
4436
|
"if-busy": ifBusy,
|
|
4376
4437
|
"no-wait": true,
|
|
4377
4438
|
prompt: `/compact${flags.instructions ? ` ${flags.instructions}` : ""}`,
|
|
4378
4439
|
},
|
|
4379
|
-
|
|
4440
|
+
adapter,
|
|
4380
4441
|
);
|
|
4381
|
-
|
|
4382
|
-
const
|
|
4383
|
-
|
|
4384
|
-
|
|
4442
|
+
const pause = deps.sleep ?? sleep;
|
|
4443
|
+
const capture = deps.captureVisible ?? captureVisible;
|
|
4444
|
+
await waitForTmuxIdle(adapter, opts, deps);
|
|
4445
|
+
let transcriptPath;
|
|
4446
|
+
let threadId;
|
|
4447
|
+
if (cli === "claude") {
|
|
4448
|
+
transcriptPath = await (deps.claudeTmuxTranscript ?? claudeTmuxTranscript)(
|
|
4449
|
+
opts.session,
|
|
4450
|
+
opts.configDir,
|
|
4451
|
+
);
|
|
4452
|
+
} else {
|
|
4453
|
+
const thread = await (
|
|
4454
|
+
deps.resolveCodexTmuxThread ?? resolveCodexTmuxThread
|
|
4455
|
+
)(opts.session, deps);
|
|
4456
|
+
threadId = thread.threadId;
|
|
4457
|
+
transcriptPath = await (deps.findCodexRollout ?? findCodexRollout)(
|
|
4458
|
+
threadId,
|
|
4459
|
+
);
|
|
4460
|
+
}
|
|
4461
|
+
const readSnapshot = () =>
|
|
4462
|
+
cli === "claude"
|
|
4463
|
+
? readClaudeTranscript(transcriptPath)
|
|
4464
|
+
: readCodexContext(transcriptPath, threadId);
|
|
4465
|
+
const before = transcriptPath && (await readSnapshot());
|
|
4466
|
+
if (!before || !Number.isInteger(before.compactCount))
|
|
4467
|
+
throw new Error(
|
|
4468
|
+
`compact requires a discoverable ${cli === "claude" ? "Claude transcript" : "Codex rollout"} for this tmux pane`,
|
|
4469
|
+
);
|
|
4470
|
+
let snapshot = before;
|
|
4471
|
+
const done = () => snapshot?.compactCount > before.compactCount;
|
|
4472
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
4473
|
+
const confirmSubmission = async (visible) => {
|
|
4474
|
+
const submissionDeadline = Math.min(
|
|
4475
|
+
deadline,
|
|
4476
|
+
Date.now() + (cli === "codex" ? 1000 : 5000),
|
|
4477
|
+
);
|
|
4478
|
+
do {
|
|
4479
|
+
snapshot = await readSnapshot();
|
|
4480
|
+
if (
|
|
4481
|
+
done() ||
|
|
4482
|
+
(cli === "claude"
|
|
4483
|
+
? /Compacting conversation/i.test(stripAnsi(visible))
|
|
4484
|
+
: /Compacting context/i.test(stripAnsi(visible)))
|
|
4485
|
+
)
|
|
4486
|
+
return true;
|
|
4487
|
+
await pause(
|
|
4488
|
+
Math.min(
|
|
4489
|
+
opts.pollIntervalMs,
|
|
4490
|
+
Math.max(0, submissionDeadline - Date.now()),
|
|
4491
|
+
),
|
|
4492
|
+
);
|
|
4493
|
+
visible = await capture(null, opts.session);
|
|
4494
|
+
} while (Date.now() < submissionDeadline);
|
|
4495
|
+
return false;
|
|
4496
|
+
};
|
|
4497
|
+
await doAskViaTmux(
|
|
4498
|
+
adapter,
|
|
4499
|
+
{
|
|
4500
|
+
...opts,
|
|
4501
|
+
skipBusyCheck: true,
|
|
4502
|
+
skipContextGuard: true,
|
|
4503
|
+
submitAttempts: cli === "codex" ? 2 : 1,
|
|
4504
|
+
confirmSubmission,
|
|
4505
|
+
},
|
|
4506
|
+
deps,
|
|
4385
4507
|
);
|
|
4386
|
-
const
|
|
4387
|
-
|
|
4508
|
+
const result = {
|
|
4509
|
+
cli,
|
|
4510
|
+
session: opts.session,
|
|
4511
|
+
compact: { submitted: true, done: done() },
|
|
4512
|
+
compacted: done(),
|
|
4513
|
+
};
|
|
4514
|
+
if (thenPrompt) {
|
|
4515
|
+
const requestId = randomBytes(6).toString("hex");
|
|
4516
|
+
const thenOpts = {
|
|
4517
|
+
...opts,
|
|
4518
|
+
prompt: `[tfx-live req=${requestId}]\n${thenPrompt}`,
|
|
4519
|
+
requestId,
|
|
4520
|
+
threadId,
|
|
4521
|
+
noWait: true,
|
|
4522
|
+
settleMs: 0,
|
|
4523
|
+
transport: cli === "codex" ? "queue" : "tmux",
|
|
4524
|
+
skipBusyCheck: true,
|
|
4525
|
+
skipContextGuard: true,
|
|
4526
|
+
queueOnly: true,
|
|
4527
|
+
};
|
|
4528
|
+
try {
|
|
4529
|
+
if (cli === "codex") {
|
|
4530
|
+
const queued = await doAskViaQueue(adapter, thenOpts, deps);
|
|
4531
|
+
// biome-ignore lint/suspicious/noThenProperty: CLI 계약의 then은 함수가 아닌 큐 결과다.
|
|
4532
|
+
result.then = {
|
|
4533
|
+
...queued,
|
|
4534
|
+
requestId,
|
|
4535
|
+
queued: queued.inputSent === true,
|
|
4536
|
+
};
|
|
4537
|
+
if (!result.then.queued) {
|
|
4538
|
+
result.ok = false;
|
|
4539
|
+
result.error =
|
|
4540
|
+
queued.status === "unknown"
|
|
4541
|
+
? `${queued.error}; --then may already be queued, use wait --request-id and do not resend`
|
|
4542
|
+
: queued.error;
|
|
4543
|
+
}
|
|
4544
|
+
} else {
|
|
4545
|
+
await doAskViaTmux(adapter, thenOpts, deps);
|
|
4546
|
+
const queueDeadline = Date.now() + Math.min(opts.timeoutMs, 5000);
|
|
4547
|
+
let confirmedBy;
|
|
4548
|
+
do {
|
|
4549
|
+
const transcript = await readClaudeTranscript(transcriptPath, {
|
|
4550
|
+
requestId,
|
|
4551
|
+
});
|
|
4552
|
+
if (transcript?.enqueued) confirmedBy = "enqueue";
|
|
4553
|
+
else if (transcript?.userSeen) confirmedBy = "user";
|
|
4554
|
+
else if (
|
|
4555
|
+
/queued messages/i.test(
|
|
4556
|
+
stripAnsi(await capture(null, opts.session)),
|
|
4557
|
+
)
|
|
4558
|
+
)
|
|
4559
|
+
confirmedBy = "tui";
|
|
4560
|
+
if (confirmedBy) break;
|
|
4561
|
+
await pause(
|
|
4562
|
+
Math.min(
|
|
4563
|
+
opts.pollIntervalMs,
|
|
4564
|
+
Math.max(0, queueDeadline - Date.now()),
|
|
4565
|
+
),
|
|
4566
|
+
);
|
|
4567
|
+
} while (Date.now() < queueDeadline);
|
|
4568
|
+
// biome-ignore lint/suspicious/noThenProperty: CLI 계약의 then은 함수가 아닌 큐 결과다.
|
|
4569
|
+
result.then = {
|
|
4570
|
+
requestId,
|
|
4571
|
+
queued: Boolean(confirmedBy) && confirmedBy !== "user",
|
|
4572
|
+
transport: "tmux",
|
|
4573
|
+
...(confirmedBy === "user" ? { delivered: true } : {}),
|
|
4574
|
+
...(confirmedBy ? { confirmedBy } : {}),
|
|
4575
|
+
};
|
|
4576
|
+
if (!confirmedBy) {
|
|
4577
|
+
result.ok = false;
|
|
4578
|
+
result.error =
|
|
4579
|
+
"Claude --then was sent but enqueue could not be confirmed; use wait --request-id, do not resend";
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
} catch (error) {
|
|
4583
|
+
// compact 는 이미 제출됐으므로 그 정보를 지우지 않고 then 실패만 결과에 담는다.
|
|
4584
|
+
// biome-ignore lint/suspicious/noThenProperty: CLI 계약의 then은 함수가 아닌 큐 결과다.
|
|
4585
|
+
result.then = { requestId, queued: false, error: error.message };
|
|
4586
|
+
result.ok = false;
|
|
4587
|
+
result.error = `compact was submitted but --then failed: ${error.message}`;
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
if (!Object.hasOwn(flags, "no-wait")) {
|
|
4591
|
+
do {
|
|
4592
|
+
snapshot = await readSnapshot();
|
|
4593
|
+
if (done()) break;
|
|
4594
|
+
await pause(
|
|
4595
|
+
Math.min(opts.pollIntervalMs, Math.max(0, deadline - Date.now())),
|
|
4596
|
+
);
|
|
4597
|
+
} while (Date.now() < deadline);
|
|
4598
|
+
if (!done()) result.timedOut = true;
|
|
4599
|
+
}
|
|
4600
|
+
result.compacted = result.compact.done = done();
|
|
4601
|
+
if (done()) {
|
|
4602
|
+
Object.assign(result, snapshot.compact);
|
|
4603
|
+
Object.assign(result.compact, snapshot.compact);
|
|
4604
|
+
}
|
|
4605
|
+
return result;
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
function codexGoalStatus(goal) {
|
|
4609
|
+
return {
|
|
4610
|
+
state: goal
|
|
4611
|
+
? ({ usageLimited: "usage_limited", budgetLimited: "budget_limited" }[
|
|
4612
|
+
goal.status
|
|
4613
|
+
] ?? goal.status)
|
|
4614
|
+
: "none",
|
|
4615
|
+
objective: goal?.objective ?? null,
|
|
4616
|
+
judgedBy: "model",
|
|
4617
|
+
...(goal
|
|
4618
|
+
? {
|
|
4619
|
+
tokensUsed: goal.tokensUsed,
|
|
4620
|
+
tokenBudget: goal.tokenBudget ?? null,
|
|
4621
|
+
updatedAt: new Date(goal.updatedAt * 1000).toISOString(),
|
|
4622
|
+
}
|
|
4623
|
+
: {}),
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
async function readCodexGoal(threadId) {
|
|
4628
|
+
let db;
|
|
4629
|
+
try {
|
|
4630
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
4631
|
+
if (
|
|
4632
|
+
major < 22 ||
|
|
4633
|
+
(major === 22 && minor < 13) ||
|
|
4634
|
+
(major === 23 && minor < 4)
|
|
4635
|
+
)
|
|
4636
|
+
throw new Error("Node 22.13+ with read-only node:sqlite is required");
|
|
4637
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
4638
|
+
db = new DatabaseSync(
|
|
4639
|
+
pathJoin(
|
|
4640
|
+
process.env.CODEX_HOME ?? pathJoin(homedir(), ".codex"),
|
|
4641
|
+
"goals_1.sqlite",
|
|
4642
|
+
),
|
|
4643
|
+
{ readOnly: true },
|
|
4644
|
+
);
|
|
4645
|
+
const row = db
|
|
4646
|
+
.prepare(
|
|
4647
|
+
"SELECT objective, status, token_budget AS tokenBudget, tokens_used AS tokensUsed, updated_at_ms / 1000.0 AS updatedAt FROM thread_goals WHERE thread_id = ?",
|
|
4648
|
+
)
|
|
4649
|
+
.get(threadId);
|
|
4650
|
+
return codexGoalStatus(row);
|
|
4651
|
+
} catch (error) {
|
|
4652
|
+
// Node >=22 전체에서 sqlite 를 지원하지는 않으므로 미확인을 none 으로 보고하지 않는다.
|
|
4653
|
+
return {
|
|
4654
|
+
state: null,
|
|
4655
|
+
objective: null,
|
|
4656
|
+
judgedBy: "model",
|
|
4657
|
+
warning: `Goal status unavailable (read-only node:sqlite): ${error.message}`,
|
|
4658
|
+
};
|
|
4659
|
+
} finally {
|
|
4660
|
+
db?.close();
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
async function codexGoal(action, opts, deps = {}) {
|
|
4665
|
+
const { threadId, objective, tokenBudget } = opts;
|
|
4666
|
+
let fallbackReason;
|
|
4667
|
+
let client;
|
|
4668
|
+
try {
|
|
4669
|
+
const socketPath = (deps.resolveSocket ?? resolveCodexDaemonSocket)(
|
|
4670
|
+
opts.codexSocket,
|
|
4671
|
+
);
|
|
4672
|
+
client = (
|
|
4673
|
+
deps.clientFactory ?? ((options) => new JsonRpcWsUdsClient(options))
|
|
4674
|
+
)({ socketPath, connectTimeoutMs: 5000 });
|
|
4675
|
+
await client.connect();
|
|
4676
|
+
await client.request(
|
|
4677
|
+
"initialize",
|
|
4678
|
+
{
|
|
4679
|
+
clientInfo: { name: "triflux-tfx-live", version: "1.0.0" },
|
|
4680
|
+
capabilities: { experimentalApi: true },
|
|
4681
|
+
},
|
|
4682
|
+
5000,
|
|
4683
|
+
);
|
|
4684
|
+
client.notify("initialized", {});
|
|
4685
|
+
let cursor;
|
|
4686
|
+
let loaded = false;
|
|
4687
|
+
do {
|
|
4688
|
+
const page = await client.request(
|
|
4689
|
+
"thread/loaded/list",
|
|
4690
|
+
cursor ? { cursor } : {},
|
|
4691
|
+
5000,
|
|
4692
|
+
);
|
|
4693
|
+
loaded = page.data.includes(threadId);
|
|
4694
|
+
cursor = page.nextCursor;
|
|
4695
|
+
} while (!loaded && cursor);
|
|
4696
|
+
if (!loaded) fallbackReason = "thread-not-loaded";
|
|
4697
|
+
} catch (error) {
|
|
4698
|
+
fallbackReason = `daemon-unavailable: ${error.message}`;
|
|
4699
|
+
}
|
|
4700
|
+
try {
|
|
4701
|
+
if (fallbackReason) {
|
|
4702
|
+
if (tokenBudget !== undefined)
|
|
4703
|
+
throw new Error(
|
|
4704
|
+
`--budget requires a loaded Codex daemon thread; tmux fallback cannot set a budget (${fallbackReason})`,
|
|
4705
|
+
);
|
|
4706
|
+
return {
|
|
4707
|
+
...(action === "status"
|
|
4708
|
+
? await (deps.readGoal ?? readCodexGoal)(threadId)
|
|
4709
|
+
: {}),
|
|
4710
|
+
transport: action === "status" ? "sqlite" : "tmux",
|
|
4711
|
+
fallbackReason,
|
|
4712
|
+
};
|
|
4713
|
+
}
|
|
4714
|
+
const params = { threadId, origin: "user" };
|
|
4715
|
+
// 쓰기를 시도한 뒤의 오류는 폴백하지 않는다. 성공 여부가 불명확한 명령을 중복 전송할 수 있다.
|
|
4716
|
+
if (action !== "status") {
|
|
4717
|
+
const cleared = await client.request("thread/goal/clear", params, 10000);
|
|
4718
|
+
if (action === "clear")
|
|
4719
|
+
return {
|
|
4720
|
+
...codexGoalStatus(null),
|
|
4721
|
+
...cleared,
|
|
4722
|
+
confirmed: true,
|
|
4723
|
+
transport: "daemon",
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
4726
|
+
const result =
|
|
4727
|
+
action === "set"
|
|
4728
|
+
? await client
|
|
4729
|
+
.request(
|
|
4730
|
+
"thread/goal/set",
|
|
4731
|
+
{
|
|
4732
|
+
...params,
|
|
4733
|
+
objective,
|
|
4734
|
+
status: "active",
|
|
4735
|
+
...(tokenBudget !== undefined ? { tokenBudget } : {}),
|
|
4736
|
+
},
|
|
4737
|
+
10000,
|
|
4738
|
+
)
|
|
4739
|
+
.catch((error) => {
|
|
4740
|
+
throw new Error(
|
|
4741
|
+
`${error.message}; clear was applied and there is currently no goal`,
|
|
4742
|
+
);
|
|
4743
|
+
})
|
|
4744
|
+
: await client.request("thread/goal/get", { threadId }, 10000);
|
|
4745
|
+
return {
|
|
4746
|
+
...codexGoalStatus(result.goal),
|
|
4747
|
+
...(action === "set" ? { confirmed: true } : {}),
|
|
4748
|
+
transport: "daemon",
|
|
4749
|
+
};
|
|
4750
|
+
} finally {
|
|
4751
|
+
client?.close();
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
async function goal(action, flags) {
|
|
4756
|
+
if (!["set", "status", "clear"].includes(action))
|
|
4757
|
+
throw new Error("goal requires set, status, or clear");
|
|
4758
|
+
if (flags.remote !== undefined)
|
|
4759
|
+
throw new Error("goal does not support --remote");
|
|
4760
|
+
const session = splitTmuxTarget(requireFlag(flags, "session")).target;
|
|
4761
|
+
let cli = flags.cli;
|
|
4762
|
+
if (!cli) {
|
|
4763
|
+
for (const candidate of ["claude", "codex", "agy"]) {
|
|
4764
|
+
if (await paneCliProcess(session, candidate)) {
|
|
4765
|
+
cli = candidate;
|
|
4766
|
+
break;
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
if (!cli)
|
|
4770
|
+
throw new Error("Cannot infer session CLI; specify --cli claude|codex");
|
|
4771
|
+
}
|
|
4772
|
+
if (cli === "agy") throw new Error("agy has no native goal");
|
|
4773
|
+
const adapter = selectAdapter({ ...flags, cli });
|
|
4774
|
+
if (flags.budget !== undefined && (cli !== "codex" || action !== "set"))
|
|
4775
|
+
throw new Error("--budget is supported only by goal set --cli codex");
|
|
4776
|
+
const tokenBudget = integerFlag(flags, "budget", undefined);
|
|
4777
|
+
if (tokenBudget !== undefined && !Number.isSafeInteger(tokenBudget))
|
|
4778
|
+
throw new Error("--budget must be a safe positive integer");
|
|
4779
|
+
const objective =
|
|
4780
|
+
action === "set"
|
|
4781
|
+
? requireFlag(flags, "condition").replace(/\s+/gu, " ").trim()
|
|
4782
|
+
: null;
|
|
4783
|
+
if (
|
|
4784
|
+
action === "set" &&
|
|
4785
|
+
(!objective || /^(clear|pause|resume|edit)(?: |$)/i.test(objective))
|
|
4786
|
+
)
|
|
4388
4787
|
throw new Error(
|
|
4389
|
-
"
|
|
4788
|
+
"--condition must not be blank or start with a reserved command (clear, pause, resume, edit)",
|
|
4390
4789
|
);
|
|
4391
|
-
|
|
4790
|
+
const base = { cli, session };
|
|
4791
|
+
let codexResult;
|
|
4792
|
+
let threadId;
|
|
4793
|
+
if (cli === "codex") {
|
|
4794
|
+
({ threadId } = await codexThreadFromFlags(flags));
|
|
4795
|
+
codexResult = await codexGoal(action, {
|
|
4796
|
+
threadId,
|
|
4797
|
+
objective,
|
|
4798
|
+
tokenBudget,
|
|
4799
|
+
codexSocket: flags["codex-socket"],
|
|
4800
|
+
});
|
|
4801
|
+
if (codexResult.transport !== "tmux") return { ...base, ...codexResult };
|
|
4802
|
+
}
|
|
4803
|
+
const transcriptPath =
|
|
4804
|
+
cli === "claude"
|
|
4805
|
+
? await claudeTmuxTranscript(session, flags["config-dir"])
|
|
4806
|
+
: null;
|
|
4807
|
+
let snapshot =
|
|
4808
|
+
cli === "claude" ? await readClaudeTranscript(transcriptPath) : null;
|
|
4809
|
+
const unavailable = {
|
|
4810
|
+
state: null,
|
|
4811
|
+
objective: null,
|
|
4812
|
+
judgedBy: "evaluator",
|
|
4813
|
+
warning: "Claude transcript unavailable for this tmux pane",
|
|
4814
|
+
};
|
|
4815
|
+
if (action === "status")
|
|
4816
|
+
return {
|
|
4817
|
+
...base,
|
|
4818
|
+
...(snapshot?.goal ?? unavailable),
|
|
4819
|
+
transport: "tmux",
|
|
4820
|
+
};
|
|
4821
|
+
// 달성 뒤 자동 해제됐거나 없던 goal 은 /goal clear 가 기록을 남기지 않아 확인할 수 없다.
|
|
4822
|
+
if (action === "clear" && snapshot?.goal && snapshot.goal.state !== "active")
|
|
4823
|
+
return {
|
|
4824
|
+
...base,
|
|
4825
|
+
...snapshot.goal,
|
|
4826
|
+
transport: "tmux",
|
|
4827
|
+
confirmed: true,
|
|
4828
|
+
alreadyCleared: true,
|
|
4829
|
+
};
|
|
4830
|
+
const opts = askOpts(
|
|
4831
|
+
{
|
|
4832
|
+
...flags,
|
|
4833
|
+
cli,
|
|
4834
|
+
"if-busy":
|
|
4835
|
+
flags["if-busy"] ?? (action === "clear" ? "interrupt" : "wait"),
|
|
4836
|
+
transport: "tmux",
|
|
4837
|
+
"no-wait": true,
|
|
4838
|
+
"no-relay-tag": true,
|
|
4839
|
+
prompt: `/goal ${action === "set" ? objective : "clear"}`,
|
|
4840
|
+
},
|
|
4841
|
+
adapter,
|
|
4842
|
+
);
|
|
4843
|
+
let interrupted = false;
|
|
4844
|
+
if (action === "clear" && opts.ifBusy === "interrupt") {
|
|
4845
|
+
const visible = await captureVisible(null, session);
|
|
4846
|
+
// Claude 반복 턴은 화면에 esc 안내가 없어도 active goal 을 계속 실행할 수 있다.
|
|
4847
|
+
if (
|
|
4848
|
+
snapshot?.goal?.state === "active" ||
|
|
4849
|
+
adapter.isBusy(visible) ||
|
|
4850
|
+
(cli === "codex" && isCodexLoadingCapture(visible))
|
|
4851
|
+
) {
|
|
4852
|
+
await doInterruptViaTmux(adapter, opts);
|
|
4853
|
+
await sleep(opts.pollIntervalMs);
|
|
4854
|
+
interrupted = true;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
await waitForTmuxIdle(adapter, opts);
|
|
4858
|
+
// Esc 가 직전 입력을 복원해도 clear 명령 앞에 붙지 않게 비운다.
|
|
4859
|
+
if (interrupted) await runTmux(null, ["send-keys", "-t", session, "C-u"]);
|
|
4860
|
+
snapshot =
|
|
4861
|
+
cli === "claude" ? await readClaudeTranscript(transcriptPath) : null;
|
|
4862
|
+
const beforeCount = snapshot?.goalSentinel?.count ?? 0;
|
|
4863
|
+
const inputStartedAt = Date.now();
|
|
4864
|
+
await doAskViaTmux(adapter, {
|
|
4392
4865
|
...opts,
|
|
4393
4866
|
ifBusy: "fail",
|
|
4394
4867
|
skipContextGuard: true,
|
|
4395
4868
|
});
|
|
4396
|
-
const deadline = Date.now() + opts.timeoutMs;
|
|
4869
|
+
const deadline = Date.now() + Math.min(opts.timeoutMs, 15_000);
|
|
4870
|
+
let confirmed = false;
|
|
4871
|
+
if (cli === "codex") {
|
|
4872
|
+
do {
|
|
4873
|
+
snapshot = await readCodexGoal(threadId);
|
|
4874
|
+
confirmed =
|
|
4875
|
+
action === "set"
|
|
4876
|
+
? snapshot.state === "active" &&
|
|
4877
|
+
snapshot.objective === objective &&
|
|
4878
|
+
Date.parse(snapshot.updatedAt) >= inputStartedAt
|
|
4879
|
+
: snapshot.state === "none";
|
|
4880
|
+
if (confirmed || snapshot.state === null || Date.now() >= deadline) break;
|
|
4881
|
+
await sleep(Math.min(opts.pollIntervalMs, deadline - Date.now()));
|
|
4882
|
+
} while (Date.now() < deadline);
|
|
4883
|
+
// 입력은 이미 들어갔으므로 오류로 끝내면 재전송을 부른다. 미확인으로만 알린다.
|
|
4884
|
+
return {
|
|
4885
|
+
...base,
|
|
4886
|
+
...codexResult,
|
|
4887
|
+
...snapshot,
|
|
4888
|
+
inputSent: true,
|
|
4889
|
+
confirmed,
|
|
4890
|
+
...(!confirmed && snapshot.state !== null
|
|
4891
|
+
? { warning: "No matching Codex goal observed after input" }
|
|
4892
|
+
: {}),
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
// 갓 띄운 세션은 첫 입력 뒤에야 자기 transcript 가 생기므로 매번 경로를 다시 찾는다.
|
|
4397
4896
|
do {
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
printJson({
|
|
4401
|
-
cli: "claude",
|
|
4402
|
-
session: opts.session,
|
|
4403
|
-
compacted: true,
|
|
4404
|
-
...snapshot.compact,
|
|
4405
|
-
});
|
|
4406
|
-
return;
|
|
4407
|
-
}
|
|
4408
|
-
await sleep(
|
|
4409
|
-
Math.min(opts.pollIntervalMs, Math.max(0, deadline - Date.now())),
|
|
4897
|
+
snapshot = await readClaudeTranscript(
|
|
4898
|
+
await claudeTmuxTranscript(session, flags["config-dir"]),
|
|
4410
4899
|
);
|
|
4900
|
+
const sentinel = snapshot?.goalSentinel;
|
|
4901
|
+
confirmed =
|
|
4902
|
+
sentinel?.count > beforeCount &&
|
|
4903
|
+
Date.parse(sentinel.updatedAt) >= inputStartedAt &&
|
|
4904
|
+
(action === "set"
|
|
4905
|
+
? sentinel.met === false && sentinel.condition === objective
|
|
4906
|
+
: sentinel.met === true);
|
|
4907
|
+
if (confirmed || Date.now() >= deadline) break;
|
|
4908
|
+
await sleep(Math.min(opts.pollIntervalMs, deadline - Date.now()));
|
|
4411
4909
|
} while (Date.now() < deadline);
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4910
|
+
return {
|
|
4911
|
+
...base,
|
|
4912
|
+
...(snapshot?.goal ?? unavailable),
|
|
4913
|
+
transport: "tmux",
|
|
4914
|
+
confirmed,
|
|
4915
|
+
...(!confirmed
|
|
4916
|
+
? { warning: "No new matching goal_status sentinel observed after input" }
|
|
4917
|
+
: {}),
|
|
4918
|
+
};
|
|
4418
4919
|
}
|
|
4419
4920
|
|
|
4420
4921
|
async function interrupt(flags) {
|
|
@@ -5343,7 +5844,7 @@ function printJson(value) {
|
|
|
5343
5844
|
}
|
|
5344
5845
|
|
|
5345
5846
|
async function main() {
|
|
5346
|
-
const { command, flags } = parseCli(process.argv.slice(2));
|
|
5847
|
+
const { command, flags, action } = parseCli(process.argv.slice(2));
|
|
5347
5848
|
|
|
5348
5849
|
if (command === "help") {
|
|
5349
5850
|
process.stdout.write(`${usage(flags.subcommand)}\n`);
|
|
@@ -5359,7 +5860,11 @@ async function main() {
|
|
|
5359
5860
|
} else if (command === "queue") {
|
|
5360
5861
|
await queue(flags);
|
|
5361
5862
|
} else if (command === "compact") {
|
|
5362
|
-
await
|
|
5863
|
+
const result = await doCompact(flags);
|
|
5864
|
+
printJson(result);
|
|
5865
|
+
if (result.ok === false) process.exitCode = 1;
|
|
5866
|
+
} else if (command === "goal") {
|
|
5867
|
+
printJson(await goal(action, flags));
|
|
5363
5868
|
} else if (command === "rename") {
|
|
5364
5869
|
await rename(flags);
|
|
5365
5870
|
} else if (command === "stop") {
|
|
@@ -5392,12 +5897,14 @@ export {
|
|
|
5392
5897
|
buildTmuxCommand,
|
|
5393
5898
|
callRemoteLive,
|
|
5394
5899
|
classifyPeerExitReason,
|
|
5900
|
+
codexGoal,
|
|
5395
5901
|
composerShowsPrompt,
|
|
5396
5902
|
createPeerSignalController,
|
|
5397
5903
|
derivePeerStatus,
|
|
5398
5904
|
discoverClaudeTmuxSessions,
|
|
5399
5905
|
discoverCodexTmuxSessions,
|
|
5400
5906
|
doAskViaTmux,
|
|
5907
|
+
doCompact,
|
|
5401
5908
|
extractAssistantResponse,
|
|
5402
5909
|
extractClaudeCompletedTaskListResponse,
|
|
5403
5910
|
extractResponseSinceMarker,
|
|
@@ -5405,6 +5912,7 @@ export {
|
|
|
5405
5912
|
inspectCodexTmuxPane,
|
|
5406
5913
|
inspectUnsetEnv,
|
|
5407
5914
|
isCodexLoadingCapture,
|
|
5915
|
+
isSlashCommand,
|
|
5408
5916
|
parseCodexTmuxSessions,
|
|
5409
5917
|
parsePeerClosure,
|
|
5410
5918
|
parseRemoteLiveJson,
|