fix: Remove Matrix integration — notifications move to forge + OpenClaw (#732)
Remove all Matrix/Dendrite infrastructure: - Delete lib/matrix_listener.sh (long-poll daemon), lib/matrix_listener.service (systemd unit), lib/hooks/on-stop-matrix.sh (response streaming hook) - Remove matrix_send() and matrix_send_ctx() from lib/env.sh - Remove MATRIX_HOMESERVER auto-detection, MATRIX_THREAD_MAP from lib/env.sh - Remove [matrix] section parsing from lib/load-project.sh - Remove Matrix hook installation from lib/agent-session.sh - Remove notify/notify_ctx helpers and Matrix thread tracking from dev/dev-agent.sh and action/action-agent.sh - Remove all matrix_send calls from dev-poll.sh, phase-handler.sh, action-poll.sh, vault-poll.sh, vault-fire.sh, vault-reject.sh, review-poll.sh, review-pr.sh, supervisor-poll.sh, formula-session.sh - Remove Matrix listener startup from docker/agents/entrypoint.sh - Remove append_dendrite_compose() and setup_matrix() from bin/disinto - Remove --matrix flag from disinto init - Clean Matrix references from .env.example, projects/*.toml.example, formulas/*.toml, AGENTS.md, BOOTSTRAP.md, README.md, RESOURCES.md, PHASE-PROTOCOL.md, and all agent AGENTS.md/PROMPT.md files Status visibility now via Codeberg PR/issue activity. Human interaction via vault items through forge. Proactive alerts via OpenClaw heartbeats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7996bb6c06
commit
23949083c0
43 changed files with 73 additions and 1157 deletions
|
|
@ -6,12 +6,11 @@ sourced as needed.
|
|||
|
||||
| File | What it provides | Sourced by |
|
||||
|---|---|---|
|
||||
| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`FORGE_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `forge_api()`, `forge_api_all()` (accepts optional second TOKEN parameter, defaults to `$FORGE_TOKEN`), `woodpecker_api()`, `wpdb()`, `matrix_send()`, `matrix_send_ctx()`. Auto-loads project TOML if `PROJECT_TOML` is set. Auto-detects `MATRIX_HOMESERVER`: defaults to `http://dendrite:8008` inside a container (`DISINTO_CONTAINER=1`) or `http://localhost:8008` on bare metal; can be overridden via `.env`. **Container note**: when `DISINTO_CONTAINER=1`, `.env` is NOT re-sourced — compose already injects env vars (including `FORGE_URL=http://forgejo:3000`) and re-sourcing would clobber them. | Every agent |
|
||||
| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`FORGE_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `forge_api()`, `forge_api_all()` (accepts optional second TOKEN parameter, defaults to `$FORGE_TOKEN`), `woodpecker_api()`, `wpdb()`. Auto-loads project TOML if `PROJECT_TOML` is set. **Container note**: when `DISINTO_CONTAINER=1`, `.env` is NOT re-sourced — compose already injects env vars (including `FORGE_URL=http://forgejo:3000`) and re-sourcing would clobber them. | Every agent |
|
||||
| `lib/ci-helpers.sh` | `ci_passed()` — returns 0 if CI state is "success" (or no CI configured). `ci_required_for_pr()` — returns 0 if PR has code files (CI required), 1 if non-code only (CI not required). `is_infra_step()` — returns 0 if a single CI step failure matches infra heuristics (clone/git exit 128, any exit 137, log timeout patterns). `classify_pipeline_failure()` — returns "infra \<reason>" if any failed Woodpecker step matches infra heuristics via `is_infra_step()`, else "code". `ensure_priority_label()` — looks up (or creates) the `priority` label and returns its ID; caches in `_PRIORITY_LABEL_ID`. `ci_commit_status <sha>` — queries Woodpecker directly for CI state, falls back to forge commit status API. `ci_pipeline_number <sha>` — returns the Woodpecker pipeline number for a commit, falls back to parsing forge status `target_url`. | dev-poll, review-poll, review-pr, supervisor-poll |
|
||||
| `lib/ci-debug.sh` | CLI tool for Woodpecker CI: `list`, `status`, `logs`, `failures` subcommands. Not sourced — run directly. | Humans / dev-agent (tool access) |
|
||||
| `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `FORGE_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, Matrix config, etc.). | env.sh (when `PROJECT_TOML` is set), supervisor-poll (per-project iteration) |
|
||||
| `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `FORGE_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, mirror 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` / `blocked by #N` patterns. Inline scan skips fenced code blocks to prevent false positives from code examples in issue bodies. 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 tmux session injection (dev, action, vault, review) or well-known files (`/tmp/{agent}-escalation-reply` for supervisor/gardener). Handles all agent reply routing. Uses `nohup` for robustness and validates TOML path before passing to exec-inject.sh. In compose mode, started as a background process by `docker/agents/entrypoint.sh`; on bare metal, run as systemd service (see `matrix_listener.service`). | Standalone daemon |
|
||||
| `lib/formula-session.sh` | `acquire_cron_lock()`, `check_memory()`, `load_formula()`, `build_context_block()`, `consume_escalation_reply()`, `start_formula_session()`, `formula_phase_callback()`, `build_prompt_footer()`, `build_graph_section()`, `run_formula_and_monitor(AGENT [TIMEOUT] [CALLBACK])` — shared helpers for formula-driven cron agents (lock, memory guard, formula loading, prompt assembly, tmux session, monitor loop, crash recovery). `build_graph_section()` generates the structural-analysis section (runs `lib/build-graph.py`, formats JSON output) — previously duplicated in planner-run.sh and predictor-run.sh, now shared here. `formula_phase_callback()` handles `PHASE:escalate` (unified escalation path — kills the session; callers may follow up via Matrix). `run_formula_and_monitor` accepts an optional CALLBACK (default: `formula_phase_callback`) so callers can install custom merge-through or escalation handlers. | planner-run.sh, predictor-run.sh, gardener-run.sh, supervisor-run.sh, dev-agent.sh, action-agent.sh |
|
||||
| `lib/guard.sh` | `check_active(agent_name)` — reads `$FACTORY_ROOT/state/.{agent_name}-active`; exits 0 (skip) if the file is absent. Factory is off by default — state files must be created to enable each agent. **Logs a message to stderr** when skipping (`[check_active] SKIP: state file not found`), so agent dropout is visible in cron logs. Sourced by dev-poll.sh, review-poll.sh, action-poll.sh, predictor-run.sh, supervisor-run.sh. | cron entry points |
|
||||
| `lib/mirrors.sh` | `mirror_push()` — pushes `$PRIMARY_BRANCH` + tags to all configured mirror remotes (fire-and-forget background pushes). Reads `MIRROR_NAMES` and `MIRROR_*` vars exported by `load-project.sh` from the `[mirrors]` TOML section. Failures are logged but never block the pipeline. Sourced by dev-poll.sh and dev/phase-handler.sh — called after every successful merge. | dev-poll.sh, phase-handler.sh |
|
||||
|
|
@ -19,4 +18,4 @@ sourced as needed.
|
|||
| `lib/secret-scan.sh` | `scan_for_secrets()` — detects potential secrets (API keys, bearer tokens, private keys, URLs with embedded credentials) in text; returns 1 if secrets found. `redact_secrets()` — replaces detected secret patterns with `[REDACTED]`. | file-action-issue.sh, phase-handler.sh |
|
||||
| `lib/file-action-issue.sh` | `file_action_issue()` — dedup check, secret scan, label lookup, and issue creation for formula-driven cron wrappers. Sets `FILED_ISSUE_NUM` on success. Returns 4 if secrets detected in body. | (available for future use) |
|
||||
| `lib/tea-helpers.sh` | `tea_file_issue(title, body, labels...)` — create issue via tea CLI with secret scanning; sets `FILED_ISSUE_NUM`. `tea_relabel(issue_num, labels...)` — replace labels using tea's `edit` subcommand (not `label`). `tea_comment(issue_num, body)` — add comment with secret scanning. `tea_close(issue_num)` — close issue. All use `TEA_LOGIN` and `FORGE_REPO` from env.sh. Labels by name (no ID lookup). Tea binary download verified via sha256 checksum. Sourced by env.sh when `tea` binary is available. | env.sh (conditional) |
|
||||
| `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()`, `write_compact_context()`. `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. Also installs a StopFailure hook (matcher `rate_limit\|server_error\|authentication_failed\|billing_error`) that writes `PHASE:failed` with an `api_error` reason to the phase file and touches the phase-changed marker, so the orchestrator discovers API errors within one poll cycle instead of waiting for idle timeout. Also installs a SessionStart hook (matcher `compact`) that re-injects phase protocol instructions after context compaction — callers write the context file via `write_compact_context(phase_file, content)`, and the hook (`on-compact-reinject.sh`) outputs the file content to stdout so Claude retains critical instructions. When `MATRIX_THREAD_ID` is exported, also installs a Stop hook (`on-stop-matrix.sh`) that streams each Claude response to the Matrix thread. When `phase_file` is set, passes it to the idle stop hook (`on-idle-stop.sh`) so the hook can **nudge Claude** (up to 2 times) if Claude returns to the prompt without writing to the phase file — the hook injects a tmux reminder asking Claude to signal PHASE:done or PHASE:awaiting_ci. The PreToolUse guard hook (`on-pretooluse-guard.sh`) receives the session name as a third argument — formula agents (`gardener-*`, `planner-*`, `predictor-*`, `supervisor-*`) are identified this way and allowed to access `FACTORY_ROOT` from worktrees (they need env.sh, AGENTS.md, formulas/, lib/). **OAuth flock**: when `DISINTO_CONTAINER=1`, Claude CLI is wrapped in `flock -w 300 ~/.claude/session.lock` to queue concurrent token refresh attempts and prevent rotation races across agents sharing the same credentials. `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 `PHASE:escalate` / other `PHASE:*` string. **Unified escalation**: `PHASE:escalate` is the signal that a session needs human input (renamed from `PHASE:needs_human`). **Callers must handle `idle_prompt`** in both their callback and their post-loop exit handler — see [`docs/PHASE-PROTOCOL.md` idle_prompt](docs/PHASE-PROTOCOL.md#idle_prompt-exit-reason) for the full contract. | dev-agent.sh, action-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()`, `write_compact_context()`. `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. Also installs a StopFailure hook (matcher `rate_limit\|server_error\|authentication_failed\|billing_error`) that writes `PHASE:failed` with an `api_error` reason to the phase file and touches the phase-changed marker, so the orchestrator discovers API errors within one poll cycle instead of waiting for idle timeout. Also installs a SessionStart hook (matcher `compact`) that re-injects phase protocol instructions after context compaction — callers write the context file via `write_compact_context(phase_file, content)`, and the hook (`on-compact-reinject.sh`) outputs the file content to stdout so Claude retains critical instructions. When `phase_file` is set, passes it to the idle stop hook (`on-idle-stop.sh`) so the hook can **nudge Claude** (up to 2 times) if Claude returns to the prompt without writing to the phase file — the hook injects a tmux reminder asking Claude to signal PHASE:done or PHASE:awaiting_ci. The PreToolUse guard hook (`on-pretooluse-guard.sh`) receives the session name as a third argument — formula agents (`gardener-*`, `planner-*`, `predictor-*`, `supervisor-*`) are identified this way and allowed to access `FACTORY_ROOT` from worktrees (they need env.sh, AGENTS.md, formulas/, lib/). **OAuth flock**: when `DISINTO_CONTAINER=1`, Claude CLI is wrapped in `flock -w 300 ~/.claude/session.lock` to queue concurrent token refresh attempts and prevent rotation races across agents sharing the same credentials. `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 `PHASE:escalate` / other `PHASE:*` string. **Unified escalation**: `PHASE:escalate` is the signal that a session needs human input (renamed from `PHASE:needs_human`). **Callers must handle `idle_prompt`** in both their callback and their post-loop exit handler — see [`docs/PHASE-PROTOCOL.md` idle_prompt](docs/PHASE-PROTOCOL.md#idle_prompt-exit-reason) for the full contract. | dev-agent.sh, action-agent.sh |
|
||||
|
|
|
|||
|
|
@ -290,32 +290,6 @@ 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"
|
||||
local model_flag=""
|
||||
if [ -n "${CLAUDE_MODEL:-}" ]; then
|
||||
|
|
|
|||
86
lib/env.sh
86
lib/env.sh
|
|
@ -85,18 +85,6 @@ export CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}"
|
|||
# Factory processes must never phone home or auto-update mid-session (#725).
|
||||
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
||||
|
||||
# Matrix homeserver: inside compose Dendrite is at http://dendrite:8008,
|
||||
# on bare metal it defaults to http://localhost:8008.
|
||||
if [ -z "${MATRIX_HOMESERVER:-}" ]; then
|
||||
if [ "${DISINTO_CONTAINER:-}" = "1" ]; then
|
||||
export MATRIX_HOMESERVER="http://dendrite:8008"
|
||||
else
|
||||
export MATRIX_HOMESERVER="http://localhost:8008"
|
||||
fi
|
||||
else
|
||||
export MATRIX_HOMESERVER
|
||||
fi
|
||||
|
||||
# Shared log helper
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*"
|
||||
|
|
@ -158,80 +146,6 @@ wpdb() {
|
|||
-t "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
# Matrix messaging helper — usage: matrix_send <prefix> <message> [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.
|
||||
# Thread map: use persistent data dir inside container, /tmp on bare metal
|
||||
if [ "${DISINTO_CONTAINER:-}" = "1" ]; then
|
||||
MATRIX_THREAD_MAP="${MATRIX_THREAD_MAP:-${DISINTO_DATA_DIR}/matrix-thread-map}"
|
||||
else
|
||||
MATRIX_THREAD_MAP="${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}"
|
||||
fi
|
||||
matrix_send() {
|
||||
[ -z "${MATRIX_TOKEN:-}" ] && return 0
|
||||
local prefix="$1" msg="$2" thread_id="${3:-}" ctx_tag="${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}] ${msg}" --arg t "$thread_id" \
|
||||
'{msgtype:"m.text",body:$m,"m.relates_to":{rel_type:"m.thread",event_id:$t}}')
|
||||
else
|
||||
body=$(jq -nc --arg m "[${prefix}] ${msg}" '{msgtype:"m.text",body:$m}')
|
||||
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"
|
||||
# Register thread root for listener dispatch (escalations only)
|
||||
if [ -z "$thread_id" ]; then
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "$event_id" "$prefix" "$(date +%s)" "${ctx_tag}" "${PROJECT_NAME:-}" >> "$MATRIX_THREAD_MAP" 2>/dev/null || true
|
||||
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
|
||||
}
|
||||
|
||||
# Source tea helpers (available when tea binary is installed)
|
||||
if command -v tea &>/dev/null; then
|
||||
# shellcheck source=tea-helpers.sh
|
||||
|
|
|
|||
|
|
@ -327,7 +327,6 @@ run_formula_and_monitor() {
|
|||
|
||||
agent_inject_into_session "$SESSION_NAME" "$PROMPT"
|
||||
log "Prompt sent to tmux session"
|
||||
matrix_send "$agent_name" "${agent_name^} session started for ${FORGE_REPO}" 2>/dev/null || true
|
||||
|
||||
log "Monitoring phase file: ${PHASE_FILE}"
|
||||
_FORMULA_CRASH_COUNT=0
|
||||
|
|
@ -351,8 +350,6 @@ run_formula_and_monitor() {
|
|||
esac
|
||||
fi
|
||||
|
||||
matrix_send "$agent_name" "${agent_name^} session finished (${FINAL_PHASE:-no phase})" 2>/dev/null || true
|
||||
|
||||
# Preserve worktree on crash for debugging; clean up on success
|
||||
if [ "${_MONITOR_LOOP_EXIT:-}" = "crashed" ]; then
|
||||
log "PRESERVED crashed worktree for debugging: ${_FORMULA_SESSION_WORKDIR:-}"
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
#!/usr/bin/env 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
|
||||
|
|
@ -58,17 +58,6 @@ svc = cfg.get('services', {})
|
|||
if 'containers' in svc:
|
||||
emit('PROJECT_CONTAINERS', svc['containers'])
|
||||
|
||||
# [matrix] section
|
||||
mx = cfg.get('matrix', {})
|
||||
if 'room_id' in mx:
|
||||
emit('MATRIX_ROOM_ID', mx['room_id'])
|
||||
if 'bot_user' in mx:
|
||||
emit('MATRIX_BOT_USER', mx['bot_user'])
|
||||
if 'token_env' in mx:
|
||||
emit('MATRIX_TOKEN_ENV', mx['token_env'])
|
||||
if 'mention_user' in mx:
|
||||
emit('MATRIX_MENTION_USER', mx['mention_user'])
|
||||
|
||||
# [monitoring] section
|
||||
mon = cfg.get('monitoring', {})
|
||||
for key in ['check_prs', 'check_dev_agent', 'check_pipeline_stall']:
|
||||
|
|
@ -110,10 +99,4 @@ if [ -z "${PROJECT_REPO_ROOT:-}" ] && [ -n "${PROJECT_NAME:-}" ]; then
|
|||
export PROJECT_REPO_ROOT="/home/${USER}/${PROJECT_NAME}"
|
||||
fi
|
||||
|
||||
# Resolve MATRIX_TOKEN from env var name (token_env points to an env var, not the token itself)
|
||||
if [ -n "${MATRIX_TOKEN_ENV:-}" ]; then
|
||||
export MATRIX_TOKEN="${!MATRIX_TOKEN_ENV:-}"
|
||||
unset MATRIX_TOKEN_ENV
|
||||
fi
|
||||
|
||||
unset _PROJECT_TOML _PROJECT_VARS _key _val
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
# Legacy systemd unit for bare-metal deployments (disinto init --bare).
|
||||
# In compose mode, the matrix listener runs inside the agent container
|
||||
# as a background process — see docker/agents/entrypoint.sh.
|
||||
[Unit]
|
||||
Description=Disinto Matrix Listener
|
||||
After=network.target dendrite.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/home/admin/disinto/lib/matrix_listener.sh
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
User=admin
|
||||
WorkingDirectory=/home/admin/disinto
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -1,350 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# matrix_listener.sh — Long-poll Matrix sync daemon
|
||||
#
|
||||
# Listens for replies in the Matrix coordination room and dispatches them
|
||||
# to the appropriate agent via well-known files.
|
||||
#
|
||||
# Dispatch:
|
||||
# Thread reply to [supervisor] message → /tmp/supervisor-escalation-reply
|
||||
# Thread reply to [gardener] message → /tmp/gardener-escalation-reply
|
||||
# Thread reply to [dev] message → injected into dev tmux session (or /tmp/dev-escalation-reply)
|
||||
# Thread reply to [review] message → injected into review tmux session
|
||||
# Thread reply to [vault] message → APPROVE/REJECT dispatched via vault-fire/vault-reject
|
||||
# Thread reply to [action] message → injected into action tmux session
|
||||
#
|
||||
# In compose mode, started by docker/agents/entrypoint.sh as a background process.
|
||||
# On bare metal, run as systemd service (see matrix_listener.service) or manually:
|
||||
# ./matrix_listener.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Load shared environment
|
||||
source "$(dirname "$0")/../lib/env.sh"
|
||||
|
||||
# Pidfile guard — prevent duplicate listener processes.
|
||||
# Inside a container the PID file from a previous run is stale (container
|
||||
# restart resets the PID namespace), so we only honour it when the recorded
|
||||
# PID is still alive.
|
||||
PIDFILE="/tmp/matrix-listener.pid"
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
OLD_PID=$(cat "$PIDFILE" 2>/dev/null || true)
|
||||
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
|
||||
echo "Listener already running (PID $OLD_PID)" >&2
|
||||
exit 0
|
||||
fi
|
||||
# Stale pidfile (previous container run or crashed process) — remove it
|
||||
rm -f "$PIDFILE"
|
||||
fi
|
||||
echo $$ > "$PIDFILE"
|
||||
trap 'rm -f "$PIDFILE"' EXIT
|
||||
|
||||
SINCE_FILE="/tmp/matrix-listener-since"
|
||||
THREAD_MAP="${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}"
|
||||
ACKED_FILE="/tmp/matrix-listener-acked"
|
||||
LOGFILE="${FACTORY_ROOT}/supervisor/matrix-listener.log"
|
||||
SYNC_TIMEOUT=30000 # 30s long-poll
|
||||
BACKOFF=5
|
||||
MAX_BACKOFF=60
|
||||
|
||||
log() {
|
||||
printf '[%s] listener: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE"
|
||||
}
|
||||
|
||||
# Validate Matrix config
|
||||
if [ -z "${MATRIX_TOKEN:-}" ] || [ -z "${MATRIX_ROOM_ID:-}" ]; then
|
||||
echo "MATRIX_TOKEN and MATRIX_ROOM_ID must be set in .env" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build sync filter — only our room, only messages
|
||||
FILTER=$(jq -nc --arg room "$MATRIX_ROOM_ID" '{
|
||||
room: {
|
||||
rooms: [$room],
|
||||
timeline: {types: ["m.room.message"], limit: 20},
|
||||
state: {types: []},
|
||||
ephemeral: {types: []}
|
||||
},
|
||||
presence: {types: []}
|
||||
}')
|
||||
|
||||
# Load previous sync token
|
||||
SINCE=""
|
||||
if [ -f "$SINCE_FILE" ]; then
|
||||
SINCE=$(cat "$SINCE_FILE" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
log "started (since=${SINCE:-initial})"
|
||||
|
||||
# Do an initial sync without timeout to catch up, then switch to long-poll
|
||||
INITIAL=true
|
||||
|
||||
while true; do
|
||||
# Build sync URL
|
||||
SYNC_URL="${MATRIX_HOMESERVER}/_matrix/client/v3/sync?filter=$(jq -rn --arg f "$FILTER" '$f | @uri')&timeout=${SYNC_TIMEOUT}"
|
||||
if [ -n "$SINCE" ]; then
|
||||
SYNC_URL="${SYNC_URL}&since=${SINCE}"
|
||||
fi
|
||||
if [ "$INITIAL" = true ]; then
|
||||
# First sync: no timeout, just catch up
|
||||
SYNC_URL="${MATRIX_HOMESERVER}/_matrix/client/v3/sync?filter=$(jq -rn --arg f "$FILTER" '$f | @uri')"
|
||||
[ -n "$SINCE" ] && SYNC_URL="${SYNC_URL}&since=${SINCE}"
|
||||
INITIAL=false
|
||||
fi
|
||||
|
||||
# Long-poll
|
||||
RESPONSE=$(curl -s --max-time $((SYNC_TIMEOUT / 1000 + 30)) \
|
||||
-H "Authorization: Bearer ${MATRIX_TOKEN}" \
|
||||
"$SYNC_URL" 2>/dev/null) || {
|
||||
log "sync failed, backing off ${BACKOFF}s"
|
||||
sleep "$BACKOFF"
|
||||
BACKOFF=$((BACKOFF * 2 > MAX_BACKOFF ? MAX_BACKOFF : BACKOFF * 2))
|
||||
continue
|
||||
}
|
||||
|
||||
# Reset backoff on success
|
||||
BACKOFF=5
|
||||
|
||||
# Extract next_batch
|
||||
NEXT_BATCH=$(printf '%s' "$RESPONSE" | jq -r '.next_batch // empty' 2>/dev/null)
|
||||
if [ -z "$NEXT_BATCH" ]; then
|
||||
log "no next_batch in response"
|
||||
sleep 5
|
||||
continue
|
||||
fi
|
||||
|
||||
# Save cursor
|
||||
printf '%s' "$NEXT_BATCH" > "$SINCE_FILE"
|
||||
SINCE="$NEXT_BATCH"
|
||||
|
||||
# Extract timeline events from our room
|
||||
EVENTS=$(printf '%s' "$RESPONSE" | jq -c --arg room "$MATRIX_ROOM_ID" '
|
||||
.rooms.join[$room].timeline.events[]? |
|
||||
select(.type == "m.room.message") |
|
||||
select(.sender != "'"${MATRIX_BOT_USER}"'")
|
||||
' 2>/dev/null) || continue
|
||||
|
||||
[ -z "$EVENTS" ] && continue
|
||||
|
||||
while IFS= read -r event; do
|
||||
SENDER=$(printf '%s' "$event" | jq -r '.sender')
|
||||
BODY=$(printf '%s' "$event" | jq -r '.content.body // ""')
|
||||
# Check if this is a thread reply
|
||||
THREAD_ROOT=$(printf '%s' "$event" | jq -r '.content."m.relates_to" | select(.rel_type == "m.thread") | .event_id // empty' 2>/dev/null)
|
||||
|
||||
if [ -z "$THREAD_ROOT" ] || [ -z "$BODY" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Look up thread root in our mapping
|
||||
if [ ! -f "$THREAD_MAP" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
AGENT=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $2}' "$THREAD_MAP" 2>/dev/null)
|
||||
|
||||
if [ -z "$AGENT" ]; then
|
||||
log "reply to unknown thread ${THREAD_ROOT:0:20} from ${SENDER}"
|
||||
continue
|
||||
fi
|
||||
|
||||
log "reply from ${SENDER} to [${AGENT}] thread: ${BODY:0:100}"
|
||||
|
||||
case "$AGENT" in
|
||||
supervisor)
|
||||
printf '%s\t%s\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$SENDER" "$BODY" >> /tmp/supervisor-escalation-reply
|
||||
# Acknowledge
|
||||
matrix_send "supervisor" "✓ received, will act on next poll" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
;;
|
||||
gardener)
|
||||
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
|
||||
;;
|
||||
dev)
|
||||
# Route reply into the dev tmux session using context_tag (issue number)
|
||||
# Thread map columns: 1=thread_id, 2=agent, 3=timestamp, 4=issue, 5=project
|
||||
DEV_ISSUE=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $4}' "$THREAD_MAP" 2>/dev/null || true)
|
||||
DEV_PROJECT=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $5}' "$THREAD_MAP" 2>/dev/null || true)
|
||||
DEV_INJECTED=false
|
||||
if [ -n "$DEV_ISSUE" ]; then
|
||||
DEV_SESSION="dev-${DEV_PROJECT}-${DEV_ISSUE}"
|
||||
DEV_PHASE_FILE="/tmp/dev-session-${DEV_PROJECT}-${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:escalate" ] || [ "$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}"
|
||||
# Reply on first successful injection only — no reply on subsequent ones
|
||||
if ! grep -qF "$THREAD_ROOT" "$ACKED_FILE" 2>/dev/null; then
|
||||
matrix_send "dev" "✓ Guidance forwarded to dev session for #${DEV_ISSUE}" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
printf '%s\n' "$THREAD_ROOT" >> "$ACKED_FILE"
|
||||
fi
|
||||
else
|
||||
log "WARN: dev session '${DEV_SESSION}' busy (phase: ${DEV_CUR_PHASE:-active}), queuing message for issue #${DEV_ISSUE}"
|
||||
matrix_send "dev" "❌ Could not inject: dev session for #${DEV_ISSUE} is busy (phase: ${DEV_CUR_PHASE:-active}), message queued" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
log "WARN: tmux session '${DEV_SESSION}' not found for issue #${DEV_ISSUE} (project: ${DEV_PROJECT:-UNSET})"
|
||||
matrix_send "dev" "❌ Could not inject: tmux session '${DEV_SESSION}' not found (project: ${DEV_PROJECT:-UNSET})" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
log "dev thread ${THREAD_ROOT:0:20} has no issue mapping"
|
||||
matrix_send "dev" "❌ Could not inject: no issue mapping for this thread" "$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)
|
||||
# Route human questions to persistent review tmux session
|
||||
# Thread map columns: 1=thread_id, 2=agent, 3=timestamp, 4=pr_num, 5=project
|
||||
REVIEW_PR_NUM=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $4}' "$THREAD_MAP" 2>/dev/null || true)
|
||||
REVIEW_PROJECT=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $5}' "$THREAD_MAP" 2>/dev/null || true)
|
||||
if [ -n "$REVIEW_PR_NUM" ]; then
|
||||
REVIEW_SESSION="review-${REVIEW_PROJECT}-${REVIEW_PR_NUM}"
|
||||
REVIEW_PHASE_FILE="/tmp/review-session-${REVIEW_PROJECT}-${REVIEW_PR_NUM}.phase"
|
||||
if tmux has-session -t "$REVIEW_SESSION" 2>/dev/null; then
|
||||
# Skip injection if Claude is mid-review (phase file absent = actively writing)
|
||||
REVIEW_CUR_PHASE=$(head -1 "$REVIEW_PHASE_FILE" 2>/dev/null | tr -d '[:space:]' || true)
|
||||
if [ -z "$REVIEW_CUR_PHASE" ]; then
|
||||
log "WARN: review session '${REVIEW_SESSION}' is mid-review, deferring question for PR #${REVIEW_PR_NUM}"
|
||||
matrix_send "review" "❌ Could not inject: reviewer is mid-review for PR #${REVIEW_PR_NUM}, try again shortly" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
else
|
||||
REVIEW_INJECT_MSG="Human question from ${SENDER} in Matrix:
|
||||
|
||||
${BODY}
|
||||
|
||||
Please answer this question about your review. Explain your reasoning."
|
||||
REVIEW_INJECT_TMP=$(mktemp /tmp/review-q-inject-XXXXXX)
|
||||
printf '%s' "$REVIEW_INJECT_MSG" > "$REVIEW_INJECT_TMP"
|
||||
tmux load-buffer -b "review-q-${REVIEW_PR_NUM}" "$REVIEW_INJECT_TMP" || true
|
||||
tmux paste-buffer -t "$REVIEW_SESSION" -b "review-q-${REVIEW_PR_NUM}" || true
|
||||
sleep 0.5
|
||||
tmux send-keys -t "$REVIEW_SESSION" "" Enter || true
|
||||
tmux delete-buffer -b "review-q-${REVIEW_PR_NUM}" 2>/dev/null || true
|
||||
rm -f "$REVIEW_INJECT_TMP"
|
||||
log "review question from ${SENDER} injected into ${REVIEW_SESSION}"
|
||||
# Reply on first successful injection only
|
||||
if ! grep -qF "$THREAD_ROOT" "$ACKED_FILE" 2>/dev/null; then
|
||||
matrix_send "review" "✓ Question forwarded to reviewer session for PR #${REVIEW_PR_NUM}" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
printf '%s\n' "$THREAD_ROOT" >> "$ACKED_FILE"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "WARN: tmux session '${REVIEW_SESSION}' not found for PR #${REVIEW_PR_NUM} (project: ${REVIEW_PROJECT:-UNSET})"
|
||||
matrix_send "review" "❌ Could not inject: tmux session '${REVIEW_SESSION}' not found (project: ${REVIEW_PROJECT:-UNSET})" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
log "review thread ${THREAD_ROOT:0:20} has no PR mapping"
|
||||
matrix_send "review" "❌ Could not inject: no PR mapping for this thread" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
;;
|
||||
action)
|
||||
# Route reply into the action tmux session using context_tag (issue number)
|
||||
ACTION_ISSUE=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $4}' "$THREAD_MAP" 2>/dev/null || true)
|
||||
ACTION_PROJECT=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $5}' "$THREAD_MAP" 2>/dev/null || true)
|
||||
if [ -n "$ACTION_ISSUE" ]; then
|
||||
ACTION_SESSION="action-${ACTION_PROJECT}-${ACTION_ISSUE}"
|
||||
if tmux has-session -t "$ACTION_SESSION" 2>/dev/null; then
|
||||
ACTION_INJECT_MSG="Human reply from ${SENDER} in Matrix:
|
||||
|
||||
${BODY}
|
||||
|
||||
Continue with the action formula based on this response."
|
||||
ACTION_INJECT_TMP=$(mktemp /tmp/action-q-inject-XXXXXX)
|
||||
printf '%s' "$ACTION_INJECT_MSG" > "$ACTION_INJECT_TMP"
|
||||
tmux load-buffer -b "action-q-${ACTION_ISSUE}" "$ACTION_INJECT_TMP" || true
|
||||
tmux paste-buffer -t "$ACTION_SESSION" -b "action-q-${ACTION_ISSUE}" || true
|
||||
sleep 0.5
|
||||
tmux send-keys -t "$ACTION_SESSION" "" Enter || true
|
||||
tmux delete-buffer -b "action-q-${ACTION_ISSUE}" 2>/dev/null || true
|
||||
rm -f "$ACTION_INJECT_TMP"
|
||||
log "human reply from ${SENDER} injected into ${ACTION_SESSION}"
|
||||
# Reply on first successful injection only
|
||||
if ! grep -qF "$THREAD_ROOT" "$ACKED_FILE" 2>/dev/null; then
|
||||
matrix_send "action" "✓ Reply forwarded to action session for issue #${ACTION_ISSUE}" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
printf '%s\n' "$THREAD_ROOT" >> "$ACKED_FILE"
|
||||
fi
|
||||
else
|
||||
log "WARN: tmux session '${ACTION_SESSION}' not found for issue #${ACTION_ISSUE}"
|
||||
matrix_send "action" "❌ Could not inject: tmux session '${ACTION_SESSION}' not found" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
log "action thread ${THREAD_ROOT:0:20} has no issue mapping"
|
||||
matrix_send "action" "❌ Could not inject: no issue mapping for this thread" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
;;
|
||||
vault)
|
||||
# Route reply to vault tmux session if one exists (unified escalation path)
|
||||
VAULT_ISSUE=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $4}' "$THREAD_MAP" 2>/dev/null || true)
|
||||
VAULT_PROJECT=$(awk -F'\t' -v id="$THREAD_ROOT" '$1 == id {print $5}' "$THREAD_MAP" 2>/dev/null || true)
|
||||
VAULT_INJECTED=false
|
||||
if [ -n "$VAULT_ISSUE" ]; then
|
||||
VAULT_SESSION="vault-${VAULT_PROJECT:-default}-${VAULT_ISSUE}"
|
||||
if tmux has-session -t "$VAULT_SESSION" 2>/dev/null; then
|
||||
VAULT_INJECT_MSG="Human reply from ${SENDER} in Matrix:
|
||||
|
||||
${BODY}
|
||||
|
||||
Interpret this response and decide how to proceed."
|
||||
VAULT_INJECT_TMP=$(mktemp /tmp/vault-q-inject-XXXXXX)
|
||||
printf '%s' "$VAULT_INJECT_MSG" > "$VAULT_INJECT_TMP"
|
||||
tmux load-buffer -b "vault-q-${VAULT_ISSUE}" "$VAULT_INJECT_TMP" || true
|
||||
tmux paste-buffer -t "$VAULT_SESSION" -b "vault-q-${VAULT_ISSUE}" || true
|
||||
sleep 0.5
|
||||
tmux send-keys -t "$VAULT_SESSION" "" Enter || true
|
||||
tmux delete-buffer -b "vault-q-${VAULT_ISSUE}" 2>/dev/null || true
|
||||
rm -f "$VAULT_INJECT_TMP"
|
||||
VAULT_INJECTED=true
|
||||
log "human reply from ${SENDER} injected into ${VAULT_SESSION}"
|
||||
if ! grep -qF "$THREAD_ROOT" "$ACKED_FILE" 2>/dev/null; then
|
||||
matrix_send "vault" "✓ Reply forwarded to vault session" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
printf '%s\n' "$THREAD_ROOT" >> "$ACKED_FILE"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# Fallback: parse APPROVE/REJECT for non-session vault actions
|
||||
if [ "$VAULT_INJECTED" = false ]; then
|
||||
VAULT_CMD=$(echo "$BODY" | tr '[:lower:]' '[:upper:]' | grep -oP '^\s*(APPROVE|REJECT)\s+\S+' | head -1 || true)
|
||||
if [ -n "$VAULT_CMD" ]; then
|
||||
VAULT_ACTION=$(echo "$VAULT_CMD" | awk '{print $1}')
|
||||
VAULT_ID=$(echo "$BODY" | awk '{print $2}') # preserve original case for ID
|
||||
log "vault dispatch: $VAULT_ACTION $VAULT_ID"
|
||||
VAULT_DIR="${FACTORY_ROOT}/vault"
|
||||
if [ "$VAULT_ACTION" = "APPROVE" ]; then
|
||||
if bash "${VAULT_DIR}/vault-fire.sh" "$VAULT_ID" >> "${VAULT_DIR}/vault.log" 2>&1; then
|
||||
matrix_send "vault" "✓ approved and fired: ${VAULT_ID}" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
else
|
||||
matrix_send "vault" "✓ approved but fire failed — will retry: ${VAULT_ID}" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
bash "${VAULT_DIR}/vault-reject.sh" "$VAULT_ID" "rejected by ${SENDER}" >> "${VAULT_DIR}/vault.log" 2>&1 || true
|
||||
matrix_send "vault" "✓ rejected: ${VAULT_ID}" "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
log "vault: free-text reply (no session, no APPROVE/REJECT): ${BODY:0:100}"
|
||||
matrix_send "vault" "⚠️ No active vault session. Reply with APPROVE <id> or REJECT <id>, or wait for a vault session to start." "$THREAD_ROOT" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
log "no handler for agent '${AGENT}'"
|
||||
;;
|
||||
esac
|
||||
|
||||
done <<< "$EVENTS"
|
||||
done
|
||||
Loading…
Add table
Add a link
Reference in a new issue