aiterm-mcp 0.31.2 → 0.33.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/dist/core.js CHANGED
@@ -13,13 +13,15 @@ import * as path from "node:path";
13
13
  import { createHash, randomBytes } from "node:crypto";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import * as rtk from "./rtk.js";
16
+ import { paneTokenHint } from "./harnesses/pane-tokens.js";
17
+ import { readRuntimeProcesses, processSubtree, processIdentity, backgroundProcesses } from "./process-runtime.js";
16
18
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
17
- import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
19
+ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
18
20
  import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
19
- import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
20
- import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexLaunchBlockingDialog, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
21
- 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, claudeSessionTranscriptPath, claudeApiErrorFromLine, } from "./harnesses/claude.js";
22
- import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
21
+ import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
22
+ import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
23
+ import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeStartupAction, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, } from "./harnesses/claude.js";
24
+ import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
23
25
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
24
26
  export { AitermError } from "./errors.js";
25
27
  export { tmuxSpawnEnv } from "./tmux-runtime.js";
@@ -651,19 +653,21 @@ function rtkRewrite(text) {
651
653
  // Windows native pane の既定 shell 解決。裸の "bash" は PATH 上で System32 の
652
654
  // WSL launcher (bash.exe) に解決されてしまうため、Git for Windows の bash.exe を
653
655
  // 明示解決する(WSL 非依存の裁定に従う)。AITERM_BASH で上書き可。
654
- export function openSession(name, shell = "bash") {
656
+ export function openSession(name, shell = "bash", envVars = []) {
655
657
  shell = resolveWinPaneShell(shell);
658
+ for (const key of envVars) {
659
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
660
+ throw new AitermError("env_varsの環境変数名が不正です", 2);
661
+ }
656
662
  try {
657
663
  fs.mkdirSync(SOCKDIR, { recursive: true });
658
664
  }
659
665
  catch (error) {
660
666
  ownTelemetryFailure("AITERM.PERSISTENCE_WRITE_FAILED", error);
661
667
  }
662
- // macOS の /bin/bash は 3.2 で、起動時に zsh 移行バナーを出して最初の read を汚す。darwin かつ bash の
663
- // ときだけ -e で環境変数を渡して抑止する。-e は tmux>=3.2 が必要だが macOS の Homebrew tmux は常に該当。
664
- // (古い tmux<3.2 が残る Linux/WSL で -e を渡すと new-session が落ちるため、darwin 限定にする。)
668
+ // macOS標準bashの移行bannerを抑止する。環境変数の注入方法はterminal runtimeが所有する。
665
669
  const banner = process.platform === "darwin" && path.basename(shell) === "bash"
666
- ? ["-e", "BASH_SILENCE_DEPRECATION_WARNING=1"]
670
+ ? ["BASH_SILENCE_DEPRECATION_WARNING=1"]
667
671
  : [];
668
672
  // 並行性対策: 複数エージェントが同時に名前なし open すると autoName が同じ t{i} を返し得る(TOCTOU)。
669
673
  // 自動採番は初回のみ読みやすい t{i}、衝突したら乱数 nonce 名でリトライする(線形リトライは高並行で
@@ -680,9 +684,22 @@ export function openSession(name, shell = "bash") {
680
684
  }
681
685
  else {
682
686
  // -f は端末個人の設定ファイルを読まないための空 config。
683
- const r = tmux("new-session", "-d", "-s", nm, ...banner, "-f", TMUX_EMPTY_CONFIG, shell);
684
- if (r.code === 0)
687
+ const environment = [...banner, ...envVars.filter(key => key !== "AITERM_SESSION_ID" && process.env[key] !== undefined)
688
+ .map(key => `${key}=${process.env[key]}`), `AITERM_SESSION_ID=${nm}`];
689
+ const launch = sessionEnvironmentLaunch(shell, environment);
690
+ const r = tmux("new-session", "-d", "-s", nm, ...launch.args, "-f", TMUX_EMPTY_CONFIG, launch.shell);
691
+ if (r.code === 0) {
692
+ if (launch.register_after_start)
693
+ for (const entry of environment) {
694
+ const at = entry.indexOf("=");
695
+ const registered = tmux("set-environment", "-t", nm, entry.slice(0, at), entry.slice(at + 1));
696
+ if (registered.code !== 0) {
697
+ tmux("kill-session", "-t", nm);
698
+ throw new AitermError("session環境変数を登録できません", 2);
699
+ }
700
+ }
685
701
  break;
702
+ }
686
703
  // 自動採番かつ「重複名」由来の失敗(他エージェントが同名を先に取った)なら次名でリトライ。
687
704
  const dup = /duplicate|already exists/i.test(r.stderr);
688
705
  if (explicit || !dup)
@@ -1021,14 +1038,172 @@ export async function readOutput(name, o = {}) {
1021
1038
  return body + "\n" + meta + agentMeta + completionSuffix(status);
1022
1039
  return body + "\n" + meta + agentMeta;
1023
1040
  }
1024
- export function listSessions() {
1025
- const r = tmux("list-sessions", "-F", "#{session_name}\t#{pane_current_command}\t#{?session_attached,attached,detached}\t#{window_width}x#{window_height}");
1026
- if (r.code === 0 && r.stdout.trim()) {
1027
- return r.stdout
1028
- .replace(/\s+$/, "")
1029
- .split("\n")
1030
- .map((line) => {
1031
- const name = line.split("\t", 1)[0];
1041
+ function decodeActivityCursor(cursor, name) {
1042
+ let value;
1043
+ try {
1044
+ value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
1045
+ }
1046
+ catch {
1047
+ throw new AitermError("活動観測cursorを読めません", 2);
1048
+ }
1049
+ if (value?.session_id !== name || typeof value.pane_identity !== "string"
1050
+ || typeof value.screen_digest !== "string" || !value.processes || typeof value.processes !== "object"
1051
+ || Array.isArray(value.processes) || !Object.values(value.processes).every(cpu => typeof cpu === "number" && Number.isFinite(cpu) && cpu >= 0)
1052
+ || !value.background_processes || typeof value.background_processes !== "object" || Array.isArray(value.background_processes)
1053
+ || !Object.values(value.background_processes).every(cpu => typeof cpu === "number" && Number.isFinite(cpu) && cpu >= 0))
1054
+ throw new AitermError("活動観測cursorが対象sessionの形式と一致しません", 2);
1055
+ return value;
1056
+ }
1057
+ function selectHarnessProcesses(meta, rows, subtree) {
1058
+ const matches = (row) => {
1059
+ const first = /^(?:"([^"]+)"|(\S+))/.exec(row.command);
1060
+ const executable = path.posix.basename((first?.[1] ?? first?.[2] ?? "").replace(/\\/g, "/"))
1061
+ .replace(/\.exe$/i, "").replace(/^-/, "").toLowerCase();
1062
+ const command = row.command.replace(/\\/g, "/").replace(/\.exe(?=["\s]|$)/gi, "").replace(/"/g, "");
1063
+ const tokens = row.command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
1064
+ const argument = tokens[1]?.replace(/^["']|["']$/g, "").replace(/\\/g, "/");
1065
+ const launchedScript = meta.agent_executable !== undefined && argument === meta.agent_executable.replace(/\\/g, "/");
1066
+ return launchedScript || (!SHELLS.has(executable) && executable !== "pwsh" && AGENT_COMMAND_PATTERNS[meta.kind].test(command));
1067
+ };
1068
+ // WindowsのMSYS execでnative親子関係が切れる場合も、launch固有引数で相関する。
1069
+ const correlated = rows.filter(row => matches(row) && row.command.includes(meta.launch_id));
1070
+ const candidates = correlated.length ? correlated : subtree.filter(matches);
1071
+ const roots = candidates.filter(row => !candidates.some(parent => row.parent_pid === parent.pid));
1072
+ return roots;
1073
+ }
1074
+ export function observeSession(name, cursor) {
1075
+ assertSessionName(name);
1076
+ const previous = cursor === undefined ? null : decodeActivityCursor(cursor, name);
1077
+ const listed = listSessionsResult().sessions.find(session => session.session_id === name);
1078
+ const result = {
1079
+ schema: "aiterm.pty-observe-result.v1", session_id: name, observed_at: new Date().toISOString(),
1080
+ exists: !!listed, harness: null, launch_id: null, state: "missing", reason: "session_missing",
1081
+ pane_alive: false, harness_alive: null, pane_process: null, harness_process: null, process_identity: null,
1082
+ token_hint: null,
1083
+ activity: { cursor: null, output_changed: null, cpu_seconds: null, cpu_delta_seconds: null, cpu_delta_complete: null,
1084
+ background_cpu_seconds: null, background_cpu_delta_seconds: null, background_cpu_delta_complete: null },
1085
+ };
1086
+ if (!listed)
1087
+ return result;
1088
+ const pane = tmux("display-message", "-p", "-t", name, "#{pane_pid}\t#{pane_dead}");
1089
+ if (pane.code !== 0)
1090
+ throw new AitermError("paneの生存情報を取得できません", 2);
1091
+ const [pidText, dead] = pane.stdout.trim().split("\t");
1092
+ const panePid = Number(pidText);
1093
+ if (!Number.isSafeInteger(panePid) || panePid < 1 || !["0", "1"].includes(dead))
1094
+ throw new AitermError("paneの生存情報の形式を認識できません", 2);
1095
+ const meta = tryLoadAgentMetadata(name);
1096
+ result.harness = meta ? agentHarness(meta.kind) : null;
1097
+ result.launch_id = meta?.launch_id ?? null;
1098
+ if (dead === "1") {
1099
+ result.state = "dead";
1100
+ result.reason = "pane_dead";
1101
+ result.harness_alive = meta ? false : null;
1102
+ return result;
1103
+ }
1104
+ const rows = readRuntimeProcesses();
1105
+ const root = rows.find(row => row.pid === panePid);
1106
+ result.pane_alive = root ? true : null;
1107
+ result.state = "unknown";
1108
+ result.reason = root ? "ordinary_pty" : "native_pane_process_unresolved";
1109
+ if (!root)
1110
+ return result;
1111
+ result.pane_process = processIdentity(root);
1112
+ const subtree = processSubtree(rows, root.pid);
1113
+ const candidates = meta ? selectHarnessProcesses(meta, rows, subtree) : [];
1114
+ const agent = candidates.length === 1 ? candidates[0] : null;
1115
+ if (meta) {
1116
+ result.harness_process = agent ? processIdentity(agent) : null;
1117
+ // Cursorにはlaunch固有argvがない。Windowsの切れた親子関係だけで死亡を断定しない。
1118
+ result.harness_alive = candidates.length ? true : isWin && meta.kind === "cursor" ? null : false;
1119
+ if (result.harness_alive === false) {
1120
+ result.state = "dead";
1121
+ result.reason = "harness_exited";
1122
+ }
1123
+ else if (result.harness_alive === null)
1124
+ result.reason = "harness_process_unresolved";
1125
+ else if (!agent)
1126
+ result.reason = "harness_process_ambiguous";
1127
+ result.process_identity = result.harness_process;
1128
+ }
1129
+ else {
1130
+ const leaders = subtree.filter(row => row.parent_pid === root.pid && (isWin || row.process_group_id === row.pid));
1131
+ result.process_identity = leaders.length > 1 ? null : processIdentity(leaders[0] ?? root);
1132
+ if (leaders.length > 1)
1133
+ result.reason = "process_identity_ambiguous";
1134
+ }
1135
+ const captured = tmux("capture-pane", "-p", "-J", "-t", name, "-S", "-200");
1136
+ if (captured.code !== 0)
1137
+ throw new AitermError("paneの活動観測を取得できません", 2);
1138
+ const screen = captured.stdout;
1139
+ result.token_hint = meta ? paneTokenHint(screen) : null;
1140
+ if (meta && agent) {
1141
+ const observation = meta.kind === "grok" || meta.kind === "composer" ? grokPaneObservation(screen)
1142
+ : meta.kind === "codex" ? codexPaneObservation(screen)
1143
+ : meta.kind === "claude" ? claudePaneObservation(screen) : cursorPaneObservation(screen);
1144
+ result.state = observation.state;
1145
+ result.reason = observation.reason;
1146
+ if (agent.stopped === true || processSubtree(rows, agent.pid).some(row => row.process_group_id === agent.process_group_id && row.stopped === true)) {
1147
+ result.state = "blocked";
1148
+ result.reason = "harness_stopped";
1149
+ }
1150
+ }
1151
+ const activityRows = agent && !subtree.some(row => row.pid === agent.pid)
1152
+ ? [...subtree, ...processSubtree(rows, agent.pid)] : subtree;
1153
+ const processes = Object.fromEntries(activityRows.map(row => [`${row.pid}:${row.started_identity}`, row.cpu_seconds]));
1154
+ const backgroundCpu = Object.fromEntries(backgroundProcesses(activityRows, root)
1155
+ .map(row => [`${row.pid}:${row.started_identity}`, row.cpu_seconds]));
1156
+ const sample = {
1157
+ session_id: name, pane_identity: `${root.pid}:${root.started_identity}`,
1158
+ screen_digest: createHash("sha256").update(screen).digest("hex"), processes, background_processes: backgroundCpu,
1159
+ };
1160
+ const comparable = previous?.pane_identity === sample.pane_identity;
1161
+ result.activity = {
1162
+ cursor: Buffer.from(JSON.stringify(sample)).toString("base64url"),
1163
+ output_changed: comparable ? previous.screen_digest !== sample.screen_digest : null,
1164
+ cpu_seconds: Object.values(processes).reduce((sum, cpu) => sum + cpu, 0),
1165
+ cpu_delta_seconds: comparable ? Object.entries(processes).reduce((sum, [identity, cpu]) => sum + cpu - (previous.processes[identity] ?? 0), 0) : null,
1166
+ cpu_delta_complete: comparable ? Object.keys(previous.processes).every(identity => identity in processes) : null,
1167
+ background_cpu_seconds: Object.values(backgroundCpu).reduce((sum, cpu) => sum + cpu, 0),
1168
+ background_cpu_delta_seconds: comparable ? Object.entries(backgroundCpu).reduce((sum, [identity, cpu]) => sum + cpu - (previous.background_processes[identity] ?? 0), 0) : null,
1169
+ background_cpu_delta_complete: comparable ? Object.keys(previous.background_processes).every(identity => identity in backgroundCpu) : null,
1170
+ };
1171
+ return result;
1172
+ }
1173
+ export function listSessionsResult(envKeys = []) {
1174
+ for (const key of envKeys) {
1175
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
1176
+ throw new AitermError("env_keysの環境変数名が不正です", 2);
1177
+ }
1178
+ const r = tmux("list-sessions", "-F", "#{session_name}\t#{pane_current_command}\t#{session_attached}\t#{window_width}\t#{window_height}");
1179
+ const sessions = [];
1180
+ if (r.code !== 0 && !/no server running|No such file or directory/i.test(r.stderr))
1181
+ throw new AitermError("セッション一覧を取得できません: " + r.stderr.trim(), 2);
1182
+ if (r.code === 0)
1183
+ for (const line of r.stdout.trim().split("\n").filter(Boolean)) {
1184
+ const [name, command, attached, width, height] = line.split("\t");
1185
+ assertSessionName(name);
1186
+ const environment = {};
1187
+ for (const key of envKeys) {
1188
+ const value = tmux("show-environment", "-t", name, key);
1189
+ if (value.code !== 0 && !/unknown variable|not found|not set/i.test(value.stderr))
1190
+ throw new AitermError("session環境変数を照会できません", 2);
1191
+ // psmuxは指定以外の内部キーも出すため、要求されたキーと完全一致した行だけを返す。
1192
+ const selected = value.stdout.split(/\r?\n/).find(entry => entry.startsWith(`${key}=`));
1193
+ environment[key] = selected === undefined ? null : selected.slice(key.length + 1);
1194
+ }
1195
+ const meta = tryLoadAgentMetadata(name);
1196
+ sessions.push({ session_id: name, current_command: normalizePaneCommand(command), attached: Number(attached) > 0,
1197
+ width: Number(width), height: Number(height), harness: meta ? agentHarness(meta.kind) : null, environment });
1198
+ }
1199
+ return { schema: "aiterm.pty-list-result.v1", observed_at: new Date().toISOString(), sessions };
1200
+ }
1201
+ export function listSessions(result = listSessionsResult()) {
1202
+ if (result.sessions.length) {
1203
+ return result.sessions
1204
+ .map((session) => {
1205
+ const name = session.session_id;
1206
+ const line = `${name}\t${session.current_command}\t${session.attached ? "attached" : "detached"}\t${session.width}x${session.height}`;
1032
1207
  const meta = tryLoadAgentMetadata(name);
1033
1208
  if (!meta)
1034
1209
  return line;
@@ -1222,6 +1397,72 @@ function createAgentLineageContext(kind, sessionId, seed) {
1222
1397
  delegationAllowed: true,
1223
1398
  };
1224
1399
  }
1400
+ export function runAgentApproval(options) {
1401
+ const { action, session_id: name, approval_choice: selected, observed_prompt_digest: expected } = options;
1402
+ assertSessionName(name);
1403
+ if (action === "inspect" && (selected !== undefined || expected !== undefined))
1404
+ throw new AitermError("inspectでは承認選択とdigestを指定できません", 2);
1405
+ if (action === "respond" && (selected === undefined || expected === undefined))
1406
+ throw new AitermError("respondにはapproval_choiceとobserved_prompt_digestが必要です", 2);
1407
+ const release = action === "respond" ? acquireSessionSendFileLock(name) : () => { };
1408
+ try {
1409
+ const meta = loadAgentMetadata(name);
1410
+ const result = {
1411
+ schema: "aiterm.agent-approval-result.v1", action, status: "blocked", session_id: name,
1412
+ harness: agentHarness(meta.kind), launch_id: meta.launch_id, reason: "harness_unsupported",
1413
+ kind: null, prompt: null, prompt_digest: null, choices: [], selected_choice: null, at: new Date().toISOString(),
1414
+ };
1415
+ if (meta.kind !== "codex")
1416
+ return result;
1417
+ const captured = tmux("capture-pane", "-p", "-J", "-t", name, "-S", "-200");
1418
+ if (captured.code !== 0)
1419
+ throw new AitermError("現在の承認画面を取得できません", 2);
1420
+ const screen = captured.stdout;
1421
+ const state = codexPaneObservation(screen);
1422
+ const dialog = codexApprovalDialog(screen);
1423
+ if (!dialog) {
1424
+ result.status = state.state === "blocked" || state.state === "unknown" || action === "respond" ? "blocked" : "none";
1425
+ result.reason = state.state === "blocked" ? "unknown_dialog" : state.state === "unknown" ? "unrecognized_screen" : "no_current_dialog";
1426
+ return result;
1427
+ }
1428
+ result.kind = dialog.kind;
1429
+ result.prompt = dialog.prompt;
1430
+ result.prompt_digest = `sha256:${createHash("sha256").update(`${meta.launch_id}\n${dialog.canonical}`).digest("hex")}`;
1431
+ result.choices = dialog.choices.map(({ decision, label }) => ({ decision, label }));
1432
+ result.reason = "approval_required";
1433
+ result.status = "approval_required";
1434
+ if (dialog.selected_index === null || dialog.choices.length === 0) {
1435
+ result.status = "blocked";
1436
+ result.reason = "unsupported_choices";
1437
+ return result;
1438
+ }
1439
+ if (action === "inspect")
1440
+ return result;
1441
+ if (expected !== result.prompt_digest) {
1442
+ result.status = "blocked";
1443
+ result.reason = "dialog_changed";
1444
+ return result;
1445
+ }
1446
+ const choice = dialog.choices.find(entry => entry.decision === selected);
1447
+ if (!choice) {
1448
+ result.status = "blocked";
1449
+ result.reason = "choice_unavailable";
1450
+ return result;
1451
+ }
1452
+ const distance = choice.index - dialog.selected_index;
1453
+ const keys = [...Array(Math.abs(distance)).fill(distance > 0 ? "Down" : "Up"), "Enter"];
1454
+ const sent = tmux("send-keys", "-t", name, ...keys);
1455
+ if (sent.code !== 0)
1456
+ throw new AitermError("Codex承認入力を送れませんでした", 2);
1457
+ result.status = "submitted";
1458
+ result.reason = "choice_submitted";
1459
+ result.selected_choice = selected ?? null;
1460
+ return result;
1461
+ }
1462
+ finally {
1463
+ release();
1464
+ }
1465
+ }
1225
1466
  const DEFAULT_AGENT_DONE_TIMEOUT = 600;
1226
1467
  const agentMetadataNegativeCache = new Map();
1227
1468
  let agentTuiReadyStableSamplesTestOverride = null;
@@ -1706,6 +1947,15 @@ function loadAgentMetadata(name) {
1706
1947
  if (m.event_file !== expectedEvent) {
1707
1948
  throw new AitermError("agent metadata の path が現在の secure state root と一致しません", 2);
1708
1949
  }
1950
+ if (m.initial_prompt_delivery !== undefined && (!["not_requested", "not_sent", "submitted_unconfirmed", "started"].includes(m.initial_prompt_delivery.status)
1951
+ || typeof m.initial_prompt_delivery.reason !== "string"
1952
+ || ![true, false, null].includes(m.initial_prompt_delivery.turn_started)
1953
+ || (m.initial_prompt_cursor !== null && !Number.isSafeInteger(m.initial_prompt_cursor))))
1954
+ throw new AitermError("初手delivery metadataが不正です", 2);
1955
+ const deliveryFields = m.initial_prompt_delivery === undefined ? {} : {
1956
+ initial_prompt_delivery: m.initial_prompt_delivery, initial_prompt_cursor: m.initial_prompt_cursor,
1957
+ };
1958
+ const executableFields = m.agent_executable === undefined ? {} : { agent_executable: m.agent_executable };
1709
1959
  if (m.kind === "claude") {
1710
1960
  const expectedSettings = agentManagedClaudeSettingsPath(name, m.launch_id);
1711
1961
  const expectedResult = agentClaudeResultPath(name, m.launch_id);
@@ -1731,6 +1981,8 @@ function loadAgentMetadata(name) {
1731
1981
  ...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
1732
1982
  vendor_session_id: m.vendor_session_id,
1733
1983
  initial_prompt: normalizeInitialPromptState(m.initial_prompt),
1984
+ ...deliveryFields,
1985
+ ...executableFields,
1734
1986
  launch_operation_id: launchOperationId,
1735
1987
  launch_request_digest: launchRequestDigest,
1736
1988
  hook_route: "shared_claude_settings",
@@ -1760,6 +2012,8 @@ function loadAgentMetadata(name) {
1760
2012
  ...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
1761
2013
  vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
1762
2014
  initial_prompt: normalizeInitialPromptState(m.initial_prompt),
2015
+ ...deliveryFields,
2016
+ ...executableFields,
1763
2017
  hook_route: "shared_codex_home",
1764
2018
  completion_route: "codex_transcript",
1765
2019
  ...loadAgentLineageFields(m, true),
@@ -1785,6 +2039,8 @@ function loadAgentMetadata(name) {
1785
2039
  ...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
1786
2040
  vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
1787
2041
  initial_prompt: normalizeInitialPromptState(m.initial_prompt),
2042
+ ...deliveryFields,
2043
+ ...executableFields,
1788
2044
  hook_route: "shared_cursor_home",
1789
2045
  completion_route: "cursor_transcript",
1790
2046
  ...loadAgentLineageFields(m, true),
@@ -1818,6 +2074,8 @@ function loadAgentMetadata(name) {
1818
2074
  ...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
1819
2075
  vendor_session_id: m.vendor_session_id,
1820
2076
  initial_prompt: normalizeInitialPromptState(m.initial_prompt),
2077
+ ...deliveryFields,
2078
+ ...executableFields,
1821
2079
  hook_route: "shared_grok_home",
1822
2080
  completion_route: "grok_transcript",
1823
2081
  ...loadAgentLineageFields(m, true),
@@ -2169,6 +2427,8 @@ export async function readAgentTranscriptResult(name, o = {}) {
2169
2427
  text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable);
2170
2428
  }
2171
2429
  else {
2430
+ if (!done)
2431
+ transcriptUnavailable();
2172
2432
  text = grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable);
2173
2433
  }
2174
2434
  if (!text.trim())
@@ -2493,9 +2753,10 @@ function isAgentTuiBusy(kind, screen) {
2493
2753
  // ready gate 用: 入力欄マーカーがあっても busy 表示中は ready と数えない。
2494
2754
  // frontend 推定(inferAgentFrontend)は「agent TUI が前面か」を見るだけなので isAgentTuiReady のまま。
2495
2755
  function isAgentTuiIdleReady(kind, screen) {
2496
- if (!isAgentTuiReady(kind, screen))
2497
- return false;
2498
- return !isAgentTuiBusy(kind, screen);
2756
+ const observation = kind === "grok" || kind === "composer" ? grokPaneObservation(screen)
2757
+ : kind === "codex" ? codexPaneObservation(screen)
2758
+ : kind === "claude" ? claudePaneObservation(screen) : cursorPaneObservation(screen);
2759
+ return observation.state === "idle";
2499
2760
  }
2500
2761
  // 起動側が明示応答すべき既知UI。ここで自動承認せず、ready timeoutを待たずに
2501
2762
  // `initial_prompt=not_sent`を返してsessionを生かしたままcallerへ制御を戻す。
@@ -2503,10 +2764,7 @@ function isAgentTuiActionRequired(kind, screen) {
2503
2764
  if (kind === "grok" || kind === "composer")
2504
2765
  return grokLaunchBlockingDialog(screen) !== null;
2505
2766
  if (kind === "codex") {
2506
- return codexLaunchBlockingDialog(screen) !== null
2507
- || screen.includes("Hooks need review")
2508
- || screen.includes("Allow the room MCP server to run tool")
2509
- || screen.includes("Would you like to run the following command?");
2767
+ return codexPaneObservation(screen).state === "blocked";
2510
2768
  }
2511
2769
  if (kind === "claude") {
2512
2770
  return /new MCP servers? found in this project/iu.test(screen)
@@ -2795,6 +3053,39 @@ async function sendAgentPromptText(name, text, kind) {
2795
3053
  }
2796
3054
  sendKey(name, "Enter", { preserveAgentOperation: true });
2797
3055
  }
3056
+ function setInitialDelivery(meta, value, cursor) {
3057
+ meta.initial_prompt_delivery = value;
3058
+ meta.initial_prompt_cursor = cursor;
3059
+ writeAgentMetadata(meta);
3060
+ }
3061
+ async function prepareAgentInput(name, meta, options) {
3062
+ await ensureAgentOwnsPaneInput(name, meta.kind);
3063
+ let ready = await waitAgentTuiReady(name, meta, options.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
3064
+ const handled = new Set();
3065
+ while (!ready.ready) {
3066
+ const action = meta.kind === "codex" ? codexStartupAction(ready.lastScreen, options.trust_project === true)
3067
+ : meta.kind === "claude" ? claudeStartupAction(ready.lastScreen, options.trust_project === true)
3068
+ : meta.kind === "grok" || meta.kind === "composer" ? grokStartupAction(ready.lastScreen, options.trust_project === true) : null;
3069
+ if (!action || handled.has(action.kind))
3070
+ break;
3071
+ handled.add(action.kind);
3072
+ for (const key of action.keys) {
3073
+ sendKey(name, key, { preserveAgentOperation: true });
3074
+ await sleep(AGENT_SUBMIT_DELAY_MS);
3075
+ }
3076
+ ready = await waitAgentTuiReady(name, meta, options.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
3077
+ }
3078
+ if (!ready.ready) {
3079
+ const state = meta.kind === "grok" || meta.kind === "composer" ? grokPaneObservation(ready.lastScreen)
3080
+ : meta.kind === "codex" ? codexPaneObservation(ready.lastScreen)
3081
+ : meta.kind === "claude" ? claudePaneObservation(ready.lastScreen) : cursorPaneObservation(ready.lastScreen);
3082
+ return { status: "blocked", reason: state.reason };
3083
+ }
3084
+ const live = observeSession(name);
3085
+ if (live.harness_alive !== true || live.harness_process === null || live.state !== "idle")
3086
+ return { status: "blocked", reason: live.reason };
3087
+ return { status: "ready", reason: "composer_ready" };
3088
+ }
2798
3089
  export async function sendInitialAgentPrompt(name, text, o = {}) {
2799
3090
  assertSessionName(name);
2800
3091
  const meta = loadAgentMetadata(name);
@@ -2805,29 +3096,12 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2805
3096
  throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。初回応答完了後に再度操作してください。`, 2);
2806
3097
  }
2807
3098
  setInitialPromptState(meta, "not_sent");
2808
- // 起動直後の hooks 実行で bash が前面に残ると ready gate が恒久 false になり、brief 未送信で
2809
- // 席が巻き戻る(実測 2026-09-04: Codex 0.153 の初回起動が3回中2回)。打鍵の前に前面と raw を整える。
2810
- await ensureAgentOwnsPaneInput(name, meta.kind);
2811
- let ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2812
- // 無人Claude起動でCLI自身が順に出す、bypass mode確認とworkspace trustだけを
2813
- // 起動処理の一部として進める。通常turn中の権限確認やMCP承認には触れない。
2814
- for (let confirmations = 0; !ready.ready && meta.kind === "claude" && confirmations < 3 && isClaudeManagedLaunchConfirmation(ready.lastScreen); confirmations++) {
2815
- sendKey(name, "Down", { preserveAgentOperation: true });
2816
- await sleep(AGENT_SUBMIT_DELAY_MS);
2817
- sendKey(name, "Enter", { preserveAgentOperation: true });
2818
- await sleep(AGENT_SUBMIT_DELAY_MS);
2819
- ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2820
- }
2821
- if (!ready.ready) {
2822
- // ready失敗は成功形で返さず明示エラーにする(実被弾 2026-08-25: Codexのupdate確認ダイアログで
2823
- // 未送信のまま成功形receiptが返り、呼び出し側が40分気づけなかった)。sessionは調査/復旧用に残る。
2824
- const dialog = meta.kind === "codex" ? codexLaunchBlockingDialog(ready.lastScreen)
2825
- : meta.kind === "grok" || meta.kind === "composer" ? grokLaunchBlockingDialog(ready.lastScreen) : null;
2826
- const causeNote = dialog
2827
- ? `${dialog}が入力を塞いでいます。pty_read(screen:true)で画面を確認し、pty_keyでダイアログに応答してから、pty_sendでpromptを送ってください。`
2828
- : `pty_read(screen:true)で画面を確認し、入力受付になってからpty_sendでpromptを送ってください。`;
2829
- throw new AitermError(`initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples} harness=${agentHarness(meta.kind)}\n` +
2830
- `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態にならず、prompt は送信していません。${causeNote}`, 2);
3099
+ setInitialDelivery(meta, { status: "not_sent", reason: "preparing", turn_started: false }, null);
3100
+ const startup = await prepareAgentInput(name, meta, o);
3101
+ if (startup.status !== "ready") {
3102
+ setInitialDelivery(meta, { status: "not_sent", reason: startup.reason, turn_started: false }, null);
3103
+ throw new AitermError(`initial_prompt=not_sent vendor=${meta.kind} ready=false harness=${agentHarness(meta.kind)}\n` +
3104
+ `agent session '${name}' の起動準備が完了していないため、promptは送信していません。reason=${startup.reason}`, 2);
2831
3105
  }
2832
3106
  const promptText = meta.kind === "cursor" ? cursorPromptWithLineage(meta, text) : text;
2833
3107
  const startOffset = agentCompletionCursor(meta);
@@ -2836,6 +3110,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2836
3110
  prepareSendText(text, { raw: false });
2837
3111
  reserveAnonymousClaudeTurn(meta);
2838
3112
  }
3113
+ setInitialDelivery(meta, { status: "submitted_unconfirmed", reason: "dispatch_in_progress", turn_started: null }, startOffset);
2839
3114
  await sendAgentPromptText(name, promptText, meta.kind);
2840
3115
  setInitialPromptState(meta, "pending");
2841
3116
  }
@@ -2863,12 +3138,40 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2863
3138
  setInitialPromptState(meta, "failed");
2864
3139
  throw e;
2865
3140
  }
3141
+ let delivery = { status: "submitted_unconfirmed", reason: "start_unconfirmed", turn_started: null };
3142
+ const deadline = performance.now() + 3000;
3143
+ do {
3144
+ const screen = captureScreen(name, AGENT_TUI_READY_LINES);
3145
+ const state = meta.kind === "grok" || meta.kind === "composer" ? grokPaneObservation(screen)
3146
+ : meta.kind === "codex" ? codexPaneObservation(screen)
3147
+ : meta.kind === "claude" ? claudePaneObservation(screen) : cursorPaneObservation(screen);
3148
+ if (state.state === "busy") {
3149
+ delivery = { status: "started", reason: "turn_running", turn_started: true };
3150
+ break;
3151
+ }
3152
+ if (state.state === "blocked" && ["command_approval", "mcp_approval", "tool_approval"].includes(state.reason)) {
3153
+ delivery = { status: "started", reason: "approval_required", turn_started: true };
3154
+ break;
3155
+ }
3156
+ const completed = await observeAgentDone(name, { cursor: startOffset, timeout: 0 });
3157
+ if (completed.outcome === "done") {
3158
+ delivery = { status: "started", reason: "turn_completed", turn_started: true };
3159
+ break;
3160
+ }
3161
+ if (state.state === "blocked" || completed.outcome === "error" || completed.outcome === "rate_limited") {
3162
+ delivery.reason = state.state === "blocked" ? state.reason : completed.outcome;
3163
+ break;
3164
+ }
3165
+ await sleep(100);
3166
+ } while (performance.now() < deadline);
3167
+ setInitialDelivery(meta, delivery, startOffset);
2866
3168
  return {
2867
3169
  text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset} harness=${agentHarness(meta.kind)}\n` +
2868
3170
  `起動時 prompt を送信した。${agentDispatchGuide(name, startOffset)}` +
2869
3171
  agentSubmitResidueWarning(name, residue.residue),
2870
3172
  event_cursor: startOffset,
2871
3173
  submit_residue: residue.residue,
3174
+ initial_prompt: delivery,
2872
3175
  };
2873
3176
  }
2874
3177
  export function isAgentSession(name) {
@@ -3520,7 +3823,7 @@ export function openAgent(kind, opts = {}) {
3520
3823
  let sid;
3521
3824
  let hint;
3522
3825
  try {
3523
- [sid, hint] = openSession(opts.session_name ?? null, "bash");
3826
+ [sid, hint] = openSession(opts.session_name ?? null, "bash", envVars);
3524
3827
  }
3525
3828
  catch (error) {
3526
3829
  if (launchOperationId !== null && sessionExists(opts.session_name)) {
@@ -3541,8 +3844,11 @@ export function openAgent(kind, opts = {}) {
3541
3844
  ? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
3542
3845
  : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
3543
3846
  : null;
3544
- if (meta)
3847
+ if (meta) {
3848
+ meta.agent_executable = bin;
3849
+ writeAgentMetadata(meta);
3545
3850
  agentMetadataNegativeCache.delete(sid);
3851
+ }
3546
3852
  launchNote = buildAgentLaunchNote(kind, model, effort, meta);
3547
3853
  const cmd = buildAgentCmd(kind, binForCmd, model, effort, opts.prompt ?? null, meta);
3548
3854
  const envPrefix = agentEnvPrefix(meta, sid, envVars);
@@ -3582,6 +3888,19 @@ export function openAgent(kind, opts = {}) {
3582
3888
  `起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
3583
3889
  ];
3584
3890
  }
3891
+ export class AgentLaunchPromptError extends AitermError {
3892
+ session_id;
3893
+ initial_prompt;
3894
+ event_cursor;
3895
+ startup;
3896
+ constructor(message, code, session_id, initial_prompt, event_cursor, startup = { status: "blocked", reason: initial_prompt.reason }) {
3897
+ super(message, code);
3898
+ this.session_id = session_id;
3899
+ this.initial_prompt = initial_prompt;
3900
+ this.event_cursor = event_cursor;
3901
+ this.startup = startup;
3902
+ }
3903
+ }
3585
3904
  export async function openAgentWithInitialPrompt(kind, opts = {}) {
3586
3905
  const mission = opts.prompt ?? null;
3587
3906
  const sourceSessionId = opts.throughline_source_session ?? null;
@@ -3624,7 +3943,21 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3624
3943
  write_scope: opts.write_scope,
3625
3944
  env_vars: opts.env_vars,
3626
3945
  });
3627
- return [sid, hint, null, null];
3946
+ const delivery = { status: "not_requested", reason: "not_requested", turn_started: false };
3947
+ let startup = { status: "not_checked", reason: "startup_not_requested" };
3948
+ if (opts.trust_project === true) {
3949
+ try {
3950
+ startup = await prepareAgentInput(sid, loadAgentMetadata(sid), {
3951
+ trust_project: true, ready_timeout: opts.ready_timeout ?? undefined,
3952
+ });
3953
+ }
3954
+ catch (error) {
3955
+ throw new AgentLaunchPromptError(`session_id: ${sid}\n起動準備中に失敗しました。${error instanceof Error ? error.message : String(error)}`, error instanceof AitermError ? error.code : 1, sid, delivery, null, { status: "blocked", reason: "startup_failed" });
3956
+ }
3957
+ if (startup.status !== "ready")
3958
+ throw new AgentLaunchPromptError(`session_id: ${sid}\n起動準備を完了できませんでした。reason=${startup.reason}`, 2, sid, delivery, null, startup);
3959
+ }
3960
+ return [sid, hint, null, null, delivery, startup];
3628
3961
  }
3629
3962
  const [sid, hint] = openAgent(kind, {
3630
3963
  session_name: opts.session_name ?? null,
@@ -3640,13 +3973,17 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3640
3973
  try {
3641
3974
  const initial = await sendInitialAgentPrompt(sid, prompt, {
3642
3975
  ready_timeout: opts.ready_timeout ?? undefined,
3976
+ trust_project: opts.trust_project,
3643
3977
  });
3644
- return [sid, `${hint}\n${initial.text}`, initial.event_cursor, initial.submit_residue];
3978
+ return [sid, `${hint}\n${initial.text}`, initial.event_cursor, initial.submit_residue, initial.initial_prompt,
3979
+ { status: "ready", reason: "composer_ready" }];
3645
3980
  }
3646
3981
  catch (e) {
3647
3982
  const code = e instanceof AitermError ? e.code : 1;
3648
3983
  const message = e instanceof Error ? e.message : String(e);
3649
- throw new AitermError(`session_id: ${sid}\n` +
3650
- `起動後の初回 prompt 処理で失敗しました。session は調査/復旧用に残しています。\n${message}`, code);
3984
+ const meta = loadAgentMetadata(sid);
3985
+ throw new AgentLaunchPromptError(`session_id: ${sid}\n` +
3986
+ `起動後の初回 prompt 処理で失敗しました。session は調査/復旧用に残しています。\n${message}`, code, sid, meta.initial_prompt_delivery ?? { status: "not_sent", reason: "startup_failed", turn_started: false }, meta.initial_prompt_cursor ?? null, { status: meta.initial_prompt_delivery?.status === "submitted_unconfirmed" ? "ready" : "blocked",
3987
+ reason: meta.initial_prompt_delivery?.reason ?? "startup_failed" });
3651
3988
  }
3652
3989
  }