triflux 10.61.2 → 10.62.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 +19 -2
- package/bin/tfx-live.mjs +349 -9
- package/bin/triflux.mjs +9 -2
- package/hub/team/claude-transcript.mjs +26 -0
- package/hub/team/retry-state-machine.mjs +16 -24
- package/hub/team/session-context.mjs +1 -1
- 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 +19 -2
- package/skills/tfx-setup/SKILL.md +1 -1
|
@@ -62,7 +62,7 @@ owner availability를 실제로 검출한 경우에만 `owner unavailable → tf
|
|
|
62
62
|
| 분석 | 분석해, 어떻게 돌아가?, 구조가 뭐야 | tfx-auto (`--mode consensus --shape panel`) |
|
|
63
63
|
| 계획 | 계획, 어떻게 하지, 설계해 | superpowers `writing-plans`; TFX 다중모델 계획·실행은 `tfx-auto --mode deep` |
|
|
64
64
|
| 요구사항 명확화 | 요구사항, 인터뷰, 목표 구체화 | host `deep-interview`; 목표 변환은 Claude Code 기본 `/goal` |
|
|
65
|
-
| Codex 프로필 관리 | 프로필, 모델, effort 변경 | `~/.
|
|
65
|
+
| Codex 프로필 관리 | 프로필, 모델, effort 변경 | `~/.config/triflux/models.json` 가족별 고정 또는 자동 해석 |
|
|
66
66
|
| 검색 | 찾아, 어디있어, 파일 찾아 | Claude 기본 Explore 에이전트 |
|
|
67
67
|
| 리서치 (빠른) | 검색해줘, 찾아봐, 공식문서, 이거 뭐야 | tfx-research |
|
|
68
68
|
| 리서치 (자율) | 자율 리서치, 검색하고 정리해, research and plan | tfx-research (Auto 모드) |
|
|
@@ -102,23 +102,28 @@ owner availability를 실제로 검출한 경우에만 `owner unavailable → tf
|
|
|
102
102
|
|
|
103
103
|
### 모델 배치
|
|
104
104
|
|
|
105
|
-
>
|
|
105
|
+
> 근거: [ADR-0037](../../docs/adr/0037-model-family-aliases.md). 모델 목록의 같은 가족에서 사용할 수 있는 최신 버전을 실행 때 해석한다.
|
|
106
106
|
|
|
107
|
-
Codex 역할별 프로필의 SSOT는 `scripts/lib/agent-route-policy.mjs`이고,
|
|
107
|
+
Codex 역할별 프로필의 SSOT는 `scripts/lib/agent-route-policy.mjs`이고, `scripts/lib/model-family-resolver.mjs`가 관리 파일 `~/.codex/<프로필>.config.toml`의 모델 ID를 갱신한다. `designer`, `writer` 항목은 직접 Codex를 지정한 경우에만 적용하며 일반 실행의 provider는 `hub/team/agent-map.json`이 정한다.
|
|
108
108
|
|
|
109
109
|
| 프로필 | triflux lane |
|
|
110
110
|
|---------|--------------|
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
111
|
+
| `astra_xhigh` | architect, planner, critic, analyst, debugger, deep-executor, security-reviewer, scientist-deep, designer(직접 지정) |
|
|
112
|
+
| `astra_max` / `astra_ultra` | `TFX_CODEX_PROFILE=max/ultra`, 재시도 1단계 |
|
|
113
|
+
| `sol_high` | executor, codex, code-reviewer, quality-reviewer, verifier, test-engineer, qa-tester, scientist, document-specialist |
|
|
114
|
+
| `sol_med` | cleanup, deslop |
|
|
115
|
+
| `luna_high` | build-fixer, writer(직접 지정) |
|
|
116
|
+
| `luna_low` | spark, explore 재매핑, ChatGPT tier 폴백 |
|
|
117
|
+
| `flash_low` / `flash` / `flash_high` | Antigravity 분류·요약 / 생성·작성 / 검토·설계 |
|
|
117
118
|
| `fable` | `--retry auto-escalate` 최종 단계 |
|
|
118
119
|
| `opus` | 메타 라우팅, planning gate, 스킬의 `model="opus"` lane |
|
|
119
120
|
| `sonnet` | Claude native test-engineer, qa-tester, verifier |
|
|
120
121
|
| `haiku` | Claude native explore, claude |
|
|
121
122
|
|
|
123
|
+
Codex는 `models_cache.json`의 공개 모델 중 effort를 지원하는 후보를 숫자 버전순으로 고른다. 캐시가 없으면 `codex debug models`를 읽는다. agy는 `agy models`의 Flash 목록을 하루 동안 캐시한다. setup과 update도 같은 해석기를 쓴다. 옛 버전 프로필 이름은 가족 별칭으로 정규화한다.
|
|
124
|
+
|
|
125
|
+
`~/.config/triflux/models.json`에 `{"sol":"gpt-6-sol","flash":"gemini-3.7-flash"}`처럼 적으면 해당 가족을 고정한다. Windows는 `%APPDATA%\triflux`를 쓴다. 해석 기록과 agy 목록도 이 상태 디렉터리에 보관한다. 목록 조회 실패 시 마지막 성공 결과, 그다음 내장 기본값을 사용한다. Claude 별칭 라우팅은 유지한다.
|
|
126
|
+
|
|
122
127
|
## CLI 라우팅
|
|
123
128
|
|
|
124
129
|
`codex exec`와 `agy --print`를 직접 호출하지 않고 tfx 스킬을 거친다. headless-guard 훅은 제거되었으므로 호출자가 지킨다.
|
package/README.ko.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/triflux"><img src="https://img.shields.io/npm/v/triflux?style=flat-square&color=FFAF00&label=npm" alt="npm version"></a>
|
|
15
15
|
<a href="https://www.npmjs.com/package/triflux"><img src="https://img.shields.io/npm/dm/triflux?style=flat-square&color=F5C242" alt="npm downloads"></a>
|
|
16
16
|
<a href="https://github.com/tellang/triflux/stargazers"><img src="https://img.shields.io/github/stars/tellang/triflux?style=flat-square&color=FFAF00" alt="GitHub stars"></a>
|
|
17
|
-
<img src="https://img.shields.io/badge/node-%3E%
|
|
17
|
+
<img src="https://img.shields.io/badge/node-%3E%3D22-374151?style=flat-square" alt="Node >= 22">
|
|
18
18
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-374151?style=flat-square" alt="License: MIT"></a>
|
|
19
19
|
</p>
|
|
20
20
|
|
|
@@ -109,15 +109,17 @@ Codex 프로필은 `~/.codex/<프로필>.config.toml`에서 직접 관리한다.
|
|
|
109
109
|
## 모델과 프로필
|
|
110
110
|
|
|
111
111
|
Codex는 이름 붙은 프로필로 실행한다. 모델 ID는 `~/.codex/<프로필>.config.toml`이, 역할별 프로필
|
|
112
|
-
배정은 [`scripts/lib/agent-route-policy.mjs`](scripts/lib/agent-route-policy.mjs)가 정한다. Claude는
|
|
113
|
-
|
|
112
|
+
배정은 [`scripts/lib/agent-route-policy.mjs`](scripts/lib/agent-route-policy.mjs)가 정한다. 실행·setup·update 때 Codex와 Antigravity 가족 프로필을 사용 가능한 최신 버전으로 갱신한다. Codex는 로컬 모델 캐시에서 effort 지원 여부를 확인하고, Antigravity는 `agy models` 결과를 하루 동안 캐시한다. 옛 프로필 이름도 계속 동작한다. Claude는 기존 별칭을 유지한다.
|
|
113
|
+
|
|
114
|
+
`~/.config/triflux/models.json`에 `{"sol":"gpt-6-sol","flash":"gemini-3.7-flash"}`처럼 적으면 가족별로 고정한다(Windows: `%APPDATA%\triflux`). 목록을 읽지 못하면 마지막 해석 결과, 그다음 내장 기본값을 쓴다.
|
|
114
115
|
|
|
115
116
|
| 프로필 / 별칭 | 레인 |
|
|
116
117
|
| --- | --- |
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
118
|
+
| `astra_xhigh` | 아키텍처, 계획, 비평, 디버깅, 보안 리뷰, 깊은 실행 |
|
|
119
|
+
| `astra_max`, `astra_ultra` | 가장 어려운 단일 작업(`TFX_CODEX_PROFILE=max\|ultra`). `max`는 `auto-escalate` 첫 단계 |
|
|
120
|
+
| `sol_high` / `sol_med` | 기본 구현, 리뷰, 검증, 테스트, 문서 / 정리 작업 |
|
|
121
|
+
| `luna_high` / `luna_low` | 빌드 수정, 글쓰기 / 지연 우선 조회 |
|
|
122
|
+
| `flash_low` / `flash` / `flash_high` | Antigravity 분류 / 생성 / 검토 |
|
|
121
123
|
| Claude `fable` | `--retry auto-escalate` 마지막 단계 |
|
|
122
124
|
| Claude `opus` / `sonnet` / `haiku` | 메타 라우팅과 계획 게이트 / Claude 네이티브 QA·검증 / 빠른 탐색 |
|
|
123
125
|
|
|
@@ -155,7 +157,7 @@ Claude Code Bash 도구의 600초 제한에 걸리지 않는다. 이후 `--job-s
|
|
|
155
157
|
행에서 Enter를 누르면 그 워커가 도는 tmux pane이 열린다. 끄려면 `--no-native-bridge-ui`를 준다.
|
|
156
158
|
|
|
157
159
|
**재시도와 승격.** `--retry ralph`는 끝나거나 막힐 때(같은 실패 3회 연속)까지 반복한다.
|
|
158
|
-
`--retry auto-escalate`는 Codex `
|
|
160
|
+
`--retry auto-escalate`는 Codex `astra_max`에서 Claude `fable`로 올라간다. 체인은
|
|
159
161
|
`.triflux/config/escalation-chain.json`으로 바꿀 수 있다.
|
|
160
162
|
|
|
161
163
|
**머신 프로파일.** setup은 이 머신에서 쓸 CLI와 timeout 정책을
|
|
@@ -218,7 +220,7 @@ graph TD
|
|
|
218
220
|
|
|
219
221
|
## 기여
|
|
220
222
|
|
|
221
|
-
Node
|
|
223
|
+
Node 22 이상이 필요하다. 테스트, 린트, 패키지 경계, 미러·릴리즈 점검, 상태 스냅샷은
|
|
222
224
|
[CONTRIBUTING.md](CONTRIBUTING.md)에 있다. 릴리즈는 자동이다. 버전을 올린 커밋이 `main`에
|
|
223
225
|
머지되면 CI 통과 뒤 `release.yml`이 태그, GitHub 릴리즈, OIDC Trusted Publishing을 통한 npm 게시까지 진행한다.
|
|
224
226
|
결정 기록은 [docs/adr/](docs/adr/README.md)에 있다.
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/triflux"><img src="https://img.shields.io/npm/v/triflux?style=flat-square&color=FFAF00&label=npm" alt="npm version"></a>
|
|
15
15
|
<a href="https://www.npmjs.com/package/triflux"><img src="https://img.shields.io/npm/dm/triflux?style=flat-square&color=F5C242" alt="npm downloads"></a>
|
|
16
16
|
<a href="https://github.com/tellang/triflux/stargazers"><img src="https://img.shields.io/github/stars/tellang/triflux?style=flat-square&color=FFAF00" alt="GitHub stars"></a>
|
|
17
|
-
<img src="https://img.shields.io/badge/node-%3E%
|
|
17
|
+
<img src="https://img.shields.io/badge/node-%3E%3D22-374151?style=flat-square" alt="Node >= 22">
|
|
18
18
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-374151?style=flat-square" alt="License: MIT"></a>
|
|
19
19
|
</p>
|
|
20
20
|
|
|
@@ -112,14 +112,17 @@ The full contract, including `--lead`, `--options`, `--experts`, and conflict ru
|
|
|
112
112
|
|
|
113
113
|
Codex runs through named profiles; model IDs live in `~/.codex/<profile>.config.toml`, and the
|
|
114
114
|
role → profile map lives in [`scripts/lib/agent-route-policy.mjs`](scripts/lib/agent-route-policy.mjs).
|
|
115
|
-
|
|
115
|
+
Codex and Antigravity family profiles select the newest available model at dispatch, setup, and update. Codex reads its local model cache and checks supported effort; Antigravity caches `agy models` for one day. Old profile names remain compatible. Claude keeps its existing aliases.
|
|
116
|
+
|
|
117
|
+
Pin a family in `~/.config/triflux/models.json`, for example `{"sol":"gpt-6-sol","flash":"gemini-3.7-flash"}` (Windows: `%APPDATA%\triflux`). Without a usable catalog, resolution keeps the last result or the built-in default.
|
|
116
118
|
|
|
117
119
|
| Profile / alias | Lanes |
|
|
118
120
|
| --- | --- |
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
121
|
+
| `astra_xhigh` | Architecture, planning, critique, debugging, security review, deep execution |
|
|
122
|
+
| `astra_max`, `astra_ultra` | Hardest single tasks (`TFX_CODEX_PROFILE=max\|ultra`); `max` is the first `auto-escalate` step |
|
|
123
|
+
| `sol_high` / `sol_med` | Default implementation, review, verification, tests, docs / cleanup |
|
|
124
|
+
| `luna_high` / `luna_low` | Build fixes, writing / latency-first lookups |
|
|
125
|
+
| `flash_low` / `flash` / `flash_high` | Antigravity classification / generation / review |
|
|
123
126
|
| Claude `fable` | Final `--retry auto-escalate` step |
|
|
124
127
|
| Claude `opus` / `sonnet` / `haiku` | Meta routing and planning gates / Claude-native QA and verification / fast exploration |
|
|
125
128
|
|
|
@@ -158,7 +161,7 @@ the `claude agents` panel unless you pass `--no-native-bridge-ui`. Press Enter o
|
|
|
158
161
|
tmux pane the worker runs in.
|
|
159
162
|
|
|
160
163
|
**Retry and escalation.** `--retry ralph` loops until done or stuck (three identical failures).
|
|
161
|
-
`--retry auto-escalate` steps from Codex `
|
|
164
|
+
`--retry auto-escalate` steps from Codex `astra_max` to Claude `fable`; override the chain in
|
|
162
165
|
`.triflux/config/escalation-chain.json`.
|
|
163
166
|
|
|
164
167
|
**Machine profile.** Setup records which CLIs this machine may use and its timeout policy in
|
|
@@ -219,7 +222,7 @@ Windows) and `hub/team/wt-manager.mjs`. Agent rules:
|
|
|
219
222
|
|
|
220
223
|
## Contributing
|
|
221
224
|
|
|
222
|
-
Node
|
|
225
|
+
Node 22 or newer. See [CONTRIBUTING.md](CONTRIBUTING.md) for tests, lint, package boundaries,
|
|
223
226
|
mirror and release checks, and state snapshots. Releases are automated: merging a version-bump
|
|
224
227
|
commit to `main` runs `release.yml` after CI passes; it tags, creates the GitHub release, and publishes to npm
|
|
225
228
|
through OIDC Trusted Publishing. Decisions are recorded in [docs/adr/](docs/adr/README.md).
|
|
@@ -3,7 +3,7 @@ name: tfx-live
|
|
|
3
3
|
description: >
|
|
4
4
|
Claude·Codex·agy 세션을 실시간으로 생성·질의·대기·압축·종료하거나,
|
|
5
5
|
tmux/daemon UDS 연결과 peer 중계를 운영할 때 사용한다.
|
|
6
|
-
argument-hint: "<start|ask|wait|queue|compact|stop|rename|interrupt|probe|list-sessions|peer|converse|goal-driven> [--cli codex|claude|agy] ..."
|
|
6
|
+
argument-hint: "<start|ask|wait|queue|goal|compact|stop|rename|interrupt|probe|list-sessions|peer|converse|goal-driven> [--cli codex|claude|agy] ..."
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# tfx-live
|
|
@@ -84,6 +84,23 @@ tfx-live queue --cli codex --thread <UUID> --delete <queuedMessageId>
|
|
|
84
84
|
|
|
85
85
|
`queue`는 TUI가 아직 가져가지 않은 항목을 보고 지운다. Codex app-server의 실험 API `thread/queue/list`·`thread/queue/delete`를 stdio로 호출하며 큐 저장소 파일은 건드리지 않는다. 목록 항목은 `id`(ask 결과의 `queuedMessageId`와 같다), `requestId`, `text`다. 삭제 결과의 `missing`은 이미 TUI가 가져갔거나 없는 항목이다. 유휴 TUI는 쌓인 항목을 곧 가져가므로 지울 수 있는 것은 사실상 바쁜 세션과 닫힌 세션의 항목이다. 가져간 뒤의 처리 여부는 `wait`로 확인한다.
|
|
86
86
|
|
|
87
|
+
### 네이티브 goal
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
tfx-live goal set --session cl1 --condition "관련 단위 테스트가 모두 통과한다"
|
|
91
|
+
tfx-live goal set --session cx1 --cli codex --condition "관련 단위 테스트가 모두 통과한다" --budget 100000
|
|
92
|
+
tfx-live goal status --session cx1
|
|
93
|
+
tfx-live goal clear --session cx1
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`--cli`를 생략하면 세션 프로세스에서 추정한다. 로컬 Claude와 Codex만 지원하며 `--remote`와 agy는 거부한다. `--budget`은 Codex set의 daemon 경로에서만 받고 Claude와 tmux 폴백에서는 오류로 거부한다.
|
|
97
|
+
|
|
98
|
+
Claude는 tmux로 `/goal <조건>`과 `/goal clear`를 보내고 transcript의 마지막 `goal_status`로 상태를 읽는다. 별도 평가자의 완료는 `complete`, 수동 해제는 `none`이며 `judgedBy: "evaluator"`와 평가 이유를 보고한다. transcript를 찾지 못하면 상태는 `null`이고 경고를 남긴다. set/clear 뒤 새 sentinel을 최대 15초 확인하며 없으면 `confirmed: false`와 경고를 반환한다. 이미 해제된 goal의 clear는 입력 없이 `alreadyCleared: true`로 답한다. set은 기본 `--if-busy wait`, clear는 미충족 goal이 턴을 계속 이어 가도 끊을 수 있게 기본 `--if-busy interrupt`다. 평가자가 해제하지 않고 멈춘 goal은 `active`로 남으므로 리드가 `goal clear`로 정리한다.
|
|
99
|
+
|
|
100
|
+
Codex는 공유 daemon UDS의 `thread/goal/*`를 우선 쓴다. `--codex-socket PATH|default`로 소켓을 고르고 기존 thread 해석 순서와 `--thread UUID`를 따른다. 새 goal은 `origin: "user"`로 clear 후 active set한다. 소켓이 없거나 thread가 미적재면 set/clear는 tmux 슬래시로, status는 `$CODEX_HOME/goals_1.sqlite`의 읽기 전용 조회로 폴백한다. `node:sqlite`를 지원하지 않는 Node에서는 상태를 `null`로 두고 경고한다. 결과에 `transport: "daemon"|"tmux"|"sqlite"`와 폴백 이유를 남긴다. tmux 폴백의 set/clear는 sqlite로 최대 15초 확인하고, 확인하지 못하면 재전송을 막으려고 오류 대신 `confirmed: false`와 경고를 반환한다. Codex의 `judgedBy: "model"`, `complete`는 모델 자기 판정이므로 리드의 교차 검증이 필요하다.
|
|
101
|
+
|
|
102
|
+
Claude와 Codex는 네이티브 goal을 우선 쓰고, goal-driven은 agy처럼 네이티브 goal이 없을 때 쓰는 방식이다. 현재 agy의 goal-driven 실행은 지원하지 않는다.
|
|
103
|
+
|
|
87
104
|
### 컨텍스트 판단
|
|
88
105
|
|
|
89
106
|
분모는 공식 모델 창이고 실행 한도가 더 작으면 그 한도 아래에서 운영한다. 아래 비율은 공식 권고가 아닌 **[추론] 운영값**이다. 모델 문서가 바뀌면 한도도 갱신한다.
|
|
@@ -124,7 +141,7 @@ tfx-live ask --cli codex --session cx1 --prompt "진행 상황을 보고해줘"
|
|
|
124
141
|
tfx-live wait --cli codex --session cx1 --request-id <requestId> --timeout 120
|
|
125
142
|
```
|
|
126
143
|
|
|
127
|
-
`ask --no-wait`는 Claude와 tmux에서 `status: "submitted"`, Codex queue에서 `status: "queued"`와 함께 `inputSent: true`, `done: false`, `submittedAt`, `target`, `requestId`를 반환한다. `ask`, `peer`, `converse`, `goal-driven`은 `[tfx-live req=<requestId>]`를 프롬프트 앞에 붙인다. `--no-relay-tag`로 제거하면 `wait --request-id`가 해당 요청을 찾을 수 없다.
|
|
144
|
+
`ask --no-wait`는 Claude와 tmux에서 `status: "submitted"`, Codex queue에서 `status: "queued"`와 함께 `inputSent: true`, `done: false`, `submittedAt`, `target`, `requestId`를 반환한다. `ask`, `peer`, `converse`, `goal-driven`은 `[tfx-live req=<requestId>]`를 프롬프트 앞에 붙인다. `--no-relay-tag`로 제거하면 `wait --request-id`가 해당 요청을 찾을 수 없다. 슬래시 명령(`/rename`, `/clear` 등)은 CLI와 상관없이 표식 없이 보낸다. 응답 턴이 없는 Claude 내장 명령(`/rename`, `/clear`, `/compact`, `/model`, `/effort`, `/color`)은 기다리지 않고 `status: "submitted"`로 반환한다. 압축 완료까지 기다리려면 `compact` 명령을 쓴다.
|
|
128
145
|
|
|
129
146
|
`wait`는 Claude transcript에서 표식을 user 메시지, `queue-operation`의 `enqueue` content, queued command attachment에서 찾는다. 표식이 소비된 턴이 끝난 뒤 마지막 assistant 텍스트를 응답으로 쓴다. 표식 없는 `wait`는 최신 user turn을 본다. `isSidechain`과 합성 API 오류 메시지는 정상 응답에서 제외한다. 요청을 소비한 턴에서 합성 오류가 나타나면 오류 문구와 `status: "failed"`를 반환한다. daemon의 `idle`만으로 완료를 단정하지 않는다. timeout은 `status: "working"`, `timedOut: true`, `done: false`다.
|
|
130
147
|
|
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
|
|
|
@@ -102,6 +109,9 @@ function usage(command) {
|
|
|
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
111
|
" tfx-live compact --cli claude --session NAME [--instructions TEXT] [--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) {
|
|
@@ -3233,10 +3244,8 @@ async function doAsk(adapter, opts) {
|
|
|
3233
3244
|
const requestId = opts.requestId ?? randomBytes(6).toString("hex");
|
|
3234
3245
|
const tag = `[tfx-live req=${requestId}]`;
|
|
3235
3246
|
// Codex 는 접힌 발신자 표시가 없어 첫 줄 머리말로 보낸 세션을 드러낸다.
|
|
3236
|
-
// 슬래시 명령은 첫 글자가 / 여야 실행되므로 표식을 붙이지 않는다.
|
|
3237
|
-
const bare =
|
|
3238
|
-
opts.noRelayTag ||
|
|
3239
|
-
(["codex", "agy"].includes(adapter.cli) && isSlashCommand(opts.prompt));
|
|
3247
|
+
// 슬래시 명령은 CLI 와 상관없이 첫 글자가 / 여야 실행되므로 표식을 붙이지 않는다.
|
|
3248
|
+
const bare = opts.noRelayTag || isSlashCommand(opts.prompt);
|
|
3240
3249
|
const from =
|
|
3241
3250
|
adapter.cli === "codex" && !bare
|
|
3242
3251
|
? (opts.from ?? (await resolveSenderName()))
|
|
@@ -3248,7 +3257,15 @@ async function doAsk(adapter, opts) {
|
|
|
3248
3257
|
: `${header}\n${opts.prompt}`;
|
|
3249
3258
|
try {
|
|
3250
3259
|
return {
|
|
3251
|
-
...(await dispatchAsk(adapter, {
|
|
3260
|
+
...(await dispatchAsk(adapter, {
|
|
3261
|
+
...opts,
|
|
3262
|
+
prompt,
|
|
3263
|
+
requestId,
|
|
3264
|
+
noRelayTag: bare,
|
|
3265
|
+
noWait:
|
|
3266
|
+
opts.noWait ||
|
|
3267
|
+
(adapter.cli === "claude" && CLAUDE_SILENT_SLASH.test(opts.prompt)),
|
|
3268
|
+
})),
|
|
3252
3269
|
requestId,
|
|
3253
3270
|
...(opts.transportReason
|
|
3254
3271
|
? { transportReason: opts.transportReason }
|
|
@@ -3980,9 +3997,15 @@ function transportFlag(flags, adapter, short, sessionId) {
|
|
|
3980
3997
|
|
|
3981
3998
|
// 절대 경로로 시작하는 프롬프트는 슬래시 명령이 아니다.
|
|
3982
3999
|
function isSlashCommand(prompt) {
|
|
3983
|
-
|
|
4000
|
+
const word = String(prompt ?? "").match(/^\/[a-z][\w-]*(?=\s|$)/i)?.[0];
|
|
4001
|
+
// `/tmp 를 확인해` 처럼 이 기기에 실제로 있는 경로로 시작하는 문장은 명령이 아니다.
|
|
4002
|
+
return Boolean(word) && !existsSync(word);
|
|
3984
4003
|
}
|
|
3985
4004
|
|
|
4005
|
+
// 응답 턴을 만들지 않는 Claude 내장 명령. 스킬 명령은 응답을 만드므로 기다린다.
|
|
4006
|
+
const CLAUDE_SILENT_SLASH =
|
|
4007
|
+
/^\/(?:rename|clear|compact|model|effort|color)(?:\s|$)/i;
|
|
4008
|
+
|
|
3986
4009
|
function askOpts(flags, adapter) {
|
|
3987
4010
|
const short = flags.short;
|
|
3988
4011
|
const sessionId = flags["session-id"];
|
|
@@ -4417,6 +4440,319 @@ async function compact(flags) {
|
|
|
4417
4440
|
});
|
|
4418
4441
|
}
|
|
4419
4442
|
|
|
4443
|
+
function codexGoalStatus(goal) {
|
|
4444
|
+
return {
|
|
4445
|
+
state: goal
|
|
4446
|
+
? ({ usageLimited: "usage_limited", budgetLimited: "budget_limited" }[
|
|
4447
|
+
goal.status
|
|
4448
|
+
] ?? goal.status)
|
|
4449
|
+
: "none",
|
|
4450
|
+
objective: goal?.objective ?? null,
|
|
4451
|
+
judgedBy: "model",
|
|
4452
|
+
...(goal
|
|
4453
|
+
? {
|
|
4454
|
+
tokensUsed: goal.tokensUsed,
|
|
4455
|
+
tokenBudget: goal.tokenBudget ?? null,
|
|
4456
|
+
updatedAt: new Date(goal.updatedAt * 1000).toISOString(),
|
|
4457
|
+
}
|
|
4458
|
+
: {}),
|
|
4459
|
+
};
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
async function readCodexGoal(threadId) {
|
|
4463
|
+
let db;
|
|
4464
|
+
try {
|
|
4465
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
4466
|
+
if (
|
|
4467
|
+
major < 22 ||
|
|
4468
|
+
(major === 22 && minor < 13) ||
|
|
4469
|
+
(major === 23 && minor < 4)
|
|
4470
|
+
)
|
|
4471
|
+
throw new Error("Node 22.13+ with read-only node:sqlite is required");
|
|
4472
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
4473
|
+
db = new DatabaseSync(
|
|
4474
|
+
pathJoin(
|
|
4475
|
+
process.env.CODEX_HOME ?? pathJoin(homedir(), ".codex"),
|
|
4476
|
+
"goals_1.sqlite",
|
|
4477
|
+
),
|
|
4478
|
+
{ readOnly: true },
|
|
4479
|
+
);
|
|
4480
|
+
const row = db
|
|
4481
|
+
.prepare(
|
|
4482
|
+
"SELECT objective, status, token_budget AS tokenBudget, tokens_used AS tokensUsed, updated_at_ms / 1000.0 AS updatedAt FROM thread_goals WHERE thread_id = ?",
|
|
4483
|
+
)
|
|
4484
|
+
.get(threadId);
|
|
4485
|
+
return codexGoalStatus(row);
|
|
4486
|
+
} catch (error) {
|
|
4487
|
+
// Node >=22 전체에서 sqlite 를 지원하지는 않으므로 미확인을 none 으로 보고하지 않는다.
|
|
4488
|
+
return {
|
|
4489
|
+
state: null,
|
|
4490
|
+
objective: null,
|
|
4491
|
+
judgedBy: "model",
|
|
4492
|
+
warning: `Goal status unavailable (read-only node:sqlite): ${error.message}`,
|
|
4493
|
+
};
|
|
4494
|
+
} finally {
|
|
4495
|
+
db?.close();
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
async function codexGoal(action, opts, deps = {}) {
|
|
4500
|
+
const { threadId, objective, tokenBudget } = opts;
|
|
4501
|
+
let fallbackReason;
|
|
4502
|
+
let client;
|
|
4503
|
+
try {
|
|
4504
|
+
const socketPath = (deps.resolveSocket ?? resolveCodexDaemonSocket)(
|
|
4505
|
+
opts.codexSocket,
|
|
4506
|
+
);
|
|
4507
|
+
client = (
|
|
4508
|
+
deps.clientFactory ?? ((options) => new JsonRpcWsUdsClient(options))
|
|
4509
|
+
)({ socketPath, connectTimeoutMs: 5000 });
|
|
4510
|
+
await client.connect();
|
|
4511
|
+
await client.request(
|
|
4512
|
+
"initialize",
|
|
4513
|
+
{
|
|
4514
|
+
clientInfo: { name: "triflux-tfx-live", version: "1.0.0" },
|
|
4515
|
+
capabilities: { experimentalApi: true },
|
|
4516
|
+
},
|
|
4517
|
+
5000,
|
|
4518
|
+
);
|
|
4519
|
+
client.notify("initialized", {});
|
|
4520
|
+
let cursor;
|
|
4521
|
+
let loaded = false;
|
|
4522
|
+
do {
|
|
4523
|
+
const page = await client.request(
|
|
4524
|
+
"thread/loaded/list",
|
|
4525
|
+
cursor ? { cursor } : {},
|
|
4526
|
+
5000,
|
|
4527
|
+
);
|
|
4528
|
+
loaded = page.data.includes(threadId);
|
|
4529
|
+
cursor = page.nextCursor;
|
|
4530
|
+
} while (!loaded && cursor);
|
|
4531
|
+
if (!loaded) fallbackReason = "thread-not-loaded";
|
|
4532
|
+
} catch (error) {
|
|
4533
|
+
fallbackReason = `daemon-unavailable: ${error.message}`;
|
|
4534
|
+
}
|
|
4535
|
+
try {
|
|
4536
|
+
if (fallbackReason) {
|
|
4537
|
+
if (tokenBudget !== undefined)
|
|
4538
|
+
throw new Error(
|
|
4539
|
+
`--budget requires a loaded Codex daemon thread; tmux fallback cannot set a budget (${fallbackReason})`,
|
|
4540
|
+
);
|
|
4541
|
+
return {
|
|
4542
|
+
...(action === "status"
|
|
4543
|
+
? await (deps.readGoal ?? readCodexGoal)(threadId)
|
|
4544
|
+
: {}),
|
|
4545
|
+
transport: action === "status" ? "sqlite" : "tmux",
|
|
4546
|
+
fallbackReason,
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
const params = { threadId, origin: "user" };
|
|
4550
|
+
// 쓰기를 시도한 뒤의 오류는 폴백하지 않는다. 성공 여부가 불명확한 명령을 중복 전송할 수 있다.
|
|
4551
|
+
if (action !== "status") {
|
|
4552
|
+
const cleared = await client.request("thread/goal/clear", params, 10000);
|
|
4553
|
+
if (action === "clear")
|
|
4554
|
+
return {
|
|
4555
|
+
...codexGoalStatus(null),
|
|
4556
|
+
...cleared,
|
|
4557
|
+
confirmed: true,
|
|
4558
|
+
transport: "daemon",
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
const result =
|
|
4562
|
+
action === "set"
|
|
4563
|
+
? await client
|
|
4564
|
+
.request(
|
|
4565
|
+
"thread/goal/set",
|
|
4566
|
+
{
|
|
4567
|
+
...params,
|
|
4568
|
+
objective,
|
|
4569
|
+
status: "active",
|
|
4570
|
+
...(tokenBudget !== undefined ? { tokenBudget } : {}),
|
|
4571
|
+
},
|
|
4572
|
+
10000,
|
|
4573
|
+
)
|
|
4574
|
+
.catch((error) => {
|
|
4575
|
+
throw new Error(
|
|
4576
|
+
`${error.message}; clear was applied and there is currently no goal`,
|
|
4577
|
+
);
|
|
4578
|
+
})
|
|
4579
|
+
: await client.request("thread/goal/get", { threadId }, 10000);
|
|
4580
|
+
return {
|
|
4581
|
+
...codexGoalStatus(result.goal),
|
|
4582
|
+
...(action === "set" ? { confirmed: true } : {}),
|
|
4583
|
+
transport: "daemon",
|
|
4584
|
+
};
|
|
4585
|
+
} finally {
|
|
4586
|
+
client?.close();
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
async function goal(action, flags) {
|
|
4591
|
+
if (!["set", "status", "clear"].includes(action))
|
|
4592
|
+
throw new Error("goal requires set, status, or clear");
|
|
4593
|
+
if (flags.remote !== undefined)
|
|
4594
|
+
throw new Error("goal does not support --remote");
|
|
4595
|
+
const session = splitTmuxTarget(requireFlag(flags, "session")).target;
|
|
4596
|
+
let cli = flags.cli;
|
|
4597
|
+
if (!cli) {
|
|
4598
|
+
for (const candidate of ["claude", "codex", "agy"]) {
|
|
4599
|
+
if (await paneCliProcess(session, candidate)) {
|
|
4600
|
+
cli = candidate;
|
|
4601
|
+
break;
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
if (!cli)
|
|
4605
|
+
throw new Error("Cannot infer session CLI; specify --cli claude|codex");
|
|
4606
|
+
}
|
|
4607
|
+
if (cli === "agy") throw new Error("agy has no native goal");
|
|
4608
|
+
const adapter = selectAdapter({ ...flags, cli });
|
|
4609
|
+
if (flags.budget !== undefined && (cli !== "codex" || action !== "set"))
|
|
4610
|
+
throw new Error("--budget is supported only by goal set --cli codex");
|
|
4611
|
+
const tokenBudget = integerFlag(flags, "budget", undefined);
|
|
4612
|
+
if (tokenBudget !== undefined && !Number.isSafeInteger(tokenBudget))
|
|
4613
|
+
throw new Error("--budget must be a safe positive integer");
|
|
4614
|
+
const objective =
|
|
4615
|
+
action === "set"
|
|
4616
|
+
? requireFlag(flags, "condition").replace(/\s+/gu, " ").trim()
|
|
4617
|
+
: null;
|
|
4618
|
+
if (
|
|
4619
|
+
action === "set" &&
|
|
4620
|
+
(!objective || /^(clear|pause|resume|edit)(?: |$)/i.test(objective))
|
|
4621
|
+
)
|
|
4622
|
+
throw new Error(
|
|
4623
|
+
"--condition must not be blank or start with a reserved command (clear, pause, resume, edit)",
|
|
4624
|
+
);
|
|
4625
|
+
const base = { cli, session };
|
|
4626
|
+
let codexResult;
|
|
4627
|
+
let threadId;
|
|
4628
|
+
if (cli === "codex") {
|
|
4629
|
+
({ threadId } = await codexThreadFromFlags(flags));
|
|
4630
|
+
codexResult = await codexGoal(action, {
|
|
4631
|
+
threadId,
|
|
4632
|
+
objective,
|
|
4633
|
+
tokenBudget,
|
|
4634
|
+
codexSocket: flags["codex-socket"],
|
|
4635
|
+
});
|
|
4636
|
+
if (codexResult.transport !== "tmux") return { ...base, ...codexResult };
|
|
4637
|
+
}
|
|
4638
|
+
const transcriptPath =
|
|
4639
|
+
cli === "claude"
|
|
4640
|
+
? await claudeTmuxTranscript(session, flags["config-dir"])
|
|
4641
|
+
: null;
|
|
4642
|
+
let snapshot =
|
|
4643
|
+
cli === "claude" ? await readClaudeTranscript(transcriptPath) : null;
|
|
4644
|
+
const unavailable = {
|
|
4645
|
+
state: null,
|
|
4646
|
+
objective: null,
|
|
4647
|
+
judgedBy: "evaluator",
|
|
4648
|
+
warning: "Claude transcript unavailable for this tmux pane",
|
|
4649
|
+
};
|
|
4650
|
+
if (action === "status")
|
|
4651
|
+
return {
|
|
4652
|
+
...base,
|
|
4653
|
+
...(snapshot?.goal ?? unavailable),
|
|
4654
|
+
transport: "tmux",
|
|
4655
|
+
};
|
|
4656
|
+
// 달성 뒤 자동 해제됐거나 없던 goal 은 /goal clear 가 기록을 남기지 않아 확인할 수 없다.
|
|
4657
|
+
if (action === "clear" && snapshot?.goal && snapshot.goal.state !== "active")
|
|
4658
|
+
return {
|
|
4659
|
+
...base,
|
|
4660
|
+
...snapshot.goal,
|
|
4661
|
+
transport: "tmux",
|
|
4662
|
+
confirmed: true,
|
|
4663
|
+
alreadyCleared: true,
|
|
4664
|
+
};
|
|
4665
|
+
const opts = askOpts(
|
|
4666
|
+
{
|
|
4667
|
+
...flags,
|
|
4668
|
+
cli,
|
|
4669
|
+
"if-busy":
|
|
4670
|
+
flags["if-busy"] ?? (action === "clear" ? "interrupt" : "wait"),
|
|
4671
|
+
transport: "tmux",
|
|
4672
|
+
"no-wait": true,
|
|
4673
|
+
"no-relay-tag": true,
|
|
4674
|
+
prompt: `/goal ${action === "set" ? objective : "clear"}`,
|
|
4675
|
+
},
|
|
4676
|
+
adapter,
|
|
4677
|
+
);
|
|
4678
|
+
let interrupted = false;
|
|
4679
|
+
if (action === "clear" && opts.ifBusy === "interrupt") {
|
|
4680
|
+
const visible = await captureVisible(null, session);
|
|
4681
|
+
// Claude 반복 턴은 화면에 esc 안내가 없어도 active goal 을 계속 실행할 수 있다.
|
|
4682
|
+
if (
|
|
4683
|
+
snapshot?.goal?.state === "active" ||
|
|
4684
|
+
adapter.isBusy(visible) ||
|
|
4685
|
+
(cli === "codex" && isCodexLoadingCapture(visible))
|
|
4686
|
+
) {
|
|
4687
|
+
await doInterruptViaTmux(adapter, opts);
|
|
4688
|
+
await sleep(opts.pollIntervalMs);
|
|
4689
|
+
interrupted = true;
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
await waitForTmuxIdle(adapter, opts);
|
|
4693
|
+
// Esc 가 직전 입력을 복원해도 clear 명령 앞에 붙지 않게 비운다.
|
|
4694
|
+
if (interrupted) await runTmux(null, ["send-keys", "-t", session, "C-u"]);
|
|
4695
|
+
snapshot =
|
|
4696
|
+
cli === "claude" ? await readClaudeTranscript(transcriptPath) : null;
|
|
4697
|
+
const beforeCount = snapshot?.goalSentinel?.count ?? 0;
|
|
4698
|
+
const inputStartedAt = Date.now();
|
|
4699
|
+
await doAskViaTmux(adapter, {
|
|
4700
|
+
...opts,
|
|
4701
|
+
ifBusy: "fail",
|
|
4702
|
+
skipContextGuard: true,
|
|
4703
|
+
});
|
|
4704
|
+
const deadline = Date.now() + Math.min(opts.timeoutMs, 15_000);
|
|
4705
|
+
let confirmed = false;
|
|
4706
|
+
if (cli === "codex") {
|
|
4707
|
+
do {
|
|
4708
|
+
snapshot = await readCodexGoal(threadId);
|
|
4709
|
+
confirmed =
|
|
4710
|
+
action === "set"
|
|
4711
|
+
? snapshot.state === "active" &&
|
|
4712
|
+
snapshot.objective === objective &&
|
|
4713
|
+
Date.parse(snapshot.updatedAt) >= inputStartedAt
|
|
4714
|
+
: snapshot.state === "none";
|
|
4715
|
+
if (confirmed || snapshot.state === null || Date.now() >= deadline) break;
|
|
4716
|
+
await sleep(Math.min(opts.pollIntervalMs, deadline - Date.now()));
|
|
4717
|
+
} while (Date.now() < deadline);
|
|
4718
|
+
// 입력은 이미 들어갔으므로 오류로 끝내면 재전송을 부른다. 미확인으로만 알린다.
|
|
4719
|
+
return {
|
|
4720
|
+
...base,
|
|
4721
|
+
...codexResult,
|
|
4722
|
+
...snapshot,
|
|
4723
|
+
inputSent: true,
|
|
4724
|
+
confirmed,
|
|
4725
|
+
...(!confirmed && snapshot.state !== null
|
|
4726
|
+
? { warning: "No matching Codex goal observed after input" }
|
|
4727
|
+
: {}),
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
// 갓 띄운 세션은 첫 입력 뒤에야 자기 transcript 가 생기므로 매번 경로를 다시 찾는다.
|
|
4731
|
+
do {
|
|
4732
|
+
snapshot = await readClaudeTranscript(
|
|
4733
|
+
await claudeTmuxTranscript(session, flags["config-dir"]),
|
|
4734
|
+
);
|
|
4735
|
+
const sentinel = snapshot?.goalSentinel;
|
|
4736
|
+
confirmed =
|
|
4737
|
+
sentinel?.count > beforeCount &&
|
|
4738
|
+
Date.parse(sentinel.updatedAt) >= inputStartedAt &&
|
|
4739
|
+
(action === "set"
|
|
4740
|
+
? sentinel.met === false && sentinel.condition === objective
|
|
4741
|
+
: sentinel.met === true);
|
|
4742
|
+
if (confirmed || Date.now() >= deadline) break;
|
|
4743
|
+
await sleep(Math.min(opts.pollIntervalMs, deadline - Date.now()));
|
|
4744
|
+
} while (Date.now() < deadline);
|
|
4745
|
+
return {
|
|
4746
|
+
...base,
|
|
4747
|
+
...(snapshot?.goal ?? unavailable),
|
|
4748
|
+
transport: "tmux",
|
|
4749
|
+
confirmed,
|
|
4750
|
+
...(!confirmed
|
|
4751
|
+
? { warning: "No new matching goal_status sentinel observed after input" }
|
|
4752
|
+
: {}),
|
|
4753
|
+
};
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4420
4756
|
async function interrupt(flags) {
|
|
4421
4757
|
const adapter = selectAdapter(flags);
|
|
4422
4758
|
printJson(await doInterrupt(adapter, interruptOpts(flags, adapter)));
|
|
@@ -5343,7 +5679,7 @@ function printJson(value) {
|
|
|
5343
5679
|
}
|
|
5344
5680
|
|
|
5345
5681
|
async function main() {
|
|
5346
|
-
const { command, flags } = parseCli(process.argv.slice(2));
|
|
5682
|
+
const { command, flags, action } = parseCli(process.argv.slice(2));
|
|
5347
5683
|
|
|
5348
5684
|
if (command === "help") {
|
|
5349
5685
|
process.stdout.write(`${usage(flags.subcommand)}\n`);
|
|
@@ -5360,6 +5696,8 @@ async function main() {
|
|
|
5360
5696
|
await queue(flags);
|
|
5361
5697
|
} else if (command === "compact") {
|
|
5362
5698
|
await compact(flags);
|
|
5699
|
+
} else if (command === "goal") {
|
|
5700
|
+
printJson(await goal(action, flags));
|
|
5363
5701
|
} else if (command === "rename") {
|
|
5364
5702
|
await rename(flags);
|
|
5365
5703
|
} else if (command === "stop") {
|
|
@@ -5392,6 +5730,7 @@ export {
|
|
|
5392
5730
|
buildTmuxCommand,
|
|
5393
5731
|
callRemoteLive,
|
|
5394
5732
|
classifyPeerExitReason,
|
|
5733
|
+
codexGoal,
|
|
5395
5734
|
composerShowsPrompt,
|
|
5396
5735
|
createPeerSignalController,
|
|
5397
5736
|
derivePeerStatus,
|
|
@@ -5405,6 +5744,7 @@ export {
|
|
|
5405
5744
|
inspectCodexTmuxPane,
|
|
5406
5745
|
inspectUnsetEnv,
|
|
5407
5746
|
isCodexLoadingCapture,
|
|
5747
|
+
isSlashCommand,
|
|
5408
5748
|
parseCodexTmuxSessions,
|
|
5409
5749
|
parsePeerClosure,
|
|
5410
5750
|
parseRemoteLiveJson,
|