diff --git a/dev/dev-agent.sh b/dev/dev-agent.sh index dffe7e9..773baa3 100755 --- a/dev/dev-agent.sh +++ b/dev/dev-agent.sh @@ -44,6 +44,10 @@ PHASE_FILE="/tmp/dev-session-${PROJECT_NAME}-${ISSUE}.phase" SESSION_NAME="dev-${PROJECT_NAME}-${ISSUE}" IMPL_SUMMARY_FILE="/tmp/dev-impl-summary-${PROJECT_NAME}-${ISSUE}.txt" +# Matrix thread tracking — one thread per issue for conversational notifications +THREAD_FILE="/tmp/dev-thread-${PROJECT_NAME}-${ISSUE}" +CODEBERG_WEB="https://codeberg.org/${CODEBERG_REPO}" + # Timing PHASE_POLL_INTERVAL=30 # seconds between phase checks IDLE_TIMEOUT=7200 # 2h: kill session if phase stale this long @@ -71,7 +75,17 @@ status() { } notify() { - matrix_send "dev" "🔧 #${ISSUE}: $*" 2>/dev/null || true + local thread_id="" + [ -f "${THREAD_FILE}" ] && thread_id=$(cat "$THREAD_FILE" 2>/dev/null || true) + matrix_send "dev" "🔧 #${ISSUE}: $*" "${thread_id}" 2>/dev/null || true +} + +# notify_ctx — Send rich notification with HTML links/context into the issue thread +notify_ctx() { + local plain="$1" html="$2" + local thread_id="" + [ -f "${THREAD_FILE}" ] && thread_id=$(cat "$THREAD_FILE" 2>/dev/null || true) + matrix_send_ctx "dev" "🔧 #${ISSUE}: ${plain}" "🔧 #${ISSUE}: ${html}" "${thread_id}" 2>/dev/null || true } # --- Phase helpers --- @@ -246,10 +260,12 @@ do_merge() { "${API}/issues/${ISSUE}" \ -d '{"state":"closed"}' >/dev/null 2>&1 || true cleanup_labels - notify "✅ PR #${pr} merged! Issue #${ISSUE} done." + notify_ctx \ + "✅ PR #${pr} merged! Issue #${ISSUE} done." \ + "✅ PR #${pr} merged! Issue #${ISSUE} done." kill_tmux_session cleanup_worktree - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$THREAD_FILE" exit 0 else log "merge failed (HTTP ${http_code}) — attempting rebase and retry" @@ -285,14 +301,16 @@ do_merge() { -d '{"Do":"merge","delete_branch_after_merge":true}') if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then log "PR #${pr} merged after rebase!" - notify "✅ PR #${pr} merged! Issue #${ISSUE} done." + notify_ctx \ + "✅ PR #${pr} merged! Issue #${ISSUE} done." \ + "✅ PR #${pr} merged! Issue #${ISSUE} done." curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \ -H "Content-Type: application/json" \ "${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true cleanup_labels kill_tmux_session cleanup_worktree - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" exit 0 fi else @@ -853,6 +871,14 @@ log "initial prompt sent to tmux session" # Signal to dev-poll.sh that we're running (session is up) echo '{"status":"ready"}' > "$PREFLIGHT_RESULT" +# Create Matrix thread for this issue (or reuse existing one) +if [ ! -f "${THREAD_FILE}" ] || [ -z "$(cat "$THREAD_FILE" 2>/dev/null)" ]; then + ISSUE_URL="${CODEBERG_WEB}/issues/${ISSUE}" + _thread_id=$(matrix_send "dev" "🔧 Issue #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" "" "${ISSUE}") || true + if [ -n "${_thread_id:-}" ]; then + printf '%s' "$_thread_id" > "$THREAD_FILE" + fi +fi notify "tmux session ${SESSION_NAME} started for issue #${ISSUE}: ${ISSUE_TITLE}" # ============================================================================= @@ -972,7 +998,10 @@ Phase file: ${PHASE_FILE}" if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number') log "created PR #${PR_NUMBER}" - notify "PR #${PR_NUMBER} created for issue #${ISSUE}: ${ISSUE_TITLE}" + PR_URL="${CODEBERG_WEB}/pulls/${PR_NUMBER}" + notify_ctx \ + "PR #${PR_NUMBER} created: ${ISSUE_TITLE}" \ + "PR #${PR_NUMBER} created: ${ISSUE_TITLE}" elif [ "$PR_HTTP_CODE" = "409" ]; then # PR already exists (race condition) — find it FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \ @@ -1085,7 +1114,10 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee log "CI failure not recoverable after ${CI_FIX_COUNT} fix attempts — escalating" echo "{\"issue\":${ISSUE},\"pr\":${PR_NUMBER},\"reason\":\"ci_exhausted\",\"step\":\"${FAILED_STEP:-unknown}\",\"attempts\":${CI_FIX_COUNT},\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \ >> "${FACTORY_ROOT}/supervisor/escalations.jsonl" - notify "CI exhausted after ${CI_FIX_COUNT} attempts — escalated to supervisor" + PIPELINE_URL="${WOODPECKER_SERVER}/repos/${WOODPECKER_REPO_ID}/pipeline/${PIPELINE_NUM:-0}" + notify_ctx \ + "CI exhausted after ${CI_FIX_COUNT} attempts — escalated to supervisor" \ + "CI exhausted after ${CI_FIX_COUNT} attempts on PR #${PR_NUMBER} | Pipeline
Step: ${FAILED_STEP:-unknown} — escalated to supervisor" printf 'PHASE:failed\nReason: ci_exhausted after %d attempts\n' "$CI_FIX_COUNT" > "$PHASE_FILE" LAST_PHASE_MTIME=$(stat -c %Y "$PHASE_FILE" 2>/dev/null || echo 0) continue @@ -1101,6 +1133,13 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee "$CI_FIX_COUNT" "$MAX_CI_FIXES" "${FAILED_STEP:-unknown}" "${FAILED_EXIT:-?}" "$CI_ERROR_LOG" \ > "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" 2>/dev/null || true + # Notify Matrix with rich CI failure context + _ci_pipeline_url="${WOODPECKER_SERVER}/repos/${WOODPECKER_REPO_ID}/pipeline/${PIPELINE_NUM:-0}" + _ci_snippet=$(printf '%s' "${CI_ERROR_LOG:-}" | tail -5 | head -c 500) + notify_ctx \ + "CI failed on PR #${PR_NUMBER}: step=${FAILED_STEP:-unknown} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES})" \ + "CI failed on PR #${PR_NUMBER} | Pipeline #${PIPELINE_NUM:-?}
Step: ${FAILED_STEP:-unknown} (exit ${FAILED_EXIT:-?})
Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}
${_ci_snippet:-no logs}
" + inject_into_session "CI failed on PR #${PR_NUMBER} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}). Failed step: ${FAILED_STEP:-unknown} (exit code ${FAILED_EXIT:-?}, pipeline #${PIPELINE_NUM:-?}) @@ -1238,14 +1277,16 @@ Instructions: if [ "$PR_STATE" != "open" ]; then if [ "$PR_MERGED" = "true" ]; then log "PR #${PR_NUMBER} was merged externally" - notify "✅ PR #${PR_NUMBER} merged externally! Issue #${ISSUE} done." + notify_ctx \ + "✅ PR #${PR_NUMBER} merged externally! Issue #${ISSUE} done." \ + "✅ PR #${PR_NUMBER} merged externally! Issue #${ISSUE} done." curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \ -H "Content-Type: application/json" \ "${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true cleanup_labels kill_tmux_session cleanup_worktree - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" exit 0 else log "PR #${PR_NUMBER} was closed WITHOUT merge — NOT closing issue" @@ -1270,7 +1311,12 @@ Instructions: elif [ "$CURRENT_PHASE" = "PHASE:needs_human" ]; then status "needs human input on issue #${ISSUE}" 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}}" + _issue_url="${CODEBERG_WEB}/issues/${ISSUE}" + _pr_link="" + [ -n "${PR_NUMBER:-}" ] && _pr_link=" | PR #${PR_NUMBER}" + notify_ctx \ + "⚠️ Issue #${ISSUE} (PR #${PR_NUMBER:-none}) needs human input.${HUMAN_REASON:+ Reason: ${HUMAN_REASON}}" \ + "⚠️ Issue #${ISSUE}${_pr_link} needs human input.${HUMAN_REASON:+ Reason: ${HUMAN_REASON}}
Reply in this thread to send guidance to the dev agent." log "phase: needs_human — notified via Matrix, waiting for external injection" # Don't inject anything — supervisor-poll.sh (#81) injects human replies, gardener-poll.sh as backup @@ -1383,13 +1429,15 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000) CLAIMED=false # Don't unclaim again in cleanup() kill_tmux_session cleanup_worktree - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" break else # Genuine unrecoverable failure — escalate to supervisor log "session failed: ${FAILURE_REASON}" - notify "❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}" + notify_ctx \ + "❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}" \ + "❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}${PR_NUMBER:+ | PR #${PR_NUMBER}}" echo "{\"issue\":${ISSUE},\"pr\":${PR_NUMBER:-0},\"reason\":\"${FAILURE_REASON}\",\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \ >> "${FACTORY_ROOT}/supervisor/escalations.jsonl" @@ -1408,7 +1456,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000) else cleanup_worktree fi - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" break fi diff --git a/lib/env.sh b/lib/env.sh index e05fdc5..80cd7ed 100755 --- a/lib/env.sh +++ b/lib/env.sh @@ -74,12 +74,13 @@ wpdb() { -t "$@" 2>/dev/null } -# Matrix messaging helper — usage: matrix_send [thread_event_id] +# Matrix messaging helper — usage: matrix_send [thread_event_id] [context_tag] # Returns event_id on stdout. Registers threads for listener dispatch. +# context_tag is stored in the thread map (e.g. issue number) for routing replies. MATRIX_THREAD_MAP="${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" matrix_send() { [ -z "${MATRIX_TOKEN:-}" ] && return 0 - local prefix="$1" msg="$2" thread_id="${3:-}" + local prefix="$1" msg="$2" thread_id="${3:-}" ctx_tag="${4:-}" local room_encoded="${MATRIX_ROOM_ID//!/%21}" local txn="$(date +%s%N)$$" local body @@ -101,7 +102,42 @@ matrix_send() { printf '%s' "$event_id" # Register thread root for listener dispatch (escalations only) if [ -z "$thread_id" ]; then - printf '%s\t%s\t%s\n' "$event_id" "$prefix" "$(date +%s)" >> "$MATRIX_THREAD_MAP" 2>/dev/null || true + printf '%s\t%s\t%s\t%s\n' "$event_id" "$prefix" "$(date +%s)" "${ctx_tag}" >> "$MATRIX_THREAD_MAP" 2>/dev/null || true fi fi } + +# matrix_send_ctx — Send rich Matrix message with HTML formatting +# Usage: matrix_send_ctx [thread_event_id] +# Use for notifications that benefit from links, code blocks, or structured content. +matrix_send_ctx() { + [ -z "${MATRIX_TOKEN:-}" ] && return 0 + local prefix="$1" plain="$2" html="$3" thread_id="${4:-}" + local room_encoded="${MATRIX_ROOM_ID//!/%21}" + local txn + txn="$(date +%s%N)$$" + local body + if [ -n "$thread_id" ]; then + body=$(jq -nc \ + --arg m "[${prefix}] ${plain}" \ + --arg h "[${prefix}] ${html}" \ + --arg t "$thread_id" \ + '{msgtype:"m.text",body:$m,format:"org.matrix.custom.html",formatted_body:$h,"m.relates_to":{rel_type:"m.thread",event_id:$t}}') + else + body=$(jq -nc \ + --arg m "[${prefix}] ${plain}" \ + --arg h "[${prefix}] ${html}" \ + '{msgtype:"m.text",body:$m,format:"org.matrix.custom.html",formatted_body:$h}') + fi + local response + response=$(curl -s -X PUT \ + -H "Authorization: Bearer ${MATRIX_TOKEN}" \ + -H "Content-Type: application/json" \ + "${MATRIX_HOMESERVER}/_matrix/client/v3/rooms/${room_encoded}/send/m.room.message/${txn}" \ + -d "$body" 2>/dev/null) || return 0 + local event_id + event_id=$(printf '%s' "$response" | jq -r '.event_id // empty' 2>/dev/null) + if [ -n "$event_id" ]; then + printf '%s' "$event_id" + fi +} diff --git a/lib/matrix_listener.sh b/lib/matrix_listener.sh index 9dbd35d..fac8fd0 100755 --- a/lib/matrix_listener.sh +++ b/lib/matrix_listener.sh @@ -142,8 +142,44 @@ while true; do matrix_send "gardener" "✓ received, will act on next poll" "$THREAD_ROOT" >/dev/null 2>&1 || true ;; dev) + # Write to flat file for backward compat 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 poll" "$THREAD_ROOT" >/dev/null 2>&1 || true + + # Route reply into the dev tmux session using context_tag (issue number) + DEV_ISSUE=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $4}' "$THREAD_MAP" 2>/dev/null || true) + if [ -n "$DEV_ISSUE" ]; then + DEV_SESSION="dev-${PROJECT_NAME}-${DEV_ISSUE}" + DEV_PHASE_FILE="/tmp/dev-session-${PROJECT_NAME}-${DEV_ISSUE}.phase" + if tmux has-session -t "$DEV_SESSION" 2>/dev/null; then + DEV_CUR_PHASE=$(head -1 "$DEV_PHASE_FILE" 2>/dev/null | tr -d '[:space:]' || true) + if [ "$DEV_CUR_PHASE" = "PHASE:needs_human" ] || [ "$DEV_CUR_PHASE" = "PHASE:awaiting_review" ]; then + DEV_INJECT_MSG="Human guidance from ${SENDER} in Matrix: + +${BODY} + +Consider this guidance for your current work." + DEV_INJECT_TMP=$(mktemp /tmp/dev-q-inject-XXXXXX) + printf '%s' "$DEV_INJECT_MSG" > "$DEV_INJECT_TMP" + tmux load-buffer -b "dev-q-${DEV_ISSUE}" "$DEV_INJECT_TMP" || true + tmux paste-buffer -t "$DEV_SESSION" -b "dev-q-${DEV_ISSUE}" || true + sleep 0.5 + tmux send-keys -t "$DEV_SESSION" "" Enter || true + tmux delete-buffer -b "dev-q-${DEV_ISSUE}" 2>/dev/null || true + rm -f "$DEV_INJECT_TMP" + log "human guidance from ${SENDER} injected into ${DEV_SESSION}" + matrix_send "dev" "✓ guidance forwarded to dev session for issue #${DEV_ISSUE}" "$THREAD_ROOT" >/dev/null 2>&1 || true + else + log "dev session ${DEV_SESSION} is busy (phase: ${DEV_CUR_PHASE:-active}), queuing" + matrix_send "dev" "✓ received — session is busy, will be available when dev pauses" "$THREAD_ROOT" >/dev/null 2>&1 || true + fi + else + log "dev session ${DEV_SESSION} not found for issue #${DEV_ISSUE}" + matrix_send "dev" "dev session not active for issue #${DEV_ISSUE}" "$THREAD_ROOT" >/dev/null 2>&1 || true + fi + else + log "dev thread ${THREAD_ROOT:0:20} has no issue mapping" + matrix_send "dev" "✓ received, will act on next poll" "$THREAD_ROOT" >/dev/null 2>&1 || true + fi ;; review) # Route human questions to persistent review tmux session