diff --git a/container/codex-runner/package-lock.json b/container/codex-runner/package-lock.json index 3c1b3f4..f070aa1 100644 --- a/container/codex-runner/package-lock.json +++ b/container/codex-runner/package-lock.json @@ -7,11 +7,148 @@ "": { "name": "nanoclaw-codex-runner", "version": "1.0.0", + "dependencies": { + "@openai/codex-sdk": "^0.114.0" + }, "devDependencies": { "@types/node": "^22.10.7", "typescript": "^5.7.3" } }, + "node_modules/@openai/codex": { + "version": "0.114.0", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0.tgz", + "integrity": "sha512-HMo8LRR6CtfKkaa28xvFK6eOarmBFTDfsrS9GJtEoaspGGemFok494CpafDspiTZaHZZGHfSUe5SWTUxYq7OaA==", + "license": "Apache-2.0", + "bin": { + "codex": "bin/codex.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.114.0-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.114.0-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.114.0-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.114.0-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.114.0-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.114.0-win32-x64" + } + }, + "node_modules/@openai/codex-darwin-arm64": { + "name": "@openai/codex", + "version": "0.114.0-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-darwin-arm64.tgz", + "integrity": "sha512-c9dlgo9O+66alr3s3G36fKE3KaO2+xrLZ/QcU3oujDruV86pqgVSEK2njHi+WIyyOa6m2AyWxdaHLEbKxPCNRg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-darwin-x64": { + "name": "@openai/codex", + "version": "0.114.0-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-darwin-x64.tgz", + "integrity": "sha512-oPAPeZSaJZ1AQneFPSJu44uqbZr63Bxut9FOTWZwuSqYx4YEees7i0HJmJcqQc0XnCbYtHJdqo/i07Uv374NLw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-arm64": { + "name": "@openai/codex", + "version": "0.114.0-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-linux-arm64.tgz", + "integrity": "sha512-oUFZGZgMiEqmo85C+dfhckpVApH25alLWfwBgfKr2IRgdx/tovy8vN101f6kj01E6nDDe4LfJdNSZGtZht4fRA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-x64": { + "name": "@openai/codex", + "version": "0.114.0-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-linux-x64.tgz", + "integrity": "sha512-MF6RhxfBoccccd5CYII2C7TL2TvYzombBQhanDZfQAajr6n7IuoazCmoHDlrFHS4AcSw9bYA4MRlrwEjbEjgsw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-sdk": { + "version": "0.114.0", + "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.114.0.tgz", + "integrity": "sha512-28tV+pvoQhhoRADBCtlg24fR6LDTyeUA6C65NBTvYFnDoQVPJcHcAwRrbuWmUMcXdLrXwY2bmiMxyfXlUWQzgw==", + "license": "Apache-2.0", + "dependencies": { + "@openai/codex": "0.114.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@openai/codex-win32-arm64": { + "name": "@openai/codex", + "version": "0.114.0-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-win32-arm64.tgz", + "integrity": "sha512-CnRMHopj3en9aqQ2UaDW7EgpEGkxHdZVLLRq2cOy5D0HyuzF6Qb6595ADoFVJHEmPeN5Iz/KUbiGs5GLDjUwOA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-win32-x64": { + "name": "@openai/codex", + "version": "0.114.0-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.114.0-win32-x64.tgz", + "integrity": "sha512-ztRsH5Z+gPVZ5ZInx6HzXsTFFkuEdjk3Ay0UGVOhRRCWvevXQi/SL4eU8hwysCYrs8K/WRq3mo4c95w9zQ2wLw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, "node_modules/@types/node": { "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", diff --git a/container/codex-runner/package.json b/container/codex-runner/package.json index 2a623e2..51cd5c2 100644 --- a/container/codex-runner/package.json +++ b/container/codex-runner/package.json @@ -8,7 +8,9 @@ "build": "tsc", "start": "node dist/index.js" }, - "dependencies": {}, + "dependencies": { + "@openai/codex-sdk": "^0.114.0" + }, "devDependencies": { "@types/node": "^22.10.7", "typescript": "^5.7.3" diff --git a/container/codex-runner/src/index.ts b/container/codex-runner/src/index.ts index 67e4634..74c7e8f 100644 --- a/container/codex-runner/src/index.ts +++ b/container/codex-runner/src/index.ts @@ -1,9 +1,9 @@ /** - * NanoClaw Codex Runner (app-server mode) + * NanoClaw Codex Runner (SDK mode) * - * Spawns a single `codex app-server` process and communicates via JSON-RPC - * over stdio. Supports streaming responses, session persistence (threadId), - * and mid-turn message injection via turn/steer. + * Uses @openai/codex-sdk which wraps `codex exec`. This ensures complete + * task execution per turn — the agent finishes all work before responding, + * unlike app-server mode which can end turns prematurely. * * Input protocol: * Stdin: Full ContainerInput JSON (read until EOF) @@ -14,10 +14,9 @@ * Each result is wrapped in OUTPUT_START_MARKER / OUTPUT_END_MARKER pairs. */ -import { spawn, ChildProcess } from 'child_process'; +import { Codex, type Thread, type UserInput, type ThreadOptions } from '@openai/codex-sdk'; import fs from 'fs'; import path from 'path'; -import readline from 'readline'; // ── Types ────────────────────────────────────────────────────────── @@ -39,20 +38,6 @@ interface ContainerOutput { error?: string; } -interface JsonRpcRequest { - method: string; - id?: number; - params?: Record; -} - -interface JsonRpcResponse { - id?: number; - method?: string; - result?: Record; - error?: { code: number; message: string }; - params?: Record; -} - // ── Constants ────────────────────────────────────────────────────── const GROUP_DIR = process.env.NANOCLAW_GROUP_DIR || '/workspace/group'; @@ -146,412 +131,74 @@ function waitForIpcMessage(): Promise { }); } -// ── App-Server Client ────────────────────────────────────────────── - -class CodexAppServer { - private proc: ChildProcess; - private rl: readline.Interface; - private nextId = 1; - private pending = new Map void; - reject: (err: Error) => void; - }>(); - private notificationHandler: ((msg: JsonRpcResponse) => void) | null = null; - private serverRequestHandler: ((msg: JsonRpcResponse) => void) | null = null; - - constructor() { - this.proc = spawn('codex', ['app-server'], { - stdio: ['pipe', 'pipe', 'pipe'], - cwd: EFFECTIVE_CWD, - env: { ...process.env }, - }); - - this.rl = readline.createInterface({ input: this.proc.stdout! }); - - this.rl.on('line', (line: string) => { - if (!line.trim()) return; - try { - const msg: JsonRpcResponse = JSON.parse(line); - this.handleMessage(msg); - } catch { - // Non-JSON output, ignore - } - }); - - this.proc.stderr?.on('data', (data: Buffer) => { - for (const line of data.toString().trim().split('\n')) { - if (line) log(line); - } - }); - - this.proc.on('error', (err: Error) => { - log(`App-server spawn error: ${err.message}`); - // Reject all pending requests - for (const [, { reject }] of this.pending) { - reject(err); - } - this.pending.clear(); - }); - - this.proc.on('close', (code: number | null) => { - log(`App-server exited with code ${code}`); - const err = new Error(`App-server exited with code ${code}`); - for (const [, { reject }] of this.pending) { - reject(err); - } - this.pending.clear(); - }); - } - - private handleMessage(msg: JsonRpcResponse): void { - // Response to a request we made - if (msg.id !== undefined && this.pending.has(msg.id)) { - const handler = this.pending.get(msg.id)!; - this.pending.delete(msg.id); - handler.resolve(msg); - return; - } - - // Server-initiated request (has id + method) — needs a response - if (msg.id !== undefined && msg.method) { - this.serverRequestHandler?.(msg); - return; - } - - // Notification (has method, no id) - if (msg.method) { - this.notificationHandler?.(msg); - } - } - - setNotificationHandler(handler: ((msg: JsonRpcResponse) => void) | null): void { - this.notificationHandler = handler; - } - - setServerRequestHandler(handler: ((msg: JsonRpcResponse) => void) | null): void { - this.serverRequestHandler = handler; - } - - send(msg: JsonRpcRequest): void { - this.proc.stdin!.write(JSON.stringify(msg) + '\n'); - } - - async request(method: string, params: Record = {}, timeoutMs = 30_000): Promise { - const id = this.nextId++; - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - this.pending.delete(id); - reject(new Error(`Request ${method} timed out after ${timeoutMs}ms`)); - }, timeoutMs); - - this.pending.set(id, { - resolve: (resp) => { - clearTimeout(timer); - resolve(resp); - }, - reject: (err) => { - clearTimeout(timer); - reject(err); - }, - }); - - this.send({ method, id, params }); - }); - } - - respond(id: number, result: Record): void { - this.proc.stdin!.write(JSON.stringify({ id, result }) + '\n'); - } - - async initialize(): Promise { - const resp = await this.request('initialize', { - clientInfo: { name: 'nanoclaw', title: 'NanoClaw Codex', version: '1.0' }, - capabilities: { experimentalApi: false }, - }, 15_000); - - if (resp.error) { - throw new Error(`Initialize failed: ${resp.error.message}`); - } - - // Send initialized notification - this.send({ method: 'initialized' }); - log('App-server initialized'); - } - - async startThread(): Promise { - const params: Record = { - cwd: EFFECTIVE_CWD, - approvalPolicy: 'never', - sandbox: 'danger-full-access', - experimentalRawEvents: false, - persistExtendedHistory: false, - }; - if (CODEX_MODEL) params.model = CODEX_MODEL; - - const resp = await this.request('thread/start', params, 30_000); - if (resp.error) { - throw new Error(`thread/start failed: ${resp.error.message}`); - } - - const thread = resp.result?.thread as Record | undefined; - const threadId = thread?.id as string; - log(`Thread started: ${threadId}`); - return threadId; - } - - async resumeThread(threadId: string): Promise { - const params: Record = { - threadId, - cwd: EFFECTIVE_CWD, - approvalPolicy: 'never', - sandbox: 'danger-full-access', - persistExtendedHistory: false, - }; - if (CODEX_MODEL) params.model = CODEX_MODEL; - - const resp = await this.request('thread/resume', params, 30_000); - if (resp.error) { - // If resume fails (e.g., thread not found), start a new one - log(`thread/resume failed: ${resp.error.message}, starting new thread`); - return this.startThread(); - } - - const thread = resp.result?.thread as Record | undefined; - const actualId = (thread?.id as string) || threadId; - log(`Thread resumed: ${actualId}`); - return actualId; - } - - async startTurn(threadId: string, text: string): Promise { - // Parse [Image: /absolute/path] patterns and convert to multimodal input - const imagePattern = /\[Image:\s*(\/[^\]]+)\]/g; - const input: Array> = []; - const imagePaths: string[] = []; - let match; - while ((match = imagePattern.exec(text)) !== null) { - imagePaths.push(match[1].trim()); - } - // Add text (with image tags stripped) as first input block - const cleanText = text.replace(imagePattern, '').trim(); - if (cleanText) { - input.push({ type: 'text', text: cleanText, text_elements: [] }); - } - // Add image input blocks - for (const imgPath of imagePaths) { - if (fs.existsSync(imgPath)) { - input.push({ type: 'localImage', path: imgPath }); - log(`Adding image input: ${imgPath}`); - } else { - log(`Image not found, skipping: ${imgPath}`); - } - } - if (input.length === 0) { - input.push({ type: 'text', text, text_elements: [] }); - } - - const params: Record = { - threadId, - input, - }; - if (CODEX_EFFORT) params.effort = CODEX_EFFORT; - - const resp = await this.request('turn/start', params, 30_000); - if (resp.error) { - throw new Error(`turn/start failed: ${resp.error.message}`); - } - - const turn = resp.result?.turn as Record | undefined; - const turnId = turn?.id as string; - log(`Turn started: ${turnId}`); - return turnId; - } - - async steerTurn(threadId: string, turnId: string, text: string): Promise { - const resp = await this.request('turn/steer', { - threadId, - input: [{ type: 'text', text, text_elements: [] }], - expectedTurnId: turnId, - }, 10_000); - - if (resp.error) { - log(`turn/steer failed: ${resp.error.message}`); - } - } - - async interruptTurn(threadId: string, turnId: string): Promise { - try { - await this.request('turn/interrupt', { threadId, turnId }, 10_000); - } catch (err) { - log(`turn/interrupt failed: ${err instanceof Error ? err.message : String(err)}`); - } - } - - kill(): void { - try { - this.proc.kill('SIGTERM'); - setTimeout(() => { - if (!this.proc.killed) this.proc.kill('SIGKILL'); - }, 5000); - } catch { /* ignore */ } - } - - get alive(): boolean { - return !this.proc.killed && this.proc.exitCode === null; - } -} - -// ── Turn Execution ───────────────────────────────────────────────── +// ── Input Parsing ───────────────────────────────────────────────── /** - * Execute a turn and collect the agent's text response. - * While the turn is running, polls IPC for new messages and injects - * them via turn/steer (mid-execution message injection). - * Returns when turn/completed notification is received. + * Parse [Image: /path] patterns from text and build SDK input. */ -async function executeTurn( - server: CodexAppServer, - threadId: string, - prompt: string, -): Promise<{ result: string; error?: string; turnId: string }> { - return new Promise((resolve) => { - let agentText = ''; - let turnId = ''; - let resolved = false; - let ipcPollTimer: ReturnType | null = null; +function parseInput(text: string): string | UserInput[] { + const imagePattern = /\[Image:\s*(\/[^\]]+)\]/g; + const imagePaths: string[] = []; + let match; + while ((match = imagePattern.exec(text)) !== null) { + imagePaths.push(match[1].trim()); + } - const cleanup = () => { - server.setNotificationHandler(null); - server.setServerRequestHandler(null); - if (ipcPollTimer) { - clearTimeout(ipcPollTimer); - ipcPollTimer = null; - } - }; + if (imagePaths.length === 0) return text; - // Timeout safety (5 minutes per turn) - const timer = setTimeout(() => { - if (!resolved) { - resolved = true; - cleanup(); - log('Turn execution timed out (5min)'); - resolve({ - result: agentText || '', - error: 'Turn timed out after 5 minutes', - turnId, - }); - } - }, 5 * 60 * 1000); - - // IPC polling during turn — steer messages into the running turn - const pollIpcDuringTurn = () => { - if (resolved) return; - - // Check close sentinel - if (shouldClose()) { - log('Close sentinel during turn, interrupting'); - if (turnId) { - server.interruptTurn(threadId, turnId).catch(() => {}); - } - return; - } - - // Check for new messages to steer - const messages = drainIpcInput(); - if (messages.length > 0 && turnId) { - const text = messages.join('\n'); - log(`Steering message into turn (${text.length} chars)`); - server.steerTurn(threadId, turnId, text).catch((err) => { - log(`Steer failed: ${err instanceof Error ? err.message : String(err)}`); - }); - } - - ipcPollTimer = setTimeout(pollIpcDuringTurn, IPC_POLL_MS); - }; - - // Handle notifications (streaming events) - server.setNotificationHandler((msg) => { - if (resolved) return; - - switch (msg.method) { - case 'item/agentMessage/delta': { - const delta = (msg.params as Record)?.delta as string; - if (delta) agentText += delta; - break; - } - - case 'item/completed': { - const item = (msg.params as Record)?.item as Record; - if (item?.type === 'agentMessage') { - // Use authoritative text from completed item - const text = item.text as string; - if (text) agentText = text; - } - break; - } - - case 'turn/completed': { - const turn = (msg.params as Record)?.turn as Record; - const status = turn?.status as string; - const error = turn?.error as Record | null; - - clearTimeout(timer); - resolved = true; - cleanup(); - - if (status === 'failed') { - const errMsg = (error?.message as string) || 'Turn failed'; - resolve({ result: agentText || '', error: errMsg, turnId }); - } else { - resolve({ result: agentText, turnId }); - } - break; - } - - case 'turn/started': { - const turn = (msg.params as Record)?.turn as Record; - if (turn?.id) turnId = turn.id as string; - break; - } - } - }); - - // Handle server requests (approval auto-accept) - server.setServerRequestHandler((msg) => { - if (msg.id === undefined) return; - - if (msg.method === 'item/commandExecution/requestApproval' || - msg.method === 'item/fileChange/requestApproval' || - msg.method === 'item/permissions/requestApproval') { - server.respond(msg.id, { decision: 'accept' }); - return; - } - - // Unknown server request — accept generically - server.respond(msg.id, {}); - }); - - // Start IPC polling for mid-turn message injection - ipcPollTimer = setTimeout(pollIpcDuringTurn, IPC_POLL_MS); - - // Start the turn - server.startTurn(threadId, prompt) - .then((id) => { turnId = id; }) - .catch((err) => { - if (!resolved) { - clearTimeout(timer); - resolved = true; - cleanup(); - resolve({ - result: '', - error: `Failed to start turn: ${err.message}`, - turnId: '', - }); - } - }); - }); + const input: UserInput[] = []; + const cleanText = text.replace(imagePattern, '').trim(); + if (cleanText) { + input.push({ type: 'text', text: cleanText }); + } + for (const imgPath of imagePaths) { + if (fs.existsSync(imgPath)) { + input.push({ type: 'local_image', path: imgPath }); + log(`Adding image input: ${imgPath}`); + } else { + log(`Image not found, skipping: ${imgPath}`); + } + } + return input.length > 0 ? input : text; } -// ── Main ─────────────────────────────────────────────────────────── +// ── Turn Execution ──────────────────────────────────────────────── + +/** + * Execute a single turn using the SDK. The SDK wraps `codex exec`, + * ensuring the agent completes all work before returning. + */ +async function executeTurn( + thread: Thread, + input: string | UserInput[], +): Promise<{ result: string; error?: string }> { + const ac = new AbortController(); + + // Poll close sentinel during turn + const sentinel = setInterval(() => { + if (shouldClose()) { + log('Close sentinel detected during turn, aborting'); + ac.abort(); + } + }, IPC_POLL_MS); + + try { + const turn = await thread.run(input, { signal: ac.signal }); + return { result: turn.finalResponse }; + } catch (err) { + if (ac.signal.aborted) { + return { result: '' }; + } + return { + result: '', + error: err instanceof Error ? err.message : String(err), + }; + } finally { + clearInterval(sentinel); + } +} + +// ── Main ────────────────────────────────────────────────────────── async function main(): Promise { let containerInput: ContainerInput; @@ -583,19 +230,35 @@ async function main(): Promise { prompt += '\n' + pending.join('\n'); } - // Spawn app-server - const server = new CodexAppServer(); + // Build thread options + const threadOptions: ThreadOptions = { + workingDirectory: EFFECTIVE_CWD, + approvalPolicy: 'never', + sandboxMode: 'danger-full-access', + networkAccessEnabled: true, + webSearchMode: 'live', + }; + if (CODEX_MODEL) threadOptions.model = CODEX_MODEL; + if (CODEX_EFFORT) { + threadOptions.modelReasoningEffort = CODEX_EFFORT as ThreadOptions['modelReasoningEffort']; + } + + // Create SDK instance (inherits env from parent — CODEX_HOME, OPENAI_API_KEY, etc.) + const codex = new Codex(); + + // Start or resume thread (resume may fail on first run, fallback to new thread) + let thread: Thread; + if (containerInput.sessionId) { + thread = codex.resumeThread(containerInput.sessionId, threadOptions); + log(`Thread resuming (session: ${containerInput.sessionId})`); + } else { + thread = codex.startThread(threadOptions); + log('Thread started (new session)'); + } + + let turnCount = 0; try { - await server.initialize(); - - // Start or resume thread - const threadId = containerInput.sessionId - ? await server.resumeThread(containerInput.sessionId) - : await server.startThread(); - - let turnCount = 0; - // Main turn loop while (true) { turnCount++; @@ -604,19 +267,27 @@ async function main(): Promise { writeOutput({ status: 'success', result: '[세션 턴 제한 도달. 새 메시지로 다시 시작됩니다.]', - newSessionId: threadId, + newSessionId: thread.id || undefined, }); break; } + const input = parseInput(prompt); log(`Starting turn ${turnCount}/${MAX_TURNS}...`); - const { result, error } = await executeTurn(server, threadId, prompt); + let { result, error } = await executeTurn(thread, input); + + // Fallback: if resume failed on first turn, retry with a new thread + if (error && turnCount === 1 && containerInput.sessionId) { + log(`Resume may have failed, retrying with new thread: ${error}`); + thread = codex.startThread(threadOptions); + ({ result, error } = await executeTurn(thread, input)); + } // Check close sentinel if (shouldClose()) { if (result) { - writeOutput({ status: 'success', result, newSessionId: threadId }); + writeOutput({ status: 'success', result, newSessionId: thread.id || undefined }); } log('Close sentinel detected, exiting'); break; @@ -627,14 +298,14 @@ async function main(): Promise { writeOutput({ status: 'error', result: result || null, - newSessionId: threadId, + newSessionId: thread.id || undefined, error, }); } else { writeOutput({ status: 'success', result: result || null, - newSessionId: threadId, + newSessionId: thread.id || undefined, }); } @@ -657,8 +328,6 @@ async function main(): Promise { result: null, error: errorMessage, }); - } finally { - server.kill(); } } diff --git a/package-lock.json b/package-lock.json index 14be4ac..603ad85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1982,6 +1982,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2548,6 +2549,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -2615,6 +2617,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2690,6 +2693,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -2811,6 +2815,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/src/agent-runner.ts b/src/agent-runner.ts index da252e9..1664726 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -214,7 +214,7 @@ function prepareGroupEnvironment( const authSrc = path.join(hostCodexDir, 'auth.json'); const authDst = path.join(sessionCodexDir, 'auth.json'); if (fs.existsSync(authSrc)) fs.copyFileSync(authSrc, authDst); - for (const file of ['config.toml', 'config.json', 'instructions.md']) { + for (const file of ['config.toml', 'config.json', 'AGENTS.md']) { const src = path.join(hostCodexDir, file); const dst = path.join(sessionCodexDir, file); if (fs.existsSync(src)) {