aiterm-mcp 0.28.0 → 0.28.2
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.ja.md +16 -16
- package/README.md +29 -29
- package/dist/agent-resolver.js +1 -1
- package/dist/agent-shared.js +3 -3
- package/dist/aiterm-wait-cli.js +1 -1
- package/dist/core.js +29 -29
- package/dist/{vendors → harnesses}/claude.js +2 -2
- package/dist/{vendors → harnesses}/codex.js +12 -12
- package/dist/{vendors → harnesses}/cursor.js +12 -12
- package/dist/{vendors → harnesses}/grok.js +5 -5
- package/dist/index.js +5 -5
- package/package.json +3 -3
package/README.ja.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
> **あなたの AI に、ほかの AI を操らせる。** `agent_launch`の1回の呼び出しで、実行基盤harnessとmodelを別々に選び、永続sessionを受け取る。CursorでGPT/Claude/Grokを選んでも、session・hook・transcriptはCursorが所有する。
|
|
20
20
|
>
|
|
21
|
-
> **これは何か:** AI が握る 1 本の永続 MCP 端末——その中に他のコーディングエージェントも起動できる。`ssh`・`docker exec`・REPL・別エージェントの TUI は、すべてその 1 本の端末の中へ「送るだけのテキスト」として入れ子になる。仕組みはあえて素朴——MCP クライアントが相手エージェントの端末を 1 ターンずつ操作するだけ。隠れたプロトコルも・aiterm独自の共有メモリ層も・自律的な交渉も無い。起動したagentは、直接CLIと同じproject/
|
|
21
|
+
> **これは何か:** AI が握る 1 本の永続 MCP 端末——その中に他のコーディングエージェントも起動できる。`ssh`・`docker exec`・REPL・別エージェントの TUI は、すべてその 1 本の端末の中へ「送るだけのテキスト」として入れ子になる。仕組みはあえて素朴——MCP クライアントが相手エージェントの端末を 1 ターンずつ操作するだけ。隠れたプロトコルも・aiterm独自の共有メモリ層も・自律的な交渉も無い。起動したagentは、直接CLIと同じproject/harnessの通常memory・設定を読む。
|
|
22
22
|
>
|
|
23
23
|
> **人が tmux に張り付く必要はない。** aiterm は MCP 越しにプログラムから駆動されるので、「AI が別のエージェントを起動して操作する」のに端末の前に誰も座らなくていい——オーケストレーションのループ・CI ステップ・cron から動かせる。
|
|
24
24
|
>
|
|
@@ -118,17 +118,17 @@ serverがMCP processより先に起動していても、古いserver環境に席
|
|
|
118
118
|
常駐するmodel/effort footerと入力欄でCodexを識別する。idle sessionをそのまま変更でき、
|
|
119
119
|
caller側の画面再描画、再試行、agent再起動は不要。
|
|
120
120
|
|
|
121
|
-
**v0.24.0では起動中agentの設定変更を追加。** `agent_configure`は
|
|
122
|
-
起動中のCodex/Claudeのmodelとreasoning effortを変更する。PTY、
|
|
121
|
+
**v0.24.0では起動中agentの設定変更を追加。** `agent_configure`はharness標準操作を使って、
|
|
122
|
+
起動中のCodex/Claudeのmodelとreasoning effortを変更する。PTY、harness session、会話contextは維持する。
|
|
123
123
|
|
|
124
|
-
**v0.23.0では、ローカル完結の別
|
|
124
|
+
**v0.23.0では、ローカル完結の別harness向けportable forkを追加した。** どのlauncherでも
|
|
125
125
|
`throughline_source_session`と新しいミッションを`prompt`へ渡すと、PTY作成前にローカルの
|
|
126
126
|
Throughlineから対象sessionの読み取り専用handoff contextを取得する。返された記憶はそのまま
|
|
127
127
|
ミッションの前へ置かれ、元sessionのDB所属は移動もcopyもされない。Throughlineが無い、または
|
|
128
128
|
結果が不正/空ならclean launchへfallbackせず明示失敗する。引数を省略した通常起動は従来どおり。
|
|
129
129
|
|
|
130
130
|
**v0.22.0では4 launcherを完全なプロジェクト共同作業員へ戻した。** 直接CLIを起動した時と同じ
|
|
131
|
-
`HOME`、作業tree、
|
|
131
|
+
`HOME`、作業tree、harness home、project/user/local設定、MCP、plugin、skill、permission、trust、memory、
|
|
132
132
|
session historyをそのまま使う。aitermがlaunchごとに分離するのは完了相関stateだけ。子には
|
|
133
133
|
`role=subagent`、親session、delegation depth、lineage、`delegation_allowed=true`を注入する。
|
|
134
134
|
孫以降への再委譲は禁止せず、孫はdepth 2と伸びたlineageを受け取る。既存receiptの
|
|
@@ -304,9 +304,9 @@ claude mcp add --scope user --transport stdio aiterm -- aiterm-mcp
|
|
|
304
304
|
|
|
305
305
|
## ヘッドレス: 端末に人が居ない
|
|
306
306
|
|
|
307
|
-
MCP クライアントが aiterm を stdio 越しにプログラムから駆動するので、上のすべては
|
|
307
|
+
MCP クライアントが aiterm を stdio 越しにプログラムから駆動するので、上のすべては **端末に誰も座らないまま**動く。任意のMCP対応統括役が、自分と同じharnessを含む`agent_launch`を呼び、`pty_read`で結果を読んで次へ進める——無人で。これは、人が操作する端末が向かない場所にこそ aiterm が合うということ:
|
|
308
308
|
|
|
309
|
-
- **複数エージェントのオーケストレーション** — 統括役がサブタスクを Claude / Codex / Grok / Composer
|
|
309
|
+
- **複数エージェントのオーケストレーション** — 統括役がサブタスクを Claude Code / Codex / Grok / Cursor harnessへ渡し、各々を専用の永続セッションに置き、全部を読み戻す。ComposerはGrok CLIのmodel presetとして扱う。
|
|
310
310
|
- **CI** — ジョブのステップがエージェントを起こし、操作し、片付けられる。
|
|
311
311
|
- **cron** — スケジュール実行がエージェントを起動して出力を回収できる。
|
|
312
312
|
|
|
@@ -318,7 +318,7 @@ MCP クライアントが aiterm を stdio 越しにプログラムから駆動
|
|
|
318
318
|
flowchart LR
|
|
319
319
|
AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · agent_launch · agent_configure · claude_turn · claude_approval<br/>旧launcher alias · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 15 tools"]
|
|
320
320
|
S -->|"pty_read<br/>token-reduced"| AI
|
|
321
|
-
S -->|"tmux
|
|
321
|
+
S -->|"tmux / psmux<br/>send · capture"| P["persistent PTYs<br/>再起動を跨ぐ"]
|
|
322
322
|
P -->|"ssh · docker · repl"| R["nested<br/>remote · container · REPL"]
|
|
323
323
|
P -->|"launches a fresh PTY per agent"| A["another coding-agent TUI<br/>Claude · Codex · Grok · Cursor"]
|
|
324
324
|
```
|
|
@@ -352,10 +352,10 @@ aiterm はセッションの状態も持ち越せる。組み込みツールは
|
|
|
352
352
|
|
|
353
353
|
```text
|
|
354
354
|
組み込みシェル → var= # 空。env は消え、cwd はプロジェクト直下に戻る
|
|
355
|
-
aiterm → cwd=/tmp var=hello123 # 1
|
|
355
|
+
aiterm → cwd=/tmp var=hello123 # 1 本の永続PTYが両方を保つ
|
|
356
356
|
```
|
|
357
357
|
|
|
358
|
-
cd でディレクトリを移り、環境変数を立て、ビルドを走らせる。ssh で一度ログインして、その接続のまま 10 個コマンドを打つ。REPL や起動したエージェントの TUI を 1 ターンずつ操作する。こういう流れは、1
|
|
358
|
+
cd でディレクトリを移り、環境変数を立て、ビルドを走らせる。ssh で一度ログインして、その接続のまま 10 個コマンドを打つ。REPL や起動したエージェントの TUI を 1 ターンずつ操作する。こういう流れは、1 本の永続PTYが状態を握っていて初めて成り立つ。端末に何かを覚えておいてほしいときは、aiterm を使う。
|
|
359
359
|
|
|
360
360
|
<sub>¹ いまのハーネスは ~192 KB の出力をいったんファイルに逃がして、先頭 ~2 KB だけを見せる。そのためトークン数はほぼ並ぶ。aiterm は行数を正確に返すうえ、あとから `line_range="A:B"` で好きな範囲(先頭でも末尾でも)を取り出せる。² `rtk` の grep 縮約は長い行(~80 字)を切り詰めて、あふれを `[+N more]` にまとめる。ざっと眺めるには向くが、全行をそのまま読みたいときは組み込みツールを使う。</sub>
|
|
361
361
|
|
|
@@ -365,7 +365,7 @@ aiterm は 2 つの系譜の交点にいる——端末を操作する MCP サ
|
|
|
365
365
|
|
|
366
366
|
| | **aiterm-mcp** | 1 コマンド毎の往復<br/>(例: `mcp-server-commands`) | terminal / SSH / tmux MCP<br/>(例: `iterm-mcp`, `ssh-mcp`, `tmux-mcp`) | 共有 tmux でエージェント同士<br/>(例: `smux`) |
|
|
367
367
|
| --- | --- | --- | --- | --- |
|
|
368
|
-
| 永続セッション | ✅ tmux・再起動を跨ぐ | ❌ 毎回新シェル | ⚠️ まちまち | ✅ tmux |
|
|
368
|
+
| 永続セッション | ✅ tmux / psmux・再起動を跨ぐ | ❌ 毎回新シェル | ⚠️ まちまち | ✅ tmux |
|
|
369
369
|
| SSH / コンテナ / REPL | `pty_send` 1 回でネスト | 毎コマンド接続し直し | ⚠️ ツールが分かれがち | ✅ tmux(人が操作) |
|
|
370
370
|
| 1 コールで別エージェント起動 | ✅ `agent_launch(harness=…)` | ❌ | ❌ | ⚠️ 人が動かす tmux に CLI + skills で参加 |
|
|
371
371
|
| ヘッドレス(人が tmux に居ない) | ✅ MCP 駆動・プログラム的 | ✅ | ⚠️ まちまち | ❌ 人が tmux に居る前提 |
|
|
@@ -373,7 +373,7 @@ aiterm は 2 つの系譜の交点にいる——端末を操作する MCP サ
|
|
|
373
373
|
| トークン削減読取 | ✅ コマンド別 reducer | ❌ 生出力 | ⚠️ ほぼ無し | ❌ 生 tmux |
|
|
374
374
|
| 完了検出 | 5 層: 終了 / `mark` / `until` / 静止 / timeout | 無し(毎回ブロック) | ⚠️ プロンプト一致・脆い | ❌ エージェントがペインを読む |
|
|
375
375
|
| 破壊コマンド遮断 | ✅ tripwire(`force` で越える) | ❌ | ⚠️ まちまち | ❌ |
|
|
376
|
-
| 人が同時操作 | ✅ 共有
|
|
376
|
+
| 人が同時操作 | ✅ 共有socket/namespace(`attach`) | ❌ | ⚠️ まちまち | ✅(設計の芯) |
|
|
377
377
|
|
|
378
378
|
## aiterm の立ち位置
|
|
379
379
|
|
|
@@ -448,7 +448,7 @@ handoff contextを前置きできる。この任意経路は`throughline >= 0.9.
|
|
|
448
448
|
|
|
449
449
|
`pty_send` は送信前に破壊的コマンド(`rm -rf /`, `mkfs`, `dd of=/dev/…`, `DROP TABLE` 等)を遮断し(`force: true` で越える)、ESC・ブラケットペースト終端などをサニタイズする。`pty_read` は既定で制御文字を無害化して返す(`raw: true` はバイトをそのまま返す)。これは**サンドボックスではなく tripwire**([既知の制約](#既知の制約バグではなく仕様)参照)。
|
|
450
450
|
|
|
451
|
-
1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。全OSで長いPTY入力の欠落を避けるためUTF-8境界を壊さない256-byte単位でpasteし、chunk間に10msのdrain間隔を置く。POSIX shellが前面にいる時のsanitize済み複数行は、改行を含まない単一の`eval`入力へ符号化する。shellがscript全体を所有してから先頭行を実行するため、途中で起動したpager/REPLが後続行を対話キーとして奪わない。単一行、`raw:true`、非shell前面は従来どおり直接PTYへpasteする。agent dispatch
|
|
451
|
+
1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。全OSで長いPTY入力の欠落を避けるためplatformのmultiplexerへUTF-8境界を壊さない256-byte単位でpasteし、chunk間に10msのdrain間隔を置く。POSIX shellが前面にいる時のsanitize済み複数行は、改行を含まない単一の`eval`入力へ符号化する。shellがscript全体を所有してから先頭行を実行するため、途中で起動したpager/REPLが後続行を対話キーとして奪わない。単一行、`raw:true`、非shell前面は従来どおり直接PTYへpasteする。agent dispatchはtmux互換のbracketed paste操作(`paste-buffer -p`)を使う: bracketed paste modeを要求しているpaneへは各chunkを`ESC[200~/201~`で包んで届け、chunk投入中のキー解釈による語中文字化け・submit取り落としを抑える。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。そのsessionを `pty_close` して作り直すか、全sessionを破棄できる場合だけ `pty_kill_all` で安全に掃除する。
|
|
452
452
|
|
|
453
453
|
## 人が覗く
|
|
454
454
|
|
|
@@ -472,14 +472,14 @@ handoff contextを前置きできる。この任意経路は`throughline >= 0.9.
|
|
|
472
472
|
- **`pty_send({ rtk: true })` は単行コマンドのみ+外部 `rtk` バイナリが必要**(無ければ素通し)。一方 `pty_read({ rtk: true })` の reducer は自前実装で rtk 非依存。
|
|
473
473
|
- **`pytest` reducer は件数・罫線・`FAILURES` ブロック整形が rtk 0.42.0 と byte 一致**(回帰テストで固定)。ただし `-ra`/`-rf` 時の `FAILED` 要約行の理由は**全文を保持する**(rtk 0.42.0 は最初の `" - "` 区切りで切るが、本実装は可読性優先で情報を残すため、この行は意図的に rtk と完全一致させない)。rtk が大出力時に付ける `[full output: …]`(tee ポインタ)行は read 側では再現しない。
|
|
474
474
|
- **tmux は `-f /dev/null` 起動**なので `~/.tmux.conf` を読まない(環境差を排除するため)。
|
|
475
|
-
- **全セッションが単一
|
|
475
|
+
- **全セッションが単一multiplexer endpoint(POSIXは`claude.sock`、Windows nativeは1つのpsmux namespace)を共有する。** platformの`kill-server` commandは全セッションを消す。
|
|
476
476
|
|
|
477
477
|
## 開発
|
|
478
478
|
|
|
479
479
|
```bash
|
|
480
480
|
npm install
|
|
481
481
|
npm run build # tsc → dist/
|
|
482
|
-
npm test # build してから node:test 回帰スイート(tmux 必須)
|
|
482
|
+
npm test # build してから node:test 回帰スイート(tmux または psmux 必須)
|
|
483
483
|
npm link # ローカルで `aiterm-mcp` を PATH に
|
|
484
484
|
```
|
|
485
485
|
|
|
@@ -488,7 +488,7 @@ self-hostedのmacOS native・Linux native・Windows native・WSL2で同じ`npm t
|
|
|
488
488
|
OS別の縮小suiteで代用しません。tag起点のnpm公開は4環境greenとtagged commitの`origin/main`
|
|
489
489
|
祖先確認を通過した後だけ実行します。
|
|
490
490
|
|
|
491
|
-
|
|
491
|
+
共通進行は`src/core.ts`、harness固有は`src/harnesses/`、OS差は`src/tmux-runtime.ts`/`src/agent-resolver.ts`、reducerは`src/rtk.ts`、公開面は`src/index.ts`が所有する。設計の出発点と reducer の移植元(pytest reducer は本家 rtk 0.42.0 と一致するよう移植・ただし上記の `FAILED` 行の差異は意図的・回帰テストで固定)は `prototype/python/` を参照。
|
|
492
492
|
|
|
493
493
|
## 試す
|
|
494
494
|
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
> **Let your AI orchestrate other AIs.** One `agent_launch` call selects the execution harness separately from its model and hands you a persistent session to drive. Cursor can run GPT, Claude, or Grok while Cursor still owns the session, hooks, and transcript.
|
|
20
20
|
>
|
|
21
|
-
> **What it is:** one persistent MCP terminal your AI drives — and can launch other coding agents into. `ssh`, `docker exec`, a REPL, or another agent's TUI all nest inside that one terminal as just text you send in. The mechanism is deliberately plain — your MCP client drives the other agent's terminal turn by turn: no hidden protocol, no separate aiterm-owned shared-memory layer, no autonomous negotiation. Launched agents still read the normal project and
|
|
21
|
+
> **What it is:** one persistent MCP terminal your AI drives — and can launch other coding agents into. `ssh`, `docker exec`, a REPL, or another agent's TUI all nest inside that one terminal as just text you send in. The mechanism is deliberately plain — your MCP client drives the other agent's terminal turn by turn: no hidden protocol, no separate aiterm-owned shared-memory layer, no autonomous negotiation. Launched agents still read the normal project and harness memory/configuration that a direct CLI launch would use.
|
|
22
22
|
>
|
|
23
23
|
> **No human at a tmux required.** aiterm is driven programmatically over MCP, so an AI can launch and drive another agent with no one sitting in the terminal — from an orchestration loop, a CI step, or a cron job.
|
|
24
24
|
>
|
|
@@ -96,7 +96,7 @@ toolchain behind kitepon.dev's products.
|
|
|
96
96
|
|
|
97
97
|
Fifteen tools: six **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` — to open, drive, and read one persistent terminal; one canonical **agent launcher**, `agent_launch`, which selects `claude-code`, `codex-cli`, `grok-cli`, or `cursor-cli` as the execution harness; four deprecated launcher aliases kept for migration; `agent_configure`; `claude_turn`; `claude_approval`; and `diagnostics`. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
|
|
98
98
|
|
|
99
|
-
**v0.28.0 separates the execution harness from the model.** The harness owns the agent loop, authentication, hooks, session, and transcript; `model` is what that harness runs. Cursor Agent CLI can therefore select GPT, Claude, or Grok without changing the completion contract from Cursor hooks to another
|
|
99
|
+
**v0.28.0 separates the execution harness from the model.** The harness owns the agent loop, authentication, hooks, session, and transcript; `model` is what that harness runs. Cursor Agent CLI can therefore select GPT, Claude, or Grok without changing the completion contract from Cursor hooks to another harness's. Grok Composer is a Grok CLI model preset, not another harness: use `harness: "grok-cli", model: "grok-composer-2.5-fast"`. The old four launcher tools are thin compatibility aliases over the same implementation.
|
|
100
100
|
|
|
101
101
|
**v0.25.2 stabilizes repeated in-place configuration changes, including Grok 4.6.** If Grok Build
|
|
102
102
|
1.0.3 redraws before its `/model` success notice can be observed, aiterm confirms the requested model/effort
|
|
@@ -107,7 +107,7 @@ round a failure into success; explicit `grok-4.6` launch and configuration still
|
|
|
107
107
|
`reasoning_effort`, enforce `write_scope: "read-only"` with `--sandbox read-only`, and support
|
|
108
108
|
in-place model/effort changes through `agent_configure`. Before creating a PTY, aiterm checks an
|
|
109
109
|
explicit Grok/Composer model—and Composer's default model—against the live `grok models` catalog.
|
|
110
|
-
An unavailable model fails visibly instead of letting the
|
|
110
|
+
An unavailable model fails visibly instead of letting the harness CLI fall back to another model.
|
|
111
111
|
|
|
112
112
|
**v0.24.3 forwards explicitly selected launcher environment variables from the current MCP process.**
|
|
113
113
|
Pass variable names in `env_vars`; aiterm reads their current values at launch and injects only the
|
|
@@ -121,11 +121,11 @@ startup header has scrolled out of the captured pane, aiterm recognizes Codex by
|
|
|
121
121
|
model/effort footer together with the input prompt. An idle session is therefore configured
|
|
122
122
|
directly; callers do not need to redraw the TUI, retry, or restart the agent.
|
|
123
123
|
|
|
124
|
-
**v0.24.0 adds in-place agent configuration.** `agent_configure` uses each
|
|
124
|
+
**v0.24.0 adds in-place agent configuration.** `agent_configure` uses each harness's
|
|
125
125
|
native controls to change the model and/or reasoning effort of a running Codex or Claude
|
|
126
|
-
session while preserving its PTY,
|
|
126
|
+
session while preserving its PTY, harness session, and conversation context.
|
|
127
127
|
|
|
128
|
-
**v0.23.0 adds a local, cross-
|
|
128
|
+
**v0.23.0 adds a local, cross-harness portable fork.** Pass `throughline_source_session`
|
|
129
129
|
with a mission in `prompt` to any launcher, and aiterm asks the locally installed Throughline
|
|
130
130
|
for that session's read-only handoff context before creating the PTY. The exact returned memory
|
|
131
131
|
is prepended to the mission without moving or copying the source session's database ownership.
|
|
@@ -133,7 +133,7 @@ If Throughline is missing or returns an invalid/empty result, launch fails visib
|
|
|
133
133
|
fallback. Omitting the field preserves the ordinary clean launch.
|
|
134
134
|
|
|
135
135
|
**v0.22.0 makes launched agents full project collaborators.** All four launchers now use the
|
|
136
|
-
same normal `HOME`, working tree,
|
|
136
|
+
same normal `HOME`, working tree, harness home, project/user/local configuration, MCP servers,
|
|
137
137
|
plugins, skills, permissions, trust, memory, and session history as a direct CLI launch. Aiterm
|
|
138
138
|
isolates only its own per-launch completion correlation state. Every child is told that it is a
|
|
139
139
|
sub-agent and receives its parent session, delegation depth, lineage, and
|
|
@@ -150,7 +150,7 @@ structured launch receipts so a supplied scope and its enforcement status are re
|
|
|
150
150
|
v0.20.3 prevents concurrent
|
|
151
151
|
correlated Claude/Fable sessions from turning one broken login into many competing login
|
|
152
152
|
flows. Every new Claude launch verifies the
|
|
153
|
-
|
|
153
|
+
harness-owned shared credential store before creating a PTY, while healthy credentials
|
|
154
154
|
remain reusable across concurrent and repeated sessions. The v0.20 line also distinguishes
|
|
155
155
|
a non-blocking `aiterm-wait --timeout 0` observation (`running`, exit 5) from a real timed-out
|
|
156
156
|
wait. The v0.19 line added the correlated Claude approval relay,
|
|
@@ -228,14 +228,14 @@ The canonical harness choices are:
|
|
|
228
228
|
|
|
229
229
|
`env_vars` is an allowlist of environment-variable **names**, not a name/value map. At launch,
|
|
230
230
|
aiterm reads each valid name from its current MCP process, shell-quotes present values, and places
|
|
231
|
-
them on that one
|
|
231
|
+
them on that one harness launch command. Missing names are omitted; invalid shell variable names
|
|
232
232
|
fail before session creation. There is no implicit whole-environment copy, tmux-server restart,
|
|
233
233
|
retry, or fallback. Values do not enter the MCP tool arguments, but they are delivered through the
|
|
234
|
-
PTY launch command and retained in aiterm's per-session `.lastcmd`; the launched
|
|
234
|
+
PTY launch command and retained in aiterm's per-session `.lastcmd`; the launched harness and other
|
|
235
235
|
processes with access to the same OS user may read them. Use this for non-secret seat identity and
|
|
236
236
|
workflow variables, not as a secret transport.
|
|
237
237
|
|
|
238
|
-
The selected harness CLI must be installed and authenticated. Aiterm resolves `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN` / `CURSOR_AGENT_BIN`, then the documented default binary, then `PATH`. Cursor resolution deliberately uses `cursor-agent`, never the ambiguous `agent` name. Claude and Cursor authentication are checked before a PTY exists, so a failed preflight leaves no session. All harnesses use their normal
|
|
238
|
+
The selected harness CLI must be installed and authenticated. Aiterm resolves `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN` / `CURSOR_AGENT_BIN`, then the documented default binary, then `PATH`. Cursor resolution deliberately uses `cursor-agent`, never the ambiguous `agent` name. Claude and Cursor authentication are checked before a PTY exists, so a failed preflight leaves no session. All harnesses use their normal harness-owned credential and configuration stores in place.
|
|
239
239
|
|
|
240
240
|
Portable fork is optional. When `throughline_source_session` is present, `prompt` is the required
|
|
241
241
|
new mission and `launch_operation_id` cannot be combined with it. aiterm resolves Throughline via
|
|
@@ -330,9 +330,9 @@ This registers it in `~/.claude.json`; you'll get an approval prompt the first t
|
|
|
330
330
|
|
|
331
331
|
## Headless: no human at the terminal
|
|
332
332
|
|
|
333
|
-
Because an MCP client drives aiterm programmatically over stdio, everything above can run with **nobody sitting at
|
|
333
|
+
Because an MCP client drives aiterm programmatically over stdio, everything above can run with **nobody sitting at the terminal**. Any MCP-capable orchestrator can call `agent_launch` — including a harness matching itself — then `pty_read` the result and act on it unattended. That makes aiterm a fit for exactly the places a human-driven terminal isn't:
|
|
334
334
|
|
|
335
|
-
- **Multi-agent orchestration** — an orchestrator hands sub-tasks to Claude / Codex / Grok /
|
|
335
|
+
- **Multi-agent orchestration** — an orchestrator hands sub-tasks to Claude Code / Codex / Grok / Cursor harnesses, each in its own persistent session, and reads them all back. Composer remains a Grok CLI model preset.
|
|
336
336
|
- **CI** — a job step can spin up an agent, drive it, and tear it down.
|
|
337
337
|
- **cron** — a scheduled run can launch an agent and collect its output.
|
|
338
338
|
|
|
@@ -344,7 +344,7 @@ The terminal is real and shared, so a human *can* jump in ([A human can watch](#
|
|
|
344
344
|
flowchart LR
|
|
345
345
|
AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · agent_launch · agent_configure · claude_turn · claude_approval<br/>legacy launcher aliases · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 15 tools"]
|
|
346
346
|
S -->|"pty_read<br/>token-reduced"| AI
|
|
347
|
-
S -->|"tmux
|
|
347
|
+
S -->|"tmux / psmux<br/>send · capture"| P["persistent PTYs<br/>survive restarts"]
|
|
348
348
|
P -->|"ssh · docker · repl"| R["nested<br/>remote · container · REPL"]
|
|
349
349
|
P -->|"launches a fresh PTY per agent"| A["another coding-agent harness<br/>Claude Code · Codex CLI · Grok CLI · Cursor CLI"]
|
|
350
350
|
```
|
|
@@ -380,10 +380,10 @@ aiterm also holds state across calls. The built-in tool runs each call in a fres
|
|
|
380
380
|
|
|
381
381
|
```text
|
|
382
382
|
built-in shell → var= # empty; env dropped, cwd back at project root
|
|
383
|
-
aiterm → cwd=/tmp var=hello123 # one
|
|
383
|
+
aiterm → cwd=/tmp var=hello123 # one persistent PTY holds both
|
|
384
384
|
```
|
|
385
385
|
|
|
386
|
-
`cd` then set env then build, `ssh` once then run ten commands on the authenticated session, drive a live REPL or a launched agent's TUI turn by turn — one
|
|
386
|
+
`cd` then set env then build, `ssh` once then run ten commands on the authenticated session, drive a live REPL or a launched agent's TUI turn by turn — one persistent PTY holds all of it. Reach for aiterm when the terminal has to remember something.
|
|
387
387
|
|
|
388
388
|
<sub>¹ Today's harness auto-offloads the ~192 KB dump to a file and previews only a ~2 KB head, so the token counts nearly tie; aiterm reports the accurate line count and lets `line_range="A:B"` pull any slice later, head or tail. ² The `rtk` grep reducer truncates long lines (~80 chars) and folds the overflow into `[+N more]`, which suits scanning; use the built-in tool when you need every full line.</sub>
|
|
389
389
|
|
|
@@ -393,7 +393,7 @@ aiterm sits at the intersection of two families: terminal-driving MCP servers, a
|
|
|
393
393
|
|
|
394
394
|
| | **aiterm-mcp** | one-shot shell MCP<br/>(e.g. `mcp-server-commands`) | terminal / SSH / tmux MCPs<br/>(e.g. `iterm-mcp`, `ssh-mcp`, `tmux-mcp`) | shared-tmux agent-to-agent<br/>(e.g. `smux`) |
|
|
395
395
|
| --- | --- | --- | --- | --- |
|
|
396
|
-
| Persistent session | ✅ tmux, survives restarts | ❌ new shell every call | ⚠️ varies | ✅ tmux |
|
|
396
|
+
| Persistent session | ✅ tmux / psmux, survives restarts | ❌ new shell every call | ⚠️ varies | ✅ tmux |
|
|
397
397
|
| SSH / containers / REPLs | nest with one `pty_send` | reconnect every command | ⚠️ often separate tools | ✅ tmux (human drives) |
|
|
398
398
|
| Launch another agent in one call | ✅ `agent_launch(harness=…)` | ❌ | ❌ | ⚠️ agents join a human-run tmux via a CLI + skills |
|
|
399
399
|
| Headless (no human at a tmux) | ✅ MCP-driven, programmatic | ✅ | ⚠️ varies | ❌ built around a human in the tmux |
|
|
@@ -401,7 +401,7 @@ aiterm sits at the intersection of two families: terminal-driving MCP servers, a
|
|
|
401
401
|
| Token-reduced reads | ✅ per-command reducers | ❌ raw output | ⚠️ rarely | ❌ raw tmux |
|
|
402
402
|
| Completion detection | 5-layer: exit / `mark` / `until` / quiescence / timeout | n/a (blocks per call) | ⚠️ prompt-match, fragile | ❌ agent reads the pane |
|
|
403
403
|
| Destructive-command gate | ✅ tripwire (override with `force`) | ❌ | ⚠️ varies | ❌ |
|
|
404
|
-
| Human can co-drive | ✅ shared
|
|
404
|
+
| Human can co-drive | ✅ shared socket / namespace (`attach`) | ❌ | ⚠️ varies | ✅ (its core model) |
|
|
405
405
|
|
|
406
406
|
## Where aiterm fits
|
|
407
407
|
|
|
@@ -461,18 +461,18 @@ cannot be combined with `launch_operation_id`, and leaves the source session's d
|
|
|
461
461
|
unchanged. Throughline is resolved through `THROUGHLINE_BIN` and then `PATH`; a missing or invalid
|
|
462
462
|
export fails before the PTY exists instead of silently launching clean.
|
|
463
463
|
|
|
464
|
-
When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lines), callers recover it in full with `pty_read({ agent_transcript: true })`. It returns the most recently completed turn's final assistant message in plain text with no re-prompting. Claude reads the bounded owner-only result captured by the launch-correlated Stop hook and verifies its digest/byte count; it never reads Claude's private transcript. Durable machine callers should use `claude_turn`: `issue` sends once, `recover` never sends, `pending` is distinct from unsafe or malformed state, and only `completed` carries the exact verified `raw_output`. Codex uses the normal rollout transcript's `task_complete.turn_id`; Grok/Composer use their normal session history after the last real user row. Missing or ambiguous attribution remains an explicit error.
|
|
464
|
+
When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lines), callers recover it in full with `pty_read({ agent_transcript: true })`. It returns the most recently completed turn's final assistant message in plain text with no re-prompting. Claude reads the bounded owner-only result captured by the launch-correlated Stop hook and verifies its digest/byte count; it never reads Claude's private transcript. Durable machine callers should use `claude_turn`: `issue` sends once, `recover` never sends, `pending` is distinct from unsafe or malformed state, and only `completed` carries the exact verified `raw_output`. Codex uses the normal rollout transcript's `task_complete.turn_id`; Grok/Composer use their normal session history after the last real user row; Cursor uses the normal agent transcript bound to the launch ID and current turn. Missing or ambiguous attribution remains an explicit error.
|
|
465
465
|
|
|
466
466
|
### Completion detection (5 layers)
|
|
467
467
|
|
|
468
|
-
`pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel / an `until` match / output quiescence with shell return / timeout. `mark` emits the shell's exit status on POSIX shells and `0` (success) or `1` (failure) on PowerShell; fish/csh/tcsh are rejected before send because they do not share either status syntax. When `mark` or `until` is active, that requested evidence takes precedence and a momentarily quiet shell cannot complete the read as quiescent. Agent sessions add a sixth exact layer: Codex observes normal rollout `task_complete`; Grok/Composer observe normal session `turn_ended`; Claude observes its additive launch-correlated Stop event. `aiterm-wait --cursor` performs that
|
|
468
|
+
`pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel / an `until` match / output quiescence with shell return / timeout. `mark` emits the shell's exit status on POSIX shells and `0` (success) or `1` (failure) on PowerShell; fish/csh/tcsh are rejected before send because they do not share either status syntax. When `mark` or `until` is active, that requested evidence takes precedence and a momentarily quiet shell cannot complete the read as quiescent. Agent sessions add a sixth exact layer: Codex observes normal rollout `task_complete`; Grok/Composer observe normal session `turn_ended`; Claude observes its additive launch-correlated Stop event; Cursor observes `turn_ended(status:"success")` in the launch-bound normal agent transcript. `aiterm-wait --cursor` performs that harness-specific observation without the parent blocking or polling. Pre-send readiness failures are MCP errors, and late completion remains recoverable without resending.
|
|
469
469
|
|
|
470
470
|
### Completion push for parent agents (`aiterm-wait`)
|
|
471
471
|
|
|
472
472
|
As of v0.16 a parent agent **never blocks** on aiterm — there is no wait parameter anywhere (v0.17 makes the waiter's exit codes mirror its outcome). The whole flow is dispatch + one universal waiter:
|
|
473
473
|
|
|
474
474
|
1. Launch the child with `agent_launch({ harness: ... })`; every launch shares the normal project/user environment and adds only completion correlation plus lineage. Send a turn with plain `pty_send` (or `claude_turn issue` for durable Claude operations). The call returns immediately with an `event_cursor` in its structured receipt.
|
|
475
|
-
2. Run `aiterm-wait --session <id> --cursor <event_cursor> [--operation sha256:<64hex>] [--timeout <sec>]`. It observes the
|
|
475
|
+
2. Run `aiterm-wait --session <id> --cursor <event_cursor> [--operation sha256:<64hex>] [--timeout <sec>]`. It observes the harness-owned completion source, plus Claude's additive launch hook, as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt. **Exit ≠ done**: the receipt's `outcome` is authoritative (`0` = `done`, `3` = `timeout`, `4` = `closed`, `1` = error).
|
|
476
476
|
3. **The parent never runs the waiter in its own foreground.** Waiting is correct — but the waiter is a separate process, not the parent's turn. A harness that re-invokes its agent when a background task exits (Claude Code) runs the waiter **in the background** and gets woken with zero polling. So that this is not left to interpretation, aiterm reads `clientInfo.name` from the MCP `initialize` handshake and its receipts name the concrete invocation for the detected host — for Claude Code, literally `Bash(command: "aiterm-wait …", run_in_background: true)`. Unknown or undeclared hosts get the generic "start it as a process that does not block the parent's turn" wording; nothing else about the contract changes. Every receipt leads with the same rule: dispatch and let go, then go do something else or end the turn.
|
|
477
477
|
4. Collect the result exactly as before: `pty_read(agent_transcript: true)`, or `claude_turn recover` for durable Claude operations. The waiter carries the signal, never the payload.
|
|
478
478
|
|
|
@@ -490,18 +490,18 @@ As of v0.16 a parent agent **never blocks** on aiterm — there is no wait param
|
|
|
490
490
|
|
|
491
491
|
Before sending, `pty_send` blocks destructive commands (`rm -rf /`, `mkfs`, `dd of=/dev/…`, `DROP TABLE`, …) — pass `force: true` to override — and sanitizes ESC / bracketed-paste terminators. `pty_read` neutralizes control characters in what it returns by default (`raw: true` returns the bytes verbatim). This is a **tripwire, not a sandbox** (see [Known constraints](#known-constraints-by-design-not-bugs)).
|
|
492
492
|
|
|
493
|
-
Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. Every OS pastes through
|
|
493
|
+
Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. Every OS pastes through its multiplexer in UTF-8-safe 256-byte chunks with a 10 ms drain interval; macOS, Linux, and WSL2 have all demonstrated silent middle/trailing loss when a long input is pushed without that boundary. Sanitized multiline text sent while a POSIX shell is in the foreground is encoded as one newline-free `eval` input: the shell receives the complete script before it runs the first line, so a pager or REPL started mid-script cannot consume later lines as interactive keystrokes. Single-line input, `raw:true`, and non-shell frontends remain direct PTY pastes. Agent dispatches additionally use the tmux-compatible bracketed-paste operation (`paste-buffer -p`): panes that requested bracketed-paste mode receive each chunk wrapped in `ESC[200~/201~`, hardening prompt injection against mid-word key-interpretation corruption and dropped submits. If a later chunk fails, aiterm reports the partial-send state and does not press Enter automatically. A lock left by a terminated sender fails closed before sending; close and recreate that session (or use `pty_kill_all` when every session is disposable) to clean it up safely.
|
|
494
494
|
|
|
495
495
|
## A human can watch
|
|
496
496
|
|
|
497
|
-
Sessions live on a shared tmux socket
|
|
497
|
+
Sessions live on a shared tmux socket on POSIX or a shared psmux namespace on native Windows. The attach line printed by `pty_open` and `agent_launch` lets a human attach to the same terminal and intervene, including a Claude/Codex/Grok/Cursor harness session: `tmux -S … attach -t <id>` on POSIX, or `psmux -L <namespace> attach -t <id>` on native Windows.
|
|
498
498
|
|
|
499
499
|
## Requirements
|
|
500
500
|
|
|
501
501
|
- **Node.js >= 18**
|
|
502
|
-
- **tmux** (runtime prerequisite
|
|
502
|
+
- **tmux or psmux** (platform runtime prerequisite)
|
|
503
503
|
- **macOS / Linux / WSL2** run tmux directly. On macOS install it with `brew install tmux` (stock macOS ships none). If your MCP client is launched from the **GUI** rather than a terminal, Homebrew's bin (`/opt/homebrew/bin` on Apple Silicon, `/usr/local/bin` on Intel) may be off its `PATH`; aiterm auto-searches those locations, or set **`AITERM_TMUX=/path/to/tmux`** to point at it explicitly.
|
|
504
|
-
- **Native Windows** has no tmux, so aiterm drives [psmux](https://github.com/psmux/psmux) — a tmux-CLI-compatible native multiplexer — with a per-install `-L` namespace. **No WSL is required.** Install psmux **3.3.8 or newer** (`winget install marlocarlo.psmux`; 3.3.8 is the first release whose `pipe-pane` file sink, byte-exact `paste-buffer` wire, and foreground `#{pane_current_command}` behave the way aiterm's capture/dispatch paths rely on), plus [Git for Windows](https://gitforwindows.org/) whose `bash.exe` becomes the pane shell (System32's `bash.exe` is the WSL launcher and is deliberately not used). Override resolution with **`AITERM_PSMUX`** / **`AITERM_BASH`** when the binaries live elsewhere. You reach Windows tools the same way you reach SSH: `pty_send "powershell.exe …"` nests into PowerShell. `
|
|
504
|
+
- **Native Windows** has no tmux, so aiterm drives [psmux](https://github.com/psmux/psmux) — a tmux-CLI-compatible native multiplexer — with a per-install `-L` namespace. **No WSL is required.** Install psmux **3.3.8 or newer** (`winget install marlocarlo.psmux`; 3.3.8 is the first release whose `pipe-pane` file sink, byte-exact `paste-buffer` wire, and foreground `#{pane_current_command}` behave the way aiterm's capture/dispatch paths rely on), plus [Git for Windows](https://gitforwindows.org/) whose `bash.exe` becomes the pane shell (System32's `bash.exe` is the WSL launcher and is deliberately not used). Override resolution with **`AITERM_PSMUX`** / **`AITERM_BASH`** when the binaries live elsewhere. You reach Windows tools the same way you reach SSH: `pty_send "powershell.exe …"` nests into PowerShell. The `grok-cli` harness and its deprecated aliases launch the **Windows-native** Grok CLI (`%USERPROFILE%\.grok\bin\grok.exe`, or `GROK_BIN` pointing at a `.exe`) as a Windows process, and a WSL-side grok is rejected before a session is created so product auth and session records never split across an OS boundary.
|
|
505
505
|
- For **agent harnesses**: the selected CLI, installed and authenticated through its product owner's official path — `claude`, `codex`, `grok`, or Cursor's `cursor-agent`. Portable fork additionally needs `throughline >= 0.9.0`; ordinary clean launch does not. (Not needed if you only use the PTY tools.)
|
|
506
506
|
- Optional: the [`rtk`](https://github.com/rtk-ai/rtk) binary (used by `pty_send`'s `rtk: true` delegation; works fine without it)
|
|
507
507
|
|
|
@@ -514,14 +514,14 @@ Sessions live on a shared tmux socket. The `tmux -S … attach -t <id>` line pri
|
|
|
514
514
|
- **`pty_send({ rtk: true })` is single-line only and needs the external `rtk` binary** (passthrough without it). The `pty_read({ rtk: true })` reducer, by contrast, is self-contained and rtk-independent.
|
|
515
515
|
- **The `pytest` reducer matches rtk 0.42.0** on test counts, the rule line, and `FAILURES`-block formatting (locked by regression tests). It **deliberately preserves the full failure reason** on the `FAILED` summary lines (emitted under `-ra`/`-rf`), whereas rtk 0.42.0 truncates the reason at the first `" - "` — a readability choice, so those lines are intentionally not byte-identical to rtk. The `[full output: …]` tee-pointer line rtk appends on large output is not reproduced on the read side.
|
|
516
516
|
- **tmux is started with `-f /dev/null`**, so it does not read `~/.tmux.conf` (to keep behavior reproducible across machines).
|
|
517
|
-
- **All sessions
|
|
517
|
+
- **All sessions share one multiplexer endpoint** (`claude.sock` on POSIX, one psmux namespace on native Windows). The platform's `kill-server` command removes them all.
|
|
518
518
|
|
|
519
519
|
## Development
|
|
520
520
|
|
|
521
521
|
```bash
|
|
522
522
|
npm install
|
|
523
523
|
npm run build # tsc → dist/
|
|
524
|
-
npm test # build, then the node:test regression suite (requires tmux)
|
|
524
|
+
npm test # build, then the node:test regression suite (requires tmux or psmux)
|
|
525
525
|
npm link # put `aiterm-mcp` on PATH locally
|
|
526
526
|
```
|
|
527
527
|
|
|
@@ -531,7 +531,7 @@ runners; it does not replace any OS with a reduced suite. Tag-triggered npm publ
|
|
|
531
531
|
after all four environments pass and the tagged commit is confirmed on `origin/main`. The native
|
|
532
532
|
Windows runner needs psmux ≥ 3.3.8 and Git for Windows on its PATH, and must run as an
|
|
533
533
|
interactive Windows user; `NETWORK SERVICE` lacks the per-user environment the pane shell and
|
|
534
|
-
|
|
534
|
+
harness CLIs rely on and is not a valid runner identity (the separate WSL2 runner still owns the
|
|
535
535
|
initialized WSL distro).
|
|
536
536
|
|
|
537
537
|
Logic lives in `src/core.ts` (tmux control, reduction, completion detection, safety, agent launch) and `src/rtk.ts` (per-command reducers); `src/index.ts` is the MCP surface. The design origin and the reducer's porting source (the pytest reducer is ported to match upstream rtk 0.42.0, except the deliberate `FAILED`-line difference noted above, and is locked by regression tests) are in `prototype/python/`.
|
|
@@ -552,7 +552,7 @@ If aiterm let your AI hand a task to another agent — or saved you a round-trip
|
|
|
552
552
|
## Shared agent environment
|
|
553
553
|
|
|
554
554
|
All harnesses use the caller's normal project and user environment. Aiterm does not copy,
|
|
555
|
-
symlink, filter, or replace
|
|
555
|
+
symlink, filter, or replace harness configuration, authentication, MCP, plugin, skill, permission,
|
|
556
556
|
trust, memory, or history stores. Cleanup removes only aiterm-owned launch metadata and completion
|
|
557
557
|
correlation files.
|
|
558
558
|
|
package/dist/agent-resolver.js
CHANGED
|
@@ -28,7 +28,7 @@ export function isWindowsNativeExecutable(candidate) {
|
|
|
28
28
|
// Windows の bin 受入: native 実行ファイル(.exe/.cmd/.bat)に加え、pane shell
|
|
29
29
|
// (Git Bash)が shebang で実行できる script も実在すれば受け入れる。旧 WSL 側
|
|
30
30
|
// バイナリ検査への黙ったフォールバックは廃止(別 HOME・別 auth の subagent を
|
|
31
|
-
// 作るため)。native 実行ファイルの強制が要る
|
|
31
|
+
// 作るため)。native 実行ファイルの強制が要る harness(grok/composer の実効
|
|
32
32
|
// sandbox 等)は openAgent 側の専用ゲートが明示エラーで担う。
|
|
33
33
|
export function isUsableAgentExecutableFile(candidate) {
|
|
34
34
|
if (!isWin)
|
package/dist/agent-shared.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
// tmux-runtime / errors 以外の内部moduleへ依存しない(依存方向: core →
|
|
1
|
+
// harness中立の共有プリミティブ。core と harnesses/ の両方が依存する最下層で、
|
|
2
|
+
// tmux-runtime / errors 以外の内部moduleへ依存しない(依存方向: core → harnesses → agent-shared)。
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
@@ -200,7 +200,7 @@ export function subagentInstruction(meta) {
|
|
|
200
200
|
].join("\n");
|
|
201
201
|
}
|
|
202
202
|
// launch noteのwrite_scope説明。文言はkindに依存する分岐まで含めて単一実装で持つ
|
|
203
|
-
// (
|
|
203
|
+
// (harness別noteへ複製すると文言が発散する)。
|
|
204
204
|
export function writeScopeLaunchNote(kind, writeScope) {
|
|
205
205
|
return writeScope === undefined
|
|
206
206
|
? ""
|
package/dist/aiterm-wait-cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// aiterm-wait — agent turn 完了eventの純リーダー観測CLI。
|
|
3
3
|
// 完了/timeout/close を1行のJSON receiptで返してexitする。lock・PTY・dispatch状態には一切触れない。
|
|
4
4
|
// 親AIホストのバックグラウンドタスクとして起動し、exitを「観測終了の通知」として使う。
|
|
5
|
-
// exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=
|
|
5
|
+
// exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=harness利用上限 / 1=エラー)。
|
|
6
6
|
// --timeout 0 は待たずに一度だけ観測する照会で、未完了は running(timeout と混同させない)。
|
|
7
7
|
// receipt の outcome が正で、done 以外は未完了。timeout の既定は core の DEFAULT_AGENT_DONE_TIMEOUT(600秒)。
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
package/dist/core.js
CHANGED
|
@@ -15,10 +15,10 @@ import * as rtk from "./rtk.js";
|
|
|
15
15
|
import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
|
|
16
16
|
import { isWin, SOCKDIR, tmuxCommand, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
|
|
17
17
|
import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
|
|
18
|
-
import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, grokInitializationComplete, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./
|
|
19
|
-
import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./
|
|
20
|
-
import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, } from "./
|
|
21
|
-
import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, CURSOR_COMPOSER_MARKER_RE, validateCursorModelEffort, } from "./
|
|
18
|
+
import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, grokInitializationComplete, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
|
|
19
|
+
import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
|
|
20
|
+
import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, } from "./harnesses/claude.js";
|
|
21
|
+
import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, CURSOR_COMPOSER_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
|
|
22
22
|
import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
|
|
23
23
|
export { AitermError } from "./errors.js";
|
|
24
24
|
export { tmuxSpawnEnv } from "./tmux-runtime.js";
|
|
@@ -1789,18 +1789,18 @@ function scanAgentDoneLines(lines, meta, expectedOperationId = null) {
|
|
|
1789
1789
|
if (expectedOperationId && ev.operation_id !== expectedOperationId)
|
|
1790
1790
|
continue;
|
|
1791
1791
|
if (meta.vendor_session_id)
|
|
1792
|
-
return { event: ev, malformedEvents,
|
|
1792
|
+
return { event: ev, malformedEvents, ambiguousHarnessSession: false };
|
|
1793
1793
|
if (candidate?.vendor_session_id &&
|
|
1794
1794
|
ev.vendor_session_id &&
|
|
1795
1795
|
candidate.vendor_session_id !== ev.vendor_session_id) {
|
|
1796
|
-
return { event: null, malformedEvents,
|
|
1796
|
+
return { event: null, malformedEvents, ambiguousHarnessSession: true };
|
|
1797
1797
|
}
|
|
1798
1798
|
if (!candidate)
|
|
1799
1799
|
candidate = ev;
|
|
1800
1800
|
}
|
|
1801
|
-
return { event: candidate, malformedEvents,
|
|
1801
|
+
return { event: candidate, malformedEvents, ambiguousHarnessSession: false };
|
|
1802
1802
|
}
|
|
1803
|
-
function
|
|
1803
|
+
function bindAgentHarnessSession(meta, ev) {
|
|
1804
1804
|
if (!meta.vendor_session_id && ev.vendor_session_id) {
|
|
1805
1805
|
meta.vendor_session_id = ev.vendor_session_id;
|
|
1806
1806
|
}
|
|
@@ -1816,7 +1816,7 @@ function bindCompletedInitialPrompt(meta) {
|
|
|
1816
1816
|
if (!done) {
|
|
1817
1817
|
throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
|
|
1818
1818
|
}
|
|
1819
|
-
|
|
1819
|
+
bindAgentHarnessSession(meta, done);
|
|
1820
1820
|
setInitialPromptState(meta, "done");
|
|
1821
1821
|
return;
|
|
1822
1822
|
}
|
|
@@ -1825,7 +1825,7 @@ function bindCompletedInitialPrompt(meta) {
|
|
|
1825
1825
|
if (!done) {
|
|
1826
1826
|
throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
|
|
1827
1827
|
}
|
|
1828
|
-
|
|
1828
|
+
bindAgentHarnessSession(meta, done);
|
|
1829
1829
|
setInitialPromptState(meta, "done");
|
|
1830
1830
|
return;
|
|
1831
1831
|
}
|
|
@@ -1844,7 +1844,7 @@ function bindCompletedInitialPrompt(meta) {
|
|
|
1844
1844
|
const lines = text.split("\n");
|
|
1845
1845
|
const tail = lines.pop() ?? "";
|
|
1846
1846
|
const scanned = scanAgentDoneLines(lines, meta);
|
|
1847
|
-
if (scanned.
|
|
1847
|
+
if (scanned.ambiguousHarnessSession) {
|
|
1848
1848
|
throw new AitermError("agent event file に複数の vendor_session_id が混在しています。該当セッションを閉じて起動し直してください。", 2);
|
|
1849
1849
|
}
|
|
1850
1850
|
if (!scanned.event) {
|
|
@@ -1852,7 +1852,7 @@ function bindCompletedInitialPrompt(meta) {
|
|
|
1852
1852
|
const partial = tail.trim() ? " partial_event=true" : "";
|
|
1853
1853
|
throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了 event をまだ確認できません。${agentWaitGuide(meta.aiterm_session)}${malformed}${partial}`, 2);
|
|
1854
1854
|
}
|
|
1855
|
-
|
|
1855
|
+
bindAgentHarnessSession(meta, scanned.event);
|
|
1856
1856
|
setInitialPromptState(meta, "done");
|
|
1857
1857
|
}
|
|
1858
1858
|
function tryLoadAgentMetadata(name) {
|
|
@@ -1923,7 +1923,7 @@ function completedClaudeOperationEvent(meta, operationId) {
|
|
|
1923
1923
|
}
|
|
1924
1924
|
return match;
|
|
1925
1925
|
}
|
|
1926
|
-
function
|
|
1926
|
+
function recoverAgentHarnessSession(meta) {
|
|
1927
1927
|
if (meta.vendor_session_id)
|
|
1928
1928
|
return;
|
|
1929
1929
|
if (meta.kind === "codex") {
|
|
@@ -1952,12 +1952,12 @@ function recoverAgentVendorSession(meta) {
|
|
|
1952
1952
|
const lines = text.split("\n");
|
|
1953
1953
|
lines.pop(); // hook は newline 完結eventだけを確定済みとして扱う。
|
|
1954
1954
|
const scanned = scanAgentDoneLines(lines, meta);
|
|
1955
|
-
if (scanned.
|
|
1955
|
+
if (scanned.ambiguousHarnessSession) {
|
|
1956
1956
|
throw new AitermError("agent event file に複数の vendor_session_id が混在しています。該当セッションを閉じて起動し直してください。", 2);
|
|
1957
1957
|
}
|
|
1958
1958
|
if (!scanned.event?.vendor_session_id)
|
|
1959
1959
|
return;
|
|
1960
|
-
|
|
1960
|
+
bindAgentHarnessSession(meta, scanned.event);
|
|
1961
1961
|
writeAgentMetadata(meta);
|
|
1962
1962
|
}
|
|
1963
1963
|
function agentCompletionCursor(meta) {
|
|
@@ -2021,7 +2021,7 @@ async function settlePublishedClaudeCompletionMarker(meta, marker) {
|
|
|
2021
2021
|
}
|
|
2022
2022
|
return active;
|
|
2023
2023
|
}
|
|
2024
|
-
/** agent
|
|
2024
|
+
/** agent harness の構造化 transcript から直近完了ターンの最終回答を読む。 */
|
|
2025
2025
|
export async function readAgentTranscript(name, o = {}) {
|
|
2026
2026
|
const meta = loadAgentMetadata(name);
|
|
2027
2027
|
const operationId = o.operation_id == null ? null : validateOperationId(o.operation_id);
|
|
@@ -2038,8 +2038,8 @@ export async function readAgentTranscript(name, o = {}) {
|
|
|
2038
2038
|
}
|
|
2039
2039
|
}
|
|
2040
2040
|
// wait timeout は「失敗」ではなく状態不明。後着した同一launchの完了eventから
|
|
2041
|
-
//
|
|
2042
|
-
|
|
2041
|
+
// harness session をbindし、promptを再送せず結果だけ回収できるようにする。
|
|
2042
|
+
recoverAgentHarnessSession(meta);
|
|
2043
2043
|
if (!meta.vendor_session_id) {
|
|
2044
2044
|
throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。${agentWaitGuide(name)}`, 2);
|
|
2045
2045
|
}
|
|
@@ -2160,7 +2160,7 @@ export function agentWaitGuide(session) {
|
|
|
2160
2160
|
const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
|
|
2161
2161
|
return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
|
|
2162
2162
|
}
|
|
2163
|
-
//
|
|
2163
|
+
// harness 別の利用上限バナー。検知は「報告」専用で、完了判定や自動復旧には使わない。
|
|
2164
2164
|
// 出典(2026-08-22): grok は live 実バナーで検証、codex/claude はインストール済み実バイナリの
|
|
2165
2165
|
// 埋込文字列から抽出(codex: "You've hit your usage limit for" / claude: "Usage limit reached ·
|
|
2166
2166
|
// continuing automatically when it resets"。Claude Code はリセット時に自動継続する設計なので、
|
|
@@ -2172,7 +2172,7 @@ const AGENT_RATE_LIMIT_PATTERNS = {
|
|
|
2172
2172
|
claude: [/Usage limit reached/i],
|
|
2173
2173
|
};
|
|
2174
2174
|
const AGENT_RATE_LIMIT_SCAN_BYTES = 16 * 1024;
|
|
2175
|
-
// pane log の末尾から上限バナーを探す。読めない・無い・対象
|
|
2175
|
+
// pane log の末尾から上限バナーを探す。読めない・無い・対象 harness でないは全て null(誤検知より取りこぼし側へ倒す)。
|
|
2176
2176
|
export function detectAgentRateLimit(kind, aitermSession) {
|
|
2177
2177
|
const patterns = AGENT_RATE_LIMIT_PATTERNS[kind];
|
|
2178
2178
|
if (!patterns)
|
|
@@ -2264,7 +2264,7 @@ export async function observeAgentDone(name, o = {}) {
|
|
|
2264
2264
|
carry = parts.pop() ?? "";
|
|
2265
2265
|
const scanned = scanAgentDoneLines(parts, meta, operationId);
|
|
2266
2266
|
malformedEvents += scanned.malformedEvents;
|
|
2267
|
-
if (scanned.
|
|
2267
|
+
if (scanned.ambiguousHarnessSession) {
|
|
2268
2268
|
throw new AitermError("agent event file に複数の vendor_session_id が混在しています。該当セッションを閉じて起動し直してください。", 2);
|
|
2269
2269
|
}
|
|
2270
2270
|
if (scanned.event)
|
|
@@ -2578,7 +2578,7 @@ async function waitForGrokConfigurationResult(name, before, model, effort, timeo
|
|
|
2578
2578
|
if (added.some((line) => /^(?:Switched to |✓?\s*Default model:)/.test(line)))
|
|
2579
2579
|
return;
|
|
2580
2580
|
// Grok Build 1.0.3では成功通知が次の再描画で消えることがある。変更前には無かった
|
|
2581
|
-
// target model/effortが常駐footerへ現れた場合も、
|
|
2581
|
+
// target model/effortが常駐footerへ現れた場合も、harness自身の最終状態として受理する。
|
|
2582
2582
|
if (!footerAlreadyMatched && grokFooterHasConfiguration(screen, model, effort))
|
|
2583
2583
|
return;
|
|
2584
2584
|
await sleep(100);
|
|
@@ -2591,7 +2591,7 @@ function sendMenuChoice(name, choice) {
|
|
|
2591
2591
|
throw new AitermError(`agent設定の選択を送れませんでした: ${sent.stderr.trim() || `code=${sent.code}`}`, 2);
|
|
2592
2592
|
}
|
|
2593
2593
|
}
|
|
2594
|
-
/** 同じ対話sessionを保ったまま、
|
|
2594
|
+
/** 同じ対話sessionを保ったまま、harness標準の操作でmodel/effortを変更する。 */
|
|
2595
2595
|
export async function configureAgent(name, opts) {
|
|
2596
2596
|
assertSessionName(name);
|
|
2597
2597
|
const model = opts.model?.trim() || null;
|
|
@@ -2738,7 +2738,7 @@ export function __testCodexConfigureChoices(screen, model, effort) {
|
|
|
2738
2738
|
};
|
|
2739
2739
|
}
|
|
2740
2740
|
// v0.16.0: 親をブロックする wait 経路は廃止した。send は ready gate と submit 分離を内蔵した
|
|
2741
|
-
// dispatch として即返り、event_cursor(送信直前の
|
|
2741
|
+
// dispatch として即返り、event_cursor(送信直前のharness完了正本境界)を receipt で返す。
|
|
2742
2742
|
// 完了通知は aiterm-wait(--cursor で境界を渡す)、回収は pty_read / claude_turn recover が担う。
|
|
2743
2743
|
export async function dispatchAgentTurn(name, text, o = {}) {
|
|
2744
2744
|
assertSessionName(name);
|
|
@@ -2850,7 +2850,7 @@ function inspectClaudeOperation(meta, operationId, action) {
|
|
|
2850
2850
|
if (!done)
|
|
2851
2851
|
return { ...base, status: "unknown", raw_output: null, reason: "result_unknown" };
|
|
2852
2852
|
if (!meta.vendor_session_id && done.vendor_session_id) {
|
|
2853
|
-
|
|
2853
|
+
bindAgentHarnessSession(meta, done);
|
|
2854
2854
|
writeAgentMetadata(meta);
|
|
2855
2855
|
}
|
|
2856
2856
|
const rawOutput = readClaudeResultText(meta, done, operationId, transcriptUnavailable);
|
|
@@ -2893,8 +2893,8 @@ function portableForkPrompt(sourceSessionId, mission) {
|
|
|
2893
2893
|
}
|
|
2894
2894
|
return composePortableForkPrompt(value.context, mission);
|
|
2895
2895
|
}
|
|
2896
|
-
/**
|
|
2897
|
-
export function
|
|
2896
|
+
/** harness CLI の存在だけを安全に要約する。認証状態・実行出力・解決先 path は返さない。 */
|
|
2897
|
+
export function harnessLauncherDiagnostic(kind) {
|
|
2898
2898
|
try {
|
|
2899
2899
|
return resolveAgentBin(kind) ? "ready" : "not_applicable";
|
|
2900
2900
|
}
|
|
@@ -3082,7 +3082,7 @@ export function openAgent(kind, opts = {}) {
|
|
|
3082
3082
|
// grok.exe で実測済み(2026-08-15)。
|
|
3083
3083
|
// Windows の grok/composer は Windows native の grok.exe だけを起動する(オーナー裁定 2026-08-15:
|
|
3084
3084
|
// WindowsネイティブはWindowsネイティブで完結させ、WSL2へ持ち込まない)。WSL 側 grok を起動すると
|
|
3085
|
-
//
|
|
3085
|
+
// harness 実体が WSL process になり、auth・session 記録(events/chat_history)が WSL home 側へ分裂して
|
|
3086
3086
|
// transcript/completion を回収できない(実被弾: 2026-08-15 olc-plan-review-grok2)。
|
|
3087
3087
|
if (isWin && (kind === "grok" || kind === "composer") && !isWindowsNativeExecutable(bin)) {
|
|
3088
3088
|
ownTelemetryFailure("AITERM.VENDOR_LAUNCHER_FAILED", new AitermError(`Windows の ${label} launcher は Windows native の grok.exe だけを起動できます(現在の解決先: ${bin})。` +
|
|
@@ -3185,7 +3185,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
|
3185
3185
|
throw new AitermError("launch_operation_idはpromptなしのClaude相関launchだけで指定できます", 2);
|
|
3186
3186
|
}
|
|
3187
3187
|
// v0.16.0: launcher は常に managed(Stop hook つき)で立つ。手動運転したい場合は
|
|
3188
|
-
// pty_open で素の PTY を開き、
|
|
3188
|
+
// pty_open で素の PTY を開き、harness CLI を自分で send する。
|
|
3189
3189
|
// 第3要素は「起動時点でturnが走っているか」の event_cursor: Grok/Composer/Cursor の argv prompt は
|
|
3190
3190
|
// event file 新規作成直後の起動=境界0、prompt なしの起動は turn なし=null。
|
|
3191
3191
|
if (!prompt || (kind !== "codex" && kind !== "claude")) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Claude Code 固有の制御。完了正本は launch 固有 Stop hook が書く event/result(ADR 0025)。
|
|
2
2
|
// core 所有のサービス(transcript 不在エラー)は引数で注入し、
|
|
3
|
-
// 依存方向を core →
|
|
3
|
+
// 依存方向を core → harnesses → agent-shared の一方向に保つ。
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -44,7 +44,7 @@ export function agentClaudeDispatchReceiptPath(name, launchId, operationId) {
|
|
|
44
44
|
return path.join(agentsDir(), `${name}.${launchId}.${validated.slice("sha256:".length)}.claude-dispatch`);
|
|
45
45
|
}
|
|
46
46
|
function claudeHookScriptPath() {
|
|
47
|
-
// この module は dist/
|
|
47
|
+
// この module は dist/harnesses/ に置かれるが、stop hook 実体は dist/ 直下に build される。
|
|
48
48
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "claude-stop-hook.js");
|
|
49
49
|
}
|
|
50
50
|
// process.execPath は Homebrew 等では Cellar の版付き実体を指す。長寿命 MCP server の起動後に
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Codex 固有の制御。完了正本は root rollout transcript の task_complete(ADR 0022)。
|
|
2
2
|
// core 所有のサービス(transcript 行読取・rate limit 検知)は引数で注入し、
|
|
3
|
-
// 依存方向を core →
|
|
3
|
+
// 依存方向を core → harnesses → agent-shared の一方向に保つ。
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
@@ -65,7 +65,7 @@ export function codexConfigSummary(configPath) {
|
|
|
65
65
|
bits.push(`sandbox_mode=${sandboxMode}`);
|
|
66
66
|
return `共有 config: ${bits.join(" / ")}`;
|
|
67
67
|
}
|
|
68
|
-
export function findLatestCodexTranscript(codexHome,
|
|
68
|
+
export function findLatestCodexTranscript(codexHome, harnessSessionId) {
|
|
69
69
|
const sessionsDir = path.join(codexHome, "sessions");
|
|
70
70
|
let latestFile = null;
|
|
71
71
|
let latestMtime = -Infinity;
|
|
@@ -83,7 +83,7 @@ export function findLatestCodexTranscript(codexHome, vendorSessionId) {
|
|
|
83
83
|
visit(file);
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
|
-
if (!entry.isFile() || !entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl") || !entry.name.includes(
|
|
86
|
+
if (!entry.isFile() || !entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl") || !entry.name.includes(harnessSessionId))
|
|
87
87
|
continue;
|
|
88
88
|
try {
|
|
89
89
|
const mtimeMs = fs.statSync(file).mtimeMs;
|
|
@@ -204,14 +204,14 @@ export function bindCodexTranscriptSession(meta) {
|
|
|
204
204
|
const transcript = codexRootTranscript(meta);
|
|
205
205
|
if (!transcript)
|
|
206
206
|
return null;
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
meta.vendor_session_id =
|
|
207
|
+
const harnessSessionId = codexTranscriptSessionId(transcript);
|
|
208
|
+
if (harnessSessionId && !meta.vendor_session_id) {
|
|
209
|
+
meta.vendor_session_id = harnessSessionId;
|
|
210
210
|
writeAgentMetadata(meta);
|
|
211
211
|
}
|
|
212
212
|
return transcript;
|
|
213
213
|
}
|
|
214
|
-
export function codexCompletionEvent(meta,
|
|
214
|
+
export function codexCompletionEvent(meta, harnessSessionId, record) {
|
|
215
215
|
if (record?.type !== "event_msg" ||
|
|
216
216
|
record?.payload?.type !== "task_complete" ||
|
|
217
217
|
typeof record?.payload?.turn_id !== "string" ||
|
|
@@ -222,7 +222,7 @@ export function codexCompletionEvent(meta, vendorSessionId, record) {
|
|
|
222
222
|
vendor: "codex",
|
|
223
223
|
aiterm_session: meta.aiterm_session,
|
|
224
224
|
launch_id: meta.launch_id,
|
|
225
|
-
vendor_session_id:
|
|
225
|
+
vendor_session_id: harnessSessionId,
|
|
226
226
|
turn_id: record.payload.turn_id,
|
|
227
227
|
operation_id: null,
|
|
228
228
|
reason: "Codex transcript task_complete",
|
|
@@ -235,13 +235,13 @@ export function latestCodexCompletion(meta, readTranscriptLines) {
|
|
|
235
235
|
const transcript = codexRootTranscript(meta);
|
|
236
236
|
if (!transcript)
|
|
237
237
|
return null;
|
|
238
|
-
const
|
|
238
|
+
const harnessSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
|
|
239
239
|
let latest = null;
|
|
240
240
|
for (const line of readTranscriptLines(transcript)) {
|
|
241
241
|
if (!line.trim())
|
|
242
242
|
continue;
|
|
243
243
|
try {
|
|
244
|
-
latest = codexCompletionEvent(meta,
|
|
244
|
+
latest = codexCompletionEvent(meta, harnessSessionId, JSON.parse(line)) ?? latest;
|
|
245
245
|
}
|
|
246
246
|
catch {
|
|
247
247
|
// Codexが末尾を書込み中なら、その行は次の観測で完結してから読む。
|
|
@@ -301,7 +301,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, detectRat
|
|
|
301
301
|
parts.shift();
|
|
302
302
|
discardLeadingFragment = false;
|
|
303
303
|
}
|
|
304
|
-
const
|
|
304
|
+
const harnessSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
|
|
305
305
|
for (const line of parts) {
|
|
306
306
|
if (!line.trim())
|
|
307
307
|
continue;
|
|
@@ -310,7 +310,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, detectRat
|
|
|
310
310
|
continue;
|
|
311
311
|
}
|
|
312
312
|
try {
|
|
313
|
-
const done = codexCompletionEvent(meta,
|
|
313
|
+
const done = codexCompletionEvent(meta, harnessSessionId, JSON.parse(line));
|
|
314
314
|
if (done)
|
|
315
315
|
return observation("done", done);
|
|
316
316
|
}
|
|
@@ -21,11 +21,11 @@ export function cursorTranscriptRoot(meta) {
|
|
|
21
21
|
return null;
|
|
22
22
|
return path.join(meta.cursor_home, "projects", cursorWorkspaceId(meta.cwd ?? process.cwd()), "agent-transcripts");
|
|
23
23
|
}
|
|
24
|
-
export function cursorTranscriptForSession(meta,
|
|
24
|
+
export function cursorTranscriptForSession(meta, harnessSessionId) {
|
|
25
25
|
const root = cursorTranscriptRoot(meta);
|
|
26
|
-
if (!root || !UUID_RE.test(
|
|
26
|
+
if (!root || !UUID_RE.test(harnessSessionId))
|
|
27
27
|
return null;
|
|
28
|
-
return path.join(root,
|
|
28
|
+
return path.join(root, harnessSessionId, `${harnessSessionId}.jsonl`);
|
|
29
29
|
}
|
|
30
30
|
export function listCursorTranscripts(meta) {
|
|
31
31
|
const root = cursorTranscriptRoot(meta);
|
|
@@ -102,14 +102,14 @@ export function bindCursorTranscriptSession(meta) {
|
|
|
102
102
|
const transcript = cursorTranscript(meta);
|
|
103
103
|
if (!transcript)
|
|
104
104
|
return null;
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
meta.vendor_session_id =
|
|
105
|
+
const harnessSessionId = cursorTranscriptSessionId(transcript);
|
|
106
|
+
if (harnessSessionId && !meta.vendor_session_id) {
|
|
107
|
+
meta.vendor_session_id = harnessSessionId;
|
|
108
108
|
writeAgentMetadata(meta);
|
|
109
109
|
}
|
|
110
110
|
return transcript;
|
|
111
111
|
}
|
|
112
|
-
export function cursorCompletionEvent(meta,
|
|
112
|
+
export function cursorCompletionEvent(meta, harnessSessionId, record, turnId) {
|
|
113
113
|
if (meta.kind !== "cursor" || record?.type !== "turn_ended" || typeof record?.status !== "string")
|
|
114
114
|
return null;
|
|
115
115
|
return {
|
|
@@ -117,7 +117,7 @@ export function cursorCompletionEvent(meta, vendorSessionId, record, turnId) {
|
|
|
117
117
|
vendor: "cursor",
|
|
118
118
|
aiterm_session: meta.aiterm_session,
|
|
119
119
|
launch_id: meta.launch_id,
|
|
120
|
-
vendor_session_id:
|
|
120
|
+
vendor_session_id: harnessSessionId,
|
|
121
121
|
turn_id: turnId,
|
|
122
122
|
operation_id: null,
|
|
123
123
|
reason: `Cursor transcript turn_ended:${record.status}`,
|
|
@@ -172,12 +172,12 @@ export function latestCursorCompletion(meta, readTranscriptLines) {
|
|
|
172
172
|
const transcript = cursorTranscript(meta);
|
|
173
173
|
if (!transcript)
|
|
174
174
|
return null;
|
|
175
|
-
const
|
|
175
|
+
const harnessSessionId = meta.vendor_session_id ?? cursorTranscriptSessionId(transcript);
|
|
176
176
|
// callerとの共通signatureを保つ。Cursorのturn境界はbyte列でなくuser turn数を使う。
|
|
177
177
|
void readTranscriptLines;
|
|
178
178
|
const state = cursorTranscriptState(transcript);
|
|
179
179
|
return state.terminalRecord
|
|
180
|
-
? cursorCompletionEvent(meta,
|
|
180
|
+
? cursorCompletionEvent(meta, harnessSessionId, state.terminalRecord, `cursor:${state.userTurns}`)
|
|
181
181
|
: null;
|
|
182
182
|
}
|
|
183
183
|
export async function observeCursorDone(meta, timeout, requestedCursor, detectRateLimit) {
|
|
@@ -208,8 +208,8 @@ export async function observeCursorDone(meta, timeout, requestedCursor, detectRa
|
|
|
208
208
|
const state = cursorTranscriptState(transcript);
|
|
209
209
|
malformedEvents = state.malformedEvents;
|
|
210
210
|
if (state.userTurns > startBoundary && state.terminalRecord) {
|
|
211
|
-
const
|
|
212
|
-
const done = cursorCompletionEvent(meta,
|
|
211
|
+
const harnessSessionId = meta.vendor_session_id ?? cursorTranscriptSessionId(transcript);
|
|
212
|
+
const done = cursorCompletionEvent(meta, harnessSessionId, state.terminalRecord, `cursor:${state.userTurns}`);
|
|
213
213
|
if (done)
|
|
214
214
|
return observation("done", done);
|
|
215
215
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Grok / Composer 固有の制御。Composer は grok CLI の別モデル起動プリセットであり、
|
|
2
2
|
// モデル既定値と表示名以外は Grok と完全共通(実測 2026-08-23 棚卸し・docs/32)。
|
|
3
3
|
// core 所有のサービス(transcript 行読取・rate limit 検知)は引数で注入し、
|
|
4
|
-
// 依存方向を core →
|
|
4
|
+
// 依存方向を core → harnesses → agent-shared の一方向に保つ。
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
@@ -43,8 +43,8 @@ export function resolveAndValidateGrokAuth(srcHome) {
|
|
|
43
43
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
44
44
|
throw new AitermError("Grok 認証正本のJSONが不正です", 2);
|
|
45
45
|
// auth file 自体は O_NOFOLLOW で開いているが、中間 directory の symlink は辿り得る。
|
|
46
|
-
//
|
|
47
|
-
// 一致させ、canonical な祖先も root まで検証する。same-UID race の排他は
|
|
46
|
+
// harness に渡す正本を path swap の入口にしないため、字句正規化した絶対 path と realpath を
|
|
47
|
+
// 一致させ、canonical な祖先も root まで検証する。same-UID race の排他は harness lock の責務。
|
|
48
48
|
const lexicalPath = path.resolve(authPath);
|
|
49
49
|
const canonicalPath = fs.realpathSync(authPath);
|
|
50
50
|
if (lexicalPath !== canonicalPath)
|
|
@@ -299,7 +299,7 @@ export function grokLaunchNote(kind, model, effort, meta) {
|
|
|
299
299
|
`effort=${effort ?? "CLI/model既定"}。` + writeScopeNote);
|
|
300
300
|
}
|
|
301
301
|
// grok/composer: 検証済み auth 正本をそのままの path 形で渡す。Windows では native 強制により
|
|
302
|
-
//
|
|
302
|
+
// harness は Windows process なので、Windows ドライブパスが正しい形(WSL 形への変換はしない)。
|
|
303
303
|
export function grokEnvTokens(meta) {
|
|
304
304
|
return [
|
|
305
305
|
...(meta.grok_auth_path ? [`GROK_AUTH_PATH=${shq(meta.grok_auth_path)}`] : []),
|
|
@@ -308,7 +308,7 @@ export function grokEnvTokens(meta) {
|
|
|
308
308
|
}
|
|
309
309
|
export function grokTuiReady(screen) {
|
|
310
310
|
// Grok Build 0.2.117 は起動完了後に製品名を消し、model footerだけを残す。
|
|
311
|
-
// Composerも同じfrontendでmodel名だけが異なるため、両方を
|
|
311
|
+
// Composerも同じfrontendでmodel名だけが異なるため、両方をharness UIの根拠にする。
|
|
312
312
|
// Windows native grok.exe(1.0.4 実測)は入力欄markerを `❯` でなく `>` で描画するため両方を受ける。
|
|
313
313
|
const grokFrontend = screen.includes("Grok Build") || /\b(?:Grok|Composer)\s+[\w.()-]+/.test(screen);
|
|
314
314
|
return grokFrontend && /(^|\n|\s)[❯>]/.test(screen);
|
package/dist/index.js
CHANGED
|
@@ -43,10 +43,10 @@ function fail(e) {
|
|
|
43
43
|
*/
|
|
44
44
|
async function factoryDiagnostics() {
|
|
45
45
|
const ptyList = core.readOnlyPtyListDiagnostic();
|
|
46
|
-
const claude = core.
|
|
47
|
-
const codex = core.
|
|
48
|
-
const grok = core.
|
|
49
|
-
const cursor = core.
|
|
46
|
+
const claude = core.harnessLauncherDiagnostic("claude");
|
|
47
|
+
const codex = core.harnessLauncherDiagnostic("codex");
|
|
48
|
+
const grok = core.harnessLauncherDiagnostic("grok");
|
|
49
|
+
const cursor = core.harnessLauncherDiagnostic("cursor");
|
|
50
50
|
const runtimeErrors = await runtimeErrorStoreDiagnostic();
|
|
51
51
|
const overall = ptyList.status === "unverified" || runtimeErrors.status === "unverified" ? "unverified" : "ready";
|
|
52
52
|
return JSON.stringify({
|
|
@@ -193,7 +193,7 @@ server.registerTool("pty_send", {
|
|
|
193
193
|
server.registerTool("pty_read", {
|
|
194
194
|
description: "セッションの出力をトークン削減して読む(既定は前回読取位置からの増分)。" +
|
|
195
195
|
"削減: 制御文字除去 / 反復圧縮 / head+tail 折りたたみ+復元ヒント+メタ併記。" +
|
|
196
|
-
"agent_transcript:true は agent session の直近完了ターンの最終 assistant メッセージを公開された
|
|
196
|
+
"agent_transcript:true は agent session の直近完了ターンの最終 assistant メッセージを公開されたharness記録から平文で返す。" +
|
|
197
197
|
"長い回答が screen tail で切れた時の回収用。",
|
|
198
198
|
inputSchema: {
|
|
199
199
|
session_id: z.string(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
4
4
|
"mcpName": "io.github.kitepon/aiterm-mcp",
|
|
5
5
|
"description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"author": {
|
|
29
|
-
"name": "Quo /
|
|
29
|
+
"name": "Quo / クオ at kitepon.dev",
|
|
30
30
|
"url": "https://kitepon.dev/"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
47
|
"dist/*.js",
|
|
48
|
-
"dist/
|
|
48
|
+
"dist/harnesses/*.js",
|
|
49
49
|
"README.md",
|
|
50
50
|
"LICENSE"
|
|
51
51
|
],
|