feat(hooks): add Codex/opencode Stop-hook-equivalent poll scripts

Neither harness has a native Stop hook, so both poll for the newest
session/rollout file and post to /v1/harness/{complete,turn} the same way
Claude's Stop hook does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
kami
2026-07-28 00:09:48 +04:00
parent e363a77ae9
commit d678959d65
2 changed files with 163 additions and 0 deletions
+84
View File
@@ -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
+79
View File
@@ -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