fix: feat: supervisor-poll.sh and gardener-poll.sh inject human replies into needs_human dev sessions (#81)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9c9b3b3bde
commit
48683e508c
5 changed files with 123 additions and 4 deletions
|
|
@ -1271,8 +1271,8 @@ Instructions:
|
||||||
status "needs human input on issue #${ISSUE}"
|
status "needs human input on issue #${ISSUE}"
|
||||||
HUMAN_REASON=$(sed -n '2p' "$PHASE_FILE" 2>/dev/null | sed 's/^Reason: //' || echo "")
|
HUMAN_REASON=$(sed -n '2p' "$PHASE_FILE" 2>/dev/null | sed 's/^Reason: //' || echo "")
|
||||||
notify "⚠️ Issue #${ISSUE} (PR #${PR_NUMBER:-none}) needs human input.${HUMAN_REASON:+ Reason: ${HUMAN_REASON}}"
|
notify "⚠️ Issue #${ISSUE} (PR #${PR_NUMBER:-none}) needs human input.${HUMAN_REASON:+ Reason: ${HUMAN_REASON}}"
|
||||||
log "phase: needs_human — notified via Matrix, waiting for injection from #81/#82/#83"
|
log "phase: needs_human — notified via Matrix, waiting for external injection"
|
||||||
# Don't inject anything — other scripts (#81, #82, #83) will inject human replies
|
# Don't inject anything — supervisor-poll.sh (#81) and review-poll.sh (#82) inject replies
|
||||||
|
|
||||||
# ── PHASE: done ─────────────────────────────────────────────────────────────
|
# ── PHASE: done ─────────────────────────────────────────────────────────────
|
||||||
elif [ "$CURRENT_PHASE" = "PHASE:done" ]; then
|
elif [ "$CURRENT_PHASE" = "PHASE:done" ]; then
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,10 @@ PHASE:awaiting_review → wait for review-poll.sh to post review comment
|
||||||
on timeout (3h) → inject "no review, escalating"
|
on timeout (3h) → inject "no review, escalating"
|
||||||
|
|
||||||
PHASE:needs_human → send Matrix notification with issue/PR link
|
PHASE:needs_human → send Matrix notification with issue/PR link
|
||||||
on reply → inject human reply into session
|
on reply → supervisor-poll.sh injects reply into tmux session
|
||||||
on timeout → re-notify, then escalate after 24h
|
(gardener-poll.sh as backup if supervisor missed it)
|
||||||
|
reply file: /tmp/dev-escalation-reply (written by matrix_listener.sh)
|
||||||
|
on timeout → re-notify at 6h, escalate at 24h (supervisor-poll.sh)
|
||||||
|
|
||||||
PHASE:done → verify PR merged on Codeberg
|
PHASE:done → verify PR merged on Codeberg
|
||||||
if merged → kill tmux session, clean labels, close issue
|
if merged → kill tmux session, clean labels, close issue
|
||||||
|
|
@ -139,6 +141,8 @@ file and git history.
|
||||||
| `/tmp/dev-session-{proj}-{issue}.phase` | Claude (in session) | Current phase |
|
| `/tmp/dev-session-{proj}-{issue}.phase` | Claude (in session) | Current phase |
|
||||||
| `/tmp/ci-result-{proj}-{issue}.txt` | Orchestrator | Last CI output for injection |
|
| `/tmp/ci-result-{proj}-{issue}.txt` | Orchestrator | Last CI output for injection |
|
||||||
| `/tmp/dev-{proj}-{issue}.log` | Orchestrator | Session transcript (aspirational — path TBD when tmux session manager is implemented in #80) |
|
| `/tmp/dev-{proj}-{issue}.log` | Orchestrator | Session transcript (aspirational — path TBD when tmux session manager is implemented in #80) |
|
||||||
|
| `/tmp/dev-escalation-reply` | matrix_listener.sh | Human reply to `needs_human` escalation (consumed by supervisor-poll.sh) |
|
||||||
|
| `/tmp/dev-renotify-{proj}-{issue}` | supervisor-poll.sh | Marker to prevent duplicate 6h re-notifications |
|
||||||
| `WORKTREE` (git worktree) | dev-agent.sh | Code checkpoint |
|
| `WORKTREE` (git worktree) | dev-agent.sh | Code checkpoint |
|
||||||
|
|
||||||
## Sequence Diagram
|
## Sequence Diagram
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,47 @@ if [ -s /tmp/gardener-escalation-reply ]; then
|
||||||
log "Got escalation reply: $(echo "$ESCALATION_REPLY" | head -1)"
|
log "Got escalation reply: $(echo "$ESCALATION_REPLY" | head -1)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Inject human replies into needs_human dev sessions (backup to supervisor) ─
|
||||||
|
HUMAN_REPLY_FILE="/tmp/dev-escalation-reply"
|
||||||
|
if [ -s "$HUMAN_REPLY_FILE" ]; then
|
||||||
|
_gr_reply=$(cat "$HUMAN_REPLY_FILE")
|
||||||
|
for _gr_phase_file in /tmp/dev-session-"${PROJECT_NAME}"-*.phase; do
|
||||||
|
[ -f "$_gr_phase_file" ] || continue
|
||||||
|
_gr_phase=$(head -1 "$_gr_phase_file" 2>/dev/null | tr -d '[:space:]' || true)
|
||||||
|
[ "$_gr_phase" = "PHASE:needs_human" ] || continue
|
||||||
|
|
||||||
|
_gr_issue=$(basename "$_gr_phase_file" .phase)
|
||||||
|
_gr_issue="${_gr_issue#dev-session-${PROJECT_NAME}-}"
|
||||||
|
[ -z "$_gr_issue" ] && continue
|
||||||
|
_gr_session="dev-${PROJECT_NAME}-${_gr_issue}"
|
||||||
|
|
||||||
|
tmux has-session -t "$_gr_session" 2>/dev/null || continue
|
||||||
|
|
||||||
|
_gr_inject_msg="Human reply received for issue #${_gr_issue}:
|
||||||
|
|
||||||
|
${_gr_reply}
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
1. Read the human's guidance carefully.
|
||||||
|
2. Continue your work based on their input.
|
||||||
|
3. When done, push your changes and write the appropriate phase:
|
||||||
|
echo \"PHASE:awaiting_ci\" > \"${_gr_phase_file}\""
|
||||||
|
|
||||||
|
_gr_tmpfile=$(mktemp /tmp/human-inject-XXXXXX)
|
||||||
|
printf '%s' "$_gr_inject_msg" > "$_gr_tmpfile"
|
||||||
|
tmux load-buffer -b "human-inject-${_gr_issue}" "$_gr_tmpfile" || true
|
||||||
|
tmux paste-buffer -t "$_gr_session" -b "human-inject-${_gr_issue}" || true
|
||||||
|
sleep 0.5
|
||||||
|
tmux send-keys -t "$_gr_session" "" Enter || true
|
||||||
|
tmux delete-buffer -b "human-inject-${_gr_issue}" 2>/dev/null || true
|
||||||
|
rm -f "$_gr_tmpfile"
|
||||||
|
|
||||||
|
rm -f "$HUMAN_REPLY_FILE"
|
||||||
|
log "${PROJECT_NAME}: #${_gr_issue} human reply injected into session ${_gr_session} (gardener)"
|
||||||
|
break # only one reply to deliver
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Fetch all open issues ─────────────────────────────────────────────────
|
# ── Fetch all open issues ─────────────────────────────────────────────────
|
||||||
ISSUES_JSON=$(codeberg_api GET "/issues?state=open&type=issues&limit=50&sort=updated&direction=desc" 2>/dev/null || true)
|
ISSUES_JSON=$(codeberg_api GET "/issues?state=open&type=issues&limit=50&sort=updated&direction=desc" 2>/dev/null || true)
|
||||||
if [ -z "$ISSUES_JSON" ] || [ "$ISSUES_JSON" = "null" ]; then
|
if [ -z "$ISSUES_JSON" ] || [ "$ISSUES_JSON" = "null" ]; then
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,10 @@ while true; do
|
||||||
printf '%s\t%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SENDER" "$BODY" >> /tmp/gardener-escalation-reply
|
printf '%s\t%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SENDER" "$BODY" >> /tmp/gardener-escalation-reply
|
||||||
matrix_send "gardener" "✓ received, will act on next poll" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
matrix_send "gardener" "✓ received, will act on next poll" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||||
;;
|
;;
|
||||||
|
dev)
|
||||||
|
printf '%s\t%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SENDER" "$BODY" >> /tmp/dev-escalation-reply
|
||||||
|
matrix_send "dev" "✓ received, will inject on next supervisor poll" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
vault)
|
vault)
|
||||||
# Parse APPROVE <id> or REJECT <id> from reply
|
# Parse APPROVE <id> or REJECT <id> from reply
|
||||||
VAULT_CMD=$(echo "$BODY" | tr '[:lower:]' '[:upper:]' | grep -oP '^\s*(APPROVE|REJECT)\s+\S+' | head -1 || true)
|
VAULT_CMD=$(echo "$BODY" | tr '[:lower:]' '[:upper:]' | grep -oP '^\s*(APPROVE|REJECT)\s+\S+' | head -1 || true)
|
||||||
|
|
|
||||||
|
|
@ -513,6 +513,76 @@ check_project() {
|
||||||
--argjson prs "${_PR_COUNT:-0}" \
|
--argjson prs "${_PR_COUNT:-0}" \
|
||||||
'{ts:$ts,type:"dev",project:$proj,issues_in_backlog:$backlog,issues_blocked:$blocked,pr_open:$prs}' 2>/dev/null)" 2>/dev/null || true
|
'{ts:$ts,type:"dev",project:$proj,issues_in_backlog:$backlog,issues_blocked:$blocked,pr_open:$prs}' 2>/dev/null)" 2>/dev/null || true
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# P2d: NEEDS_HUMAN — inject human replies into blocked dev sessions
|
||||||
|
# ===========================================================================
|
||||||
|
status "P2: ${proj_name}: checking needs_human sessions"
|
||||||
|
|
||||||
|
HUMAN_REPLY_FILE="/tmp/dev-escalation-reply"
|
||||||
|
|
||||||
|
for _nh_phase_file in /tmp/dev-session-"${proj_name}"-*.phase; do
|
||||||
|
[ -f "$_nh_phase_file" ] || continue
|
||||||
|
_nh_phase=$(head -1 "$_nh_phase_file" 2>/dev/null | tr -d '[:space:]' || true)
|
||||||
|
[ "$_nh_phase" = "PHASE:needs_human" ] || continue
|
||||||
|
|
||||||
|
_nh_issue=$(basename "$_nh_phase_file" .phase)
|
||||||
|
_nh_issue="${_nh_issue#dev-session-${proj_name}-}"
|
||||||
|
[ -z "$_nh_issue" ] && continue
|
||||||
|
_nh_session="dev-${proj_name}-${_nh_issue}"
|
||||||
|
|
||||||
|
# Check tmux session is alive
|
||||||
|
if ! tmux has-session -t "$_nh_session" 2>/dev/null; then
|
||||||
|
flog "${proj_name}: #${_nh_issue} phase=needs_human but tmux session gone"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Inject human reply if available
|
||||||
|
if [ -s "$HUMAN_REPLY_FILE" ]; then
|
||||||
|
_nh_reply=$(cat "$HUMAN_REPLY_FILE")
|
||||||
|
_nh_inject_msg="Human reply received for issue #${_nh_issue}:
|
||||||
|
|
||||||
|
${_nh_reply}
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
1. Read the human's guidance carefully.
|
||||||
|
2. Continue your work based on their input.
|
||||||
|
3. When done, push your changes and write the appropriate phase:
|
||||||
|
echo \"PHASE:awaiting_ci\" > \"${_nh_phase_file}\""
|
||||||
|
|
||||||
|
_nh_tmpfile=$(mktemp /tmp/human-inject-XXXXXX)
|
||||||
|
printf '%s' "$_nh_inject_msg" > "$_nh_tmpfile"
|
||||||
|
# All tmux calls guarded: session may die between has-session and here
|
||||||
|
tmux load-buffer -b "human-inject-${_nh_issue}" "$_nh_tmpfile" || true
|
||||||
|
tmux paste-buffer -t "$_nh_session" -b "human-inject-${_nh_issue}" || true
|
||||||
|
sleep 0.5
|
||||||
|
tmux send-keys -t "$_nh_session" "" Enter || true
|
||||||
|
tmux delete-buffer -b "human-inject-${_nh_issue}" 2>/dev/null || true
|
||||||
|
rm -f "$_nh_tmpfile"
|
||||||
|
|
||||||
|
rm -f "$HUMAN_REPLY_FILE"
|
||||||
|
rm -f "/tmp/dev-renotify-${proj_name}-${_nh_issue}"
|
||||||
|
flog "${proj_name}: #${_nh_issue} human reply injected into session ${_nh_session}"
|
||||||
|
fixed "${proj_name}: Injected human reply into dev session #${_nh_issue}"
|
||||||
|
else
|
||||||
|
# No reply yet — check for timeout (re-notify at 6h, alert at 24h)
|
||||||
|
_nh_mtime=$(stat -c %Y "$_nh_phase_file" 2>/dev/null || echo 0)
|
||||||
|
_nh_now=$(date +%s)
|
||||||
|
_nh_age=$(( _nh_now - _nh_mtime ))
|
||||||
|
|
||||||
|
if [ "$_nh_age" -gt 86400 ]; then
|
||||||
|
p2 "${proj_name}: Dev session #${_nh_issue} stuck in needs_human for >24h"
|
||||||
|
elif [ "$_nh_age" -gt 21600 ]; then
|
||||||
|
_nh_renotify="/tmp/dev-renotify-${proj_name}-${_nh_issue}"
|
||||||
|
if [ ! -f "$_nh_renotify" ]; then
|
||||||
|
_nh_age_h=$(( _nh_age / 3600 ))
|
||||||
|
matrix_send "dev" "⏰ Reminder: Issue #${_nh_issue} still needs human input (waiting ${_nh_age_h}h)" 2>/dev/null || true
|
||||||
|
touch "$_nh_renotify"
|
||||||
|
flog "${proj_name}: #${_nh_issue} re-notified (needs_human for ${_nh_age_h}h)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# P4-PROJECT: Clean stale worktrees for this project
|
# P4-PROJECT: Clean stale worktrees for this project
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue