From 3ea8c183a4d6e60dc9954d24aa213cb0fbb6b774 Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 20 Mar 2026 17:41:52 +0000 Subject: [PATCH] refactor: extract shared prompt footer and monitor loop into formula-session.sh Eliminates 7 duplicate code blocks between planner-run.sh and predictor-run.sh flagged by CI duplicate-detection. Adds build_prompt_footer() and run_formula_and_monitor() helpers to lib/formula-session.sh. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/formula-session.sh | 77 ++++++++++++++++++++++++++++++++++++++ planner/planner-run.sh | 66 ++++++-------------------------- predictor/predictor-run.sh | 60 ++++------------------------- 3 files changed, 95 insertions(+), 108 deletions(-) diff --git a/lib/formula-session.sh b/lib/formula-session.sh index 3d73983..a2529de 100644 --- a/lib/formula-session.sh +++ b/lib/formula-session.sh @@ -10,6 +10,8 @@ # load_formula FORMULA_FILE — sets FORMULA_CONTENT # build_context_block FILE [FILE ...] — sets CONTEXT_BLOCK # start_formula_session SESSION WORKDIR PHASE_FILE — create tmux + claude +# build_prompt_footer [EXTRA_API] — sets PROMPT_FOOTER (API ref + env + phase) +# run_formula_and_monitor AGENT [TIMEOUT] — session start, inject, monitor, log # formula_phase_callback PHASE — standard crash-recovery callback # # Requires: lib/agent-session.sh sourced first (for create_agent_session, @@ -123,3 +125,78 @@ formula_phase_callback() { ;; esac } + +# ── Prompt + monitor helpers ────────────────────────────────────────────── + +# build_prompt_footer [EXTRA_API_LINES] +# Assembles the common Codeberg API reference + environment + phase protocol +# block for formula prompts. Sets PROMPT_FOOTER. +# Pass additional API endpoint lines (pre-formatted, newline-prefixed) via $1. +# Requires globals: CODEBERG_API, FACTORY_ROOT, PROJECT_REPO_ROOT, +# PRIMARY_BRANCH, PHASE_FILE. +build_prompt_footer() { + local extra_api="${1:-}" + # shellcheck disable=SC2034 # consumed by the calling script's PROMPT + PROMPT_FOOTER="## Codeberg API reference +Base URL: ${CODEBERG_API} +Auth header: -H \"Authorization: token \$CODEBERG_TOKEN\" + Read issue: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/issues/{number}' | jq '.body' + Create issue: curl -sf -X POST -H \"Authorization: token \$CODEBERG_TOKEN\" -H 'Content-Type: application/json' '${CODEBERG_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}'${extra_api} + List labels: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/labels' +NEVER echo or include the actual token value in output — always reference \$CODEBERG_TOKEN. + +## Environment +FACTORY_ROOT=${FACTORY_ROOT} +PROJECT_REPO_ROOT=${PROJECT_REPO_ROOT} +PRIMARY_BRANCH=${PRIMARY_BRANCH} +PHASE_FILE=${PHASE_FILE} + +## Phase protocol (REQUIRED) +When all work is done: + echo 'PHASE:done' > '${PHASE_FILE}' +On unrecoverable error: + printf 'PHASE:failed\nReason: %s\n' 'describe error' > '${PHASE_FILE}'" +} + +# run_formula_and_monitor AGENT_NAME [TIMEOUT] +# Starts the formula session, injects PROMPT, monitors phase, and logs result. +# Requires globals: SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT, +# CODEBERG_REPO, CLAUDE_MODEL (exported). +# shellcheck disable=SC2154 # SESSION_NAME, PHASE_FILE, PROJECT_REPO_ROOT, PROMPT set by caller +run_formula_and_monitor() { + local agent_name="$1" + local timeout="${2:-7200}" + + if ! start_formula_session "$SESSION_NAME" "$PROJECT_REPO_ROOT" "$PHASE_FILE"; then + exit 1 + fi + + agent_inject_into_session "$SESSION_NAME" "$PROMPT" + log "Prompt sent to tmux session" + matrix_send "$agent_name" "${agent_name^} session started for ${CODEBERG_REPO}" 2>/dev/null || true + + log "Monitoring phase file: ${PHASE_FILE}" + _FORMULA_CRASH_COUNT=0 + + monitor_phase_loop "$PHASE_FILE" "$timeout" "formula_phase_callback" + + FINAL_PHASE=$(read_phase "$PHASE_FILE") + log "Final phase: ${FINAL_PHASE:-none}" + + if [ "$FINAL_PHASE" != "PHASE:done" ]; then + case "${_MONITOR_LOOP_EXIT:-}" in + idle_prompt) + log "${agent_name}: Claude returned to prompt without writing phase signal" + ;; + idle_timeout) + log "${agent_name}: timed out with no phase signal" + ;; + *) + log "${agent_name} finished without PHASE:done (phase: ${FINAL_PHASE:-none}, exit: ${_MONITOR_LOOP_EXIT:-})" + ;; + esac + fi + + matrix_send "$agent_name" "${agent_name^} session finished (${FINAL_PHASE:-no phase})" 2>/dev/null || true + log "--- ${agent_name^} run done ---" +} diff --git a/planner/planner-run.sh b/planner/planner-run.sh index 2ad01cc..ad311e4 100755 --- a/planner/planner-run.sh +++ b/planner/planner-run.sh @@ -24,6 +24,7 @@ source "$FACTORY_ROOT/lib/agent-session.sh" source "$FACTORY_ROOT/lib/formula-session.sh" LOG_FILE="$SCRIPT_DIR/planner.log" +# shellcheck disable=SC2034 # consumed by run_formula_and_monitor SESSION_NAME="planner-${PROJECT_NAME}" PHASE_FILE="/tmp/planner-session-${PROJECT_NAME}.phase" @@ -53,6 +54,13 @@ $(cat "$MEMORY_FILE") fi # ── Build prompt ───────────────────────────────────────────────────────── +build_prompt_footer " + Relabel: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PUT -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/labels' -d '{\"labels\":[LABEL_ID]}' + Comment: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X POST -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/comments' -d '{\"body\":\"...\"}' + Close: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}' -d '{\"state\":\"closed\"}' +" + +# shellcheck disable=SC2034 # consumed by run_formula_and_monitor PROMPT="You are the strategic planner for ${CODEBERG_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling. ## Project context @@ -61,60 +69,8 @@ ${CONTEXT_BLOCK}${MEMORY_BLOCK} ## Formula ${FORMULA_CONTENT} -## Codeberg API reference -Base URL: ${CODEBERG_API} -Auth header: -H \"Authorization: token \$CODEBERG_TOKEN\" - Read issue: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/issues/{number}' | jq '.body' - Create issue: curl -sf -X POST -H \"Authorization: token \$CODEBERG_TOKEN\" -H 'Content-Type: application/json' '${CODEBERG_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}' - Relabel: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PUT -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/labels' -d '{\"labels\":[LABEL_ID]}' - Comment: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X POST -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/comments' -d '{\"body\":\"...\"}' - Close: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}' -d '{\"state\":\"closed\"}' - List labels: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/labels' -NEVER echo or include the actual token value in output — always reference \$CODEBERG_TOKEN. +${PROMPT_FOOTER}" -## Environment -FACTORY_ROOT=${FACTORY_ROOT} -PROJECT_REPO_ROOT=${PROJECT_REPO_ROOT} -PRIMARY_BRANCH=${PRIMARY_BRANCH} - -## Phase protocol (REQUIRED) -When all work is done: - echo 'PHASE:done' > '${PHASE_FILE}' -On unrecoverable error: - printf 'PHASE:failed\nReason: %s\n' 'describe error' > '${PHASE_FILE}'" - -# ── Create tmux session ───────────────────────────────────────────────── +# ── Run session ────────────────────────────────────────────────────────── export CLAUDE_MODEL="opus" -if ! start_formula_session "$SESSION_NAME" "$PROJECT_REPO_ROOT" "$PHASE_FILE"; then - exit 1 -fi - -agent_inject_into_session "$SESSION_NAME" "$PROMPT" -log "Prompt sent to tmux session" -matrix_send "planner" "Planner session started for ${CODEBERG_REPO}" 2>/dev/null || true - -# ── Phase monitoring loop ──────────────────────────────────────────────── -log "Monitoring phase file: ${PHASE_FILE}" -_FORMULA_CRASH_COUNT=0 - -monitor_phase_loop "$PHASE_FILE" 7200 "formula_phase_callback" - -FINAL_PHASE=$(read_phase "$PHASE_FILE") -log "Final phase: ${FINAL_PHASE:-none}" - -if [ "$FINAL_PHASE" != "PHASE:done" ]; then - case "${_MONITOR_LOOP_EXIT:-}" in - idle_prompt) - log "planner: Claude returned to prompt without writing phase signal" - ;; - idle_timeout) - log "planner: timed out after 2h with no phase signal" - ;; - *) - log "planner finished without PHASE:done (phase: ${FINAL_PHASE:-none}, exit: ${_MONITOR_LOOP_EXIT:-})" - ;; - esac -fi - -matrix_send "planner" "Planner session finished (${FINAL_PHASE:-no phase})" 2>/dev/null || true -log "--- Planner run done ---" +run_formula_and_monitor "planner" diff --git a/predictor/predictor-run.sh b/predictor/predictor-run.sh index 381a3f9..dfc2249 100755 --- a/predictor/predictor-run.sh +++ b/predictor/predictor-run.sh @@ -26,6 +26,7 @@ source "$FACTORY_ROOT/lib/agent-session.sh" source "$FACTORY_ROOT/lib/formula-session.sh" LOG_FILE="$SCRIPT_DIR/predictor.log" +# shellcheck disable=SC2034 # consumed by run_formula_and_monitor SESSION_NAME="predictor-${PROJECT_NAME}" PHASE_FILE="/tmp/predictor-session-${PROJECT_NAME}.phase" @@ -45,6 +46,9 @@ load_formula "$FACTORY_ROOT/formulas/run-predictor.toml" build_context_block AGENTS.md RESOURCES.md # ── Build prompt ───────────────────────────────────────────────────────── +build_prompt_footer + +# shellcheck disable=SC2034 # consumed by run_formula_and_monitor PROMPT="You are the prediction agent (goblin) for ${CODEBERG_REPO}. Work through the formula below. You MUST write PHASE:done to '${PHASE_FILE}' when finished — the orchestrator will time you out if you return to the prompt without signalling. Your role: spot patterns in infrastructure signals and file them as prediction issues. @@ -58,58 +62,8 @@ ${CONTEXT_BLOCK} ## Formula ${FORMULA_CONTENT} -## Codeberg API reference -Base URL: ${CODEBERG_API} -Auth header: -H \"Authorization: token \$CODEBERG_TOKEN\" - Read issue: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/issues/{number}' | jq '.body' - Create issue: curl -sf -X POST -H \"Authorization: token \$CODEBERG_TOKEN\" -H 'Content-Type: application/json' '${CODEBERG_API}/issues' -d '{\"title\":\"...\",\"body\":\"...\",\"labels\":[LABEL_ID]}' - List labels: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" '${CODEBERG_API}/labels' -NEVER echo or include the actual token value in output — always reference \$CODEBERG_TOKEN. +${PROMPT_FOOTER}" -## Environment -FACTORY_ROOT=${FACTORY_ROOT} -PROJECT_REPO_ROOT=${PROJECT_REPO_ROOT} -PRIMARY_BRANCH=${PRIMARY_BRANCH} -PHASE_FILE=${PHASE_FILE} - -## Phase protocol (REQUIRED) -When all work is done: - echo 'PHASE:done' > '${PHASE_FILE}' -On unrecoverable error: - printf 'PHASE:failed\nReason: %s\n' 'describe error' > '${PHASE_FILE}'" - -# ── Create tmux session ───────────────────────────────────────────────── +# ── Run session ────────────────────────────────────────────────────────── export CLAUDE_MODEL="sonnet" -if ! start_formula_session "$SESSION_NAME" "$PROJECT_REPO_ROOT" "$PHASE_FILE"; then - exit 1 -fi - -agent_inject_into_session "$SESSION_NAME" "$PROMPT" -log "Prompt sent to tmux session" -matrix_send "predictor" "Predictor session started for ${CODEBERG_REPO}" 2>/dev/null || true - -# ── Phase monitoring loop ──────────────────────────────────────────────── -log "Monitoring phase file: ${PHASE_FILE}" -_FORMULA_CRASH_COUNT=0 - -monitor_phase_loop "$PHASE_FILE" 7200 "formula_phase_callback" - -FINAL_PHASE=$(read_phase "$PHASE_FILE") -log "Final phase: ${FINAL_PHASE:-none}" - -if [ "$FINAL_PHASE" != "PHASE:done" ]; then - case "${_MONITOR_LOOP_EXIT:-}" in - idle_prompt) - log "predictor: Claude returned to prompt without writing phase signal" - ;; - idle_timeout) - log "predictor: timed out after 2h with no phase signal" - ;; - *) - log "predictor finished without PHASE:done (phase: ${FINAL_PHASE:-none}, exit: ${_MONITOR_LOOP_EXIT:-})" - ;; - esac -fi - -matrix_send "predictor" "Predictor session finished (${FINAL_PHASE:-no phase})" 2>/dev/null || true -log "--- Predictor run done ---" +run_formula_and_monitor "predictor"