diff --git a/deploy/hooks/orchestra-codex-poll.sh b/deploy/hooks/orchestra-codex-poll.sh new file mode 100755 index 0000000..39512dc --- /dev/null +++ b/deploy/hooks/orchestra-codex-poll.sh @@ -0,0 +1,84 @@ +#!/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 diff --git a/deploy/hooks/orchestra-opencode-poll.sh b/deploy/hooks/orchestra-opencode-poll.sh new file mode 100755 index 0000000..9624f81 --- /dev/null +++ b/deploy/hooks/orchestra-opencode-poll.sh @@ -0,0 +1,79 @@ +#!/bin/sh +# opencode turn-boundary/completion poller — same rationale as +# orchestra-codex-poll.sh: opencode has no native Stop hook, so this runs as +# a background loop in the pane, polling /v1/harness/turn and watching for +# the .orchestra-report.md completion marker. +# +# opencode's own SSE session-status stream (OpenCodeStatus in +# internal/herdr/occupancy.go) is a fast path this script does not use — it +# would need the pane's session id, which isn't reliably known outside the +# opencode process. Instead this falls back to the same +# most-recently-written message file under opencode's storage dir that +# OpenCodeUsage already reads as its backstop (per AUDIT.md's "Real harness +# quota sources": "the SSE stream is not rock-solid"). +# +# Requires: ORCHESTRA_TASK_ID, ORCHESTRA_URL, ORCHESTRA_WORKTREE 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-opencode-poll: ORCHESTRA_TASK_ID not set" >&2; exit 1; } +[ -z "${ORCHESTRA_URL:-}" ] && { echo "orchestra-opencode-poll: ORCHESTRA_URL not set" >&2; exit 1; } + +auth_header="" +if [ -n "${ORCHESTRA_HARNESS_TOKEN:-}" ]; then + auth_header="Authorization: Bearer ${ORCHESTRA_HARNESS_TOKEN}" +fi + +latest_message() { + find "$HOME/.local/share/opencode/storage/message" -type f -name '*.json' -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" + + msg="$(latest_message)" + [ -z "$msg" ] && continue + + if [ -f "$report_file" ]; then + report="$(cat "$report_file")" + body="$(jq -n \ + --arg task_id "$ORCHESTRA_TASK_ID" \ + --arg transcript_path "$msg" \ + --arg report "$report" \ + '{task_id: $task_id, harness: "opencode", 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-opencode-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-opencode-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-opencode-poll: turn decision is $decision" >&2 + fi +done