/**
 * codex-runner.ts — Phase B B2 (minimum-demo cut) codex agent.
 *
 * Subscribes to chunks; runs a two-stage state machine
 * (candidate → approved) gated on a successful tool_result;
 * emits an APPROVE chunk via host-cp POST /v1/chunks.
 *
 * Demo-cut simplifications (full contract deferred to B2-full):
 *   - NO SDK reasoning. Codex emits a structural APPROVE chunk
 *     using `codexApprovalDraft` (the pre-built reasoning
 *     boilerplate from C6). Full codex would invoke Claude Agent
 *     SDK with a codex-specific system prompt.
 *   - NO post-approval retraction monitor (OQ13 deferred). For
 *     demo G1, codex emits APPROVE on confirmed PR-draft; the
 *     follow-up BLOCK-on-retraction is post-demo.
 *   - NO byte-stable-chunk-on-retry adapter cache. We rely on
 *     the substrate PK (message_id+seq) idempotency at the server.
 *
 * Two-stage state machine (per Phase A `codex` section):
 *
 *   candidate    ← agent chunk matches detectCodexTrigger regex
 *      │ tool_result with is_error:false + PR-URL/push-ref output
 *      ▼
 *   approved     → emits APPROVE chunk
 *
 *   tool_result with is_error:true OR irrelevant → reverts to candidate
 *
 * Source: docs/design/olam-plan-chat-agent-runtime.md `codex` section,
 * minimum-demo cut (retraction protocol + SDK reasoning deferred).
 */
import { randomUUID } from 'node:crypto';
import { makeHostCpChunkPoster, } from './agent-sdk-to-chunks.js';
import { subscribeToChunks } from './chunks-subscriber-transport.js';
import { codexApprovalDraft, detectCodexTrigger, } from './operator-subscription.js';
/**
 * Detect a tool_result chunk that confirms a PR-draft trigger.
 * Looks for:
 *   - role='tool', kind='tool-result' (per C1's adapter mapping)
 *   - is_error:false (parsed from chunk content if JSON)
 *   - output containing a PR URL pattern OR successful-push reference
 */
export function isPrDraftConfirmation(chunk) {
    if (chunk.role !== 'tool')
        return false;
    // Parse the chunk content; tool-result blocks may be JSON-wrapped per C1.
    let parsed;
    try {
        parsed = JSON.parse(chunk.chunk);
    }
    catch {
        // Plain text — look for PR URL pattern.
        return /https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(chunk.chunk);
    }
    if (parsed === null || typeof parsed !== 'object')
        return false;
    const p = parsed;
    if (p.is_error === true)
        return false;
    const output = typeof p.output === 'string' ? p.output : JSON.stringify(p.output ?? '');
    return /https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/.test(output);
}
export function runCodex(opts) {
    const { hostCpUrl, bearer, worldId, sessionId, abortSignal: externalSignal, subscribeImpl = subscribeToChunks, postChunkImpl = (url, b) => makeHostCpChunkPoster({ sidecarUrl: url, bearer: b }), } = opts;
    const controller = new AbortController();
    if (externalSignal) {
        if (externalSignal.aborted)
            controller.abort();
        else
            externalSignal.addEventListener('abort', () => controller.abort(), { once: true });
    }
    const signal = controller.signal;
    const postChunk = postChunkImpl(hostCpUrl, bearer);
    let state = { kind: 'idle' };
    const emitApprove = async (triggerChunk, confirmationChunk) => {
        const draft = codexApprovalDraft({
            triggerChunk,
            approved: true,
            reason: `Confirmed by successful tool_result in message ${confirmationChunk.message_id}`,
        });
        const chunkRow = {
            world_id: worldId,
            session_id: sessionId,
            message_id: `codex-${randomUUID()}`,
            seq: 0,
            role: 'assistant',
            kind: 'text',
            chunk: draft.content,
            created_at: new Date().toISOString(),
        };
        try {
            await postChunk(chunkRow);
        }
        catch (err) {
            if (signal.aborted)
                return;
            // eslint-disable-next-line no-console
            console.error('[codex] APPROVE POST failed:', err);
        }
    };
    const handleChunk = async (chunk) => {
        if (signal.aborted)
            return;
        // Skip our own chunks to avoid feedback loops.
        if (chunk.actor_type === 'codex')
            return;
        if (state.kind === 'idle') {
            // Look for a candidate trigger from the driver agent.
            const trig = detectCodexTrigger(chunk);
            if (trig.matched) {
                state = {
                    kind: 'candidate',
                    triggerChunk: chunk,
                    matchedPattern: trig.matchedPattern ?? 'unknown',
                };
            }
            return;
        }
        // state.kind === 'candidate' — waiting for a tool_result.
        if (isPrDraftConfirmation(chunk)) {
            const triggerChunk = state.triggerChunk;
            state = { kind: 'idle' };
            await emitApprove(triggerChunk, chunk);
        }
        else if (chunk.role === 'tool') {
            // tool_result with is_error:true OR irrelevant output → revert to candidate.
            // We stay in candidate state in case a follow-up tool_result confirms;
            // alternative is to revert to idle and re-arm on a fresh trigger.
            // For demo cut: stay in candidate to give the agent a chance to retry.
        }
        // Non-tool chunks while in candidate: ignored (we wait for the confirmation).
    };
    const transport = subscribeImpl({
        baseUrl: hostCpUrl,
        bearer,
        worldId,
        sessionId,
        abortSignal: signal,
        onChunk: handleChunk,
    });
    return {
        stop: async () => {
            controller.abort();
            try {
                await transport.done;
            }
            catch {
                // swallow
            }
        },
        done: transport.done,
    };
}
/**
 * CLI entry point. Reads env, installs SIGTERM handler, runs the codex loop.
 */
export async function main() {
    const hostCpUrl = process.env.HOST_CP_URL;
    const bearer = process.env.HOST_CP_BEARER;
    const worldId = process.env.WORLD_ID;
    const sessionId = process.env.SESSION_ID;
    if (!hostCpUrl || !bearer || !worldId || !sessionId) {
        // eslint-disable-next-line no-console
        console.error('[codex] missing required env: HOST_CP_URL, HOST_CP_BEARER, WORLD_ID, SESSION_ID');
        process.exit(1);
    }
    const controller = new AbortController();
    process.on('SIGTERM', () => controller.abort());
    process.on('SIGINT', () => controller.abort());
    const handle = runCodex({
        hostCpUrl,
        bearer,
        worldId,
        sessionId,
        abortSignal: controller.signal,
    });
    await handle.done;
    process.exit(0);
}
if (typeof process !== 'undefined' &&
    process.argv[1] &&
    (process.argv[1].endsWith('codex-runner.js') || process.argv[1].endsWith('codex-runner.ts'))) {
    main().catch((err) => {
        // eslint-disable-next-line no-console
        console.error('[codex] fatal:', err);
        process.exit(1);
    });
}
//# sourceMappingURL=codex-runner.js.map