Merge pull request 'fix: feat: stream action-agent Claude output to Matrix thread (#293)' (#325) from fix/issue-293 into main
This commit is contained in:
commit
003f0486d1
4 changed files with 80 additions and 6 deletions
13
AGENTS.md
13
AGENTS.md
|
|
@ -218,11 +218,12 @@ issues labeled `action` that have no active tmux session, then spawns
|
|||
**Session lifecycle**:
|
||||
1. `action-poll.sh` finds open `action` issues with no active tmux session.
|
||||
2. Spawns `action-agent.sh <issue_num>`.
|
||||
3. Agent creates tmux session `action-{issue_num}`, injects prompt (formula + prior comments).
|
||||
4. Claude executes formula steps using Bash and other tools, posts progress as issue comments.
|
||||
5. For human input: Claude sends a Matrix message and waits; the reply is injected into the session by `matrix_listener.sh`.
|
||||
6. When complete: Claude closes the issue with a summary comment. Session exits.
|
||||
7. Poll detects no active session on next run — nothing further to do.
|
||||
3. Agent creates Matrix thread, exports `MATRIX_THREAD_ID` so Claude's output streams to the thread via a Stop hook (`on-stop-matrix.sh`).
|
||||
4. Agent creates tmux session `action-{issue_num}`, injects prompt (formula + prior comments).
|
||||
5. Claude executes formula steps using Bash and other tools, posts progress as issue comments. Each Claude turn is streamed to the Matrix thread for real-time human visibility.
|
||||
6. For human input: Claude sends a Matrix message and waits; the reply is injected into the session by `matrix_listener.sh`.
|
||||
7. When complete: Claude closes the issue with a summary comment. Session exits.
|
||||
8. Poll detects no active session on next run — nothing further to do.
|
||||
|
||||
**Environment variables consumed**:
|
||||
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `CODEBERG_WEB`
|
||||
|
|
@ -266,7 +267,7 @@ sourced as needed.
|
|||
| `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `CODEBERG_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, Matrix config, etc.). | env.sh (when `PROJECT_TOML` is set), supervisor-poll (per-project iteration) |
|
||||
| `lib/parse-deps.sh` | Extracts dependency issue numbers from an issue body (stdin → stdout, one number per line). Matches `## Dependencies` / `## Depends on` / `## Blocked by` sections and inline `depends on #N` patterns. Not sourced — executed via `bash lib/parse-deps.sh`. | dev-poll, supervisor-poll |
|
||||
| `lib/matrix_listener.sh` | Long-poll Matrix sync daemon. Dispatches thread replies to the correct agent via well-known files (`/tmp/{agent}-escalation-reply`). Handles supervisor, gardener, dev, review, vault, and action reply routing. Run as systemd service. | Standalone daemon |
|
||||
| `lib/agent-session.sh` | Shared tmux + Claude session helpers: `create_agent_session()`, `inject_formula()`, `agent_wait_for_claude_ready()`, `agent_inject_into_session()`, `agent_kill_session()`, `monitor_phase_loop()`, `read_phase()`. `create_agent_session(session, workdir, [phase_file])` optionally installs a PostToolUse hook (matcher `Bash\|Write`) that detects phase file writes in real-time — when Claude writes to the phase file, the hook writes a marker so `monitor_phase_loop` reacts on the next poll instead of waiting for mtime changes. `monitor_phase_loop` sets `_MONITOR_LOOP_EXIT` to one of: `done`, `idle_timeout`, `idle_prompt` (Claude returned to `❯` for 3 consecutive polls without writing any phase — callback invoked with `PHASE:failed`, session already dead), `crashed`, or a `PHASE:*` string. Agents must handle `idle_prompt` in both their callback and their post-loop exit handler. | dev-agent.sh, gardener-agent.sh |
|
||||
| `lib/agent-session.sh` | Shared tmux + Claude session helpers: `create_agent_session()`, `inject_formula()`, `agent_wait_for_claude_ready()`, `agent_inject_into_session()`, `agent_kill_session()`, `monitor_phase_loop()`, `read_phase()`. `create_agent_session(session, workdir, [phase_file])` optionally installs a PostToolUse hook (matcher `Bash\|Write`) that detects phase file writes in real-time — when Claude writes to the phase file, the hook writes a marker so `monitor_phase_loop` reacts on the next poll instead of waiting for mtime changes. When `MATRIX_THREAD_ID` is exported, also installs a Stop hook (`on-stop-matrix.sh`) that streams each Claude response to the Matrix thread. `monitor_phase_loop` sets `_MONITOR_LOOP_EXIT` to one of: `done`, `idle_timeout`, `idle_prompt` (Claude returned to `❯` for 3 consecutive polls without writing any phase — callback invoked with `PHASE:failed`, session already dead), `crashed`, or a `PHASE:*` string. Agents must handle `idle_prompt` in both their callback and their post-loop exit handler. | dev-agent.sh, gardener-agent.sh |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@ THREAD_ID=""
|
|||
if [ -n "${_thread_id:-}" ]; then
|
||||
printf '%s' "$_thread_id" > "$THREAD_FILE"
|
||||
THREAD_ID="$_thread_id"
|
||||
# Export for on-stop-matrix.sh hook (streams Claude output to thread)
|
||||
export MATRIX_THREAD_ID="$_thread_id"
|
||||
# Register thread root in map for listener dispatch (column 4 = issue number)
|
||||
printf '%s\t%s\t%s\t%s\n' "$_thread_id" "action" "$(date +%s)" "${ISSUE}" \
|
||||
>> "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true
|
||||
|
|
|
|||
|
|
@ -120,6 +120,32 @@ create_agent_session() {
|
|||
fi
|
||||
fi
|
||||
|
||||
# Install Stop hook for Matrix streaming: when MATRIX_THREAD_ID is set,
|
||||
# each Claude response is posted to the Matrix thread so humans can follow.
|
||||
local matrix_hook_script="${FACTORY_ROOT}/lib/hooks/on-stop-matrix.sh"
|
||||
if [ -n "${MATRIX_THREAD_ID:-}" ] && [ -x "$matrix_hook_script" ]; then
|
||||
if [ -f "$settings" ]; then
|
||||
jq --arg cmd "$matrix_hook_script" '
|
||||
if (.hooks.Stop // [] | any(.[]; .hooks[]?.command == $cmd))
|
||||
then .
|
||||
else .hooks.Stop = (.hooks.Stop // []) + [{
|
||||
matcher: "",
|
||||
hooks: [{type: "command", command: $cmd}]
|
||||
}]
|
||||
end
|
||||
' "$settings" > "${settings}.tmp" && mv "${settings}.tmp" "$settings"
|
||||
else
|
||||
jq -n --arg cmd "$matrix_hook_script" '{
|
||||
hooks: {
|
||||
Stop: [{
|
||||
matcher: "",
|
||||
hooks: [{type: "command", command: $cmd}]
|
||||
}]
|
||||
}
|
||||
}' > "$settings"
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$idle_marker"
|
||||
tmux new-session -d -s "$session" -c "$workdir" \
|
||||
"claude --dangerously-skip-permissions" 2>/dev/null
|
||||
|
|
|
|||
45
lib/hooks/on-stop-matrix.sh
Executable file
45
lib/hooks/on-stop-matrix.sh
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
#!/bin/bash
|
||||
# on-stop-matrix.sh — Stop hook: post Claude response to Matrix thread.
|
||||
#
|
||||
# Called by Claude Code after each assistant turn. Reads the response from
|
||||
# the hook JSON and posts it to the Matrix thread for this action session.
|
||||
#
|
||||
# Requires env vars: MATRIX_THREAD_ID, MATRIX_TOKEN, MATRIX_HOMESERVER, MATRIX_ROOM_ID
|
||||
#
|
||||
# Usage (in .claude/settings.json):
|
||||
# {"type": "command", "command": "/path/to/on-stop-matrix.sh"}
|
||||
|
||||
# Exit early if Matrix thread not configured
|
||||
if [ -z "${MATRIX_THREAD_ID:-}" ] || [ -z "${MATRIX_TOKEN:-}" ] \
|
||||
|| [ -z "${MATRIX_HOMESERVER:-}" ] || [ -z "${MATRIX_ROOM_ID:-}" ]; then
|
||||
cat > /dev/null
|
||||
exit 0
|
||||
fi
|
||||
|
||||
input=$(cat)
|
||||
|
||||
# Extract response text from hook JSON
|
||||
response=$(printf '%s' "$input" | jq -r '.last_assistant_message // empty' 2>/dev/null)
|
||||
[ -z "$response" ] && exit 0
|
||||
|
||||
# Truncate long output for readability (keep to ~4000 chars)
|
||||
MAX_LEN=4000
|
||||
if [ "${#response}" -gt "$MAX_LEN" ]; then
|
||||
response="${response:0:$MAX_LEN}
|
||||
... [truncated]"
|
||||
fi
|
||||
|
||||
# Post to Matrix thread
|
||||
room_encoded="${MATRIX_ROOM_ID//!/%21}"
|
||||
txn="$(date +%s%N)$$"
|
||||
|
||||
body=$(jq -nc \
|
||||
--arg m "$response" \
|
||||
--arg t "$MATRIX_THREAD_ID" \
|
||||
'{msgtype:"m.text",body:$m,"m.relates_to":{rel_type:"m.thread",event_id:$t}}')
|
||||
|
||||
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" > /dev/null 2>&1 || true
|
||||
Loading…
Add table
Add a link
Reference in a new issue