aiterm-mcp 0.32.0 → 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),
@@ -2495,9 +2753,10 @@ function isAgentTuiBusy(kind, screen) {
2495
2753
  // ready gate 用: 入力欄マーカーがあっても busy 表示中は ready と数えない。
2496
2754
  // frontend 推定(inferAgentFrontend)は「agent TUI が前面か」を見るだけなので isAgentTuiReady のまま。
2497
2755
  function isAgentTuiIdleReady(kind, screen) {
2498
- if (!isAgentTuiReady(kind, screen))
2499
- return false;
2500
- 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";
2501
2760
  }
2502
2761
  // 起動側が明示応答すべき既知UI。ここで自動承認せず、ready timeoutを待たずに
2503
2762
  // `initial_prompt=not_sent`を返してsessionを生かしたままcallerへ制御を戻す。
@@ -2505,10 +2764,7 @@ function isAgentTuiActionRequired(kind, screen) {
2505
2764
  if (kind === "grok" || kind === "composer")
2506
2765
  return grokLaunchBlockingDialog(screen) !== null;
2507
2766
  if (kind === "codex") {
2508
- return codexLaunchBlockingDialog(screen) !== null
2509
- || screen.includes("Hooks need review")
2510
- || screen.includes("Allow the room MCP server to run tool")
2511
- || screen.includes("Would you like to run the following command?");
2767
+ return codexPaneObservation(screen).state === "blocked";
2512
2768
  }
2513
2769
  if (kind === "claude") {
2514
2770
  return /new MCP servers? found in this project/iu.test(screen)
@@ -2797,6 +3053,39 @@ async function sendAgentPromptText(name, text, kind) {
2797
3053
  }
2798
3054
  sendKey(name, "Enter", { preserveAgentOperation: true });
2799
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
+ }
2800
3089
  export async function sendInitialAgentPrompt(name, text, o = {}) {
2801
3090
  assertSessionName(name);
2802
3091
  const meta = loadAgentMetadata(name);
@@ -2807,29 +3096,12 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2807
3096
  throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。初回応答完了後に再度操作してください。`, 2);
2808
3097
  }
2809
3098
  setInitialPromptState(meta, "not_sent");
2810
- // 起動直後の hooks 実行で bash が前面に残ると ready gate が恒久 false になり、brief 未送信で
2811
- // 席が巻き戻る(実測 2026-09-04: Codex 0.153 の初回起動が3回中2回)。打鍵の前に前面と raw を整える。
2812
- await ensureAgentOwnsPaneInput(name, meta.kind);
2813
- let ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2814
- // 無人Claude起動でCLI自身が順に出す、bypass mode確認とworkspace trustだけを
2815
- // 起動処理の一部として進める。通常turn中の権限確認やMCP承認には触れない。
2816
- for (let confirmations = 0; !ready.ready && meta.kind === "claude" && confirmations < 3 && isClaudeManagedLaunchConfirmation(ready.lastScreen); confirmations++) {
2817
- sendKey(name, "Down", { preserveAgentOperation: true });
2818
- await sleep(AGENT_SUBMIT_DELAY_MS);
2819
- sendKey(name, "Enter", { preserveAgentOperation: true });
2820
- await sleep(AGENT_SUBMIT_DELAY_MS);
2821
- ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2822
- }
2823
- if (!ready.ready) {
2824
- // ready失敗は成功形で返さず明示エラーにする(実被弾 2026-08-25: Codexのupdate確認ダイアログで
2825
- // 未送信のまま成功形receiptが返り、呼び出し側が40分気づけなかった)。sessionは調査/復旧用に残る。
2826
- const dialog = meta.kind === "codex" ? codexLaunchBlockingDialog(ready.lastScreen)
2827
- : meta.kind === "grok" || meta.kind === "composer" ? grokLaunchBlockingDialog(ready.lastScreen) : null;
2828
- const causeNote = dialog
2829
- ? `${dialog}が入力を塞いでいます。pty_read(screen:true)で画面を確認し、pty_keyでダイアログに応答してから、pty_sendでpromptを送ってください。`
2830
- : `pty_read(screen:true)で画面を確認し、入力受付になってからpty_sendでpromptを送ってください。`;
2831
- throw new AitermError(`initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples} harness=${agentHarness(meta.kind)}\n` +
2832
- `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);
2833
3105
  }
2834
3106
  const promptText = meta.kind === "cursor" ? cursorPromptWithLineage(meta, text) : text;
2835
3107
  const startOffset = agentCompletionCursor(meta);
@@ -2838,6 +3110,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2838
3110
  prepareSendText(text, { raw: false });
2839
3111
  reserveAnonymousClaudeTurn(meta);
2840
3112
  }
3113
+ setInitialDelivery(meta, { status: "submitted_unconfirmed", reason: "dispatch_in_progress", turn_started: null }, startOffset);
2841
3114
  await sendAgentPromptText(name, promptText, meta.kind);
2842
3115
  setInitialPromptState(meta, "pending");
2843
3116
  }
@@ -2865,12 +3138,40 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2865
3138
  setInitialPromptState(meta, "failed");
2866
3139
  throw e;
2867
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);
2868
3168
  return {
2869
3169
  text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset} harness=${agentHarness(meta.kind)}\n` +
2870
3170
  `起動時 prompt を送信した。${agentDispatchGuide(name, startOffset)}` +
2871
3171
  agentSubmitResidueWarning(name, residue.residue),
2872
3172
  event_cursor: startOffset,
2873
3173
  submit_residue: residue.residue,
3174
+ initial_prompt: delivery,
2874
3175
  };
2875
3176
  }
2876
3177
  export function isAgentSession(name) {
@@ -3522,7 +3823,7 @@ export function openAgent(kind, opts = {}) {
3522
3823
  let sid;
3523
3824
  let hint;
3524
3825
  try {
3525
- [sid, hint] = openSession(opts.session_name ?? null, "bash");
3826
+ [sid, hint] = openSession(opts.session_name ?? null, "bash", envVars);
3526
3827
  }
3527
3828
  catch (error) {
3528
3829
  if (launchOperationId !== null && sessionExists(opts.session_name)) {
@@ -3543,8 +3844,11 @@ export function openAgent(kind, opts = {}) {
3543
3844
  ? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
3544
3845
  : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
3545
3846
  : null;
3546
- if (meta)
3847
+ if (meta) {
3848
+ meta.agent_executable = bin;
3849
+ writeAgentMetadata(meta);
3547
3850
  agentMetadataNegativeCache.delete(sid);
3851
+ }
3548
3852
  launchNote = buildAgentLaunchNote(kind, model, effort, meta);
3549
3853
  const cmd = buildAgentCmd(kind, binForCmd, model, effort, opts.prompt ?? null, meta);
3550
3854
  const envPrefix = agentEnvPrefix(meta, sid, envVars);
@@ -3584,6 +3888,19 @@ export function openAgent(kind, opts = {}) {
3584
3888
  `起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
3585
3889
  ];
3586
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
+ }
3587
3904
  export async function openAgentWithInitialPrompt(kind, opts = {}) {
3588
3905
  const mission = opts.prompt ?? null;
3589
3906
  const sourceSessionId = opts.throughline_source_session ?? null;
@@ -3626,7 +3943,21 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3626
3943
  write_scope: opts.write_scope,
3627
3944
  env_vars: opts.env_vars,
3628
3945
  });
3629
- 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];
3630
3961
  }
3631
3962
  const [sid, hint] = openAgent(kind, {
3632
3963
  session_name: opts.session_name ?? null,
@@ -3642,13 +3973,17 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3642
3973
  try {
3643
3974
  const initial = await sendInitialAgentPrompt(sid, prompt, {
3644
3975
  ready_timeout: opts.ready_timeout ?? undefined,
3976
+ trust_project: opts.trust_project,
3645
3977
  });
3646
- 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" }];
3647
3980
  }
3648
3981
  catch (e) {
3649
3982
  const code = e instanceof AitermError ? e.code : 1;
3650
3983
  const message = e instanceof Error ? e.message : String(e);
3651
- throw new AitermError(`session_id: ${sid}\n` +
3652
- `起動後の初回 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" });
3653
3988
  }
3654
3989
  }