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 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/vendorの通常memory・設定を読む。
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`はvendor標準操作を使って、
122
- 起動中のCodex/Claudeのmodelとreasoning effortを変更する。PTY、vendor session、会話contextは維持する。
121
+ **v0.24.0では起動中agentの設定変更を追加。** `agent_configure`はharness標準操作を使って、
122
+ 起動中のCodex/Claudeのmodelとreasoning effortを変更する。PTY、harness session、会話contextは維持する。
123
123
 
124
- **v0.23.0では、ローカル完結の別vendor向けportable forkを追加した。** どのlauncherでも
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、vendor home、project/user/local設定、MCP、plugin、skill、permission、trust、memory、
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 越しにプログラムから駆動するので、上のすべては **tmux に誰も座らないまま**動く。Claude/Codexのどちらからでも、自分自身を含む任意のlauncherを呼び、`pty_read`で結果を読んで次へ進める——無人で。これは、人が操作する端末が向かない場所にこそ aiterm が合うということ:
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 send-keys<br/>capture-pane"| P["persistent PTYs<br/>tmux · survive restarts"]
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 本の tmux セッションが両方を保つ
355
+ aiterm → cwd=/tmp var=hello123 # 1 本の永続PTYが両方を保つ
356
356
  ```
357
357
 
358
- cd でディレクトリを移り、環境変数を立て、ビルドを走らせる。ssh で一度ログインして、その接続のまま 10 個コマンドを打つ。REPL や起動したエージェントの TUI を 1 ターンずつ操作する。こういう流れは、1 本の tmux セッションが状態を握っていて初めて成り立つ。端末に何かを覚えておいてほしいときは、aiterm を使う。
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
- | 人が同時操作 | ✅ 共有 tmux ソケット(`attach`) | ❌ | ⚠️ まちまち | ✅(設計の芯) |
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 の paste はさらに tmux bracketed paste(`paste-buffer -p`)を使う: bracketed paste mode を要求している pane(vendor TUI)へは各 chunk を `ESC[200~/201~` で包んで届け、チャンク投入中のキー解釈による語中文字化け・submit 取り落としを抑える。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。そのsessionを `pty_close` して作り直すか、全sessionを破棄できる場合だけ `pty_kill_all` で安全に掃除する。
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
- - **全セッションが単一 socket(POSIX では `claude.sock`)上にある。** `tmux … kill-server` は全セッションを消す。
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
- ロジックは `src/core.ts`(tmux 制御・削減・完了検出・安全・エージェント起動)と `src/rtk.ts`(コマンド別 reducer)、公開は `src/index.ts`。設計の出発点と reducer の移植元(pytest reducer は本家 rtk 0.42.0 と一致するよう移植・ただし上記の `FAILED` 行の差異は意図的・回帰テストで固定)は `prototype/python/` を参照。
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 vendor memory/configuration that a direct CLI launch would use.
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 vendor'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.
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 vendor CLI fall back to another model.
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 vendor's
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, vendor session, and conversation context.
126
+ session while preserving its PTY, harness session, and conversation context.
127
127
 
128
- **v0.23.0 adds a local, cross-vendor portable fork.** Pass `throughline_source_session`
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, vendor home, project/user/local configuration, MCP servers,
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
- vendor-owned shared credential store before creating a PTY, while healthy credentials
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 vendor launch command. Missing names are omitted; invalid shell variable names
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 vendor and other
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 vendor-owned credential and configuration stores in place.
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 a tmux**. A Claude or Codex session can call any launcher — including another instance of 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:
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 / Composer, each in its own persistent session, and reads them all back.
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 send-keys<br/>capture-pane"| P["persistent PTYs<br/>tmux · survive restarts"]
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 tmux session holds both
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 tmux session holds all of it. Reach for aiterm when the terminal has to remember something.
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 tmux socket (`attach`) | ❌ | ⚠️ varies | ✅ (its core model) |
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 vendor-specific observation without the parent blocking or polling. Pre-send readiness failures are MCP errors, and late completion remains recoverable without resending.
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 vendor-native 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).
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 tmux 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 paste with tmux bracketed paste (`paste-buffer -p`): panes that requested bracketed-paste mode (the vendor TUIs) 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.
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. The `tmux -S … attach -t <id>` line printed by `pty_open` and `agent_launch` lets a human attach to the same terminal and intervene (`Ctrl-b d` to detach), including a Claude/Codex/Grok/Cursor harness session. On native Windows the printed line is the psmux form — `psmux -L <namespace> attach -t <id>` — pointing at the same Windows-native session.
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; check with `tmux -V`. Install with `apt install tmux` / `brew install tmux`)
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. `grok_agent`/`composer_agent` 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 vendor auth and session records never split across an OS boundary.
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 live on a single socket (`claude.sock` on POSIX).** `tmux … kill-server` removes them all.
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
- vendor CLIs rely on and is not a valid runner identity (the separate WSL2 runner still owns the
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 vendor configuration, authentication, MCP, plugin, skill, permission,
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
 
@@ -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 実行ファイルの強制が要る vendor(grok/composer の実効
31
+ // 作るため)。native 実行ファイルの強制が要る harness(grok/composer の実効
32
32
  // sandbox 等)は openAgent 側の専用ゲートが明示エラーで担う。
33
33
  export function isUsableAgentExecutableFile(candidate) {
34
34
  if (!isWin)
@@ -1,5 +1,5 @@
1
- // vendor中立の共有プリミティブ。core と vendors/ の両方が依存する最下層で、
2
- // tmux-runtime / errors 以外の内部moduleへ依存しない(依存方向: core → vendors → agent-shared)。
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
- // (vendor別noteへ複製すると文言が発散する)。
203
+ // (harness別noteへ複製すると文言が発散する)。
204
204
  export function writeScopeLaunchNote(kind, writeScope) {
205
205
  return writeScope === undefined
206
206
  ? ""
@@ -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=vendor利用上限 / 1=エラー)。
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 "./vendors/grok.js";
19
- import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./vendors/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 "./vendors/claude.js";
21
- import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, CURSOR_COMPOSER_MARKER_RE, validateCursorModelEffort, } from "./vendors/cursor.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 "./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, ambiguousVendorSession: false };
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, ambiguousVendorSession: true };
1796
+ return { event: null, malformedEvents, ambiguousHarnessSession: true };
1797
1797
  }
