#!/bin/sh # Codex turn-boundary/completion poller. # # Unlike Claude Code, Codex has no native Stop hook — nothing calls out at a # turn boundary. This script is meant to run as a background loop inside the # pane alongside the codex process (AUDIT.md Phase 2 item 4 / progress.md's # "Not done: Codex/opencode Stop-hook-equivalent scripts"), polling the same # two endpoints the Claude Stop hook (orchestra-stop.sh) calls on every turn: # - completion: if the agent has written .orchestra-report.md, POST # /v1/harness/complete with harness=codex and the newest active rollout # path, then remove the marker. # - turn decision: otherwise POST /v1/harness/turn and log (never kill the # harness process on "refuse" — there's no turn boundary to refuse at # from outside the process the way exit-code-2 works for a real Stop # hook; this is advisory-only for codex until app-server integration # exists). # # Requires: ORCHESTRA_TASK_ID, ORCHESTRA_URL, ORCHESTRA_WORKTREE (the pane's # cwd) set in the pane's env. Optional: ORCHESTRA_HARNESS_TOKEN, # ORCHESTRA_POLL_INTERVAL (seconds, default 60). # Needs: jq, find, curl. set -u : "${ORCHESTRA_POLL_INTERVAL:=60}" : "${ORCHESTRA_WORKTREE:=$PWD}" [ -z "${ORCHESTRA_TASK_ID:-}" ] && { echo "orchestra-codex-poll: ORCHESTRA_TASK_ID not set" >&2; exit 1; } [ -z "${ORCHESTRA_URL:-}" ] && { echo "orchestra-codex-poll: ORCHESTRA_URL not set" >&2; exit 1; } auth_header="" if [ -n "${ORCHESTRA_HARNESS_TOKEN:-}" ]; then auth_header="Authorization: Bearer ${ORCHESTRA_HARNESS_TOKEN}" fi # Newest rollout file under ~/.codex/sessions, matching CodexActiveUsage's # own "most recently touched" heuristic (internal/herdr/occupancy.go). latest_rollout() { find "$HOME/.codex/sessions" -name 'rollout-*.jsonl' -type f -printf '%T@ %p\n' 2>/dev/null \ | sort -rn | head -n1 | cut -d' ' -f2- } report_file="${ORCHESTRA_WORKTREE%/}/.orchestra-report.md" while true; do sleep "$ORCHESTRA_POLL_INTERVAL" rollout="$(latest_rollout)" [ -z "$rollout" ] && continue if [ -f "$report_file" ]; then report="$(cat "$report_file")" body="$(jq -n \ --arg task_id "$ORCHESTRA_TASK_ID" \ --arg transcript_path "$rollout" \ --arg report "$report" \ '{task_id: $task_id, harness: "codex", transcript_path: $transcript_path, report: $report}')" if curl -fsS -X POST "${ORCHESTRA_URL%/}/v1/harness/complete" \ -H "Content-Type: application/json" \ ${auth_header:+-H "$auth_header"} \ -d "$body" >/dev/null 2>&1; then rm -f "$report_file" exit 0 else echo "orchestra-codex-poll: failed to report completion" >&2 fi continue fi body="$(jq -n --arg task_id "$ORCHESTRA_TASK_ID" '{task_id: $task_id}')" response="$(curl -fsS -X POST "${ORCHESTRA_URL%/}/v1/harness/turn" \ -H "Content-Type: application/json" \ ${auth_header:+-H "$auth_header"} \ -d "$body" 2>/dev/null)" || { echo "orchestra-codex-poll: failed to reach turn-decision endpoint" >&2 continue } decision="$(printf '%s' "$response" | jq -r '.decision // empty')" if [ "$decision" = "refuse" ] || [ "$decision" = "rotate_now" ]; then echo "orchestra-codex-poll: turn decision is $decision" >&2 fi done