Merge pull request 'fix: feat: Matrix notifications — contextual, linked, conversational (#76)' (#102) from fix/issue-76 into main
This commit is contained in:
commit
f1959101e6
3 changed files with 151 additions and 18 deletions
|
|
@ -44,6 +44,10 @@ PHASE_FILE="/tmp/dev-session-${PROJECT_NAME}-${ISSUE}.phase"
|
||||||
SESSION_NAME="dev-${PROJECT_NAME}-${ISSUE}"
|
SESSION_NAME="dev-${PROJECT_NAME}-${ISSUE}"
|
||||||
IMPL_SUMMARY_FILE="/tmp/dev-impl-summary-${PROJECT_NAME}-${ISSUE}.txt"
|
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
|
# Timing
|
||||||
PHASE_POLL_INTERVAL=30 # seconds between phase checks
|
PHASE_POLL_INTERVAL=30 # seconds between phase checks
|
||||||
IDLE_TIMEOUT=7200 # 2h: kill session if phase stale this long
|
IDLE_TIMEOUT=7200 # 2h: kill session if phase stale this long
|
||||||
|
|
@ -71,7 +75,23 @@ status() {
|
||||||
}
|
}
|
||||||
|
|
||||||
notify() {
|
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
|
||||||
|
# Falls back to plain matrix_send (which registers a thread root) when no thread exists.
|
||||||
|
notify_ctx() {
|
||||||
|
local plain="$1" html="$2"
|
||||||
|
local thread_id=""
|
||||||
|
[ -f "${THREAD_FILE}" ] && thread_id=$(cat "$THREAD_FILE" 2>/dev/null || true)
|
||||||
|
if [ -n "$thread_id" ]; then
|
||||||
|
matrix_send_ctx "dev" "🔧 #${ISSUE}: ${plain}" "🔧 #${ISSUE}: ${html}" "${thread_id}" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
# No thread — fall back to plain send so a thread root is registered
|
||||||
|
matrix_send "dev" "🔧 #${ISSUE}: ${plain}" "" "${ISSUE}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Phase helpers ---
|
# --- Phase helpers ---
|
||||||
|
|
@ -246,10 +266,12 @@ do_merge() {
|
||||||
"${API}/issues/${ISSUE}" \
|
"${API}/issues/${ISSUE}" \
|
||||||
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||||
cleanup_labels
|
cleanup_labels
|
||||||
notify "✅ PR #${pr} merged! Issue #${ISSUE} done."
|
notify_ctx \
|
||||||
|
"✅ PR #${pr} merged! Issue #${ISSUE} done." \
|
||||||
|
"✅ PR <a href='${CODEBERG_WEB}/pulls/${pr}'>#${pr}</a> merged! <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||||
kill_tmux_session
|
kill_tmux_session
|
||||||
cleanup_worktree
|
cleanup_worktree
|
||||||
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE"
|
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
log "merge failed (HTTP ${http_code}) — attempting rebase and retry"
|
log "merge failed (HTTP ${http_code}) — attempting rebase and retry"
|
||||||
|
|
@ -285,14 +307,16 @@ do_merge() {
|
||||||
-d '{"Do":"merge","delete_branch_after_merge":true}')
|
-d '{"Do":"merge","delete_branch_after_merge":true}')
|
||||||
if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then
|
if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then
|
||||||
log "PR #${pr} merged after rebase!"
|
log "PR #${pr} merged after rebase!"
|
||||||
notify "✅ PR #${pr} merged! Issue #${ISSUE} done."
|
notify_ctx \
|
||||||
|
"✅ PR #${pr} merged! Issue #${ISSUE} done." \
|
||||||
|
"✅ PR <a href='${CODEBERG_WEB}/pulls/${pr}'>#${pr}</a> merged! <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||||
curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
|
curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
|
"${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||||
cleanup_labels
|
cleanup_labels
|
||||||
kill_tmux_session
|
kill_tmux_session
|
||||||
cleanup_worktree
|
cleanup_worktree
|
||||||
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE"
|
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|
@ -853,6 +877,18 @@ log "initial prompt sent to tmux session"
|
||||||
|
|
||||||
# Signal to dev-poll.sh that we're running (session is up)
|
# Signal to dev-poll.sh that we're running (session is up)
|
||||||
echo '{"status":"ready"}' > "$PREFLIGHT_RESULT"
|
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_ctx "dev" \
|
||||||
|
"🔧 Issue #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" \
|
||||||
|
"🔧 <a href='${ISSUE_URL}'>Issue #${ISSUE}</a>: ${ISSUE_TITLE}") || true
|
||||||
|
if [ -n "${_thread_id:-}" ]; then
|
||||||
|
printf '%s' "$_thread_id" > "$THREAD_FILE"
|
||||||
|
# Register thread root in map for listener dispatch
|
||||||
|
printf '%s\t%s\t%s\t%s\n' "$_thread_id" "dev" "$(date +%s)" "${ISSUE}" >> "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
notify "tmux session ${SESSION_NAME} started for issue #${ISSUE}: ${ISSUE_TITLE}"
|
notify "tmux session ${SESSION_NAME} started for issue #${ISSUE}: ${ISSUE_TITLE}"
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -972,7 +1008,10 @@ Phase file: ${PHASE_FILE}"
|
||||||
if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then
|
if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then
|
||||||
PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number')
|
PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number')
|
||||||
log "created PR #${PR_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 <a href='${PR_URL}'>#${PR_NUMBER}</a> created: ${ISSUE_TITLE}"
|
||||||
elif [ "$PR_HTTP_CODE" = "409" ]; then
|
elif [ "$PR_HTTP_CODE" = "409" ]; then
|
||||||
# PR already exists (race condition) — find it
|
# PR already exists (race condition) — find it
|
||||||
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
|
@ -1081,11 +1120,14 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CI_FIX_COUNT=$(( CI_FIX_COUNT + 1 ))
|
CI_FIX_COUNT=$(( CI_FIX_COUNT + 1 ))
|
||||||
|
_ci_pipeline_url="${WOODPECKER_SERVER}/repos/${WOODPECKER_REPO_ID}/pipeline/${PIPELINE_NUM:-0}"
|
||||||
if [ "$CI_FIX_COUNT" -gt "$MAX_CI_FIXES" ]; then
|
if [ "$CI_FIX_COUNT" -gt "$MAX_CI_FIXES" ]; then
|
||||||
log "CI failure not recoverable after ${CI_FIX_COUNT} fix attempts — escalating"
|
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)\"}" \
|
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"
|
>> "${FACTORY_ROOT}/supervisor/escalations.jsonl"
|
||||||
notify "CI exhausted after ${CI_FIX_COUNT} attempts — escalated to supervisor"
|
notify_ctx \
|
||||||
|
"CI exhausted after ${CI_FIX_COUNT} attempts — escalated to supervisor" \
|
||||||
|
"CI exhausted after ${CI_FIX_COUNT} attempts on PR <a href='${PR_URL:-${CODEBERG_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline</a><br>Step: <code>${FAILED_STEP:-unknown}</code> — escalated to supervisor"
|
||||||
printf 'PHASE:failed\nReason: ci_exhausted after %d attempts\n' "$CI_FIX_COUNT" > "$PHASE_FILE"
|
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)
|
LAST_PHASE_MTIME=$(stat -c %Y "$PHASE_FILE" 2>/dev/null || echo 0)
|
||||||
continue
|
continue
|
||||||
|
|
@ -1101,6 +1143,12 @@ 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" \
|
"$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
|
> "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Notify Matrix with rich CI failure context
|
||||||
|
_ci_snippet=$(printf '%s' "${CI_ERROR_LOG:-}" | tail -5 | head -c 500 | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
||||||
|
notify_ctx \
|
||||||
|
"CI failed on PR #${PR_NUMBER}: step=${FAILED_STEP:-unknown} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES})" \
|
||||||
|
"CI failed on PR <a href='${PR_URL:-${CODEBERG_WEB}/pulls/${PR_NUMBER}}'>#${PR_NUMBER}</a> | <a href='${_ci_pipeline_url}'>Pipeline #${PIPELINE_NUM:-?}</a><br>Step: <code>${FAILED_STEP:-unknown}</code> (exit ${FAILED_EXIT:-?})<br>Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}<br><pre>${_ci_snippet:-no logs}</pre>"
|
||||||
|
|
||||||
inject_into_session "CI failed on PR #${PR_NUMBER} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}).
|
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:-?})
|
Failed step: ${FAILED_STEP:-unknown} (exit code ${FAILED_EXIT:-?}, pipeline #${PIPELINE_NUM:-?})
|
||||||
|
|
@ -1238,14 +1286,16 @@ Instructions:
|
||||||
if [ "$PR_STATE" != "open" ]; then
|
if [ "$PR_STATE" != "open" ]; then
|
||||||
if [ "$PR_MERGED" = "true" ]; then
|
if [ "$PR_MERGED" = "true" ]; then
|
||||||
log "PR #${PR_NUMBER} was merged externally"
|
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 <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a> merged externally! <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> done."
|
||||||
curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
|
curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
|
"${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
|
||||||
cleanup_labels
|
cleanup_labels
|
||||||
kill_tmux_session
|
kill_tmux_session
|
||||||
cleanup_worktree
|
cleanup_worktree
|
||||||
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE"
|
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
log "PR #${PR_NUMBER} was closed WITHOUT merge — NOT closing issue"
|
log "PR #${PR_NUMBER} was closed WITHOUT merge — NOT closing issue"
|
||||||
|
|
@ -1270,7 +1320,12 @@ Instructions:
|
||||||
elif [ "$CURRENT_PHASE" = "PHASE:needs_human" ]; then
|
elif [ "$CURRENT_PHASE" = "PHASE:needs_human" ]; then
|
||||||
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}}"
|
_issue_url="${CODEBERG_WEB}/issues/${ISSUE}"
|
||||||
|
_pr_link=""
|
||||||
|
[ -n "${PR_NUMBER:-}" ] && _pr_link=" | PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>"
|
||||||
|
notify_ctx \
|
||||||
|
"⚠️ Issue #${ISSUE} (PR #${PR_NUMBER:-none}) needs human input.${HUMAN_REASON:+ Reason: ${HUMAN_REASON}}" \
|
||||||
|
"⚠️ <a href='${_issue_url}'>Issue #${ISSUE}</a>${_pr_link} needs human input.${HUMAN_REASON:+ Reason: ${HUMAN_REASON}}<br>Reply in this thread to send guidance to the dev agent."
|
||||||
log "phase: needs_human — notified via Matrix, waiting for external injection"
|
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
|
# Don't inject anything — supervisor-poll.sh (#81) injects human replies, gardener-poll.sh as backup
|
||||||
|
|
||||||
|
|
@ -1383,13 +1438,15 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
|
||||||
CLAIMED=false # Don't unclaim again in cleanup()
|
CLAIMED=false # Don't unclaim again in cleanup()
|
||||||
kill_tmux_session
|
kill_tmux_session
|
||||||
cleanup_worktree
|
cleanup_worktree
|
||||||
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE"
|
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
|
||||||
break
|
break
|
||||||
|
|
||||||
else
|
else
|
||||||
# Genuine unrecoverable failure — escalate to supervisor
|
# Genuine unrecoverable failure — escalate to supervisor
|
||||||
log "session failed: ${FAILURE_REASON}"
|
log "session failed: ${FAILURE_REASON}"
|
||||||
notify "❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}"
|
notify_ctx \
|
||||||
|
"❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}" \
|
||||||
|
"❌ <a href='${CODEBERG_WEB}/issues/${ISSUE}'>Issue #${ISSUE}</a> session failed: ${FAILURE_REASON}${PR_NUMBER:+ | PR <a href='${CODEBERG_WEB}/pulls/${PR_NUMBER}'>#${PR_NUMBER}</a>}"
|
||||||
echo "{\"issue\":${ISSUE},\"pr\":${PR_NUMBER:-0},\"reason\":\"${FAILURE_REASON}\",\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
|
echo "{\"issue\":${ISSUE},\"pr\":${PR_NUMBER:-0},\"reason\":\"${FAILURE_REASON}\",\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
|
||||||
>> "${FACTORY_ROOT}/supervisor/escalations.jsonl"
|
>> "${FACTORY_ROOT}/supervisor/escalations.jsonl"
|
||||||
|
|
||||||
|
|
@ -1408,7 +1465,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000)
|
||||||
else
|
else
|
||||||
cleanup_worktree
|
cleanup_worktree
|
||||||
fi
|
fi
|
||||||
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE"
|
rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
42
lib/env.sh
42
lib/env.sh
|
|
@ -74,12 +74,13 @@ wpdb() {
|
||||||
-t "$@" 2>/dev/null
|
-t "$@" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Matrix messaging helper — usage: matrix_send <prefix> <message> [thread_event_id]
|
# Matrix messaging helper — usage: matrix_send <prefix> <message> [thread_event_id] [context_tag]
|
||||||
# Returns event_id on stdout. Registers threads for listener dispatch.
|
# 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_THREAD_MAP="${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}"
|
||||||
matrix_send() {
|
matrix_send() {
|
||||||
[ -z "${MATRIX_TOKEN:-}" ] && return 0
|
[ -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 room_encoded="${MATRIX_ROOM_ID//!/%21}"
|
||||||
local txn="$(date +%s%N)$$"
|
local txn="$(date +%s%N)$$"
|
||||||
local body
|
local body
|
||||||
|
|
@ -101,7 +102,42 @@ matrix_send() {
|
||||||
printf '%s' "$event_id"
|
printf '%s' "$event_id"
|
||||||
# Register thread root for listener dispatch (escalations only)
|
# Register thread root for listener dispatch (escalations only)
|
||||||
if [ -z "$thread_id" ]; then
|
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
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# matrix_send_ctx — Send rich Matrix message with HTML formatting
|
||||||
|
# Usage: matrix_send_ctx <prefix> <plain_text> <html_body> [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 "<b>[${prefix}]</b> ${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 "<b>[${prefix}]</b> ${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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,48 @@ while true; do
|
||||||
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)
|
dev)
|
||||||
printf '%s\t%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SENDER" "$BODY" >> /tmp/dev-escalation-reply
|
# Route reply into the dev tmux session using context_tag (issue number)
|
||||||
matrix_send "dev" "✓ received, will inject on next poll" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
DEV_ISSUE=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $4}' "$THREAD_MAP" 2>/dev/null || true)
|
||||||
|
DEV_INJECTED=false
|
||||||
|
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"
|
||||||
|
DEV_INJECTED=true
|
||||||
|
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
|
||||||
|
# Only write to flat file when direct injection didn't happen,
|
||||||
|
# to avoid supervisor/gardener poll re-injecting the same message.
|
||||||
|
if [ "$DEV_INJECTED" = false ]; then
|
||||||
|
printf '%s\t%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SENDER" "$BODY" >> /tmp/dev-escalation-reply
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
review)
|
review)
|
||||||
# Route human questions to persistent review tmux session
|
# Route human questions to persistent review tmux session
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue