aiterm-mcp 0.19.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -20,17 +20,17 @@
20
20
  >
21
21
  > *MCP = Model Context Protocol — Claude Code のようなツールが AI に機能を差し込むためのオープン標準。*
22
22
 
23
+ **工場での役割:** aiterm-mcpはdotagents開発工場が管理する自作コア10製品の一つです。
24
+ 永続PTYと外部agent実行レーンを所有し、dotagentsが製品横断の導入・統合契約を所有します。
25
+
23
26
  **言葉でなく実測で:** このリポジトリ自身の 203 テストで、`pty_read` はコンテキストに載るトークンを生ログの **約 7.1 分の 1** に減らす。しかも pass/fail の判定は畳んでも残る。→ [組み込みシェルツールとの使い分け](#組み込みシェルツールとの使い分け)
24
27
 
25
28
  13 ツール: 6 つの **PTY ツール**(`pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list`)で 1 本の永続端末を開き・操作し・読む。加えて 4 つの **エージェント起動ツール**(`claude_agent` / `codex_agent` / `grok_agent` / `composer_agent`)が別のコーディングエージェントの TUI を新しい端末の中に起動し、`claude_turn`がdurable caller向けの構造化issue/recoveryを、`claude_approval`がmanaged Claudeの相関済み承認UI中継を、`diagnostics`が安全なfactory readinessを返す。バックエンドは **tmux** なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
26
29
 
27
- **v0.18.1 を 2026-07-18 に公開**(0.18.0+stale案内文言の修理1件)。実運用障害の還流による agent dispatch の hardening:
28
- 全 dispatch/launch receipt に **submit 座礁観測** `submit_residue` を追加
29
- (送信 prompt が vendor TUI の composer に未 submit のまま残存していないかを有界に観測して報告する。
30
- 陽性証拠のみ・auto-retry なし)。agent への prompt paste は tmux **bracketed paste**(pane ごとの
31
- negotiation)で包み、語中の文字化けと Enter 取り落としを抑制。初回 prompt 前の ready gate は
32
- busy 表示中(esc to interrupt)の Codex/Claude を ready と数えない。
33
- v0.16/0.17 以来、親エージェントは aiterm 上で一切ブロックしない:
30
+ **v0.19.2 を 2026-07-20 に公開。** v0.19系では相関済みmanaged Claude approval中継を追加し、
31
+ 複数行shell配送を維持し、native Windowsのfactory diagnosticsを拡張しました。v0.18系では
32
+ `submit_residue`、tmux bracketed paste、busyなCodex/Claude画面をreadyと数えないgateにより
33
+ agent dispatchを強化しました。v0.16/0.17以来、親エージェントはaiterm上で一切ブロックしません:
34
34
  agent session への send は常に非ブロック dispatch になり、完了待ちは `aiterm-wait` 一本
35
35
  (exit code が receipt の outcome を映す: 0=done / 3=timeout=未完了 / 4=closed)、初回 prompt 付き
36
36
  launch は structured receipt にコピペ可能な `wait_command` を含む。factory diagnostics と local
@@ -290,7 +290,7 @@ consumer は `aiterm-runtime-errors snapshot` を読み、durable ingestion 後
290
290
  | `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
291
291
  | `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
292
292
 
293
- 対応するCLI(`claude` / `codex` / `grok`)の導入・認証が必要。解決順は`CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、既定path、`PATH`。前提違反はsession作成前に明示失敗する。4 launcherすべてが同じ非ブロックdispatch契約を使い、Claude/Codexの初回promptはready gate経由で送信される。Claudeはisolated managed settingsとhook-captured resultを使い、private transcriptへ依存しない。既存3 vendorのlive smokeはgreen、Claude実モデルsmokeは承認待ちでありfixture成功と混同しない。
293
+ 対応するCLI(`claude` / `codex` / `grok`)の導入・認証が必要。解決順は`CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、既定path、`PATH`。前提違反はsession作成前に明示失敗する。4 launcherすべてが同じ非ブロックdispatch契約を使い、Claude/Codexの初回promptはready gate経由で送信される。Claudeはisolated managed settingsとhook-captured resultを使い、private transcriptへ依存しない。Claude/Codex/Grok/Composerのlive smokeはすべてgreenであり、fixtureによる検証とは区別して記録する。
294
294
 
295
295
  エージェントの回答が画面 tailより長ければ、対話callerは`pty_read({ agent_transcript:true })`で再promptなしに全文回収する。Claudeはmanaged Stop hookがowner-only resultへ保存した本文をdigest/byte数で検証して返し、private transcriptを読まない。durable machine callerは`claude_turn`を使う。`issue`は一度だけ送信し、`recover`は決して再送せず、`pending`を破損やidentity不一致と区別する。検証済みの`completed`だけがexact `raw_output`を持ち、`unknown`は未dispatchと帰属不能を区別する。不一致・破損は成功statusへ丸めずtool errorのままにする。IDなしの対話turnも匿名markerで直列化するため、現在Stop待ちの間に古い回答を返さない。CodexはStop hookの`turn_id`で構造化transcriptへjoinし、Grok/Composerは最後の実user行より後ろのassistant行を採る。不在・非agent・抽出不能は明示エラー。
296
296
 
package/README.md CHANGED
@@ -20,24 +20,24 @@
20
20
  >
21
21
  > *MCP = Model Context Protocol — the open standard that lets tools like Claude Code plug capabilities into an AI.*
22
22
 
23
+ **Factory role:** aiterm-mcp is one of the ten self-owned core products managed by
24
+ the dotagents development factory. It owns the persistent PTY and external-agent
25
+ execution lane; dotagents owns the cross-product installation and integration
26
+ contract.
27
+
23
28
  **Measured, not claimed:** on this repo's own 203-test suite, a `pty_read` puts **~7.1× fewer tokens** in your context than the raw log — and the pass/fail verdict survives the fold. → [When to reach for it vs. the built-in shell](#when-to-reach-for-it-vs-the-built-in-shell)
24
29
 
25
30
  Thirteen tools: six **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` — to open, drive, and read one persistent terminal, four **agent launchers** — `claude_agent` / `codex_agent` / `grok_agent` / `composer_agent` — that each start another coding agent's TUI inside a fresh one, `claude_turn` for durable structured issue/recovery, `claude_approval` for correlated managed-Claude approval prompts, and `diagnostics` for safe factory readiness. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
26
31
 
27
- **v0.18.1 was published on 2026-07-18** (v0.18.0 plus one stale-guidance
28
- message fix). Field-failure hardening for agent
29
- dispatch: every dispatch/launch receipt now carries a **submit-strand
30
- observation** `submit_residue` (a bounded screen poll that reports when the
31
- sent prompt is still sitting unsubmitted in the vendor TUI's composer —
32
- positive evidence only, no auto-retry), agent prompt pastes are wrapped in
33
- tmux **bracketed paste** (negotiated per pane) against mid-word corruption and
34
- dropped submits, and the pre-prompt ready gate no longer counts a busy
35
- Codex/Claude screen ("esc to interrupt") as ready. As of v0.16/0.17 a parent
36
- agent never blocks on aiterm: every send to an agent session is a non-blocking
37
- dispatch, completion is one universal `aiterm-wait` waiter whose exit codes
38
- mirror the receipt outcome (`0`=done / `3`=timeout, not finished /
39
- `4`=closed), and a launch with an initial prompt returns a ready-made
40
- `wait_command` in its structured receipt.
32
+ **v0.19.2 was published on 2026-07-20.** The v0.19 line adds the correlated
33
+ managed-Claude approval relay, preserves multiline shell delivery, and extends
34
+ factory diagnostics on native Windows. The v0.18 line hardened agent dispatch
35
+ with `submit_residue`, tmux bracketed paste, and a ready gate that rejects busy
36
+ Codex/Claude screens. As of v0.16/0.17 a parent agent never blocks on aiterm:
37
+ every send to an agent session is a non-blocking dispatch, completion is one
38
+ universal `aiterm-wait` waiter whose exit codes mirror the receipt outcome
39
+ (`0`=done / `3`=timeout, not finished / `4`=closed), and a launch with an
40
+ initial prompt returns a ready-made `wait_command` in its structured receipt.
41
41
  Factory diagnostics and the local runtime-error store collect only when
42
42
  canonical dotagents config explicitly sets `collection.enabled: true`;
43
43
  collection is off by default and performs no network I/O. It ships via
@@ -84,7 +84,7 @@ codex_agent({ session_name: "codex1", cwd: "/repo",
84
84
  pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
85
85
  pty_send("codex1", "also fix the imports it broke")
86
86
  → non-blocking dispatch; receipt carries event_cursor
87
- $ aiterm-wait --session codex1 --cursor <event_cursor> # host background task; exit code 0=done, 3=timeout (not done), 4=closed
87
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # never in the parent's foreground; exit 0=done, 3=timeout (not done), 4=closed
88
88
  pty_read("codex1", { agent_transcript: true }) → collect the full answer
89
89
  ```
90
90
 
@@ -309,7 +309,7 @@ Each launcher starts a specific vendor's interactive coding-agent TUI inside a f
309
309
  | `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name?` |
310
310
  | `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name?` |
311
311
 
312
- The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). Binary resolution uses `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then each documented default location, then `PATH`. Missing binaries, invalid model/effort values, and nonexistent `cwd` fail before a session is created. All four share the same non-blocking dispatch contract for follow-up turns; `claude_agent`/`codex_agent` submit an initial `prompt` through the ready gate. Claude uses isolated managed settings and a hook-captured bounded result rather than private transcript access. Codex/Grok/Composer live smokes are green; Claude real-model smoke remains approval-gated and is not claimed from fixtures. Native Windows can launch agents but managed completion is not supported yet.
312
+ The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). Binary resolution uses `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then each documented default location, then `PATH`. Missing binaries, invalid model/effort values, and nonexistent `cwd` fail before a session is created. All four share the same non-blocking dispatch contract for follow-up turns; `claude_agent`/`codex_agent` submit an initial `prompt` through the ready gate. Claude uses isolated managed settings and a hook-captured bounded result rather than private transcript access. Claude, Codex, Grok, and Composer live smokes are green; fixture coverage remains a separate claim. Native Windows can launch agents but managed completion is not supported yet.
313
313
 
314
314
  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 managed 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`. `unknown` distinguishes `operation_not_found` from a receipt whose result can no longer be attributed. Mismatch and corruption remain tool errors rather than being folded into a successful status. ID-less interactive Claude turns are still serialized by an anonymous marker, so an older answer is not returned while the current Stop is pending. Codex joins its structured transcript on the Stop hook `turn_id`; Grok/Composer take the assistant rows after the last real user row. A missing result/transcript, a non-agent session, or an unextractable message is an explicit error, never a silent empty.
315
315
 
@@ -323,9 +323,11 @@ As of v0.16 a parent agent **never blocks** on aiterm — there is no wait param
323
323
 
324
324
  1. Launch the child (`claude_agent` / `codex_agent` / ...; every launch is managed). Send a turn with plain `pty_send` (or `claude_turn issue` for durable Claude operations). The call passes the TUI ready gate, submits, and returns immediately with an `event_cursor` in its structured receipt — plus a `submit_residue` observation: `true` means the sent text still lingered in the composer after submit (likely stranded; inspect the screen before re-pressing Enter), `false` means no residue was observed (not a proof of submission), `null` means not applicable.
325
325
  2. Run `aiterm-wait --session <id> --cursor <event_cursor> [--operation sha256:<64hex>] [--timeout <sec>]` (a launch with an initial `prompt` returns this command ready-made as `wait_command` in its structured receipt). It observes the vendor Stop-hook completion event as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt. **Exit ≠ done**: the receipt's `outcome` is authoritative, and the exit code mirrors it — `0` = `done`, `3` = `timeout` (the turn is **not** finished; default `--timeout` is 600 s), `4` = `closed`, `1` = error. On `timeout` just re-run the waiter with the same cursor. The `--cursor` boundary makes it start-order independent: no completion can slip past even if the waiter starts late.
326
- 3. Host integration picks the invocation style: 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; a host without that mechanism runs it as a foreground shell command. Either way the aiterm MCP call itself never blocks.
326
+ 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.
327
327
  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.
328
328
 
329
+ **If your host has no completion push** (no mechanism that re-invokes the agent when a background process exits), `--timeout 0` is a one-shot check instead of a wait: it scans the event file once and returns `running` (exit `5`) when the turn is still in flight, `done` (exit `0`) when it finished, `closed` (exit `4`) when the session is gone. It is deliberately absent from the receipts and tool descriptions — a host that *does* get pushed should be woken, not poll. An unknown session name is an error, never `running`, so a typo cannot masquerade as a child that is still working.
330
+
329
331
  `aiterm-wait` takes no locks, never writes session state, and never dispatches — any number can run beside the MCP server and each other, and `pty_close`/concurrent sends are unaffected.
330
332
 
331
333
  ### Token reduction
@@ -2,14 +2,16 @@
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 / 3=timeout=未完了 / 4=closed / 1=エラー)。
5
+ // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 1=エラー)。
6
+ // --timeout 0 は待たずに一度だけ観測する照会で、未完了は running(timeout と混同させない)。
6
7
  // receipt の outcome が正で、done 以外は未完了。timeout の既定は core の DEFAULT_AGENT_DONE_TIMEOUT(600秒)。
7
8
  import { fileURLToPath } from "node:url";
8
9
  import * as fs from "node:fs";
9
10
  import { AitermError, observeAgentDone } from "./core.js";
10
11
  const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
11
12
  const OPERATION_RE = /^sha256:[0-9a-f]{64}$/;
12
- const USAGE = "usage: aiterm-wait --session <name> [--cursor <event_cursor>] [--operation sha256:<64hex>] [--timeout <sec>]";
13
+ const USAGE = "usage: aiterm-wait --session <name> [--cursor <event_cursor>] [--operation sha256:<64hex>] [--timeout <sec>]" +
14
+ "(--timeout 0 は待たずに一度だけ観測する照会で、未完了は outcome=running / exit 5)";
13
15
  export function parseArgs(argv) {
14
16
  let session = null;
15
17
  let operationId = null;
@@ -65,7 +67,14 @@ function emit(value) {
65
67
  process.stdout.write(JSON.stringify(value) + "\n");
66
68
  }
67
69
  // outcome → exit code。exit status しか見えないホストでも done とそれ以外を誤読できないようにする。
68
- const OUTCOME_EXIT_CODES = { done: 0, timeout: 3, closed: 4 };
70
+ // Record で全 outcome を型に強制する: 語を足して対応表を直し忘れると undefined→exit 0 になり、
71
+ // 「まだ終わっていない」が「完了」として親へ届く。その取りこぼしを compile error で止める。
72
+ const OUTCOME_EXIT_CODES = {
73
+ done: 0,
74
+ running: 5,
75
+ timeout: 3,
76
+ closed: 4,
77
+ };
69
78
  export async function main(argv) {
70
79
  const cmd = parseArgs(argv);
71
80
  const result = await observeAgentDone(cmd.session, {
package/dist/core.js CHANGED
@@ -2497,7 +2497,7 @@ function bindCompletedInitialPrompt(meta) {
2497
2497
  }
2498
2498
  const size = safeStatSize(meta.event_file);
2499
2499
  if (size === 0) {
2500
- throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。aiterm-wait --session ${meta.aiterm_session} --cursor 0 で完了(outcome=done)を確認してから再度操作してください。`, 2);
2500
+ throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
2501
2501
  }
2502
2502
  const text = readFileRange(meta.event_file, 0, size).toString("utf8");
2503
2503
  const lines = text.split("\n");
@@ -2509,7 +2509,7 @@ function bindCompletedInitialPrompt(meta) {
2509
2509
  if (!scanned.event) {
2510
2510
  const malformed = scanned.malformedEvents ? ` malformed_events=${scanned.malformedEvents}` : "";
2511
2511
  const partial = tail.trim() ? " partial_event=true" : "";
2512
- throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了 event をまだ確認できません。aiterm-wait --session ${meta.aiterm_session} --cursor 0 で完了(outcome=done)を確認してから再度操作してください。${malformed}${partial}`, 2);
2512
+ throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了 event をまだ確認できません。${agentWaitGuide(meta.aiterm_session)}${malformed}${partial}`, 2);
2513
2513
  }
2514
2514
  bindAgentVendorSession(meta, scanned.event);
2515
2515
  setInitialPromptState(meta, "done");
@@ -2630,11 +2630,8 @@ function findLatestCodexTranscript(codexHome, vendorSessionId) {
2630
2630
  visit(sessionsDir);
2631
2631
  return latestFile;
2632
2632
  }
2633
- // 未完了系エラーの共通出口案内。pollingへ誘導せず、正規の完了待ち手段を必ず指す。
2634
- const AGENT_WAIT_GUIDE = "完了待ちは aiterm-wait --session <session_id> のバックグラウンド実行で受ける(polling不要。" +
2635
- "receiptのoutcome=doneを確認してから再取得)。";
2636
2633
  function transcriptUnavailable() {
2637
- throw new AitermError(`transcript がまだありません。ターン完了後に再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2634
+ throw new AitermError(`transcript がまだありません。ターン完了後に再取得してください。${agentWaitGuide()}`, 2);
2638
2635
  }
2639
2636
  function transcriptNotFound(vendor) {
2640
2637
  throw new AitermError(`最終 assistant メッセージを特定できませんでした(vendor=${vendor})。screen で確認してください。`, 2);
@@ -2701,18 +2698,18 @@ export async function readAgentTranscript(name, o = {}) {
2701
2698
  const active = readClaudeOperationMarker(meta);
2702
2699
  if (active) {
2703
2700
  const label = active.operationId ? `operation ${active.operationId}` : "operation_idなしのClaude turn";
2704
- throw new AitermError(`${label} はまだ完了していません。Stop完了後に同じsessionから再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2701
+ throw new AitermError(`${label} はまだ完了していません。Stop完了後に同じsessionから再取得してください。${agentWaitGuide(name)}`, 2);
2705
2702
  }
2706
2703
  }
2707
2704
  // wait timeout は「失敗」ではなく状態不明。後着した同一launchの完了eventから
2708
2705
  // vendor session をbindし、promptを再送せず結果だけ回収できるようにする。
2709
2706
  recoverAgentVendorSession(meta);
2710
2707
  if (!meta.vendor_session_id) {
2711
- throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2708
+ throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。${agentWaitGuide(name)}`, 2);
2712
2709
  }
2713
2710
  const done = latestAgentDoneEvent(meta, operationId);
2714
2711
  if (operationId && !done) {
2715
- throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2712
+ throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。${agentWaitGuide(name)}`, 2);
2716
2713
  }
2717
2714
  const turnId = done?.turn_id ?? null;
2718
2715
  let text = "";
@@ -2853,10 +2850,37 @@ function assertInitialPromptNotPendingForSend(name, force) {
2853
2850
  if (meta.initial_prompt !== "pending" && meta.initial_prompt !== "sent")
2854
2851
  return;
2855
2852
  throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。通常 pty_send は混入防止のため送信しません。` +
2856
- `aiterm-wait --session ${name} --cursor 0 で完了(outcome=done)を確認してから再度 pty_send するか、手動介入が必要な場合だけ force:true を明示してください。`, 2);
2853
+ `${agentWaitGuide(name)}完了後に再度 pty_send するか、手動介入が必要な場合だけ force:true を明示してください。`, 2);
2857
2854
  }
2858
2855
  // aiterm-wait の exit 契約(CLI と各所の案内文で共有する正)。exit≠完了: outcome が done の時だけ完了。
2859
2856
  export const AITERM_WAIT_OUTCOME_NOTE = `exit 0=done / 3=timeout(既定${DEFAULT_AGENT_DONE_TIMEOUT}秒・未完了) / 4=closed。receiptのoutcomeが正で、done以外は未完了`;
2857
+ // 親ホストの識別(MCP initialize の clientInfo.name)。完了待ちコマンドを「親のターンを塞がない
2858
+ // 起動形」で名指しするためだけに使う。分からない時は汎用文へ落ち、機能は一切変えない。
2859
+ let parentClientName = null;
2860
+ export function setParentClient(name) {
2861
+ const trimmed = typeof name === "string" ? name.trim() : "";
2862
+ parentClientName = trimmed === "" ? null : trimmed;
2863
+ }
2864
+ // 完了待ちを親のターンを塞がない形で起動する具体形。ホストが分かる時は実際の呼び出し形を名指しする
2865
+ // (抽象名詞の「バックグラウンドで」だけでは親が foreground 実行へ落ちるため・ADR 0017)。
2866
+ export function agentWaitLaunchForm(command) {
2867
+ if (parentClientName === "claude-code") {
2868
+ return `Bash(command: ${JSON.stringify(command)}, run_in_background: true)`;
2869
+ }
2870
+ return `\`${command}\` を親のターンを塞がない別プロセスとして起動`;
2871
+ }
2872
+ // dispatch / 起動時 prompt 送信後の共通案内。第一文で「待たない」を宣言し、待ち方は後段に置く。
2873
+ export function agentDispatchGuide(session, cursor) {
2874
+ const cmd = `aiterm-wait --session ${session} --cursor ${cursor}`;
2875
+ return (`投げっぱなしでよい=ここで待たない。親は自分の作業へ戻るか、このターンを終える。\n` +
2876
+ `完了通知: ${agentWaitLaunchForm(cmd)}。exit が完了通知(${AITERM_WAIT_OUTCOME_NOTE})。\n` +
2877
+ `foreground 実行は親を最大 ${DEFAULT_AGENT_DONE_TIMEOUT} 秒塞ぐので使わない。回収: pty_read(agent_transcript:true)`);
2878
+ }
2879
+ // 未完了 session へ触った時の共通案内。ここでも待つのは waiter プロセスであって親ではない。
2880
+ export function agentWaitGuide(session) {
2881
+ const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
2882
+ return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
2883
+ }
2860
2884
  // 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
2861
2885
  // event fileのtail規律(未終端行保持・増分上限)はwaitAgentDoneEventと同一だが、
2862
2886
  // vendor_session_idのbind永続化を行わない点だけ意図的に異なる(waiterは観測者であって所有者でない)。
@@ -2914,8 +2938,10 @@ export async function observeAgentDone(name, o = {}) {
2914
2938
  if (scanned.event)
2915
2939
  return observation("done", scanned.event);
2916
2940
  }
2941
+ // timeout=0 は「待たずに一度だけ見る」照会=未完了は失敗ではなく running。
2942
+ // 1秒以上を指定した待機の未完了は従来どおり timeout で、待ち方の意味は変えない。
2917
2943
  if (performance.now() >= deadline)
2918
- return observation("timeout");
2944
+ return observation(timeout === 0 ? "running" : "timeout");
2919
2945
  await sleep(AGENT_DONE_POLL_MS);
2920
2946
  }
2921
2947
  }
@@ -3147,8 +3173,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
3147
3173
  const residue = await detectAgentSubmitResidue(name, meta.kind, text);
3148
3174
  return {
3149
3175
  text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset}\n` +
3150
- `起動時 prompt を送信した。完了通知は aiterm-wait --session ${name} --cursor ${startOffset} をホストのバックグラウンドタスクとして実行し、` +
3151
- `exit 時に receipt の outcome で判定する(${AITERM_WAIT_OUTCOME_NOTE})。回収は pty_read(agent_transcript:true) を使う。` +
3176
+ `起動時 prompt を送信した。${agentDispatchGuide(name, startOffset)}` +
3152
3177
  agentSubmitResidueWarning(name, residue.residue),
3153
3178
  event_cursor: startOffset,
3154
3179
  submit_residue: residue.residue,
@@ -3622,7 +3647,7 @@ export function openAgent(kind, opts = {}) {
3622
3647
  }
3623
3648
  const driveHint = agentDone && kind === "claude"
3624
3649
  ? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
3625
- `turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch・完了通知はaiterm-wait)。中断はpty_key(${sid}, "C-c")、` +
3650
+ `turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch=投げっぱなしでよい・完了通知はaiterm-wait)。中断はpty_key(${sid}, "C-c")、` +
3626
3651
  `Stopが来ない場合の解除はpty_close(${sid})を使う。`
3627
3652
  : `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
3628
3653
  `pty_send(${sid}, "...") で入力・pty_key(${sid}, "Enter"/"Up"/"C-c" 等) で操作する(対話)。`;
package/dist/index.js CHANGED
@@ -20,6 +20,15 @@ import { createRequire } from "node:module";
20
20
  // 除去済み=どちらの静的構文も 18〜22 全域を満たせないため(実行時 require が唯一全域で動く)。
21
21
  const pkg = createRequire(import.meta.url)("../package.json");
22
22
  const server = new McpServer({ name: "aiterm", version: pkg.version });
23
+ /**
24
+ * dispatch 系の説明で共有する非ブロック規範。tool description は registerTool 時=initialize 前に
25
+ * 固定されるため親ホストを名指しできない(ホスト別の具体形は receipt 側が core.agentWaitLaunchForm で出す)。
26
+ * ここでは「待つな」を断定形で先に置き、foreground 実行の禁止までを説明の側に含める。
27
+ */
28
+ const NON_BLOCKING_RULE = "dispatch した子は投げっぱなしでよい=親はここで待たない。" +
29
+ "完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` を親のターンを塞がない別プロセスとして起動して受け、" +
30
+ `exit を完了通知として扱う(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
31
+ "この待ちコマンドを foreground で実行して親のターンを塞ぐことはしない(receipt が実際の起動形を示す)。";
23
32
  function ok(s) {
24
33
  return { content: [{ type: "text", text: s }] };
25
34
  }
@@ -88,9 +97,8 @@ server.registerTool("pty_open", {
88
97
  server.registerTool("pty_send", {
89
98
  description: "セッションへテキストを送る。通常PTYへは送信のみ(出力は pty_read で取得)。" +
90
99
  "agent session(launcher起動)への send は自動で dispatch になる: TUI の ready gate と submit 分離を通して即返り、" +
91
- "receipt の event_cursor を返す=親はブロックしない。完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` を" +
92
- "ホストのバックグラウンドタスクとして実行し、exit時にreceiptのoutcomeで判定する" +
93
- `(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
100
+ "receipt の event_cursor を返す。" +
101
+ NON_BLOCKING_RULE +
94
102
  "結果回収は pty_read(agent_transcript:true)、Claude の durable turn は claude_turn を使う。" +
95
103
  "force:true は非Claude agent sessionへの手動介入用の素送信。managed Claudeの承認UIはclaude_approvalを使う。",
96
104
  inputSchema: {
@@ -137,9 +145,8 @@ server.registerTool("pty_send", {
137
145
  content: [
138
146
  {
139
147
  type: "text",
140
- text: `dispatchした(vendor=${receipt.vendor})。完了通知: aiterm-wait --session ${receipt.session_id} --cursor ${receipt.event_cursor} を` +
141
- `ホストのバックグラウンドタスクとして実行し、exit時にreceiptのoutcomeで判定(${core.AITERM_WAIT_OUTCOME_NOTE})。` +
142
- "回収: pty_read(agent_transcript:true)" +
148
+ text: `dispatchした(vendor=${receipt.vendor})。\n` +
149
+ core.agentDispatchGuide(receipt.session_id, receipt.event_cursor) +
143
150
  core.agentSubmitResidueWarning(receipt.session_id, receipt.submit_residue),
144
151
  },
145
152
  ],
@@ -404,12 +411,12 @@ const agentEffortDesc = (kind) => kind === "claude"
404
411
  "composer は effort 自体非対応)。指定すると起動前にエラーを返す"
405
412
  : "reasoning effort(思考レベル)。low/medium/high/xhigh/max/ultra(CLI 版依存)。" +
406
413
  "ultra は max 推論+proactive 自動委譲 ON=使用量急増注意(明示要求時のみ)。省略時は端末 config/CLI 既定。";
407
- // 全launcher共通の完了受信ガイド。launch応答のwait_command(起動時promptあり時)/pty_send dispatchの
408
- // event_cursorから組んだaiterm-waitをホストのバックグラウンドタスクとして1本実行し、exit時にreceiptの
409
- // outcomeで判定する。
410
- const agentCompletionDesc = `完了通知は起動応答の wait_command(初回prompt時)または pty_send dispatch 後の ` +
411
- `aiterm-wait --session <id> --cursor <event_cursor> をホストのバックグラウンドタスクとして実行し、` +
412
- `exit時にreceiptのoutcomeで判定する(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
414
+ // 全launcher共通の完了受信ガイド。待ちコマンドは起動応答の wait_command(初回prompt時)または
415
+ // pty_send dispatch の event_cursor から組む。文型は NON_BLOCKING_RULE と同じく「待たない」が先。
416
+ const agentCompletionDesc = `起動して投げたら投げっぱなしでよい=親はここで待たない。` +
417
+ `完了通知は起動応答の wait_command(初回prompt時)または pty_send dispatch 後の ` +
418
+ `aiterm-wait --session <id> --cursor <event_cursor> を親のターンを塞がない別プロセスとして起動して受ける` +
419
+ `(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要・foreground実行はしない)。` +
413
420
  `結果回収は pty_read(agent_transcript:true)。`;
414
421
  function registerAgentTool(toolName, kind, desc) {
415
422
  const correlatedLaunchSchema = {};
@@ -492,6 +499,12 @@ registerAgentTool("composer_agent", "composer", "【Grok Build の Composer モ
492
499
  agentCompletionDesc +
493
500
  "model を引数で指定可。reasoning_effort は非対応(指定はエラー)。");
494
501
  async function main() {
502
+ // 親ホストを initialize の clientInfo.name から確定させ、receipt の完了待ちコマンドを
503
+ // そのホストの実際の起動形で名指しする(実測: claude-code は initialize → notifications/initialized
504
+ // → tools/list の順で送るため、どの tool 呼び出しより先に確定する)。取れない時は汎用文へ落ちるだけ。
505
+ server.server.oninitialized = () => {
506
+ core.setParentClient(server.server.getClientVersion()?.name ?? null);
507
+ };
495
508
  const transport = new StdioServerTransport();
496
509
  await server.connect(transport);
497
510
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "mcpName": "io.github.kitepon-rgb/aiterm-mcp",
5
5
  "description": "AI-driven persistent terminal as a local stdio MCP server (tmux-backed). Holds one local PTY; SSH and containers are just commands you send into it. Also launches interactive Claude/Codex/Grok/Composer agent TUIs in a persistent terminal. Token-reducing reads.",
6
6
  "keywords": [