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) <noreply@anthropic.com>
This commit is contained in:
parent
6fa1bf5ee9
commit
3ea8c183a4
3 changed files with 95 additions and 108 deletions
|
|
@ -10,6 +10,8 @@
|
||||||
# load_formula FORMULA_FILE — sets FORMULA_CONTENT
|
# load_formula FORMULA_FILE — sets FORMULA_CONTENT
|
||||||
# build_context_block FILE [FILE ...] — sets CONTEXT_BLOCK
|
# build_context_block FILE [FILE ...] — sets CONTEXT_BLOCK
|
||||||
# start_formula_session SESSION WORKDIR PHASE_FILE — create tmux + claude
|
# 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
|
# formula_phase_callback PHASE — standard crash-recovery callback
|
||||||
#
|
#
|
||||||
# Requires: lib/agent-session.sh sourced first (for create_agent_session,
|
# Requires: lib/agent-session.sh sourced first (for create_agent_session,
|
||||||
|
|
@ -123,3 +125,78 @@ formula_phase_callback() {
|
||||||
;;
|
;;
|
||||||
esac
|
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 ---"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ source "$FACTORY_ROOT/lib/agent-session.sh"
|
||||||
source "$FACTORY_ROOT/lib/formula-session.sh"
|
source "$FACTORY_ROOT/lib/formula-session.sh"
|
||||||
|
|
||||||
LOG_FILE="$SCRIPT_DIR/planner.log"
|
LOG_FILE="$SCRIPT_DIR/planner.log"
|
||||||
|
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
|
||||||
SESSION_NAME="planner-${PROJECT_NAME}"
|
SESSION_NAME="planner-${PROJECT_NAME}"
|
||||||
PHASE_FILE="/tmp/planner-session-${PROJECT_NAME}.phase"
|
PHASE_FILE="/tmp/planner-session-${PROJECT_NAME}.phase"
|
||||||
|
|
||||||
|
|
@ -53,6 +54,13 @@ $(cat "$MEMORY_FILE")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Build prompt ─────────────────────────────────────────────────────────
|
# ── 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.
|
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
|
## Project context
|
||||||
|
|
@ -61,60 +69,8 @@ ${CONTEXT_BLOCK}${MEMORY_BLOCK}
|
||||||
## Formula
|
## Formula
|
||||||
${FORMULA_CONTENT}
|
${FORMULA_CONTENT}
|
||||||
|
|
||||||
## Codeberg API reference
|
${PROMPT_FOOTER}"
|
||||||
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.
|
|
||||||
|
|
||||||
## Environment
|
# ── Run session ──────────────────────────────────────────────────────────
|
||||||
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 ─────────────────────────────────────────────────
|
|
||||||
export CLAUDE_MODEL="opus"
|
export CLAUDE_MODEL="opus"
|
||||||
if ! start_formula_session "$SESSION_NAME" "$PROJECT_REPO_ROOT" "$PHASE_FILE"; then
|
run_formula_and_monitor "planner"
|
||||||
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 ---"
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ source "$FACTORY_ROOT/lib/agent-session.sh"
|
||||||
source "$FACTORY_ROOT/lib/formula-session.sh"
|
source "$FACTORY_ROOT/lib/formula-session.sh"
|
||||||
|
|
||||||
LOG_FILE="$SCRIPT_DIR/predictor.log"
|
LOG_FILE="$SCRIPT_DIR/predictor.log"
|
||||||
|
# shellcheck disable=SC2034 # consumed by run_formula_and_monitor
|
||||||
SESSION_NAME="predictor-${PROJECT_NAME}"
|
SESSION_NAME="predictor-${PROJECT_NAME}"
|
||||||
PHASE_FILE="/tmp/predictor-session-${PROJECT_NAME}.phase"
|
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_context_block AGENTS.md RESOURCES.md
|
||||||
|
|
||||||
# ── Build prompt ─────────────────────────────────────────────────────────
|
# ── 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.
|
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.
|
Your role: spot patterns in infrastructure signals and file them as prediction issues.
|
||||||
|
|
@ -58,58 +62,8 @@ ${CONTEXT_BLOCK}
|
||||||
## Formula
|
## Formula
|
||||||
${FORMULA_CONTENT}
|
${FORMULA_CONTENT}
|
||||||
|
|
||||||
## Codeberg API reference
|
${PROMPT_FOOTER}"
|
||||||
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.
|
|
||||||
|
|
||||||
## Environment
|
# ── Run session ──────────────────────────────────────────────────────────
|
||||||
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 ─────────────────────────────────────────────────
|
|
||||||
export CLAUDE_MODEL="sonnet"
|
export CLAUDE_MODEL="sonnet"
|
||||||
if ! start_formula_session "$SESSION_NAME" "$PROJECT_REPO_ROOT" "$PHASE_FILE"; then
|
run_formula_and_monitor "predictor"
|
||||||
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 ---"
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue