aiterm-mcp 0.27.6 → 0.27.7

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/dist/core.js CHANGED
@@ -10,13 +10,15 @@
10
10
  import { spawnSync } from "node:child_process";
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
- import * as os from "node:os";
14
- import { createHash, randomBytes, randomUUID } from "node:crypto";
15
- import { fileURLToPath } from "node:url";
13
+ import { createHash, randomBytes } from "node:crypto";
16
14
  import * as rtk from "./rtk.js";
17
15
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
18
- import { isWin, SOCKDIR, SOCK, WIN_NS, tmuxCommand, tmuxCommandWithInput } from "./tmux-runtime.js";
19
- import { resolveAgentBin, spawnAgentControlCommand, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
16
+ import { isWin, SOCKDIR, tmuxCommand, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
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, 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 { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
20
22
  export { AitermError } from "./errors.js";
21
23
  export { tmuxSpawnEnv } from "./tmux-runtime.js";
22
24
  // 完了検出
@@ -37,20 +39,13 @@ const NON_POSIX_MARK_SHELLS = new Set(["fish", "csh", "tcsh"]);
37
39
  // エコーは rc=%d(リテラル)。数字アンカーでエコーに免疫化し、部分一致による早期誤完了を防ぐ(B1)。
38
40
  // send の printf 書式(`rc=%d`)と対で保守すること。
39
41
  const MARK_DONE_RE = /<<<AITERM_DONE rc=[0-9]+>>>/;
40
- const LAUNCH_ID_RE = /^[0-9a-f]{32}$/;
41
- const OPERATION_ID_RE = /^sha256:[0-9a-f]{64}$/;
42
42
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
43
43
  const AGENT_LINEAGE_RE = /^[A-Za-z0-9_.:-]+(?:>[A-Za-z0-9_.:-]+)*$/;
44
- const AGENT_DONE_POLL_MS = 100;
45
44
  const AGENT_DONE_SETTLE_MIN_MS = 250;
46
45
  const AGENT_DONE_SCREEN_SETTLE_POLL_MS = 100;
47
46
  const AGENT_DONE_SCREEN_SETTLE_MAX_POLLS = 5;
48
47
  const AGENT_DONE_SCREEN_SETTLE_MIN_SAMPLES = 3;
49
48
  const AGENT_SUBMIT_DELAY_MS = 250;
50
- const AGENT_EVENT_MAX_BYTES = 1024 * 1024;
51
- const AGENT_EVENT_TAIL_BYTES = 64 * 1024;
52
- const CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES = 16 * 1024 * 1024;
53
- const GROK_TRANSCRIPT_INCREMENT_MAX_BYTES = 16 * 1024 * 1024;
54
49
  const AGENT_METADATA_NEGATIVE_CACHE_TTL_MS = 2_000;
55
50
  const AGENT_TUI_READY_TIMEOUT_MS = 30_000;
56
51
  const AGENT_TUI_READY_POLL_MS = 500;
@@ -63,10 +58,6 @@ const AGENT_SUBMIT_RESIDUE_POLL_MS = 300;
63
58
  const AGENT_SUBMIT_RESIDUE_MAX_SAMPLES = 5;
64
59
  const AGENT_SUBMIT_RESIDUE_TAIL_CHARS = 32;
65
60
  const AGENT_SUBMIT_RESIDUE_MIN_TAIL_CHARS = 8;
66
- const GROK_AUTH_MAX_BYTES = 64 * 1024;
67
- const GROK_MODELS_MAX_BYTES = 1024 * 1024;
68
- const GROK_MODELS_TIMEOUT_MS = 15_000;
69
- const CLAUDE_RESULT_MAX_BYTES = 4 * 1024 * 1024;
70
61
  // 出力削減(RTK の CAP 思想を移植)
71
62
  const MAX_LINES_BEFORE_ELIDE = 60;
72
63
  const HEAD_LINES = 30;
@@ -116,9 +107,6 @@ const KEYMAP = {
116
107
  function tmux(...args) {
117
108
  return tmuxCommand(true, ...args);
118
109
  }
119
- function tmuxWithInput(input, ...args) {
120
- return tmuxCommandWithInput(true, input, ...args);
121
- }
122
110
  function tmuxCleanup(...args) {
123
111
  return tmuxCommand(false, ...args);
124
112
  }
@@ -129,13 +117,7 @@ function paneCurrentCommand(name) {
129
117
  const r = tmux("display-message", "-p", "-t", name, "#{pane_current_command}");
130
118
  if (r.code !== 0)
131
119
  return "";
132
- const cmd = r.stdout.trim();
133
- // Windows(psmux)は "bash.exe" やフルパス形で報告しうる。SHELLS 等の POSIX 名
134
- // 集合と突合できるよう、basename・.exe 除去・小文字化へ正規化する(Windows の
135
- // 実行ファイル名は case-insensitive)。POSIX は従来どおり無加工。
136
- if (!isWin)
137
- return cmd;
138
- return path.basename(cmd).replace(/\.exe$/i, "").toLowerCase();
120
+ return normalizePaneCommand(r.stdout.trim());
139
121
  }
140
122
  function pasteBufferSupportsNoSanitizeFlag() {
141
123
  const listed = tmux("list-commands");
@@ -187,104 +169,6 @@ function assertSendTextSize(text, context = "送信文字列") {
187
169
  throw new AitermError(`${context}が${MAX_SEND_BYTES} bytesを超えています(${bytes} bytes)`, 2);
188
170
  }
189
171
  }
190
- // session 名はファイルパス(logpath 等)と pipe-pane の /bin/sh 文字列へ流れる。英数 _ - のみ・64字に
191
- // 限定し、パストラバーサル(../)とシェルインジェクション(' でのクオート破り・$・; 等)を全入口で断つ。
192
- function assertSessionName(name) {
193
- if (!/^[A-Za-z0-9_-]{1,64}$/.test(name))
194
- throw new AitermError(`session 名は英数字と _ - のみ・64文字以内にしてください: ${JSON.stringify(name)}`, 2);
195
- }
196
- function currentUid() {
197
- // Windows(native)は process.getuid を持たない。以前はここで throw していたが、
198
- // agent metadata の存在確認経由で素の pty_send まで巻き込んで全 send を殺していた。
199
- // Windows の fs.Stats.uid は常に 0 なので、ここも 0 を返せば owner 比較
200
- // (st.uid !== currentUid()) は自然に通過する。Windows は POSIX owner 検証を
201
- // 持たない(NTFS ACL は別体系)という既知の制約の明示的受容であり、POSIX 側は
202
- // getuid をそのまま返すため挙動不変。
203
- if (typeof process.getuid !== "function")
204
- return 0;
205
- return process.getuid();
206
- }
207
- function runtimeStateBase() {
208
- const xdg = process.env.XDG_RUNTIME_DIR;
209
- if (xdg) {
210
- try {
211
- if (fs.statSync(xdg).isDirectory())
212
- return xdg;
213
- }
214
- catch {
215
- /* XDG_RUNTIME_DIR が壊れている CI/非 login 環境では os.tmpdir() に戻す */
216
- }
217
- }
218
- return os.tmpdir();
219
- }
220
- function stateRoot() {
221
- const uid = currentUid();
222
- const base = runtimeStateBase();
223
- return path.join(base, `aiterm-mcp-${uid}`);
224
- }
225
- function ensureStateRoot() {
226
- // state root は OS が与える per-user runtime dir(XDG_RUNTIME_DIR / os.tmpdir())の下に作る。
227
- // 以前はここで symlink・owner・mode を検査していたが、共有 /tmp に敵対的な同居主体がいる
228
- // 前提の防御であり、対応 OS の既定配置では成立しない(オーナー裁定 2026-08-19)。
229
- // 作成時の 0o700 は検査ではなく妥当な既定として残す。経路の異常は以降の
230
- // open/stat が OS エラーとしてそのまま露出させる。
231
- const root = stateRoot();
232
- fs.mkdirSync(root, { recursive: true, mode: 0o700 });
233
- fs.mkdirSync(path.join(root, "agents"), { recursive: true, mode: 0o700 });
234
- return root;
235
- }
236
- function agentsDir() {
237
- return path.join(ensureStateRoot(), "agents");
238
- }
239
- function agentEventPath(name, launchId) {
240
- assertSessionName(name);
241
- if (!LAUNCH_ID_RE.test(launchId))
242
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
243
- return path.join(agentsDir(), `${name}.${launchId}.events.jsonl`);
244
- }
245
- function agentMetadataPath(name, launchId) {
246
- assertSessionName(name);
247
- if (!LAUNCH_ID_RE.test(launchId))
248
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
249
- return path.join(agentsDir(), `${name}.${launchId}.agent.json`);
250
- }
251
- function agentWaitLockPath(name, launchId) {
252
- assertSessionName(name);
253
- if (!LAUNCH_ID_RE.test(launchId))
254
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
255
- return path.join(agentsDir(), `${name}.${launchId}.wait.lock`);
256
- }
257
- function agentManagedClaudeSettingsPath(name, launchId) {
258
- assertSessionName(name);
259
- if (!LAUNCH_ID_RE.test(launchId))
260
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
261
- return path.join(agentsDir(), `${name}.${launchId}.claude-settings.json`);
262
- }
263
- function agentClaudeResultPath(name, launchId) {
264
- assertSessionName(name);
265
- if (!LAUNCH_ID_RE.test(launchId))
266
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
267
- return path.join(agentsDir(), `${name}.${launchId}.claude-result.json`);
268
- }
269
- function agentClaudeOperationPath(name, launchId) {
270
- assertSessionName(name);
271
- if (!LAUNCH_ID_RE.test(launchId))
272
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
273
- return path.join(agentsDir(), `${name}.${launchId}.claude-operation.json`);
274
- }
275
- function agentClaudeApprovalReceiptPath(name, launchId) {
276
- assertSessionName(name);
277
- if (!LAUNCH_ID_RE.test(launchId))
278
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
279
- return path.join(agentsDir(), `${name}.${launchId}.claude-approval.json`);
280
- }
281
- function agentClaudeDispatchReceiptPath(name, launchId, operationId) {
282
- assertSessionName(name);
283
- if (!LAUNCH_ID_RE.test(launchId))
284
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
285
- const validated = validateOperationId(operationId);
286
- return path.join(agentsDir(), `${name}.${launchId}.${validated.slice("sha256:".length)}.claude-dispatch`);
287
- }
288
172
  function existingAgentsDir() {
289
173
  // 既存の agents dir があればその path、無ければ null(作成はしない)。
290
174
  // 以前はここで symlink・owner・mode を検査していたが撤去した(オーナー裁定 2026-08-19)。
@@ -381,8 +265,7 @@ function readLastcmd(name) {
381
265
  }
382
266
  }
383
267
  export function attachHint(name) {
384
- // Windows は native psmux(tmux CLI 互換)を -L namespace で叩く。
385
- const cmd = isWin ? `psmux -L ${WIN_NS} attach -t ${name}` : `tmux -S ${SOCK} attach -t ${name}`;
268
+ const cmd = attachCommand(name);
386
269
  return (`このセッションを自分の目で見る/介入する:\n` +
387
270
  ` ${cmd}\n` +
388
271
  ` (抜けるには Ctrl-b d)`);
@@ -502,27 +385,6 @@ function captureScreen(name, lines) {
502
385
  const r = tmux(...args);
503
386
  return r.code === 0 ? r.stdout : "";
504
387
  }
505
- const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
506
- // Windows 専用: pipe-pane のログは psmux server 側の in-process sink が書く=完了検知と
507
- // 書き手が別 process のため、完了/セッション消滅の報告後も末尾数百バイトが遅れて現れうる。
508
- // 完了と判定する直前にログサイズが伸びなくなるまで待ち、readOutput が末尾欠けの出力を
509
- // 返さないようにする(POSIX の tmux は /bin/sh sink・同一 fs で実測上不要)。
510
- async function settleWinLog(name) {
511
- let prev = -1;
512
- for (let i = 0; i < 8; i++) {
513
- let sz = 0;
514
- try {
515
- sz = fs.statSync(logpath(name)).size;
516
- }
517
- catch {
518
- sz = 0;
519
- }
520
- if (sz === prev)
521
- return;
522
- prev = sz;
523
- await sleep(POLL * 1000);
524
- }
525
- }
526
388
  /** 完了境界: dead / mark sentinel 一致 / until 一致 / (出力静止 ∧ シェル復帰)=quiescent / (ネスト中+until/mark無しで出力静止)=nested(未確定・早期返却) / timeout。 */
527
389
  async function waitCompletion(name, untilStr, untilRegex, timeout) {
528
390
  // 締切は単調時計で測る。Date.now() は NTP 補正やサスペンドで巻き戻り、長時間待ちで誤判定する(Python は time.monotonic)。
@@ -591,8 +453,7 @@ async function waitCompletion(name, untilStr, untilRegex, timeout) {
591
453
  catch {
592
454
  /* noop */
593
455
  }
594
- if (isWin)
595
- await settleWinLog(name);
456
+ await settlePaneLog(logpath(name), POLL);
596
457
  return [true, "mark"];
597
458
  }
598
459
  if (until && until(neu))
@@ -602,8 +463,7 @@ async function waitCompletion(name, untilStr, untilRegex, timeout) {
602
463
  // 静止時のみ has-session を叩いて dead を判定する。dead 検出は最大 1 poll 遅れるだけ。
603
464
  const growing = lastSize !== null && size > lastSize;
604
465
  if (!growing && !sessionExists(name)) {
605
- if (isWin)
606
- await settleWinLog(name);
466
+ await settlePaneLog(logpath(name), POLL);
607
467
  return [true, "dead"];
608
468
  }
609
469
  if (size === lastSize) {
@@ -619,8 +479,7 @@ async function waitCompletion(name, untilStr, untilRegex, timeout) {
619
479
  stable = 0;
620
480
  }
621
481
  else if (!until && !markActive && SHELLS.has(fg)) {
622
- if (isWin)
623
- await settleWinLog(name);
482
+ await settlePaneLog(logpath(name), POLL);
624
483
  return [true, "quiescent"]; // 出力静止 ∧ シェル復帰 = 確証つき完了
625
484
  }
626
485
  else if (!until && !markActive && fg !== "") {
@@ -630,8 +489,7 @@ async function waitCompletion(name, untilStr, untilRegex, timeout) {
630
489
  // until/markActive のときは指定した証拠を待つべく早期返却せず、shell builtinや
631
490
  // 非シェル前面(sleep 等)でも待ち続ける。
632
491
  // fg==="" は前面コマンド取得失敗=ネスト断定不可なので早期返却せず従来どおり timeout まで待つ。
633
- if (isWin)
634
- await settleWinLog(name);
492
+ await settlePaneLog(logpath(name), POLL);
635
493
  return [false, "nested"];
636
494
  }
637
495
  }
@@ -707,8 +565,8 @@ export function openSession(name, shell = "bash") {
707
565
  throw new AitermError(`session '${nm}' は既に存在します(list で確認)`, 2);
708
566
  }
709
567
  else {
710
- // -f は端末個人の設定ファイルを読まないための空 config(Windows は NUL デバイス)。
711
- const r = tmux("new-session", "-d", "-s", nm, ...banner, "-f", isWin ? "NUL" : "/dev/null", shell);
568
+ // -f は端末個人の設定ファイルを読まないための空 config。
569
+ const r = tmux("new-session", "-d", "-s", nm, ...banner, "-f", TMUX_EMPTY_CONFIG, shell);
712
570
  if (r.code === 0)
713
571
  break;
714
572
  // 自動採番かつ「重複名」由来の失敗(他エージェントが同名を先に取った)なら次名でリトライ。
@@ -851,34 +709,15 @@ export function send(name, text, o = {}) {
851
709
  const partial = i > 0
852
710
  ? " 先行chunkはPTYに入力済みでEnterは未送信です。再送前に入力を確認・消去してください。"
853
711
  : "";
854
- // psmux の load-buffer は stdin (`-`) 非対応で path 位置引数だけを取る。
855
- // Windows は owner-only の SOCKDIR 内へ一時ファイル経由で渡す。
856
- let loaded;
857
- if (isWin) {
858
- const chunkFile = path.join(SOCKDIR, `${bufferName}.chunk`);
859
- try {
860
- fs.writeFileSync(chunkFile, chunks[i], { encoding: "utf8" });
861
- loaded = tmux("load-buffer", "-b", bufferName, chunkFile);
862
- }
863
- finally {
864
- try {
865
- fs.unlinkSync(chunkFile);
866
- }
867
- catch { /* noop */ }
868
- }
869
- }
870
- else {
871
- loaded = tmuxWithInput(chunks[i], "load-buffer", "-b", bufferName, "-");
872
- }
712
+ const loaded = loadPtyBufferChunk(true, bufferName, chunks[i]);
873
713
  if (loaded.code !== 0) {
874
714
  tmuxCleanup("delete-buffer", "-b", bufferName);
875
715
  throw new AitermError(`tmux bufferへの送信準備に失敗しました` +
876
716
  `(chunk ${i + 1}/${chunks.length}): ${loaded.stderr.trim() || `code=${loaded.code}`}.${partial}`, 2);
877
717
  }
878
- // -r: LF→CR 置換を無効化。-Sは対応新版だけでvis(3)制御文字変換を無効化する。
718
+ // -Sは対応新版だけでvis(3)制御文字変換を無効化する。
879
719
  // send 自身の raw/sanitize 契約だけを真実とし、tmux 側で黙って再変換させない。
880
- // psmux は -r 非対応(受理フラグは d/p/b/t のみ)のため Windows では付けない。
881
- const pasteArgs = isWin ? ["paste-buffer", "-d"] : ["paste-buffer", "-d", "-r"];
720
+ const pasteArgs = pasteBufferBaseArgs();
882
721
  if (o.bracketedPaste)
883
722
  pasteArgs.push("-p");
884
723
  if (pasteSupportsNoSanitize)
@@ -1270,309 +1109,9 @@ function createAgentLineageContext(kind, sessionId, seed) {
1270
1109
  delegationAllowed: true,
1271
1110
  };
1272
1111
  }
1273
- function agentLineageFields(context) {
1274
- return {
1275
- agent_role: context.agentRole,
1276
- parent_session_id: context.parentSessionId,
1277
- delegation_depth: context.delegationDepth,
1278
- lineage: context.lineage,
1279
- delegation_allowed: context.delegationAllowed,
1280
- };
1281
- }
1282
- function subagentInstruction(meta) {
1283
- if (meta.agent_role !== "subagent" ||
1284
- !meta.parent_session_id ||
1285
- !Number.isSafeInteger(meta.delegation_depth) ||
1286
- !meta.lineage ||
1287
- meta.delegation_allowed !== true) {
1288
- throw new AitermError("sub-agent instructionに必要なlineage metadataがありません", 2);
1289
- }
1290
- return [
1291
- "<aiterm_subagent_context>",
1292
- "あなたはaitermから起動されたsub-agentであり、root agentではありません。",
1293
- `AITERM_AGENT_LAUNCH_ID=${meta.launch_id}`,
1294
- `role=${meta.agent_role}`,
1295
- `parent_session_id=${meta.parent_session_id}`,
1296
- `delegation_depth=${meta.delegation_depth}`,
1297
- `lineage=${meta.lineage}`,
1298
- "delegation_allowed=true",
1299
- "任務の所有権を保ち、結果を親へ返してください。必要なら追加のsub-agentへ委譲してよいです。",
1300
- "ただし、同じ任務全体を同型agentへ反射的に丸投げして自己複製ループを作らないでください。",
1301
- "</aiterm_subagent_context>",
1302
- ].join("\n");
1303
- }
1304
1112
  const DEFAULT_AGENT_DONE_TIMEOUT = 600;
1305
1113
  const agentMetadataNegativeCache = new Map();
1306
1114
  let agentTuiReadyStableSamplesTestOverride = null;
1307
- function claudeHookScriptPath() {
1308
- return path.join(path.dirname(fileURLToPath(import.meta.url)), "claude-stop-hook.js");
1309
- }
1310
- // process.execPath は Homebrew 等では Cellar の版付き実体を指す。長寿命 MCP server の起動後に
1311
- // runtime が更新されるとその実体だけが消え、既に生成済みの hook が exit 127 になる。
1312
- // hook は server と同じ継承 PATH から node を毎回解決し、安定した package script を実行する。
1313
- function nodeHookCommand(hookScript) {
1314
- return `${shq("node")} ${shq(hookScript)}`;
1315
- }
1316
- function safeStatSize(p) {
1317
- try {
1318
- return fs.statSync(p).size;
1319
- }
1320
- catch {
1321
- return 0;
1322
- }
1323
- }
1324
- function readFileRange(p, from, to) {
1325
- const len = Math.max(0, to - from);
1326
- if (len === 0)
1327
- return Buffer.alloc(0);
1328
- let fd;
1329
- try {
1330
- fd = fs.openSync(p, "r");
1331
- const buf = Buffer.alloc(len);
1332
- const n = fs.readSync(fd, buf, 0, len, from);
1333
- return n === len ? buf : buf.subarray(0, Math.max(0, n));
1334
- }
1335
- catch {
1336
- return Buffer.alloc(0);
1337
- }
1338
- finally {
1339
- if (fd !== undefined) {
1340
- try {
1341
- fs.closeSync(fd);
1342
- }
1343
- catch {
1344
- /* noop */
1345
- }
1346
- }
1347
- }
1348
- }
1349
- function writeJson0600(p, v) {
1350
- // truncate-in-place はクラッシュ/ENOSPC の窓で空・途中 JSON を残すので、temp→rename の原子的置換にする
1351
- const tmp = `${p}.${randomBytes(6).toString("hex")}.tmp`;
1352
- fs.writeFileSync(tmp, JSON.stringify(v, null, 2) + "\n", { mode: 0o600, flag: "wx" });
1353
- try {
1354
- fs.chmodSync(tmp, 0o600);
1355
- }
1356
- catch {
1357
- /* noop */
1358
- }
1359
- try {
1360
- fs.renameSync(tmp, p);
1361
- }
1362
- catch (e) {
1363
- try {
1364
- fs.unlinkSync(tmp);
1365
- }
1366
- catch {
1367
- /* noop */
1368
- }
1369
- throw e;
1370
- }
1371
- }
1372
- function writeText0600(p, text) {
1373
- fs.writeFileSync(p, text, { mode: 0o600 });
1374
- try {
1375
- fs.chmodSync(p, 0o600);
1376
- }
1377
- catch {
1378
- /* noop */
1379
- }
1380
- }
1381
- function createEmpty0600(p) {
1382
- // O_EXCL が「既存 path なら失敗」を保証するため、新規作成の一意性はこれで足りる。
1383
- // O_NOFOLLOW は撤去した(オーナー裁定 2026-08-19)。
1384
- const fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
1385
- fs.closeSync(fd);
1386
- }
1387
- function realCodexHome() {
1388
- return process.env.CODEX_HOME || path.join(process.env.HOME ?? os.homedir(), ".codex");
1389
- }
1390
- function readCodexConfigPins(configPath) {
1391
- let body;
1392
- try {
1393
- body = fs.readFileSync(configPath, "utf8");
1394
- }
1395
- catch {
1396
- return { model: { present: false, value: null }, effort: { present: false, value: null } };
1397
- }
1398
- const rows = body.split(/\r?\n/);
1399
- let firstTable = rows.findIndex((l) => /^\s*\[/.test(l));
1400
- if (firstTable === -1)
1401
- firstTable = rows.length;
1402
- const pick = (key) => {
1403
- for (const l of rows.slice(0, firstTable)) {
1404
- const m = l.match(new RegExp(`^\\s*(?:"${key}"|'${key}'|${key})\\s*=\\s*(.*)$`));
1405
- if (m) {
1406
- const v = m[1].trim().match(/^"([^"\\]*)"\s*(?:#.*)?$/);
1407
- return { present: true, value: v ? v[1] : null };
1408
- }
1409
- }
1410
- return { present: false, value: null };
1411
- };
1412
- return { model: pick("model"), effort: pick("model_reasoning_effort") };
1413
- }
1414
- function codexConfigSummary(configPath) {
1415
- let body;
1416
- try {
1417
- body = fs.readFileSync(configPath, "utf8");
1418
- }
1419
- catch {
1420
- return "";
1421
- }
1422
- const rows = body.split(/\r?\n/);
1423
- const mcpServers = rows.filter((line) => /^\s*\[mcp_servers\./.test(line)).length;
1424
- const firstTable = rows.findIndex((line) => /^\s*\[/.test(line));
1425
- const topLevel = rows.slice(0, firstTable === -1 ? rows.length : firstTable);
1426
- const valueOf = (key) => {
1427
- const row = topLevel.find((line) => new RegExp(`^\\s*(?:"${key}"|'${key}'|${key})\\s*=\\s*(.+?)\\s*(?:#.*)?$`).test(line));
1428
- if (!row)
1429
- return null;
1430
- const raw = row.match(/=\s*(.+?)(?:\s+#.*)?$/)?.[1].trim() ?? null;
1431
- if (!raw)
1432
- return null;
1433
- const quoted = raw.match(/^(?:"([^"\\]*)"|'([^'\\]*)')$/);
1434
- return quoted ? quoted[1] ?? quoted[2] : raw;
1435
- };
1436
- const bits = [`mcp_servers ${mcpServers} 個継承`];
1437
- const approvalPolicy = valueOf("approval_policy");
1438
- const sandboxMode = valueOf("sandbox_mode");
1439
- if (approvalPolicy)
1440
- bits.push(`approval_policy=${approvalPolicy}`);
1441
- if (sandboxMode)
1442
- bits.push(`sandbox_mode=${sandboxMode}`);
1443
- return `共有 config: ${bits.join(" / ")}`;
1444
- }
1445
- function createClaudeCorrelationSettings(name, launchId, model, effort) {
1446
- const hookScript = claudeHookScriptPath();
1447
- if (!fs.existsSync(hookScript)) {
1448
- throw new AitermError(`Claude Stop hook wrapper が見つかりません。npm run build を実行してください: ${hookScript}`, 2);
1449
- }
1450
- const settings = agentManagedClaudeSettingsPath(name, launchId);
1451
- writeJson0600(settings, {
1452
- ...(model ? { model } : {}),
1453
- ...(effort ? { effortLevel: effort } : {}),
1454
- hooks: {
1455
- Stop: [
1456
- {
1457
- hooks: [
1458
- {
1459
- type: "command",
1460
- command: nodeHookCommand(hookScript),
1461
- timeout: 10,
1462
- },
1463
- ],
1464
- },
1465
- ],
1466
- },
1467
- });
1468
- return settings;
1469
- }
1470
- function realGrokHome() {
1471
- return path.resolve(process.env.GROK_HOME || path.join(process.env.HOME ?? os.homedir(), ".grok"));
1472
- }
1473
- function resolveAndValidateGrokAuth(srcHome) {
1474
- const inheritedSet = Object.prototype.hasOwnProperty.call(process.env, "GROK_AUTH_PATH");
1475
- const inherited = process.env.GROK_AUTH_PATH;
1476
- if (inheritedSet && (!inherited || !path.isAbsolute(inherited)))
1477
- throw new AitermError("GROK_AUTH_PATH は空でない絶対パスで指定してください", 2);
1478
- const authPath = inherited ?? path.join(srcHome, "auth.json");
1479
- let fd;
1480
- try {
1481
- fd = fs.openSync(authPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
1482
- const st = fs.fstatSync(fd);
1483
- // Windows の fs.Stats.mode は POSIX permission bit を持たず、常に 666/777 相当を報告する
1484
- // (NTFS ACL は別体系)。currentUid と同じ既知制約の明示的受容として、Windows では
1485
- // group/other bit 検証を行わない。isFile・nlink・owner・size・O_NOFOLLOW・realpath 検証は共通に維持する。
1486
- const worldAccessible = !isWin && (st.mode & 0o077) !== 0;
1487
- if (!st.isFile() || st.nlink !== 1 || st.uid !== currentUid() || worldAccessible || st.size > GROK_AUTH_MAX_BYTES) {
1488
- throw new AitermError("Grok 認証正本の安全検証に失敗しました", 2);
1489
- }
1490
- const value = JSON.parse(fs.readFileSync(fd, "utf8"));
1491
- if (!value || typeof value !== "object" || Array.isArray(value))
1492
- throw new AitermError("Grok 認証正本のJSONが不正です", 2);
1493
- // auth file 自体は O_NOFOLLOW で開いているが、中間 directory の symlink は辿り得る。
1494
- // vendor に渡す正本を path swap の入口にしないため、字句正規化した絶対 path と realpath を
1495
- // 一致させ、canonical な祖先も root まで検証する。same-UID race の排他は vendor lock の責務。
1496
- const lexicalPath = path.resolve(authPath);
1497
- const canonicalPath = fs.realpathSync(authPath);
1498
- if (lexicalPath !== canonicalPath)
1499
- throw new AitermError("Grok 認証正本の path に symlink を含められません", 2);
1500
- for (let dir = path.dirname(canonicalPath);; dir = path.dirname(dir)) {
1501
- const dirSt = fs.lstatSync(dir);
1502
- // /tmp のような root 所有 + sticky の共有 directory は、本人所有の private な
1503
- // 直下 directory を他 UID が rename/unlink できないため許可する。sticky 無しの
1504
- // group/other writable 祖先は path swap が可能なので従来どおり拒否する。
1505
- // Windows は directory も mode bit を持たない(常に 777 相当)ため、同じ受容で除外する。
1506
- const writableByOthers = !isWin && (dirSt.mode & 0o022) !== 0;
1507
- const protectedSharedRoot = dirSt.uid === 0 && (dirSt.mode & 0o1000) !== 0;
1508
- if (!dirSt.isDirectory()
1509
- || dirSt.isSymbolicLink()
1510
- || (dirSt.uid !== currentUid() && dirSt.uid !== 0)
1511
- || (writableByOthers && !protectedSharedRoot)) {
1512
- throw new AitermError("Grok 認証正本の祖先 directory が安全ではありません", 2);
1513
- }
1514
- const parent = path.dirname(dir);
1515
- if (parent === dir)
1516
- break;
1517
- }
1518
- return canonicalPath;
1519
- }
1520
- catch (e) {
1521
- if (e.code === "ENOENT" && !inheritedSet && process.env.XAI_API_KEY)
1522
- return null;
1523
- if (e instanceof AitermError)
1524
- throw e;
1525
- throw new AitermError(e.code === "ENOENT" ? "Grok 認証正本が見つかりません。先に grok login が必要です" : "Grok 認証正本を安全に開けません", 2);
1526
- }
1527
- finally {
1528
- if (fd !== undefined)
1529
- fs.closeSync(fd);
1530
- }
1531
- }
1532
- function grokModelCatalog(bin, cwd) {
1533
- const result = spawnAgentControlCommand(bin, ["models"], cwd, {
1534
- cwd,
1535
- encoding: "utf8",
1536
- env: process.env,
1537
- timeout: GROK_MODELS_TIMEOUT_MS,
1538
- maxBuffer: GROK_MODELS_MAX_BYTES,
1539
- windowsHide: true,
1540
- });
1541
- if (result.error || result.status !== 0) {
1542
- const detail = result.error?.message || result.stderr?.trim() || `exit=${result.status ?? "unknown"}`;
1543
- throw new AitermError(`Grok model catalog を取得できません: ${detail}`, 2);
1544
- }
1545
- const text = result.stdout.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
1546
- const marker = "Available models:";
1547
- const start = text.indexOf(marker);
1548
- if (start < 0)
1549
- throw new AitermError("Grok model catalog の出力形式が不正です(Available models がありません)", 2);
1550
- const models = [];
1551
- for (const line of text.slice(start + marker.length).split(/\r?\n/)) {
1552
- const match = line.match(/^\s{2}[-*]\s+(.+?)(?:\s+\(default\))?\s*$/);
1553
- if (match?.[1])
1554
- models.push(match[1]);
1555
- }
1556
- if (models.length === 0)
1557
- throw new AitermError("Grok model catalog に利用可能なmodelがありません", 2);
1558
- return models;
1559
- }
1560
- function assertGrokModelAvailable(bin, cwd, model) {
1561
- const models = grokModelCatalog(bin, cwd);
1562
- if (!models.includes(model)) {
1563
- throw new AitermError(`Grok model catalog に ${JSON.stringify(model)} がありません。利用可能: ${models.join(", ")}。` +
1564
- "別modelへfallbackせず起動を中止しました", 2);
1565
- }
1566
- }
1567
- function writeAgentMetadata(meta) {
1568
- writeJson0600(agentMetadataPath(meta.aiterm_session, meta.launch_id), meta);
1569
- }
1570
- function validateOperationId(operationId) {
1571
- if (typeof operationId !== "string" || !OPERATION_ID_RE.test(operationId)) {
1572
- throw new AitermError("operation_id は sha256:<64 lowercase hex> で指定してください", 2);
1573
- }
1574
- return operationId;
1575
- }
1576
1115
  function writeClaudeOperationMarker(meta, operationId) {
1577
1116
  if (meta.kind !== "claude")
1578
1117
  throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
@@ -1997,82 +1536,6 @@ function liveWaitLocks(name) {
1997
1536
  }
1998
1537
  return out;
1999
1538
  }
2000
- function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId, launchRequestDigest, lineageContext, model, effort) {
2001
- const launchId = randomBytes(16).toString("hex");
2002
- const eventFile = agentEventPath(name, launchId);
2003
- const resultFile = agentClaudeResultPath(name, launchId);
2004
- createEmpty0600(eventFile);
2005
- createEmpty0600(resultFile);
2006
- const claudeSettings = createClaudeCorrelationSettings(name, launchId, model, effort);
2007
- const meta = {
2008
- kind: "claude",
2009
- aiterm_session: name,
2010
- launch_id: launchId,
2011
- event_file: eventFile,
2012
- created_at: new Date().toISOString(),
2013
- cwd,
2014
- vendor_session_id: randomUUID(),
2015
- initial_prompt: initialPrompt,
2016
- launch_operation_id: launchOperationId,
2017
- launch_request_digest: launchRequestDigest,
2018
- hook_route: "shared_claude_settings",
2019
- ...agentLineageFields(lineageContext),
2020
- node_platform: process.platform,
2021
- claude_settings: claudeSettings,
2022
- result_file: resultFile,
2023
- };
2024
- writeAgentMetadata(meta);
2025
- return meta;
2026
- }
2027
- function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}, writeScope, lineageContext) {
2028
- const launchId = randomBytes(16).toString("hex");
2029
- const eventFile = agentEventPath(name, launchId);
2030
- createEmpty0600(eventFile);
2031
- const codexHome = realCodexHome();
2032
- const meta = {
2033
- kind: "codex",
2034
- aiterm_session: name,
2035
- launch_id: launchId,
2036
- event_file: eventFile,
2037
- created_at: new Date().toISOString(),
2038
- cwd,
2039
- ...(writeScope === undefined ? {} : { write_scope: writeScope }),
2040
- vendor_session_id: null,
2041
- initial_prompt: initialPrompt,
2042
- hook_route: "shared_codex_home",
2043
- completion_route: "codex_transcript",
2044
- ...(lineageContext ? agentLineageFields(lineageContext) : {}),
2045
- node_platform: process.platform,
2046
- codex_home: codexHome,
2047
- };
2048
- writeAgentMetadata(meta);
2049
- return meta;
2050
- }
2051
- function createGrokAgentMetadata(kind, name, cwd, initialPrompt, authPath, writeScope, lineageContext) {
2052
- const launchId = randomBytes(16).toString("hex");
2053
- const eventFile = agentEventPath(name, launchId);
2054
- createEmpty0600(eventFile);
2055
- const grokHome = realGrokHome();
2056
- const meta = {
2057
- kind,
2058
- aiterm_session: name,
2059
- launch_id: launchId,
2060
- event_file: eventFile,
2061
- created_at: new Date().toISOString(),
2062
- cwd,
2063
- ...(writeScope === undefined ? {} : { write_scope: writeScope }),
2064
- vendor_session_id: randomUUID(),
2065
- initial_prompt: initialPrompt,
2066
- hook_route: "shared_grok_home",
2067
- completion_route: "grok_transcript",
2068
- ...(lineageContext ? agentLineageFields(lineageContext) : {}),
2069
- node_platform: process.platform,
2070
- grok_home: grokHome,
2071
- grok_auth_path: authPath,
2072
- };
2073
- writeAgentMetadata(meta);
2074
- return meta;
2075
- }
2076
1539
  function loadAgentLineageFields(m, required) {
2077
1540
  const present = m.agent_role !== undefined ||
2078
1541
  m.parent_session_id !== undefined ||
@@ -2319,7 +1782,7 @@ function bindCompletedInitialPrompt(meta) {
2319
1782
  if (meta.initial_prompt !== "pending" && meta.initial_prompt !== "sent")
2320
1783
  return;
2321
1784
  if (meta.kind === "codex") {
2322
- const done = latestCodexCompletion(meta);
1785
+ const done = latestCodexCompletion(meta, readTranscriptLines);
2323
1786
  if (!done) {
2324
1787
  throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
2325
1788
  }
@@ -2328,7 +1791,7 @@ function bindCompletedInitialPrompt(meta) {
2328
1791
  return;
2329
1792
  }
2330
1793
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2331
- if (!latestGrokCompletion(meta)) {
1794
+ if (!latestGrokCompletion(meta, readTranscriptLines)) {
2332
1795
  throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
2333
1796
  }
2334
1797
  setInitialPromptState(meta, "done");
@@ -2363,9 +1826,9 @@ function tryLoadAgentMetadata(name) {
2363
1826
  }
2364
1827
  function latestAgentDoneEvent(meta, expectedOperationId = null) {
2365
1828
  if (meta.kind === "codex")
2366
- return latestCodexCompletion(meta);
1829
+ return latestCodexCompletion(meta, readTranscriptLines);
2367
1830
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2368
- return latestGrokCompletion(meta);
1831
+ return latestGrokCompletion(meta, readTranscriptLines);
2369
1832
  }
2370
1833
  const size = safeStatSize(meta.event_file);
2371
1834
  if (size === 0)
@@ -2425,7 +1888,7 @@ function recoverAgentVendorSession(meta) {
2425
1888
  const transcript = bindCodexTranscriptSession(meta);
2426
1889
  if (!transcript)
2427
1890
  return;
2428
- if (meta.initial_prompt === "pending" && latestCodexCompletion(meta))
1891
+ if (meta.initial_prompt === "pending" && latestCodexCompletion(meta, readTranscriptLines))
2429
1892
  setInitialPromptState(meta, "done");
2430
1893
  return;
2431
1894
  }
@@ -2447,263 +1910,6 @@ function recoverAgentVendorSession(meta) {
2447
1910
  bindAgentVendorSession(meta, scanned.event);
2448
1911
  writeAgentMetadata(meta);
2449
1912
  }
2450
- function findLatestCodexTranscript(codexHome, vendorSessionId) {
2451
- const sessionsDir = path.join(codexHome, "sessions");
2452
- let latestFile = null;
2453
- let latestMtime = -Infinity;
2454
- const visit = (dir) => {
2455
- let entries;
2456
- try {
2457
- entries = fs.readdirSync(dir, { withFileTypes: true });
2458
- }
2459
- catch {
2460
- return;
2461
- }
2462
- for (const entry of entries) {
2463
- const file = path.join(dir, entry.name);
2464
- if (entry.isDirectory()) {
2465
- visit(file);
2466
- continue;
2467
- }
2468
- if (!entry.isFile() || !entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl") || !entry.name.includes(vendorSessionId))
2469
- continue;
2470
- try {
2471
- const mtimeMs = fs.statSync(file).mtimeMs;
2472
- if (mtimeMs > latestMtime) {
2473
- latestFile = file;
2474
- latestMtime = mtimeMs;
2475
- }
2476
- }
2477
- catch {
2478
- // 探索中に消えた transcript は候補にしない。候補が無ければ明示エラーにする。
2479
- }
2480
- }
2481
- };
2482
- visit(sessionsDir);
2483
- return latestFile;
2484
- }
2485
- function listCodexTranscripts(codexHome) {
2486
- const sessionsDir = path.join(codexHome, "sessions");
2487
- const files = [];
2488
- const visit = (dir) => {
2489
- let entries;
2490
- try {
2491
- entries = fs.readdirSync(dir, { withFileTypes: true });
2492
- }
2493
- catch {
2494
- return;
2495
- }
2496
- for (const entry of entries) {
2497
- const file = path.join(dir, entry.name);
2498
- if (entry.isDirectory())
2499
- visit(file);
2500
- else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl"))
2501
- files.push(file);
2502
- }
2503
- };
2504
- visit(sessionsDir);
2505
- return files.sort();
2506
- }
2507
- function codexTranscriptMatchesLaunch(file, meta) {
2508
- const createdAt = Date.parse(meta.created_at);
2509
- try {
2510
- const st = fs.statSync(file);
2511
- if (Number.isFinite(createdAt) && st.mtimeMs + 5_000 < createdAt)
2512
- return false;
2513
- }
2514
- catch {
2515
- return false;
2516
- }
2517
- const size = Math.min(safeStatSize(file), 1024 * 1024);
2518
- if (size === 0)
2519
- return false;
2520
- const marker = `AITERM_AGENT_LAUNCH_ID=${meta.launch_id}`;
2521
- let rootCli = false;
2522
- let launchMarker = false;
2523
- for (const line of readFileRange(file, 0, size).toString("utf8").split("\n")) {
2524
- if (!line.trim())
2525
- continue;
2526
- let record;
2527
- try {
2528
- record = JSON.parse(line);
2529
- }
2530
- catch {
2531
- continue;
2532
- }
2533
- if (record?.type === "session_meta") {
2534
- rootCli = record?.payload?.originator === "codex-tui" && record?.payload?.source === "cli";
2535
- if (!rootCli)
2536
- return false;
2537
- }
2538
- if (record?.type === "response_item" &&
2539
- record?.payload?.type === "message" &&
2540
- record?.payload?.role === "developer" &&
2541
- Array.isArray(record?.payload?.content)) {
2542
- launchMarker ||= record.payload.content.some((item) => (item?.type === "input_text" || item?.type === "output_text") &&
2543
- typeof item?.text === "string" &&
2544
- item.text.includes(marker));
2545
- }
2546
- if (rootCli && launchMarker)
2547
- return true;
2548
- }
2549
- return false;
2550
- }
2551
- function codexTranscriptSessionId(file) {
2552
- const size = Math.min(safeStatSize(file), AGENT_EVENT_TAIL_BYTES);
2553
- if (size === 0)
2554
- return null;
2555
- const text = readFileRange(file, 0, size).toString("utf8");
2556
- for (const line of text.split("\n")) {
2557
- if (!line.trim())
2558
- continue;
2559
- try {
2560
- const record = JSON.parse(line);
2561
- if (record?.type === "session_meta" && typeof record?.payload?.id === "string" && record.payload.id) {
2562
- return record.payload.id;
2563
- }
2564
- }
2565
- catch {
2566
- // startup中の未完結行は後のpollで読み直す。
2567
- }
2568
- }
2569
- return null;
2570
- }
2571
- function codexRootTranscript(meta) {
2572
- if (meta.kind !== "codex" || !meta.codex_home)
2573
- return null;
2574
- if (meta.vendor_session_id)
2575
- return findLatestCodexTranscript(meta.codex_home, meta.vendor_session_id);
2576
- if (meta.hook_route === "shared_codex_home") {
2577
- const matches = listCodexTranscripts(meta.codex_home).filter((file) => codexTranscriptMatchesLaunch(file, meta));
2578
- if (matches.length > 1) {
2579
- throw new AitermError("共有CODEX_HOMEに同じlaunch markerのroot rolloutが複数あります。sessionを閉じて起動し直してください。", 2);
2580
- }
2581
- return matches[0] ?? null;
2582
- }
2583
- return listCodexTranscripts(meta.codex_home)[0] ?? null;
2584
- }
2585
- function bindCodexTranscriptSession(meta) {
2586
- const transcript = codexRootTranscript(meta);
2587
- if (!transcript)
2588
- return null;
2589
- const vendorSessionId = codexTranscriptSessionId(transcript);
2590
- if (vendorSessionId && !meta.vendor_session_id) {
2591
- meta.vendor_session_id = vendorSessionId;
2592
- writeAgentMetadata(meta);
2593
- }
2594
- return transcript;
2595
- }
2596
- function codexCompletionEvent(meta, vendorSessionId, record) {
2597
- if (record?.type !== "event_msg" ||
2598
- record?.payload?.type !== "task_complete" ||
2599
- typeof record?.payload?.turn_id !== "string" ||
2600
- !record.payload.turn_id)
2601
- return null;
2602
- return {
2603
- type: "agent_done",
2604
- vendor: "codex",
2605
- aiterm_session: meta.aiterm_session,
2606
- launch_id: meta.launch_id,
2607
- vendor_session_id: vendorSessionId,
2608
- turn_id: record.payload.turn_id,
2609
- operation_id: null,
2610
- reason: "Codex transcript task_complete",
2611
- done_status: "turn_done",
2612
- stop_hook_active: false,
2613
- at: typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString(),
2614
- };
2615
- }
2616
- function latestCodexCompletion(meta) {
2617
- const transcript = codexRootTranscript(meta);
2618
- if (!transcript)
2619
- return null;
2620
- const vendorSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
2621
- let latest = null;
2622
- for (const line of readTranscriptLines(transcript)) {
2623
- if (!line.trim())
2624
- continue;
2625
- try {
2626
- latest = codexCompletionEvent(meta, vendorSessionId, JSON.parse(line)) ?? latest;
2627
- }
2628
- catch {
2629
- // Codexが末尾を書込み中なら、その行は次の観測で完結してから読む。
2630
- }
2631
- }
2632
- return latest;
2633
- }
2634
- function grokSessionDirectory(meta) {
2635
- if ((meta.kind !== "grok" && meta.kind !== "composer") || !meta.grok_home || !meta.vendor_session_id)
2636
- return null;
2637
- const cwd = meta.cwd ?? process.cwd();
2638
- return path.join(meta.grok_home, "sessions", encodeURIComponent(cwd), meta.vendor_session_id);
2639
- }
2640
- function grokEventsTranscript(meta) {
2641
- const dir = grokSessionDirectory(meta);
2642
- return dir ? path.join(dir, "events.jsonl") : null;
2643
- }
2644
- function grokCompletionEvent(meta, record) {
2645
- if ((meta.kind !== "grok" && meta.kind !== "composer") ||
2646
- record?.type !== "turn_ended" ||
2647
- (record?.outcome !== "completed" && record?.outcome !== "cancelled"))
2648
- return null;
2649
- const turnId = typeof record?.ts === "string" || typeof record?.ts === "number" ? String(record.ts) : null;
2650
- return {
2651
- type: "agent_done",
2652
- vendor: meta.kind,
2653
- aiterm_session: meta.aiterm_session,
2654
- launch_id: meta.launch_id,
2655
- vendor_session_id: meta.vendor_session_id,
2656
- turn_id: turnId,
2657
- operation_id: null,
2658
- reason: `Grok transcript turn_ended:${record.outcome}`,
2659
- done_status: "turn_done",
2660
- stop_hook_active: false,
2661
- at: typeof record?.ts === "string" ? record.ts : new Date().toISOString(),
2662
- };
2663
- }
2664
- function latestGrokCompletion(meta) {
2665
- const transcript = grokEventsTranscript(meta);
2666
- if (!transcript || !fs.existsSync(transcript))
2667
- return null;
2668
- let latest = null;
2669
- for (const line of readTranscriptLines(transcript)) {
2670
- if (!line.trim())
2671
- continue;
2672
- try {
2673
- latest = grokCompletionEvent(meta, JSON.parse(line)) ?? latest;
2674
- }
2675
- catch {
2676
- // 末尾書込み中のlineは次の観測で完結してから読む。
2677
- }
2678
- }
2679
- return latest;
2680
- }
2681
- function grokInitializationComplete(meta) {
2682
- const transcript = grokEventsTranscript(meta);
2683
- if (!transcript || !fs.existsSync(transcript))
2684
- return false;
2685
- const size = safeStatSize(transcript);
2686
- const from = Math.max(0, size - GROK_TRANSCRIPT_INCREMENT_MAX_BYTES);
2687
- const lines = readFileRange(transcript, from, size).toString("utf8").split("\n");
2688
- if (from > 0)
2689
- lines.shift();
2690
- for (let index = lines.length - 1; index >= 0; index -= 1) {
2691
- const line = lines[index].trim();
2692
- if (!line)
2693
- continue;
2694
- try {
2695
- const record = JSON.parse(line);
2696
- if (record?.type === "mcp_init_completed")
2697
- return true;
2698
- if (record?.type === "mcp_init_started")
2699
- return false;
2700
- }
2701
- catch {
2702
- // 末尾書込み中や無関係な破損lineはready判定を進めない。
2703
- }
2704
- }
2705
- return false;
2706
- }
2707
1913
  function agentCompletionCursor(meta) {
2708
1914
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2709
1915
  const transcript = grokEventsTranscript(meta);
@@ -2734,47 +1940,6 @@ function readTranscriptLines(file) {
2734
1940
  throw new AitermError(`transcript を読めません: ${e.message}`, 2);
2735
1941
  }
2736
1942
  }
2737
- function readClaudeResultText(meta, done, operationId) {
2738
- if (meta.kind !== "claude" || !meta.result_file || !done.result_digest || done.result_bytes == null) {
2739
- transcriptUnavailable();
2740
- }
2741
- let st;
2742
- try {
2743
- st = fs.lstatSync(meta.result_file);
2744
- }
2745
- catch {
2746
- transcriptUnavailable();
2747
- }
2748
- if (!st.isFile() ||
2749
- st.isSymbolicLink() ||
2750
- st.uid !== currentUid() ||
2751
- st.nlink !== 1 ||
2752
- (!isWin && (st.mode & 0o077) !== 0) ||
2753
- st.size > CLAUDE_RESULT_MAX_BYTES + 4096) {
2754
- throw new AitermError("Claude result file の安全検証に失敗しました", 2);
2755
- }
2756
- let result;
2757
- try {
2758
- result = JSON.parse(fs.readFileSync(meta.result_file, "utf8"));
2759
- }
2760
- catch {
2761
- throw new AitermError("Claude result file を読めません", 2);
2762
- }
2763
- const keys = result && typeof result === "object" && !Array.isArray(result) ? Object.keys(result).sort() : [];
2764
- if (keys.join(",") !== "operation_id,result_bytes,result_digest,schema,text,vendor_session_id" ||
2765
- result.schema !== "aiterm.claude-turn-result.v2" ||
2766
- result.operation_id !== done.operation_id ||
2767
- (operationId !== null && result.operation_id !== operationId) ||
2768
- result.vendor_session_id !== meta.vendor_session_id ||
2769
- result.result_digest !== done.result_digest ||
2770
- result.result_bytes !== done.result_bytes ||
2771
- typeof result.text !== "string" ||
2772
- Buffer.byteLength(result.text, "utf8") !== done.result_bytes ||
2773
- createHash("sha256").update(result.text, "utf8").digest("hex") !== done.result_digest) {
2774
- throw new AitermError("Claude result file が完了eventと一致しません", 2);
2775
- }
2776
- return result.text;
2777
- }
2778
1943
  const CLAUDE_COMPLETION_MARKER_SETTLE_TIMEOUT_MS = 1_000;
2779
1944
  function claudeCompletionWasPublishedAfterMarker(meta, marker) {
2780
1945
  let markerStat;
@@ -2833,75 +1998,13 @@ export async function readAgentTranscript(name, o = {}) {
2833
1998
  if (meta.kind === "claude") {
2834
1999
  if (!done)
2835
2000
  transcriptUnavailable();
2836
- text = readClaudeResultText(meta, done, operationId);
2001
+ text = readClaudeResultText(meta, done, operationId, transcriptUnavailable);
2837
2002
  }
2838
2003
  else if (meta.kind === "codex") {
2839
- if (!meta.codex_home)
2840
- transcriptUnavailable();
2841
- const transcript = findLatestCodexTranscript(meta.codex_home, meta.vendor_session_id);
2842
- if (!transcript)
2843
- transcriptUnavailable();
2844
- const lines = readTranscriptLines(transcript);
2845
- const matching = [];
2846
- let finalAnswer = "";
2847
- for (const line of lines) {
2848
- if (!line.trim())
2849
- continue;
2850
- let record;
2851
- try {
2852
- record = JSON.parse(line);
2853
- }
2854
- catch {
2855
- continue;
2856
- }
2857
- const payload = record?.payload;
2858
- if (record?.type === "response_item" &&
2859
- payload?.type === "message" &&
2860
- payload?.role === "assistant" &&
2861
- payload?.internal_chat_message_metadata_passthrough?.turn_id === turnId &&
2862
- Array.isArray(payload?.content)) {
2863
- for (const item of payload.content) {
2864
- if (item?.type === "output_text" && typeof item.text === "string")
2865
- matching.push(item.text);
2866
- }
2867
- }
2868
- if (record?.type === "event_msg" &&
2869
- payload?.type === "agent_message" &&
2870
- payload?.phase === "final_answer" &&
2871
- typeof payload?.message === "string") {
2872
- finalAnswer = payload.message;
2873
- }
2874
- }
2875
- text = matching.join("\n") || finalAnswer;
2004
+ text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable);
2876
2005
  }
2877
2006
  else {
2878
- if (!meta.grok_home)
2879
- transcriptUnavailable();
2880
- // cwd 未指定で起動した TUI はサーバープロセスの cwd を継承する。metadata に null が残る既存
2881
- // launch との互換のため、その実際の起動 cwd を path 導出に使う(launch 側は変更しない)。
2882
- const cwd = meta.cwd ?? process.cwd();
2883
- const transcript = path.join(meta.grok_home, "sessions", encodeURIComponent(cwd), meta.vendor_session_id, "chat_history.jsonl");
2884
- const lines = readTranscriptLines(transcript);
2885
- let lastUser = -1;
2886
- const records = [];
2887
- for (const line of lines) {
2888
- if (!line.trim())
2889
- continue;
2890
- try {
2891
- const record = JSON.parse(line);
2892
- records.push(record);
2893
- if (record?.type === "user" && !("synthetic_reason" in record))
2894
- lastUser = records.length - 1;
2895
- }
2896
- catch {
2897
- // 外部 transcript の壊れた1行は残りの完結行を読む妨げにしない。
2898
- }
2899
- }
2900
- text = records
2901
- .slice(lastUser + 1)
2902
- .filter((record) => record?.type === "assistant" && typeof record?.content === "string")
2903
- .map((record) => record.content)
2904
- .join("\n");
2007
+ text = grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable);
2905
2008
  }
2906
2009
  if (!text.trim())
2907
2010
  transcriptNotFound(meta.kind);
@@ -3040,166 +2143,6 @@ export function detectAgentRateLimit(kind, aitermSession) {
3040
2143
  }
3041
2144
  return null;
3042
2145
  }
3043
- async function observeCodexDone(meta, timeout, requestedCursor) {
3044
- const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
3045
- let transcript = codexRootTranscript(meta);
3046
- const startOffset = requestedCursor ?? (transcript ? safeStatSize(transcript) : 0);
3047
- let cursor = startOffset;
3048
- let carry = "";
3049
- let malformedEvents = 0;
3050
- let discardLeadingFragment = false;
3051
- let initializedBoundary = false;
3052
- const deadline = performance.now() + timeout * 1000;
3053
- const observation = (outcome, ev = null, rateLimit = null) => ({
3054
- schema: "aiterm.agent-wait-result.v1",
3055
- session_id: meta.aiterm_session,
3056
- launch_id: meta.launch_id,
3057
- vendor: "codex",
3058
- outcome,
3059
- operation_id: null,
3060
- vendor_session_id: ev?.vendor_session_id ?? meta.vendor_session_id ?? null,
3061
- turn_id: ev?.turn_id ?? null,
3062
- malformed_events: malformedEvents,
3063
- at: ev?.at ?? null,
3064
- rate_limit: rateLimit,
3065
- });
3066
- for (;;) {
3067
- if (!fs.existsSync(metadataFile))
3068
- return observation("closed");
3069
- transcript ??= codexRootTranscript(meta);
3070
- if (transcript) {
3071
- if (!initializedBoundary) {
3072
- if (cursor > 0) {
3073
- const previous = readFileRange(transcript, cursor - 1, cursor).toString("utf8");
3074
- discardLeadingFragment = previous !== "\n";
3075
- }
3076
- initializedBoundary = true;
3077
- }
3078
- const size = safeStatSize(transcript);
3079
- if (size < cursor) {
3080
- throw new AitermError("Codex transcript が完了待機中に短くなりました。該当セッションを閉じて起動し直してください。", 2);
3081
- }
3082
- if (size - startOffset > CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES) {
3083
- throw new AitermError("Codex transcript のturn増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
3084
- }
3085
- if (size > cursor) {
3086
- carry += readFileRange(transcript, cursor, size).toString("utf8");
3087
- cursor = size;
3088
- const parts = carry.split("\n");
3089
- carry = parts.pop() ?? "";
3090
- if (discardLeadingFragment && parts.length > 0) {
3091
- parts.shift();
3092
- discardLeadingFragment = false;
3093
- }
3094
- const vendorSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
3095
- for (const line of parts) {
3096
- if (!line.trim())
3097
- continue;
3098
- if (Buffer.byteLength(line, "utf8") > AGENT_EVENT_MAX_BYTES) {
3099
- malformedEvents++;
3100
- continue;
3101
- }
3102
- try {
3103
- const done = codexCompletionEvent(meta, vendorSessionId, JSON.parse(line));
3104
- if (done)
3105
- return observation("done", done);
3106
- }
3107
- catch {
3108
- malformedEvents++;
3109
- }
3110
- }
3111
- }
3112
- }
3113
- {
3114
- const limited = detectAgentRateLimit(meta.kind, meta.aiterm_session);
3115
- if (limited)
3116
- return observation("rate_limited", null, limited);
3117
- }
3118
- if (performance.now() >= deadline)
3119
- return observation(timeout === 0 ? "running" : "timeout");
3120
- await sleep(AGENT_DONE_POLL_MS);
3121
- }
3122
- }
3123
- async function observeGrokDone(meta, timeout, requestedCursor) {
3124
- const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
3125
- const transcript = grokEventsTranscript(meta);
3126
- if (!transcript)
3127
- throw new AitermError("Grok transcriptのsession相関情報がありません", 2);
3128
- const startOffset = requestedCursor ?? safeStatSize(transcript);
3129
- let cursor = startOffset;
3130
- let carry = "";
3131
- let malformedEvents = 0;
3132
- let discardLeadingFragment = false;
3133
- let initializedBoundary = false;
3134
- const deadline = performance.now() + timeout * 1000;
3135
- const observation = (outcome, ev = null, rateLimit = null) => ({
3136
- schema: "aiterm.agent-wait-result.v1",
3137
- session_id: meta.aiterm_session,
3138
- launch_id: meta.launch_id,
3139
- vendor: meta.kind,
3140
- outcome,
3141
- operation_id: null,
3142
- vendor_session_id: meta.vendor_session_id,
3143
- turn_id: ev?.turn_id ?? null,
3144
- malformed_events: malformedEvents,
3145
- at: ev?.at ?? null,
3146
- rate_limit: rateLimit,
3147
- });
3148
- for (;;) {
3149
- if (!fs.existsSync(metadataFile))
3150
- return observation("closed");
3151
- if (fs.existsSync(transcript)) {
3152
- if (!initializedBoundary) {
3153
- if (cursor > 0) {
3154
- const previous = readFileRange(transcript, cursor - 1, cursor).toString("utf8");
3155
- discardLeadingFragment = previous !== "\n";
3156
- }
3157
- initializedBoundary = true;
3158
- }
3159
- const size = safeStatSize(transcript);
3160
- if (size < cursor) {
3161
- throw new AitermError("Grok transcript が完了待機中に短くなりました。該当セッションを閉じて起動し直してください。", 2);
3162
- }
3163
- if (size - startOffset > GROK_TRANSCRIPT_INCREMENT_MAX_BYTES) {
3164
- throw new AitermError("Grok transcript のturn増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
3165
- }
3166
- if (size > cursor) {
3167
- carry += readFileRange(transcript, cursor, size).toString("utf8");
3168
- cursor = size;
3169
- const parts = carry.split("\n");
3170
- carry = parts.pop() ?? "";
3171
- if (discardLeadingFragment && parts.length > 0) {
3172
- parts.shift();
3173
- discardLeadingFragment = false;
3174
- }
3175
- for (const line of parts) {
3176
- if (!line.trim())
3177
- continue;
3178
- if (Buffer.byteLength(line, "utf8") > AGENT_EVENT_MAX_BYTES) {
3179
- malformedEvents++;
3180
- continue;
3181
- }
3182
- try {
3183
- const done = grokCompletionEvent(meta, JSON.parse(line));
3184
- if (done)
3185
- return observation("done", done);
3186
- }
3187
- catch {
3188
- malformedEvents++;
3189
- }
3190
- }
3191
- }
3192
- }
3193
- {
3194
- const limited = detectAgentRateLimit(meta.kind, meta.aiterm_session);
3195
- if (limited)
3196
- return observation("rate_limited", null, limited);
3197
- }
3198
- if (performance.now() >= deadline)
3199
- return observation(timeout === 0 ? "running" : "timeout");
3200
- await sleep(AGENT_DONE_POLL_MS);
3201
- }
3202
- }
3203
2146
  // 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
3204
2147
  // Codexはrollout transcript、他vendorはevent fileを増分走査し、vendor_session_idのbind永続化を
3205
2148
  // 行わない(waiterは観測者であって所有者でない)。
@@ -3214,10 +2157,10 @@ export async function observeAgentDone(name, o = {}) {
3214
2157
  }
3215
2158
  const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
3216
2159
  if (meta.kind === "codex" && meta.completion_route === "codex_transcript") {
3217
- return observeCodexDone(meta, timeout, o.cursor);
2160
+ return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit);
3218
2161
  }
3219
2162
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
3220
- return observeGrokDone(meta, timeout, o.cursor);
2163
+ return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit);
3221
2164
  }
3222
2165
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
3223
2166
  // 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
@@ -3277,21 +2220,11 @@ export async function observeAgentDone(name, o = {}) {
3277
2220
  }
3278
2221
  }
3279
2222
  function isAgentTuiReady(kind, screen) {
3280
- if (kind === "claude") {
3281
- return screen.includes("Claude Code") && /(^|\n)\s*❯/.test(screen);
3282
- }
3283
- if (kind === "codex") {
3284
- // 起動直後は製品header、長寿命sessionでは常駐footerがCodex TUIの識別子になる。
3285
- // capture-paneは直近45行だけなので、会話が進むとheaderは正常に画面外へ流れる。
3286
- const codexFrontend = screen.includes("OpenAI Codex")
3287
- || /(^|\n)\s*\S+\s+(?:low|medium|high|xhigh|max|ultra)(?:\s+fast)?\s+·\s+\S.*$/m.test(screen);
3288
- return codexFrontend && /(^|\n)\s*[›>]/.test(screen);
3289
- }
3290
- // Grok Build 0.2.117 は起動完了後に製品名を消し、model footerだけを残す。
3291
- // Composerも同じfrontendでmodel名だけが異なるため、両方をvendor UIの根拠にする。
3292
- // Windows native grok.exe(1.0.4 実測)は入力欄markerを `❯` でなく `>` で描画するため両方を受ける。
3293
- const grokFrontend = screen.includes("Grok Build") || /\b(?:Grok|Composer)\s+[\w.()-]+/.test(screen);
3294
- return grokFrontend && /(^|\n|\s)[❯>]/.test(screen);
2223
+ if (kind === "claude")
2224
+ return claudeTuiReady(screen);
2225
+ if (kind === "codex")
2226
+ return codexTuiReady(screen);
2227
+ return grokTuiReady(screen);
3295
2228
  }
3296
2229
  // Codex/Claude は実行中に「(esc to interrupt)」を表示する(実機採取)。startup 側の処理
3297
2230
  // (MCP initialize 等)が走ったまま composer だけ描画されている画面は入力受付とみなさない。
@@ -3356,7 +2289,7 @@ function agentSubmitResidueOnScreen(kind, screen, tail) {
3356
2289
  // 入力欄マーカーは ready 判定と同じ記号を行頭基準で探す。submit 済みの transcript echo は
3357
2290
  // マーカー行より上に出るため、最後のマーカー行以降だけを composer 領域として見る。
3358
2291
  // grok/composer は Windows native 描画(`>`・実測 1.0.4)も ready 判定と同様に受ける。
3359
- const markerRe = kind === "codex" ? /^\s*[›>]/ : kind === "claude" ? /^\s*❯/ : /(^|\s)[❯>]/;
2292
+ const markerRe = kind === "codex" ? CODEX_COMPOSER_MARKER_RE : kind === "claude" ? CLAUDE_COMPOSER_MARKER_RE : GROK_COMPOSER_MARKER_RE;
3360
2293
  let markerIdx = -1;
3361
2294
  for (let i = lines.length - 1; i >= 0; i--) {
3362
2295
  if (markerRe.test(lines[i])) {
@@ -3544,21 +2477,6 @@ function lineCounts(screen) {
3544
2477
  }
3545
2478
  return counts;
3546
2479
  }
3547
- function grokFooterHasConfiguration(screen, model, effort) {
3548
- const modelMatch = model?.match(/^grok-(\d+(?:\.\d+)*)$/) ?? null;
3549
- if (model && !modelMatch)
3550
- return false;
3551
- const modelLabel = modelMatch ? `Grok ${modelMatch[1]}` : null;
3552
- return screen.split("\n").some((line) => {
3553
- if (!line.includes("·"))
3554
- return false;
3555
- if (modelLabel && !line.includes(`${modelLabel} (`))
3556
- return false;
3557
- if (effort && !line.includes(`(${effort})`))
3558
- return false;
3559
- return modelLabel !== null || effort !== null;
3560
- });
3561
- }
3562
2480
  async function waitForGrokConfigurationResult(name, before, model, effort, timeoutMs = 3_000) {
3563
2481
  const beforeCounts = lineCounts(before);
3564
2482
  const footerAlreadyMatched = grokFooterHasConfiguration(before, model, effort);
@@ -3592,41 +2510,6 @@ function sendMenuChoice(name, choice) {
3592
2510
  throw new AitermError(`agent設定の選択を送れませんでした: ${sent.stderr.trim() || `code=${sent.code}`}`, 2);
3593
2511
  }
3594
2512
  }
3595
- function codexModelChoice(screen, model) {
3596
- for (const line of screen.slice(screen.lastIndexOf("Select Model and Effort")).split("\n")) {
3597
- const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+(\S+)/);
3598
- if (match?.[2] === model)
3599
- return match[1];
3600
- }
3601
- return null;
3602
- }
3603
- function codexEffortChoice(screen, effort) {
3604
- const labels = {
3605
- low: /^Low\b/i,
3606
- medium: /^Medium\b/i,
3607
- high: /^High\b/i,
3608
- xhigh: /^Extra high\b/i,
3609
- max: /^Max\b/i,
3610
- ultra: /^Ultra\b/i,
3611
- };
3612
- const wanted = labels[effort.toLowerCase()];
3613
- if (!wanted)
3614
- return null;
3615
- for (const line of screen.split("\n")) {
3616
- const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+(.+?)\s{2,}/);
3617
- if (match && wanted.test(match[2]))
3618
- return match[1];
3619
- }
3620
- return null;
3621
- }
3622
- function codexMoreReasoningChoice(screen) {
3623
- for (const line of screen.split("\n")) {
3624
- const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+More reasoning/);
3625
- if (match)
3626
- return match[1];
3627
- }
3628
- return null;
3629
- }
3630
2513
  /** 同じ対話sessionを保ったまま、vendor標準の操作でmodel/effortを変更する。 */
3631
2514
  export async function configureAgent(name, opts) {
3632
2515
  assertSessionName(name);
@@ -3839,47 +2722,13 @@ function inspectClaudeOperation(meta, operationId, action) {
3839
2722
  bindAgentVendorSession(meta, done);
3840
2723
  writeAgentMetadata(meta);
3841
2724
  }
3842
- const rawOutput = readClaudeResultText(meta, done, operationId);
2725
+ const rawOutput = readClaudeResultText(meta, done, operationId, transcriptUnavailable);
3843
2726
  return { ...base, status: "completed", raw_output: rawOutput, reason: null };
3844
2727
  }
3845
2728
  // ── 対話型エージェント起動(Claude / Codex / Grok Build(Grok) / Grok Build(Composer))──────
3846
2729
  // aiterm の永続端末に、指定モデルの対話エージェント TUI を起動する。以後は pty_read で画面を
3847
2730
  // 読み、pty_send で操作する=aiterm の対話パラダイムそのもの。モデルはツールごとに固定し、
3848
2731
  // reasoning effort は引数で渡す。CLI 未導入環境は明示エラー(動くフリをしない)。
3849
- const CLAUDE_AUTH_STATUS_TIMEOUT_MS = 5_000;
3850
- function assertClaudeAuthenticationReady(bin) {
3851
- const result = spawnAgentControlCommand(bin, ["auth", "status", "--json"], process.cwd(), {
3852
- encoding: "utf8",
3853
- timeout: CLAUDE_AUTH_STATUS_TIMEOUT_MS,
3854
- maxBuffer: 64 * 1024,
3855
- });
3856
- let status = null;
3857
- try {
3858
- status = JSON.parse((result.stdout ?? "").trim());
3859
- }
3860
- catch {
3861
- status = null;
3862
- }
3863
- if (result.error == null &&
3864
- result.status === 0 &&
3865
- status !== null &&
3866
- typeof status === "object" &&
3867
- !Array.isArray(status) &&
3868
- status.loggedIn === true) {
3869
- return;
3870
- }
3871
- if (status !== null &&
3872
- typeof status === "object" &&
3873
- !Array.isArray(status) &&
3874
- status.loggedIn === false) {
3875
- throw new AitermError("Claude Codeの認証を利用できません。sessionは作成していません。" +
3876
- "通常端末で `claude doctor` を実行し、Keychain/credential storeを直してから一度だけ `claude auth login` を実行してください。" +
3877
- "aiterm相関付きClaude session内で /login を繰り返さないでください。", 2);
3878
- }
3879
- const timedOut = result.error && result.error.code === "ETIMEDOUT";
3880
- throw new AitermError(`Claude Codeの認証状態を起動前に確認できません${timedOut ? "(5秒でtimeout)" : ""}。sessionは作成していません。` +
3881
- "`claude auth status --json` と `claude doctor` が成功することを通常端末で確認してください。", 2);
3882
- }
3883
2732
  const THROUGHLINE_HANDOFF_CONTEXT_SCHEMA = "throughline.handoff_context.v1";
3884
2733
  const PORTABLE_FORK_MISSION_SEPARATOR = "\n\n---\n\n## Portable fork mission\n\n";
3885
2734
  export function composePortableForkPrompt(context, mission) {
@@ -3922,66 +2771,12 @@ export function vendorLauncherDiagnostic(kind) {
3922
2771
  return "unverified";
3923
2772
  }
3924
2773
  }
3925
- // 単一引用符で安全に包む(' は '\'' で脱出)。send は raw:true で送るため自前で quote する。
3926
- function shq(s) {
3927
- return `'${s.replace(/'/g, "'\\''")}'`;
3928
- }
3929
- // grok CLI はモデル未指定だと端末側 default に従うため、ツール契約として既定 slug を固定する。
3930
- // codex は既定 slug を持たず端末 config/CLI 既定に委ねる(起動応答で実効値を報告する)。
3931
- // grok 既定は dotagents 規範(docs/02_models.md: xAI 旗艦 = grok-4.6)に従う。
3932
- const GROK_MODEL_DEFAULTS = {
3933
- grok: "grok-4.6",
3934
- composer: "grok-composer-2.5-fast",
3935
- };
3936
- const CLAUDE_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
3937
2774
  function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
3938
- const parts = [shq(bin)];
3939
- if (kind === "claude") {
3940
- if (meta?.kind === "claude") {
3941
- parts.push("--setting-sources", shq("user,project,local"), "--settings", shq(meta.claude_settings ?? ""), "--session-id", shq(meta.vendor_session_id ?? ""), "--append-system-prompt", shq(subagentInstruction(meta)));
3942
- }
3943
- if (model)
3944
- parts.push("--model", shq(model));
3945
- if (effort)
3946
- parts.push("--effort", shq(effort));
3947
- }
3948
- else if (kind === "codex") {
3949
- // `codex --help` で確認した実在フラグ。read-only 宣言だけはCLI sandboxへ落とし、
3950
- // launcher自身が実効能力壁を作る。パス説明はCodex CLIに同等のallowlist引数がないため宣言のまま残す。
3951
- if (meta?.kind === "codex" && meta.write_scope === "read-only")
3952
- parts.push("--sandbox", "read-only");
3953
- // model/effort は共有configを書き換えず、CLI引数で明示して起動単位に優先する。
3954
- if (model)
3955
- parts.push("-m", shq(model));
3956
- if (effort)
3957
- parts.push("-c", `model_reasoning_effort=${shq(effort)}`);
3958
- if (meta?.kind === "codex" && meta.hook_route === "shared_codex_home") {
3959
- parts.push("-c", `developer_instructions=${shq(subagentInstruction(meta))}`);
3960
- }
3961
- }
3962
- else {
3963
- // grok / composer は同じ grok CLI をモデル違いで起動する。
3964
- parts.push("--no-auto-update");
3965
- if (meta?.kind === "grok" || meta?.kind === "composer")
3966
- parts.push("--no-alt-screen");
3967
- parts.push("--model", shq(model ?? GROK_MODEL_DEFAULTS[kind]));
3968
- if (effort)
3969
- parts.push("--reasoning-effort", shq(effort));
3970
- if ((meta?.kind === "grok" || meta?.kind === "composer") && meta.write_scope === "read-only") {
3971
- // read-only は sandbox が実効書込み禁止を作るため、MCP ツール許可ダイアログの自動承認を
3972
- // 付けても能力は増えない。無人 subagent が初回 MCP 使用の許可待ちで停止する実障害への対処。
3973
- // read-only 以外の launch には付けない=権限拡大しない。
3974
- parts.push("--sandbox", "read-only", "--always-approve");
3975
- }
3976
- if ((meta?.kind === "grok" || meta?.kind === "composer") && meta.hook_route === "shared_grok_home") {
3977
- parts.push("--session-id", shq(meta.vendor_session_id ?? ""), "--rules", shq(subagentInstruction(meta)));
3978
- }
3979
- if ((meta?.kind === "grok" || meta?.kind === "composer") && prompt)
3980
- parts.push("--verbatim");
3981
- }
3982
- if (prompt)
3983
- parts.push(shq(prompt)); // 初手プロンプト(任意)
3984
- return parts.join(" ");
2775
+ if (kind === "claude")
2776
+ return buildClaudeAgentCmd(bin, model, effort, prompt, meta);
2777
+ if (kind === "codex")
2778
+ return buildCodexAgentCmd(bin, model, effort, prompt, meta);
2779
+ return buildGrokAgentCmd(kind, bin, model, effort, prompt, meta);
3985
2780
  }
3986
2781
  function agentEnvPrefix(meta, sid, envVars = []) {
3987
2782
  const inherited = envVars.flatMap((name) => {
@@ -4005,60 +2800,16 @@ function agentEnvPrefix(meta, sid, envVars = []) {
4005
2800
  ];
4006
2801
  if (meta.kind === "claude" || meta.kind === "codex")
4007
2802
  return common;
4008
- // grok/composer: 検証済み auth 正本をそのままの path 形で渡す。Windows では native 強制により
4009
- // vendor は Windows process なので、Windows ドライブパスが正しい形(WSL 形への変換はしない)。
4010
- return [
4011
- ...(meta.grok_auth_path ? [`GROK_AUTH_PATH=${shq(meta.grok_auth_path)}`] : []),
4012
- "GROK_DISABLE_AUTOUPDATER=1",
4013
- ...common,
4014
- ];
2803
+ return [...grokEnvTokens(meta), ...common];
4015
2804
  })();
4016
2805
  return tokens.length ? tokens.join(" ") + " " : "";
4017
2806
  }
4018
- function agentLabel(kind) {
4019
- return kind === "claude"
4020
- ? "Claude Code"
4021
- : kind === "composer"
4022
- ? "Grok Build(Composer)"
4023
- : kind === "grok"
4024
- ? "Grok Build(Grok)"
4025
- : "Codex";
4026
- }
4027
- // 起動応答にモデル/effort の実効値と出所を明示する。codex は端末 config のピン(model /
4028
- // model_reasoning_effort)が対話子へ波及する構造のため、引数・端末config継承・CLI既定の
4029
- // どれで起動したかを起動時点で可視化し、実効 effort=ultra は proactive 自動委譲 ON を警告する。
4030
2807
  function buildAgentLaunchNote(kind, model, effort, meta) {
4031
- const writeScopeNote = meta?.write_scope === undefined
4032
- ? ""
4033
- : (kind === "codex" || kind === "grok" || kind === "composer") && meta.write_scope === "read-only"
4034
- ? `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。${agentLabel(kind)} CLIへ --sandbox read-only を付与し、書込みを実効禁止。` +
4035
- (kind === "codex" ? "" : "MCPツール許可は --always-approve で自動承認(sandbox内のため能力拡大なし)。")
4036
- : `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。パス単位のsandbox allowlistに対応するCLI引数がないため宣言の記録のみ(構造的unsupported)。`;
4037
- if (kind === "claude") {
4038
- return `起動設定: model=${model ?? "CLI既定"} effort=${effort ?? "CLI既定"}。${writeScopeNote}`;
4039
- }
4040
- if (kind !== "codex") {
4041
- return (`起動設定: model=${model ?? GROK_MODEL_DEFAULTS[kind]}(${model ? "引数" : "ツール既定"})。` +
4042
- `effort=${effort ?? "CLI/model既定"}。` + writeScopeNote);
4043
- }
4044
- const configPath = meta?.kind === "codex" && meta.codex_home
4045
- ? path.join(meta.codex_home, "config.toml")
4046
- : path.join(realCodexHome(), "config.toml");
4047
- const pins = readCodexConfigPins(configPath);
4048
- const describePin = (arg, pin) => arg
4049
- ? `${arg}(引数)`
4050
- : pin.present
4051
- ? pin.value
4052
- ? `${pin.value}(端末config継承)`
4053
- : "端末config継承(値未解析)"
4054
- : "CLI既定";
4055
- const effectiveEffort = effort ?? (pins.effort.present ? pins.effort.value : null);
4056
- const launch = `起動設定: model=${describePin(model, pins.model)} effort=${describePin(effort, pins.effort)}。` +
4057
- (effectiveEffort === "ultra"
4058
- ? "⚠ effort=ultra は max 推論+proactive 自動委譲 ON(子エージェント自動生成・使用量急増に注意)。"
4059
- : "");
4060
- const summary = meta?.kind === "codex" && meta.codex_home ? codexConfigSummary(configPath) : "";
4061
- return (summary ? `${launch}\n${summary}\n` : launch) + writeScopeNote;
2808
+ if (kind === "claude")
2809
+ return claudeLaunchNote(model, effort, meta);
2810
+ if (kind !== "codex")
2811
+ return grokLaunchNote(kind, model, effort, meta);
2812
+ return codexLaunchNote(model, effort, meta);
4062
2813
  }
4063
2814
  function claudeLaunchRequestDigest({ sessionName, model, effort, cwd, agentDone, }) {
4064
2815
  const canonical = JSON.stringify({
@@ -4193,9 +2944,7 @@ export function openAgent(kind, opts = {}) {
4193
2944
  "Windows 版 Grok CLI を導入するか、GROK_BIN に grok.exe の絶対パスを指定してください。", 2), 2);
4194
2945
  }
4195
2946
  const binForCmd = agentBinForPaneShell(bin);
4196
- // Windows native pane(psmux + Git Bash)は Windows パスをそのまま扱える。
4197
- // Git Bash の cd はドライブパスを forward slash 形で受けるのが安全。
4198
- const cwdForCmd = cwd && isWin ? cwd.replace(/\\/g, "/") : cwd;
2947
+ const cwdForCmd = cwd ? paneCwdArgument(cwd) : cwd;
4199
2948
  const grokAuthPath = agentDone && (kind === "grok" || kind === "composer") ? resolveAndValidateGrokAuth(realGrokHome()) : null;
4200
2949
  if (kind === "grok" || kind === "composer") {
4201
2950
  const requestedModel = model ?? (kind === "composer" ? GROK_MODEL_DEFAULTS.composer : null);