1798
1798
  if (!candidate)
1799
1799
  candidate = ev;
1800
1800
  }
1801
- return { event: candidate, malformedEvents, ambiguousVendorSession: false };
1801
+ return { event: candidate, malformedEvents, ambiguousHarnessSession: false };
1802
1802
  }
1803
- function bindAgentVendorSession(meta, ev) {
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
- bindAgentVendorSession(meta, done);
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
- bindAgentVendorSession(meta, done);
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.ambiguousVendorSession) {
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
- bindAgentVendorSession(meta, scanned.event);
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 recoverAgentVendorSession(meta) {
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.ambiguousVendorSession) {
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
- bindAgentVendorSession(meta, scanned.event);
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 vendor の構造化 transcript から直近完了ターンの最終回答を読む。 */
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
- // vendor session をbindし、promptを再送せず結果だけ回収できるようにする。
2042
- recoverAgentVendorSession(meta);
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
- // vendor 別の利用上限バナー。検知は「報告」専用で、完了判定や自動復旧には使わない。
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 の末尾から上限バナーを探す。読めない・無い・対象 vendor でないは全て null(誤検知より取りこぼし側へ倒す)。
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.ambiguousVendorSession) {
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へ現れた場合も、vendor自身の最終状態として受理する。
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を保ったまま、vendor標準の操作でmodel/effortを変更する。 */
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(送信直前のvendor完了正本境界)を receipt で返す。
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
- bindAgentVendorSession(meta, done);
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
- /** vendor CLI の存在だけを安全に要約する。認証状態・実行出力・解決先 path は返さない。 */
2897
- export function vendorLauncherDiagnostic(kind) {
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
- // vendor 実体が WSL process になり、auth・session 記録(events/chat_history)が WSL home 側へ分裂して
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 を開き、vendor CLI を自分で send する。
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 → vendors → agent-shared の一方向に保つ。
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/vendors/ に置かれるが、stop hook 実体は dist/ 直下に build される。
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 → vendors → agent-shared の一方向に保つ。
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, vendorSessionId) {
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(vendorSessionId))
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 vendorSessionId = codexTranscriptSessionId(transcript);
208
- if (vendorSessionId && !meta.vendor_session_id) {
209
- meta.vendor_session_id = vendorSessionId;
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, vendorSessionId, record) {
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: vendorSessionId,
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 vendorSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
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, vendorSessionId, JSON.parse(line)) ?? latest;
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 vendorSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
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, vendorSessionId, JSON.parse(line));
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, vendorSessionId) {
24
+ export function cursorTranscriptForSession(meta, harnessSessionId) {
25
25
  const root = cursorTranscriptRoot(meta);
26
- if (!root || !UUID_RE.test(vendorSessionId))
26
+ if (!root || !UUID_RE.test(harnessSessionId))
27
27
  return null;
28
- return path.join(root, vendorSessionId, `${vendorSessionId}.jsonl`);
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 vendorSessionId = cursorTranscriptSessionId(transcript);
106
- if (vendorSessionId && !meta.vendor_session_id) {
107
- meta.vendor_session_id = vendorSessionId;
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, vendorSessionId, record, turnId) {
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: vendorSessionId,
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 vendorSessionId = meta.vendor_session_id ?? cursorTranscriptSessionId(transcript);
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, vendorSessionId, state.terminalRecord, `cursor:${state.userTurns}`)
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 vendorSessionId = meta.vendor_session_id ?? cursorTranscriptSessionId(transcript);
212
- const done = cursorCompletionEvent(meta, vendorSessionId, state.terminalRecord, `cursor:${state.userTurns}`);
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 → vendors → agent-shared の一方向に保つ。
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
- // vendor に渡す正本を path swap の入口にしないため、字句正規化した絶対 path と realpath を
47
- // 一致させ、canonical な祖先も root まで検証する。same-UID race の排他は vendor lock の責務。
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
- // vendor は Windows process なので、Windows ドライブパスが正しい形(WSL 形への変換はしない)。
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名だけが異なるため、両方をvendor UIの根拠にする。
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.vendorLauncherDiagnostic("claude");
47
- const codex = core.vendorLauncherDiagnostic("codex");
48
- const grok = core.vendorLauncherDiagnostic("grok");
49
- const cursor = core.vendorLauncherDiagnostic("cursor");
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 メッセージを公開されたvendor記録から平文で返す。" +
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.0",
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 / \u30af\u30aa at kitepon.dev",
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/vendors/*.js",
48
+ "dist/harnesses/*.js",
49
49
  "README.md",
50
50
  "LICENSE"
51
51
  ],