diff --git a/.env.example b/.env.example index 01d98ef..e559567 100644 --- a/.env.example +++ b/.env.example @@ -38,16 +38,6 @@ WOODPECKER_DB_USER=woodpecker # [CONFIG] Postgres user WOODPECKER_DB_HOST=127.0.0.1 # [CONFIG] Postgres host WOODPECKER_DB_NAME=woodpecker # [CONFIG] Postgres database name -# ── Matrix (optional — real-time notifications & escalation replies) ────── -# In compose mode, Dendrite runs inside the Docker network. `disinto init` -# provisions the bot user, room, and token automatically. -# Compose: MATRIX_HOMESERVER defaults to http://dendrite:8008 (set by env.sh) -# Bare metal: MATRIX_HOMESERVER defaults to http://localhost:8008 -MATRIX_HOMESERVER=http://dendrite:8008 # [CONFIG] Dendrite URL (compose default) -MATRIX_BOT_USER=@factory-bot:disinto.local # [CONFIG] bot's Matrix user ID -MATRIX_TOKEN= # [SECRET] bot's access token (auto-provisioned) -MATRIX_ROOM_ID= # [CONFIG] coordination room ID (auto-provisioned) - # ── Project-specific secrets ────────────────────────────────────────────── # Store all project secrets here so formulas reference env vars, never hardcode. BASE_RPC_URL= # [SECRET] on-chain RPC endpoint diff --git a/.woodpecker/agent-smoke.sh b/.woodpecker/agent-smoke.sh index c280006..0d2a016 100644 --- a/.woodpecker/agent-smoke.sh +++ b/.woodpecker/agent-smoke.sh @@ -105,7 +105,6 @@ echo "=== 2/2 Function resolution ===" # Excluded — not sourced inline by agents: # lib/tea-helpers.sh — sourced conditionally by env.sh (tea_file_issue, etc.); checked standalone below # lib/ci-debug.sh — standalone CLI tool, run directly (not sourced) -# lib/matrix_listener.sh — standalone systemd daemon (not sourced) # lib/parse-deps.sh — executed via `bash lib/parse-deps.sh` (not sourced) # lib/hooks/*.sh — Claude Code hook scripts, executed by the harness (not sourced) # @@ -189,7 +188,6 @@ check_script lib/guard.sh # Standalone lib scripts (not sourced by agents; run directly or as services). # Still checked for function resolution against LIB_FUNS + own definitions. check_script lib/ci-debug.sh -check_script lib/matrix_listener.sh check_script lib/parse-deps.sh # Agent scripts — list cross-sourced files where function scope flows across files. diff --git a/AGENTS.md b/AGENTS.md index f89e8cf..2b361cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ disinto/ │ supervisor-poll.sh — legacy bash orchestrator (superseded) ├── vault/ vault-poll.sh, vault-agent.sh, vault-fire.sh — action gating + procurement ├── action/ action-poll.sh, action-agent.sh — operational task execution -├── lib/ env.sh, agent-session.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, matrix_listener.sh, guard.sh, mirrors.sh, build-graph.py +├── lib/ env.sh, agent-session.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, guard.sh, mirrors.sh, build-graph.py ├── projects/ *.toml.example — templates; *.toml — local per-box config (gitignored) ├── formulas/ Issue templates (TOML specs for multi-step agent tasks) └── docs/ Protocol docs (PHASE-PROTOCOL.md, EVIDENCE-ARCHITECTURE.md) @@ -43,7 +43,7 @@ disinto/ - **AI**: `claude -p` (one-shot) or `claude` (interactive/tmux sessions) - **CI**: Woodpecker CI (queried via REST API + Postgres) - **VCS**: Forgejo (git + Gitea-compatible REST API) -- **Notifications**: Matrix (optional) +- **Notifications**: Forge activity (PR/issue comments), OpenClaw heartbeats ## Coding conventions diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index b5c33bf..80e7408 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -88,12 +88,6 @@ WOODPECKER_DB_USER=woodpecker WOODPECKER_DB_HOST=127.0.0.1 WOODPECKER_DB_NAME=woodpecker -# ── Optional: Matrix notifications ────────────────────────── -# MATRIX_HOMESERVER=http://localhost:8008 -# MATRIX_BOT_USER=@factory:your.server -# MATRIX_TOKEN= -# MATRIX_ROOM_ID= - # ── Tuning ────────────────────────────────────────────────── CLAUDE_TIMEOUT=7200 # seconds per Claude invocation ``` @@ -105,7 +99,7 @@ If you have an existing deployment using `CODEBERG_TOKEN` / `REVIEW_BOT_TOKEN` i ## 3. Configure Project TOML Each project needs a `projects/.toml` file with box-specific settings -(absolute paths, Woodpecker CI IDs, Matrix credentials, forge URL). These files are +(absolute paths, Woodpecker CI IDs, forge URL). These files are **gitignored** — they are local installation config, not shared code. To create one: @@ -395,38 +389,6 @@ tail -30 dev/dev-agent.log tail -30 review/review.log ``` -## 10. Optional: Matrix Notifications - -If you want real-time notifications and human-in-the-loop escalation: - -1. Set `MATRIX_*` vars in `.env` -2. Install the listener as a systemd service: - ```bash - sudo cp lib/matrix_listener.service /etc/systemd/system/ - sudo systemctl enable --now matrix_listener - ``` -3. The supervisor and gardener will post status updates and escalation threads to the configured room. Reply in-thread to answer escalations. - -### Per-project Matrix setup - -Each project can post to its own Matrix room. For each project: - -1. **Create a Matrix room** and note its room ID (e.g. `!abc123:matrix.example.org`) -2. **Create a bot user** (or reuse one) and join it to the room -3. **Add the token** to `.env` using a project-prefixed name: - ```bash - PROJECTNAME_MATRIX_TOKEN=syt_xxxxx - ``` -4. **Configure the TOML** with a `[matrix]` section: - ```toml - [matrix] - room_id = "!abc123:matrix.example.org" - bot_user = "@projectname-bot:matrix.example.org" - token_env = "PROJECTNAME_MATRIX_TOKEN" - ``` - -The `token_env` field points to the environment variable name, not the token value itself, so you can have multiple bots with separate credentials in a single `.env`. - ## Lifecycle Once running, the system operates autonomously: diff --git a/README.md b/README.md index 541adf5..2d0a798 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,6 @@ cron (weekly) ──→ planner-poll.sh ← gap-analyse VISION.md, create backl cron (*/30) ──→ vault-poll.sh ← safety gate for dangerous/irreversible actions └── claude -p: classify → auto-approve/reject or escalate -systemd ──→ matrix_listener.sh ← long-poll daemon for human replies - └── dispatches thread replies → supervisor/gardener/dev/review/vault - -all agents ──→ matrix_send() ← status updates, escalations, merge notifications ``` ## Prerequisites @@ -58,7 +54,6 @@ all agents ──→ matrix_send() ← status updates, escalations, merge no **Optional:** -- [Matrix](https://matrix.org/) homeserver ([Dendrite](https://github.com/matrix-org/dendrite) or Synapse) — real-time notifications, escalation threads with human-in-the-loop replies - [Foundry](https://getfoundry.sh/) (`forge`, `cast`, `anvil`) — only needed if your target project uses Solidity - [Node.js](https://nodejs.org/) — only needed if your target project uses Node @@ -115,10 +110,8 @@ disinto/ ├── .env.example # Template — copy to .env, add secrets + project config ├── .gitignore # Excludes .env, logs, state files ├── lib/ -│ ├── env.sh # Shared: load .env, PATH, API helpers, matrix_send() -│ ├── ci-debug.sh # Woodpecker CI log/failure helper -│ ├── matrix_listener.sh # Matrix long-poll daemon (dispatches replies) -│ └── matrix_listener.service # systemd unit for the listener +│ ├── env.sh # Shared: load .env, PATH, API helpers +│ └── ci-debug.sh # Woodpecker CI log/failure helper ├── dev/ │ ├── dev-poll.sh # Cron entry: find ready issues │ └── dev-agent.sh # Implementation agent (claude -p) @@ -160,7 +153,7 @@ disinto/ | **Review** | Every 10 min | Finds PRs without review, runs Claude-powered code review, approves or requests changes. | | **Gardener** | Daily | Grooms the issue backlog: detects duplicates, promotes `tech-debt` to `backlog`, closes stale issues, escalates ambiguous items. | | **Planner** | Weekly | Updates AGENTS.md documentation to reflect recent code changes, then gap-analyses VISION.md vs current state and creates up to 5 backlog issues for the highest-leverage gaps. | -| **Vault** | Every 30 min | Safety gate for dangerous or irreversible actions. Classifies pending actions via Claude: auto-approve, auto-reject, or escalate to a human via Matrix. | +| **Vault** | Every 30 min | Safety gate for dangerous or irreversible actions. Classifies pending actions via Claude: auto-approve, auto-reject, or escalate to a human via vault/forge. | ## Design Principles diff --git a/RESOURCES.md b/RESOURCES.md index 7cd5c23..35138f4 100644 --- a/RESOURCES.md +++ b/RESOURCES.md @@ -31,12 +31,6 @@ - domain: disinto.ai, www.disinto.ai - note: served by Caddy on harb-staging -## matrix-bot -- type: communication -- capability: post factory status, receive human replies, escalation channel -- env: MATRIX_TOKEN -- note: used by supervisor and dev-agent for notifications - ## telegram-clawy - type: communication - capability: notify human, collect decisions, relay vault requests diff --git a/action/AGENTS.md b/action/AGENTS.md index e8fb843..415e3fb 100644 --- a/action/AGENTS.md +++ b/action/AGENTS.md @@ -18,16 +18,14 @@ session, and spawns `action-agent.sh `. **Session lifecycle**: 1. `action-poll.sh` finds open `action` issues with no active tmux session. 2. Spawns `action-agent.sh `. -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-{project}-{issue_num}`, injects prompt (formula + prior comments + phase protocol). -5. Agent enters `monitor_phase_loop` (shared with dev-agent via `dev/phase-handler.sh`). -6. **Path A (git output):** Claude pushes branch → `PHASE:awaiting_ci` → handler creates PR, polls CI → injects failures → Claude fixes → push → re-poll → CI passes → `PHASE:awaiting_review` → handler polls reviews → injects REQUEST_CHANGES → Claude fixes → approved → merge → cleanup. -7. **Path B (no git output):** Claude posts results as comment, closes issue → `PHASE:done` → handler cleans up (kill session, docker compose down, remove temp files). -8. For human input: Claude sends a Matrix message and waits; the reply is injected into the session by `matrix_listener.sh`. +3. Agent creates tmux session `action-{project}-{issue_num}`, injects prompt (formula + prior comments + phase protocol). +4. Agent enters `monitor_phase_loop` (shared with dev-agent via `dev/phase-handler.sh`). +5. **Path A (git output):** Claude pushes branch → `PHASE:awaiting_ci` → handler creates PR, polls CI → injects failures → Claude fixes → push → re-poll → CI passes → `PHASE:awaiting_review` → handler polls reviews → injects REQUEST_CHANGES → Claude fixes → approved → merge → cleanup. +6. **Path B (no git output):** Claude posts results as comment, closes issue → `PHASE:done` → handler cleans up (kill session, docker compose down, remove temp files). +7. For human input: Claude writes `PHASE:escalate`; human responds via vault/forge. **Environment variables consumed**: - `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `FORGE_URL`, `PROJECT_NAME`, `FORGE_WEB` -- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Matrix notifications + human input - `ACTION_IDLE_TIMEOUT` — Max seconds before killing idle session (default 14400 = 4h) - `ACTION_MAX_LIFETIME` — Max total session wall-clock seconds (default 28800 = 8h); caps session independently of idle timeout diff --git a/action/action-agent.sh b/action/action-agent.sh index 87a3c5b..1afbfe1 100755 --- a/action/action-agent.sh +++ b/action/action-agent.sh @@ -12,7 +12,7 @@ # Path A (git output): Claude pushes → handler creates PR → CI poll → review # injection → merge → cleanup (same loop as dev-agent via phase-handler.sh) # Path B (no git output): Claude posts results → PHASE:done → cleanup -# 6. For human input: Claude asks via Matrix; reply injected via matrix_listener +# 6. For human input: Claude writes PHASE:escalate; human responds via vault/forge # 7. Cleanup on terminal phase: kill children, destroy worktree, remove temp files # # Key principle: The runtime creates and destroys. The formula preserves. @@ -35,7 +35,6 @@ source "$(dirname "$0")/../dev/phase-handler.sh" SESSION_NAME="action-${PROJECT_NAME}-${ISSUE}" LOCKFILE="/tmp/action-agent-${ISSUE}.lock" LOGFILE="${FACTORY_ROOT}/action/action-poll-${PROJECT_NAME:-default}.log" -THREAD_FILE="/tmp/action-thread-${ISSUE}" IDLE_TIMEOUT="${ACTION_IDLE_TIMEOUT:-14400}" # 4h default MAX_LIFETIME="${ACTION_MAX_LIFETIME:-28800}" # 8h default wall-clock cap SESSION_START_EPOCH=$(date +%s) @@ -55,22 +54,6 @@ log() { printf '[%s] action#%s %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$ISSUE" "$*" >> "$LOGFILE" } -notify() { - local thread_id="" - [ -f "${THREAD_FILE:-}" ] && thread_id=$(cat "$THREAD_FILE" 2>/dev/null || true) - matrix_send "action" "⚡ #${ISSUE}: $*" "${thread_id}" 2>/dev/null || true -} - -notify_ctx() { - local plain="$1" html="$2" thread_id="" - [ -f "${THREAD_FILE:-}" ] && thread_id=$(cat "$THREAD_FILE" 2>/dev/null || true) - if [ -n "$thread_id" ]; then - matrix_send_ctx "action" "⚡ #${ISSUE}: ${plain}" "⚡ #${ISSUE}: ${html}" "${thread_id}" 2>/dev/null || true - else - matrix_send "action" "⚡ #${ISSUE}: ${plain}" "" "${ISSUE}" 2>/dev/null || true - fi -} - status() { log "$*" } @@ -211,23 +194,6 @@ if [ -n "$COMMENTS_JSON" ] && [ "$COMMENTS_JSON" != "null" ] && [ "$COMMENTS_JSO "[\(.user.login) at \(.created_at[:19])]\n\(.body)\n---"' 2>/dev/null || true) fi -# --- Create Matrix thread for this issue --- -ISSUE_URL="${FORGE_WEB}/issues/${ISSUE}" -_thread_id=$(matrix_send_ctx "action" \ - "⚡ Action #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" \ - "⚡ Action #${ISSUE}: ${ISSUE_TITLE}") || true - -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\t%s\n' "$_thread_id" "action" "$(date +%s)" "${ISSUE}" "${PROJECT_NAME}" \ - >> "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true -fi - # --- Create isolated worktree --- log "creating worktree: ${WORKTREE}" cd "${PROJECT_REPO_ROOT}" @@ -241,7 +207,6 @@ export FORGE_REMOTE git fetch "${FORGE_REMOTE}" "${PRIMARY_BRANCH}" 2>/dev/null || true if ! git worktree add "$WORKTREE" "${FORGE_REMOTE}/${PRIMARY_BRANCH}" 2>&1; then log "ERROR: worktree creation failed" - notify "failed to create worktree for #${ISSUE}" exit 1 fi log "worktree ready: ${WORKTREE}" @@ -260,14 +225,6 @@ ${PRIOR_COMMENTS} " fi -THREAD_HINT="" -if [ -n "$THREAD_ID" ]; then - THREAD_HINT=" - The Matrix thread ID for this issue is: ${THREAD_ID} - Use it as the thread_event_id when sending Matrix messages so replies - are routed back to this session." -fi - # Build phase protocol from shared function (Path B covered in Instructions section above) PHASE_PROTOCOL_INSTRUCTIONS="$(build_phase_protocol_prompt "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$BRANCH")" @@ -294,9 +251,8 @@ ${PRIOR_SECTION}## Instructions \"${FORGE_API}/issues/${ISSUE}/comments\" \\ -d \"{\\\"body\\\": \\\"your comment here\\\"}\" -4. If a step requires human input or approval, send a Matrix message explaining - what you need, then wait. A human will reply and the reply will be injected - into this session automatically.${THREAD_HINT} +4. If a step requires human input or approval, write PHASE:escalate with a reason. + A human will review and respond via the forge. ### Path A: If this action produces code changes (e.g. config updates, baselines): - You are already in an isolated worktree at: ${WORKTREE} @@ -348,9 +304,6 @@ fi inject_formula "${SESSION_NAME}" "${INITIAL_PROMPT}" log "initial prompt injected into session" -matrix_send "action" "⚡ #${ISSUE}: session started — ${ISSUE_TITLE}" \ - "${THREAD_ID}" 2>/dev/null || true - # --- Wall-clock lifetime watchdog (background) --- # Caps total session time independently of idle timeout. When the cap is # hit the watchdog kills the tmux session, posts a summary comment on the @@ -383,31 +336,25 @@ monitor_phase_loop "$PHASE_FILE" "$IDLE_TIMEOUT" _on_phase_change "$SESSION_NAME # Handle exit reason from monitor_phase_loop case "${_MONITOR_LOOP_EXIT:-}" in idle_timeout) - notify_ctx \ - "session idle for $((IDLE_TIMEOUT / 3600))h — killed" \ - "session idle for $((IDLE_TIMEOUT / 3600))h — killed" # Post diagnostic comment + label blocked post_blocked_diagnostic "idle_timeout" - rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE" + rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" "$IMPL_SUMMARY_FILE" "$SCRATCH_FILE" ;; idle_prompt) # Notification + blocked label already handled by _on_phase_change(PHASE:failed) callback - rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE" + rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" "$IMPL_SUMMARY_FILE" "$SCRATCH_FILE" ;; PHASE:failed) # Check if this was a max_lifetime kill (phase file contains the reason) if grep -q 'max_lifetime' "$PHASE_FILE" 2>/dev/null; then - notify_ctx \ - "session killed — wall-clock cap ($((MAX_LIFETIME / 3600))h) reached" \ - "session killed — wall-clock cap ($((MAX_LIFETIME / 3600))h) reached" post_blocked_diagnostic "max_lifetime" fi - rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE" + rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" "$IMPL_SUMMARY_FILE" "$SCRATCH_FILE" ;; done) # Belt-and-suspenders: callback handles primary cleanup, # but ensure sentinel files are removed if callback was interrupted - rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE" + rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" "$IMPL_SUMMARY_FILE" "$SCRATCH_FILE" ;; esac diff --git a/action/action-poll.sh b/action/action-poll.sh index 5d7f3dc..100d854 100755 --- a/action/action-poll.sh +++ b/action/action-poll.sh @@ -25,12 +25,7 @@ log() { } # --- Memory guard --- -AVAIL_MB=$(awk '/MemAvailable/{printf "%d", $2/1024}' /proc/meminfo) -if [ "$AVAIL_MB" -lt 2000 ]; then - log "SKIP: only ${AVAIL_MB}MB available (need 2000MB)" - matrix_send "action" "⚠️ Low memory (${AVAIL_MB}MB) — skipping action-poll" 2>/dev/null || true - exit 0 -fi +memory_guard 2000 # --- Find open 'action' issues --- log "scanning for open action issues" diff --git a/bin/disinto b/bin/disinto index 8291f9d..4ec6293 100755 --- a/bin/disinto +++ b/bin/disinto @@ -156,7 +156,6 @@ generate_compose() { cat > "$compose_file" <<'COMPOSEEOF' # docker-compose.yml — generated by disinto init # Brings up Forgejo, Woodpecker, and the agent runtime. -# Dendrite (Matrix) is added only when init is called with --matrix. services: forgejo: @@ -272,70 +271,6 @@ COMPOSEEOF echo "Created: ${compose_file}" } -# Append Dendrite (Matrix) service to docker-compose.yml and generate config. -# Called only when --matrix flag is passed to init. -append_dendrite_compose() { - local compose_file="${FACTORY_ROOT}/docker-compose.yml" - local dendrite_config_dir="${FACTORY_ROOT}/docker/dendrite" - local dendrite_yaml="${dendrite_config_dir}/dendrite.yaml" - - mkdir -p "$dendrite_config_dir" - - # Generate a minimal dendrite.yaml - cat > "$dendrite_yaml" <<'DENDRITECFG' -# dendrite.yaml — generated by disinto init --matrix -version: 2 -global: - server_name: disinto.local - private_key: matrix_key.pem - database: - connection_string: file:dendrite.db - cache: - max_size_estimated: 512mb - jetstream: - storage_path: /etc/dendrite/jetstream -client_api: - registration_disabled: true -DENDRITECFG - echo "Created: ${dendrite_yaml}" - - # Append dendrite service before the volumes: section - python3 -c " -import sys, pathlib -p = pathlib.Path(sys.argv[1]) -text = p.read_text() -dendrite_service = ''' - dendrite: - image: matrixdotorg/dendrite-monolith:latest - restart: unless-stopped - volumes: - - dendrite-data:/etc/dendrite - - ./docker/dendrite/dendrite.yaml:/etc/dendrite/dendrite.yaml:ro - environment: - DENDRITE_DOMAIN: disinto.local - networks: - - disinto-net -''' -# Insert dendrite service before 'volumes:' line -text = text.replace('\nvolumes:\n', dendrite_service + '\nvolumes:\n', 1) -# Add dendrite-data volume -text = text.replace(' agent-data:', ' dendrite-data:\n agent-data:') -# Add MATRIX_HOMESERVER env var to agents service -text = text.replace( - ' DISINTO_CONTAINER: \"1\"', - ' MATRIX_HOMESERVER: http://dendrite:8008\n DISINTO_CONTAINER: \"1\"' -) -# Add dendrite dependency to agents service -text = text.replace( - ' - woodpecker\n networks:', - ' - woodpecker\n - dendrite\n networks:' -) -p.write_text(text) -" "$compose_file" - - echo "Updated: ${compose_file} (added Dendrite service)" -} - # Generate docker/agents/ files if they don't already exist. generate_agent_docker() { local docker_dir="${FACTORY_ROOT}/docker/agents" @@ -1108,151 +1043,6 @@ activate_woodpecker_repo() { fi } -# Provision Dendrite Matrix homeserver: create bot user, room, and access token. -# Stores MATRIX_TOKEN, MATRIX_ROOM_ID, MATRIX_BOT_USER in .env. -setup_matrix() { - local use_bare="${DISINTO_BARE:-false}" - local env_file="${FACTORY_ROOT}/.env" - - echo "" - echo "── Matrix setup ───────────────────────────────────────" - - # In compose mode, Dendrite runs inside the network at http://dendrite:8008. - # For provisioning from the host during init, we exec into the container. - local matrix_host="http://dendrite:8008" - - # Skip if MATRIX_TOKEN is already configured - if [ -n "${MATRIX_TOKEN:-}" ]; then - echo "Matrix: already configured (MATRIX_TOKEN set)" - return - fi - - if [ "$use_bare" = true ]; then - echo "Matrix: skipped in bare mode (configure manually or install Dendrite)" - echo " See: https://matrix-org.github.io/dendrite/" - return - fi - - # Wait for Dendrite to become healthy - echo -n "Waiting for Dendrite to start" - local retries=0 - while true; do - # Probe Dendrite via docker compose exec since it's not exposed on the host - local version_resp - version_resp=$(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \ - curl -sf --max-time 3 "http://localhost:8008/_matrix/client/versions" 2>/dev/null) || version_resp="" - if [ -n "$version_resp" ]; then - break - fi - retries=$((retries + 1)) - if [ "$retries" -gt 60 ]; then - echo "" - echo "Warning: Dendrite did not become ready within 60s — skipping Matrix setup" >&2 - echo " Run 'disinto init' again after Dendrite is healthy" >&2 - return - fi - echo -n "." - sleep 1 - done - echo " ready" - - # Create bot user via Dendrite's create-account tool - local bot_localpart="factory-bot" - local bot_user="@${bot_localpart}:disinto.local" - local bot_pass - bot_pass="matrix-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 24)" - - echo "Creating Matrix bot user: ${bot_user}" - docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \ - /usr/bin/create-account \ - -config /etc/dendrite/dendrite.yaml \ - -username "${bot_localpart}" \ - -password "${bot_pass}" 2>/dev/null || true - - # Log in to get an access token - local login_resp - login_resp=$(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \ - curl -sf -X POST "http://localhost:8008/_matrix/client/v3/login" \ - -H "Content-Type: application/json" \ - -d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"${bot_localpart}\"},\"password\":\"${bot_pass}\"}" \ - 2>/dev/null) || login_resp="" - - local access_token - access_token=$(printf '%s' "$login_resp" | jq -r '.access_token // empty' 2>/dev/null) || access_token="" - - if [ -z "$access_token" ]; then - echo "Warning: failed to obtain Matrix access token — skipping Matrix setup" >&2 - echo " Create the bot user manually via Dendrite admin API" >&2 - return - fi - - echo " Bot login successful" - - # Create coordination room - local room_resp - room_resp=$(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \ - curl -sf -X POST "http://localhost:8008/_matrix/client/v3/createRoom" \ - -H "Authorization: Bearer ${access_token}" \ - -H "Content-Type: application/json" \ - -d '{"room_alias_name":"factory","name":"disinto factory","topic":"Autonomous code factory coordination room","preset":"private_chat"}' \ - 2>/dev/null) || room_resp="" - - local room_id - room_id=$(printf '%s' "$room_resp" | jq -r '.room_id // empty' 2>/dev/null) || room_id="" - - if [ -z "$room_id" ]; then - # Room might already exist — try resolving the alias - local alias_resp - alias_resp=$(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \ - curl -sf "http://localhost:8008/_matrix/client/v3/directory/room/%23factory%3Adisinto.local" \ - -H "Authorization: Bearer ${access_token}" \ - 2>/dev/null) || alias_resp="" - room_id=$(printf '%s' "$alias_resp" | jq -r '.room_id // empty' 2>/dev/null) || room_id="" - fi - - if [ -z "$room_id" ]; then - echo "Warning: failed to create or find coordination room — skipping Matrix setup" >&2 - return - fi - - echo " Room: ${room_id} (alias: #factory:disinto.local)" - - # Store Matrix credentials in .env - local matrix_vars=( - "MATRIX_HOMESERVER=${matrix_host}" - "MATRIX_BOT_USER=${bot_user}" - "MATRIX_TOKEN=${access_token}" - "MATRIX_ROOM_ID=${room_id}" - ) - - for var_line in "${matrix_vars[@]}"; do - local var_name="${var_line%%=*}" - if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then - # Use Python to avoid sed delimiter collisions with opaque tokens - python3 -c " -import sys, re, pathlib -p = pathlib.Path(sys.argv[1]) -text = p.read_text() -text = re.sub(r'^' + re.escape(sys.argv[2]) + r'=.*$', sys.argv[3], text, flags=re.MULTILINE) -p.write_text(text) -" "$env_file" "$var_name" "$var_line" - else - printf '%s\n' "$var_line" >> "$env_file" - fi - done - - export MATRIX_TOKEN="$access_token" - export MATRIX_BOT_USER="$bot_user" - export MATRIX_ROOM_ID="$room_id" - export MATRIX_HOMESERVER="$matrix_host" - - echo " Credentials saved to .env" - echo "" - echo " To receive notifications in your Matrix client:" - echo " 1. Add 'ports: [\"8008:8008\"]' to the dendrite service in docker-compose.yml" - echo " 2. Join #factory:disinto.local from Element or another Matrix client" -} - # ── init command ───────────────────────────────────────────────────────────── disinto_init() { @@ -1265,7 +1055,7 @@ disinto_init() { shift # Parse flags - local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" bare=false enable_matrix=false + local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" bare=false while [ $# -gt 0 ]; do case "$1" in --branch) branch="$2"; shift 2 ;; @@ -1273,7 +1063,6 @@ disinto_init() { --ci-id) ci_id="$2"; shift 2 ;; --forge-url) forge_url_flag="$2"; shift 2 ;; --bare) bare=true; shift ;; - --matrix) enable_matrix=true; shift ;; --yes) auto_yes=true; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac @@ -1357,9 +1146,6 @@ p.write_text(text) forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|') forge_port="${forge_port:-3000}" generate_compose "$forge_port" - if [ "$enable_matrix" = true ]; then - append_dendrite_compose - fi generate_agent_docker fi @@ -1457,13 +1243,7 @@ p.write_text(text) echo "" echo "── Starting full stack ────────────────────────────────" docker compose -f "${FACTORY_ROOT}/docker-compose.yml" up -d - if [ "$enable_matrix" = true ]; then - echo "Stack: running (forgejo + woodpecker + dendrite + agents)" - # Provision Matrix now that Dendrite is running - setup_matrix - else - echo "Stack: running (forgejo + woodpecker + agents)" - fi + echo "Stack: running (forgejo + woodpecker + agents)" # Activate repo in Woodpecker now that stack is running activate_woodpecker_repo "$forge_repo" diff --git a/dev/AGENTS.md b/dev/AGENTS.md index cc44b34..eb297ee 100644 --- a/dev/AGENTS.md +++ b/dev/AGENTS.md @@ -16,7 +16,7 @@ check so approved PRs get merged even while a dev-agent session is active. **Key files**: - `dev/dev-poll.sh` — Cron scheduler: finds next ready issue, handles merge/rebase of approved PRs, tracks CI fix attempts. Formula guard skips issues labeled `formula`, `action`, `prediction/dismissed`, or `prediction/unreviewed` (replaced `prediction/backlog` — that label no longer exists) - `dev/dev-agent.sh` — Orchestrator: claims issue, creates worktree + tmux session with interactive `claude`, monitors phase file, injects CI results and review feedback, merges on approval -- `dev/phase-handler.sh` — Phase callback functions: `post_refusal_comment()`, `_on_phase_change()`, `build_phase_protocol_prompt()`. `do_merge()` detects already-merged PRs on HTTP 405 (race with dev-poll's pre-lock scan) and returns success instead of escalating. Sources `lib/mirrors.sh` and calls `mirror_push()` after every successful merge. Matrix escalation notifications include `MATRIX_MENTION_USER` HTML mention when set. +- `dev/phase-handler.sh` — Phase callback functions: `post_refusal_comment()`, `_on_phase_change()`, `build_phase_protocol_prompt()`. `do_merge()` detects already-merged PRs on HTTP 405 (race with dev-poll's pre-lock scan) and returns success instead of escalating. Sources `lib/mirrors.sh` and calls `mirror_push()` after every successful merge. - `dev/phase-test.sh` — Integration test for the phase protocol **Environment variables consumed** (via `lib/env.sh` + project TOML): @@ -26,12 +26,10 @@ check so approved PRs get merged even while a dev-agent session is active. - `PRIMARY_BRANCH` — Branch to merge into (e.g. `main`, `master`) - `WOODPECKER_REPO_ID` — CI pipeline lookups - `CLAUDE_TIMEOUT` — Max seconds for a Claude session (default 7200) -- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Notifications (optional) **FORGE_REMOTE**: `dev-agent.sh` auto-detects which git remote corresponds to `FORGE_URL` by matching the remote's push URL hostname. This is exported as `FORGE_REMOTE` and used for all git push/pull/worktree operations. Defaults to `origin` if no match found. This ensures correct behaviour when the forge is local Forgejo (remote typically named `forgejo`) rather than Codeberg (`origin`). -**Lifecycle**: dev-poll.sh (`check_active dev`) → dev-agent.sh → create Matrix -thread + export `MATRIX_THREAD_ID` → tmux `dev-{project}-{issue}` → phase file +**Lifecycle**: dev-poll.sh (`check_active dev`) → dev-agent.sh → tmux `dev-{project}-{issue}` → phase file drives CI/review loop → merge + `mirror_push()` → close issue. On respawn after `PHASE:escalate`, the stale phase file is cleared first so the session starts clean; the reinject prompt tells Claude not to re-escalate for the same reason. diff --git a/dev/dev-agent.sh b/dev/dev-agent.sh index b90c2a4..035f7a0 100755 --- a/dev/dev-agent.sh +++ b/dev/dev-agent.sh @@ -56,23 +56,6 @@ log() { printf '[%s] #%s %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$ISSUE" "$*" >> "$LOGFILE" } -notify() { - 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() { - 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 - matrix_send "dev" "🔧 #${ISSUE}: ${plain}" "" "${ISSUE}" 2>/dev/null || true - fi -} - status() { printf '[%s] dev-agent #%s: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$ISSUE" "$*" > "$STATUSFILE" log "$*" @@ -87,9 +70,6 @@ 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}" - # Scratch file for context compaction survival SCRATCH_FILE="/tmp/dev-${PROJECT_NAME}-${ISSUE}-scratch.md" @@ -246,7 +226,6 @@ log "Issue: ${ISSUE_TITLE}" ISSUE_LABELS=$(echo "$ISSUE_JSON" | jq -r '[.labels[].name] | join(",")') || true if echo "$ISSUE_LABELS" | grep -qw 'formula'; then log "SKIP: issue #${ISSUE} has 'formula' label but formula dispatch is not yet implemented (feat/formula branch not merged)" - notify "issue #${ISSUE} skipped — formula label requires feat/formula branch (not yet merged to main)" echo '{"status":"unmet_dependency","blocked_by":"formula dispatch not implemented — feat/formula branch not merged to main","suggestion":null}' > "$PREFLIGHT_RESULT" exit 0 fi @@ -362,7 +341,6 @@ This issue depends on ${BLOCKED_LIST}, which $(if [ "${#BLOCKED_BY[@]}" -eq 1 ]; fi log "BLOCKED: unmet dependencies: ${BLOCKED_BY[*]}$(if [ -n "$SUGGESTION" ]; then echo ", suggest #${SUGGESTION}"; fi)" - notify "blocked by unmet dependencies: ${BLOCKED_BY[*]}" exit 0 fi @@ -596,7 +574,7 @@ phase handler. You do not need to merge or close anything — stop and wait. \`\`\`bash printf 'PHASE:escalate\nReason: %s\n' \"describe what you need\" > \"${PHASE_FILE}\" \`\`\` -Then STOP and wait. A human will reply via Matrix and the response will be injected. +Then STOP and wait. A human will review and respond via the forge. **If refusing (too large, unmet dep, already done):** \`\`\`bash @@ -726,27 +704,6 @@ ${SCRATCH_INSTRUCTION} ${PHASE_PROTOCOL_INSTRUCTIONS}" fi -# ============================================================================= -# CREATE MATRIX THREAD (before tmux so MATRIX_THREAD_ID is available for Stop hook) -# ============================================================================= -if [ ! -f "${THREAD_FILE}" ] || [ -z "$(cat "$THREAD_FILE" 2>/dev/null)" ]; then - ISSUE_URL="${FORGE_WEB}/issues/${ISSUE}" - _thread_id=$(matrix_send_ctx "dev" \ - "🔧 Issue #${ISSUE}: ${ISSUE_TITLE} — ${ISSUE_URL}" \ - "🔧 Issue #${ISSUE}: ${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\t%s\n' "$_thread_id" "dev" "$(date +%s)" "${ISSUE}" "${PROJECT_NAME}" >> "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true - fi -fi - -# Export for on-stop-matrix.sh hook (streams Claude output to thread) -_thread_id=$(cat "$THREAD_FILE" 2>/dev/null || true) -if [ -n "${_thread_id:-}" ]; then - export MATRIX_THREAD_ID="$_thread_id" -fi - # ============================================================================= # CREATE TMUX SESSION # ============================================================================= @@ -765,8 +722,6 @@ log "initial prompt sent to tmux session" # Signal to dev-poll.sh that we're running (session is up) echo '{"status":"ready"}' > "$PREFLIGHT_RESULT" -notify "tmux session ${SESSION_NAME} started for issue #${ISSUE}: ${ISSUE_TITLE}" - status "monitoring phase: ${PHASE_FILE}" monitor_phase_loop "$PHASE_FILE" "$IDLE_TIMEOUT" _on_phase_change @@ -774,15 +729,6 @@ monitor_phase_loop "$PHASE_FILE" "$IDLE_TIMEOUT" _on_phase_change # Handle exit reason from monitor_phase_loop case "${_MONITOR_LOOP_EXIT:-}" in idle_timeout|idle_prompt) - if [ "${_MONITOR_LOOP_EXIT:-}" = "idle_prompt" ]; then - notify_ctx \ - "session finished without phase signal — killed. Marking blocked." \ - "session finished without phase signal — killed. Marking blocked.${PR_NUMBER:+ PR #${PR_NUMBER}}" - else - notify_ctx \ - "session idle for 2h — killed. Marking blocked." \ - "session idle for 2h — killed. Marking blocked.${PR_NUMBER:+ PR #${PR_NUMBER}}" - fi # Post diagnostic comment + label issue blocked post_blocked_diagnostic "${_MONITOR_LOOP_EXIT:-idle_timeout}" if [ -n "${PR_NUMBER:-}" ]; then @@ -791,7 +737,7 @@ case "${_MONITOR_LOOP_EXIT:-}" in cleanup_worktree fi rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" \ - "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE" \ + "$IMPL_SUMMARY_FILE" "$SCRATCH_FILE" \ "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" [ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}" ;; @@ -807,7 +753,7 @@ case "${_MONITOR_LOOP_EXIT:-}" in # Belt-and-suspenders: callback in phase-handler.sh handles primary cleanup, # but ensure sentinel files are removed if callback was interrupted rm -f "$PHASE_FILE" "${PHASE_FILE%.phase}.context" \ - "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "$SCRATCH_FILE" \ + "$IMPL_SUMMARY_FILE" "$SCRATCH_FILE" \ "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" [ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}" CLAIMED=false diff --git a/dev/dev-poll.sh b/dev/dev-poll.sh index e1aa644..1f81287 100755 --- a/dev/dev-poll.sh +++ b/dev/dev-poll.sh @@ -156,7 +156,6 @@ handle_ci_exhaustion() { CI_FIX_ATTEMPTS="${result#exhausted_first_time:}" log "PR #${pr_num} (issue #${issue_num}) CI exhausted (${CI_FIX_ATTEMPTS} attempts) — marking blocked" _post_ci_blocked_comment "$issue_num" "$pr_num" "$CI_FIX_ATTEMPTS" - matrix_send "dev" "🚨 PR #${pr_num} (issue #${issue_num}) CI failed after ${CI_FIX_ATTEMPTS} attempts — marked blocked" 2>/dev/null || true ;; exhausted:*) CI_FIX_ATTEMPTS="${result#exhausted:}" @@ -207,9 +206,6 @@ try_direct_merge() { # Clean up phase/session artifacts rm -f "/tmp/dev-session-${PROJECT_NAME}-${issue_num}.phase" \ "/tmp/dev-impl-summary-${PROJECT_NAME}-${issue_num}.txt" - matrix_send "dev" "✅ PR #${pr_num} (issue #${issue_num}) merged directly by dev-poll" 2>/dev/null || true - else - matrix_send "dev" "✅ PR #${pr_num} merged directly by dev-poll (chore)" 2>/dev/null || true fi # Pull merged primary branch and push to mirrors git -C "${PROJECT_REPO_ROOT:-}" fetch origin "${PRIMARY_BRANCH:-}" 2>/dev/null || true @@ -313,12 +309,7 @@ if [ -f "$LOCKFILE" ]; then fi # --- Memory guard --- -AVAIL_MB=$(awk '/MemAvailable/{printf "%d", $2/1024}' /proc/meminfo) -if [ "$AVAIL_MB" -lt 2000 ]; then - log "SKIP: only ${AVAIL_MB}MB available (need 2000MB)" - matrix_send "dev" "⚠️ Low memory (${AVAIL_MB}MB) — skipping dev-agent" 2>/dev/null || true - exit 0 -fi +memory_guard 2000 # ============================================================================= # HELPER: check if a dependency issue is fully resolved (closed + PR merged) @@ -739,7 +730,6 @@ if [ -n "${READY_PR_FOR_INCREMENT:-}" ]; then fi log "launching dev-agent for #${READY_ISSUE}" -matrix_send "dev" "🚀 Starting dev-agent on issue #${READY_ISSUE}" 2>/dev/null || true rm -f "$PREFLIGHT_RESULT" nohup "${SCRIPT_DIR}/dev-agent.sh" "$READY_ISSUE" >> "$LOGFILE" 2>&1 & diff --git a/dev/phase-handler.sh b/dev/phase-handler.sh index e1b06d6..5b6833e 100644 --- a/dev/phase-handler.sh +++ b/dev/phase-handler.sh @@ -6,7 +6,7 @@ # # Required globals (set by calling agent before or after sourcing): # ISSUE, FORGE_TOKEN, API, FORGE_WEB, PROJECT_NAME, FACTORY_ROOT -# BRANCH, PHASE_FILE, WORKTREE, IMPL_SUMMARY_FILE, THREAD_FILE +# BRANCH, PHASE_FILE, WORKTREE, IMPL_SUMMARY_FILE # PRIMARY_BRANCH, SESSION_NAME, LOGFILE, ISSUE_TITLE # WOODPECKER_REPO_ID, WOODPECKER_TOKEN, WOODPECKER_SERVER # @@ -16,7 +16,7 @@ # CLAIMED, PHASE_POLL_INTERVAL # # Calls back to agent-defined helpers: -# cleanup_worktree(), cleanup_labels(), notify(), notify_ctx(), status(), log() +# cleanup_worktree(), cleanup_labels(), status(), log() # # shellcheck shell=bash # shellcheck disable=SC2154 # globals are set in dev-agent.sh before calling @@ -167,7 +167,7 @@ echo "PHASE:awaiting_ci" > "${_pf}" \`\`\`bash printf 'PHASE:escalate\nReason: %s\n' "describe what you need" > "${_pf}" \`\`\` -Then STOP and wait. A human will reply via Matrix and the response will be injected. +Then STOP and wait. A human will review and respond via the forge. **On unrecoverable failure:** \`\`\`bash @@ -296,10 +296,6 @@ _on_phase_change() { if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number') log "created PR #${PR_NUMBER}" - PR_URL="${FORGE_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 ${FORGE_TOKEN}" \ @@ -316,7 +312,6 @@ _on_phase_change() { fi else log "ERROR: PR creation failed (HTTP ${PR_HTTP_CODE})" - notify "failed to create PR (HTTP ${PR_HTTP_CODE})" agent_inject_into_session "$SESSION_NAME" "ERROR: Could not create PR (HTTP ${PR_HTTP_CODE}). Check branch was pushed: git push ${FORGE_REMOTE:-origin} ${BRANCH}. Then write PHASE:awaiting_ci again." return 0 fi @@ -362,7 +357,6 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee if ! $CI_DONE; then log "TIMEOUT: CI didn't complete in ${CI_POLL_TIMEOUT}s" - notify "CI timeout on PR #${PR_NUMBER}" agent_inject_into_session "$SESSION_NAME" "CI TIMEOUT: CI did not complete within 30 minutes for PR #${PR_NUMBER} (SHA: ${CI_CURRENT_SHA:0:7}). This may be an infrastructure issue. Write PHASE:escalate if you cannot proceed." return 0 fi @@ -412,11 +406,6 @@ Write PHASE:awaiting_review to the phase file, then stop and wait for review fee _ci_pipeline_url="${WOODPECKER_SERVER}/repos/${WOODPECKER_REPO_ID}/pipeline/${PIPELINE_NUM:-0}" if [ "$CI_FIX_COUNT" -gt "$MAX_CI_FIXES" ]; then log "CI failure not recoverable after ${CI_FIX_COUNT} fix attempts — escalating" - local _mention_html="" - [ -n "${MATRIX_MENTION_USER:-}" ] && _mention_html="${MATRIX_MENTION_USER} " - notify_ctx \ - "CI exhausted after ${CI_FIX_COUNT} attempts — escalating for human help" \ - "${_mention_html}CI exhausted after ${CI_FIX_COUNT} attempts on PR #${PR_NUMBER} | Pipeline
Step: ${FAILED_STEP:-unknown} — escalating for human help" printf 'PHASE:escalate\nReason: ci_exhausted after %d attempts (step: %s)\n' "$CI_FIX_COUNT" "${FAILED_STEP:-unknown}" > "$PHASE_FILE" # Do NOT update LAST_PHASE_MTIME here — let the main loop detect PHASE:escalate return 0 @@ -432,12 +421,6 @@ 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_snippet=$(printf '%s' "${CI_ERROR_LOG:-}" | tail -5 | head -c 500 | sed '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 #${PR_NUMBER} | Pipeline #${PIPELINE_NUM:-?}
Step: ${FAILED_STEP:-unknown} (exit ${FAILED_EXIT:-?})
Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}
${_ci_snippet:-no logs}
" - agent_inject_into_session "$SESSION_NAME" "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:-?}) @@ -584,7 +567,7 @@ If rebase repeatedly fails, write PHASE:escalate with a reason." REVIEW_ROUND=$(( REVIEW_ROUND + 1 )) if [ "$REVIEW_ROUND" -ge "$MAX_REVIEW_ROUNDS" ]; then log "hit max review rounds (${MAX_REVIEW_ROUNDS})" - notify "PR #${PR_NUMBER}: hit ${MAX_REVIEW_ROUNDS} review rounds, needs human attention" + log "PR #${PR_NUMBER}: hit ${MAX_REVIEW_ROUNDS} review rounds, needs human attention" fi REVIEW_FOUND=true agent_inject_into_session "$SESSION_NAME" "Review feedback (round ${REVIEW_ROUND}) on PR #${PR_NUMBER}: @@ -615,20 +598,16 @@ Instructions: if [ "$PR_STATE" != "open" ]; then if [ "$PR_MERGED" = "true" ]; then log "PR #${PR_NUMBER} was merged externally" - 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 ${FORGE_TOKEN}" \ -H "Content-Type: application/json" \ "${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true cleanup_labels agent_kill_session "$SESSION_NAME" cleanup_worktree - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "${SCRATCH_FILE:-}" + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "${SCRATCH_FILE:-}" exit 0 else log "PR #${PR_NUMBER} was closed WITHOUT merge — NOT closing issue" - notify "⚠️ PR #${PR_NUMBER} closed without merge. Issue #${ISSUE} remains open." cleanup_labels agent_kill_session "$SESSION_NAME" cleanup_worktree @@ -641,7 +620,6 @@ Instructions: if ! $REVIEW_FOUND && [ "$REVIEW_POLL_ELAPSED" -ge "$REVIEW_POLL_TIMEOUT" ]; then log "TIMEOUT: no review after 3h" - notify "no review received for PR #${PR_NUMBER} after 3h" agent_inject_into_session "$SESSION_NAME" "TIMEOUT: No review received after 3 hours for PR #${PR_NUMBER}. Write PHASE:escalate to escalate to a human reviewer." fi @@ -649,30 +627,16 @@ Instructions: elif [ "$phase" = "PHASE:escalate" ]; then status "escalated — waiting for human input on issue #${ISSUE}" ESCALATE_REASON=$(sed -n '2p' "$PHASE_FILE" 2>/dev/null | sed 's/^Reason: //' || echo "") - _issue_url="${FORGE_WEB}/issues/${ISSUE}" - _pr_link="" - [ -n "${PR_NUMBER:-}" ] && _pr_link=" | PR #${PR_NUMBER}" - local _mention_html="" - [ -n "${MATRIX_MENTION_USER:-}" ] && _mention_html="${MATRIX_MENTION_USER} " - notify_ctx \ - "⚠️ Issue #${ISSUE} (PR #${PR_NUMBER:-none}) escalated — needs human input.${ESCALATE_REASON:+ Reason: ${ESCALATE_REASON}}" \ - "${_mention_html}⚠️ Issue #${ISSUE}${_pr_link} escalated — needs human input.${ESCALATE_REASON:+ Reason: ${ESCALATE_REASON}}
Reply in this thread to send guidance to the agent." - log "phase: escalate — notified via Matrix, session stays alive waiting for reply" - # Session stays alive — matrix_listener injects human reply directly + log "phase: escalate — reason: ${ESCALATE_REASON:-none}" + # Session stays alive — human input arrives via vault/forge # ── PHASE: done ───────────────────────────────────────────────────────────── # PR merged and issue closed (by orchestrator or Claude). Just clean up local state. elif [ "$phase" = "PHASE:done" ]; then if [ -n "${PR_NUMBER:-}" ]; then status "phase done — PR #${PR_NUMBER} merged, cleaning up" - notify_ctx \ - "✅ PR #${PR_NUMBER} merged! Issue #${ISSUE} done." \ - "✅ PR #${PR_NUMBER} merged! Issue #${ISSUE} done." else status "phase done — issue #${ISSUE} complete, cleaning up" - notify_ctx \ - "✅ Issue #${ISSUE} done." \ - "✅ Issue #${ISSUE} done." fi # Belt-and-suspenders: ensure in-progress label removed (idempotent) @@ -681,7 +645,7 @@ Instructions: # Local cleanup agent_kill_session "$SESSION_NAME" cleanup_worktree - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "${SCRATCH_FILE:-}" \ + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "${SCRATCH_FILE:-}" \ "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" [ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}" CLAIMED=false # Don't unclaim again in cleanup() @@ -735,7 +699,6 @@ ${BLOCKED_BY_MSG}" **Suggestion:** Work on #${SUGGESTION} first." fi post_refusal_comment "🚧" "Unmet dependency" "$COMMENT_BODY" - notify "refused #${ISSUE}: unmet dependency — ${BLOCKED_BY_MSG}" ;; too_large) REASON=$(printf '%s' "$REFUSAL_JSON" | jq -r '.reason // "unspecified"') @@ -753,7 +716,6 @@ A maintainer should split this issue or add more detail to the spec." curl -sf -X DELETE \ -H "Authorization: token ${FORGE_TOKEN}" \ "${API}/issues/${ISSUE}/labels/${BACKLOG_LABEL_ID}" >/dev/null 2>&1 || true - notify "refused #${ISSUE}: too large — ${REASON}" ;; already_done) REASON=$(printf '%s' "$REFUSAL_JSON" | jq -r '.reason // "unspecified"') @@ -767,7 +729,6 @@ Closing as already implemented." -H "Content-Type: application/json" \ "${API}/issues/${ISSUE}" \ -d '{"state":"closed"}' >/dev/null 2>&1 || true - notify "refused #${ISSUE}: already done — ${REASON}" ;; *) post_refusal_comment "❓" "Unable to proceed" "The dev-agent could not process this issue. @@ -776,14 +737,13 @@ Raw response: \`\`\`json $(printf '%s' "$REFUSAL_JSON" | head -c 2000) \`\`\`" - notify "refused #${ISSUE}: unknown reason" ;; esac CLAIMED=false # Don't unclaim again in cleanup() agent_kill_session "$SESSION_NAME" cleanup_worktree - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "${SCRATCH_FILE:-}" \ + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "${SCRATCH_FILE:-}" \ "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" [ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}" return 1 @@ -791,9 +751,6 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000) else # Genuine unrecoverable failure — label blocked with diagnostic log "session failed: ${FAILURE_REASON}" - notify_ctx \ - "❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}" \ - "❌ Issue #${ISSUE} session failed: ${FAILURE_REASON}${PR_NUMBER:+ | PR #${PR_NUMBER}}" post_blocked_diagnostic "$FAILURE_REASON" agent_kill_session "$SESSION_NAME" @@ -802,7 +759,7 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000) else cleanup_worktree fi - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "${SCRATCH_FILE:-}" \ + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "${SCRATCH_FILE:-}" \ "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" [ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}" return 1 @@ -813,12 +770,9 @@ $(printf '%s' "$REFUSAL_JSON" | head -c 2000) # diagnostic comment so humans can triage directly on the issue. elif [ "$phase" = "PHASE:crashed" ]; then log "session crashed for issue #${ISSUE}" - notify_ctx \ - "session crashed unexpectedly — marking blocked" \ - "session crashed unexpectedly — marking blocked${PR_NUMBER:+ | PR #${PR_NUMBER}}" post_blocked_diagnostic "crashed" log "PRESERVED crashed worktree for debugging: $WORKTREE" - rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE" "${SCRATCH_FILE:-}" \ + rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "${SCRATCH_FILE:-}" \ "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" [ -n "${PR_NUMBER:-}" ] && rm -f "/tmp/review-injected-${PROJECT_NAME}-${PR_NUMBER}" diff --git a/docker/agents/entrypoint.sh b/docker/agents/entrypoint.sh index 4fc3d17..42119a6 100644 --- a/docker/agents/entrypoint.sh +++ b/docker/agents/entrypoint.sh @@ -87,14 +87,6 @@ else log "tea login: skipped (tea not found or FORGE_TOKEN/FORGE_URL not set)" fi -# Start matrix listener in background (if configured) -if [ -n "${MATRIX_TOKEN:-}" ] && [ -n "${MATRIX_ROOM_ID:-}" ]; then - log "Starting matrix listener in background" - su -s /bin/bash agent -c "${DISINTO_DIR}/lib/matrix_listener.sh" & -else - log "Matrix listener: skipped (MATRIX_TOKEN or MATRIX_ROOM_ID not set)" -fi - # Run cron in the foreground. Cron jobs execute as the agent user. log "Starting cron daemon" exec cron -f diff --git a/docs/PHASE-PROTOCOL.md b/docs/PHASE-PROTOCOL.md index f2f3ad8..40d1661 100644 --- a/docs/PHASE-PROTOCOL.md +++ b/docs/PHASE-PROTOCOL.md @@ -30,7 +30,7 @@ Claude writes exactly one of these lines to the phase file when a phase ends: |----------|---------|---------------------| | `PHASE:awaiting_ci` | PR pushed, waiting for CI to run | Poll CI; inject result when done | | `PHASE:awaiting_review` | CI passed, PR open, waiting for review | Wait for `review-poll` to inject feedback | -| `PHASE:escalate` | Needs human input (any reason) | Send Matrix notification; session stays alive; 24h timeout → blocked | +| `PHASE:escalate` | Needs human input (any reason) | Send vault/forge notification; session stays alive; 24h timeout → blocked | | `PHASE:done` | Work complete, PR merged | Verify merge, kill tmux session, clean up | | `PHASE:failed` | Unrecoverable failure | Escalate to gardener/supervisor | @@ -77,9 +77,8 @@ PHASE:awaiting_review → wait for review-poll.sh to post review comment on APPROVE → inject "approved" into session on timeout (3h) → inject "no review, escalating" -PHASE:escalate → send Matrix notification with context (issue/PR link, reason) +PHASE:escalate → send vault/forge notification with context (issue/PR link, reason) session stays alive waiting for human reply - on reply → matrix_listener.sh injects reply into tmux session on timeout → 24h: label issue blocked, kill session PHASE:done → verify PR merged on forge @@ -118,7 +117,7 @@ signal to the phase file. - **Post-loop exit handler (`case $_MONITOR_LOOP_EXIT`):** Must include an `idle_prompt)` branch. Typical actions: log the event, clean up temp files, and (for agents that use escalation) write an escalation entry or notify via - Matrix. See `dev/dev-agent.sh`, `action/action-agent.sh`, and + vault/forge. See `dev/dev-agent.sh`, `action/action-agent.sh`, and `gardener/gardener-agent.sh` for reference implementations. ## Crash Recovery diff --git a/formulas/run-rent-a-human.toml b/formulas/run-rent-a-human.toml index b90d47d..9009418 100644 --- a/formulas/run-rent-a-human.toml +++ b/formulas/run-rent-a-human.toml @@ -6,7 +6,7 @@ # # Trigger: action issue created by planner or any formula. # The action-agent picks up the issue, executes these steps, writes a draft -# to vault/outreach/{platform}/drafts/, notifies the human via Matrix, +# to vault/outreach/{platform}/drafts/, notifies the human via the forge, # and closes the issue. # # YAML front matter in the dispatching action issue: @@ -150,21 +150,17 @@ Write the drafted content to the outreach directory, commit, and push. [[steps]] id = "notify-human" -title = "Notify human via Matrix" +title = "Notify human and create PR" needs = ["write-draft"] description = """ -Notify the human, create a PR, and hand off to the orchestrator for CI. +Create a PR for the draft and hand off to the orchestrator for CI. 1. Read saved state: DRAFT_FILE=$(cat /tmp/rent-a-human-path) DRAFT_TITLE=$(cat /tmp/rent-a-human-title) BRANCH=$(cat /tmp/rent-a-human-branch) -2. Send the Matrix notification: - source "$FACTORY_ROOT/lib/env.sh" - matrix_send "rent-a-human" "New {{platform}} {{action_type}} draft ready: ${DRAFT_TITLE}" - -3. Create a PR for the draft: +2. Create a PR for the draft: PR_RESPONSE=$(curl -sf -X POST \ -H "Authorization: token ${FORGE_TOKEN}" \ -H "Content-Type: application/json" \ diff --git a/formulas/run-supervisor.toml b/formulas/run-supervisor.toml index f5ae006..6d9d15a 100644 --- a/formulas/run-supervisor.toml +++ b/formulas/run-supervisor.toml @@ -8,12 +8,12 @@ # # Key differences from planner/gardener: # - Runs every 20min — lightweight health check -# - Primarily READS state, rarely WRITES (no PRs, just Matrix + journal) +# - Primarily READS state, rarely WRITES (no PRs, just journal) # - Checks vault state for pending procurement items -# - Conversation memory via Matrix thread and journal +# - Conversation memory via journal name = "run-supervisor" -description = "Factory health monitoring: assess metrics, fix issues, report via Matrix, write journal" +description = "Factory health monitoring: assess metrics, fix issues, write journal" version = 1 model = "sonnet" @@ -174,20 +174,16 @@ needs = ["health-assessment"] [[steps]] id = "report" -title = "Post health summary to Matrix" +title = "Log health summary" description = """ -Post a status summary to Matrix. Use the matrix_send function: - source "$FACTORY_ROOT/lib/env.sh" - matrix_send "supervisor" "" +Log a status summary to the journal. ### When everything is healthy -Post a brief "all clear" only if the PREVIOUS run had alerts (check journal). -Do NOT post "all clear" every 20 minutes — that would be noise. +Log a brief "all clear" only if the PREVIOUS run had alerts (check journal). +Do NOT log "all clear" every 20 minutes — that would be noise. ### When there are findings -Post a summary grouped by priority: - matrix_send "supervisor" "Supervisor health check: - +Log a summary grouped by priority with: Fixed: - @@ -195,18 +191,12 @@ Post a summary grouped by priority: - [P2] - [P3] - Status: RAM=MB Disk=% Load=" + Status: RAM=MB Disk=% Load= ### When vault items were filed (P0-P2 unresolved) -Note the vault items in the status summary: - matrix_send "supervisor" "Supervisor health check: +Note the vault items in the status summary. - Filed vault items: - - vault/pending/.md — - - Status: RAM=MB Disk=% Load=" - -Keep messages concise. Do not post identical messages to what was posted +Keep messages concise. Do not log identical messages to what was logged in the previous run (check journal for prior messages). """ needs = ["decide-actions"] diff --git a/gardener/AGENTS.md b/gardener/AGENTS.md index 342e1ec..e6d7836 100644 --- a/gardener/AGENTS.md +++ b/gardener/AGENTS.md @@ -30,7 +30,6 @@ directly from cron like the planner, predictor, and supervisor. **Environment variables consumed**: - `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT` - `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by gardener-run.sh) -- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` **Lifecycle**: gardener-run.sh (cron 0,6,12,18) → `check_active gardener` → lock + memory guard → load formula + context → create tmux session → diff --git a/lib/AGENTS.md b/lib/AGENTS.md index 4b0ed29..9f7b9ef 100644 --- a/lib/AGENTS.md +++ b/lib/AGENTS.md @@ -6,17 +6,16 @@ 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 \" 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 ` — queries Woodpecker directly for CI state, falls back to forge commit status API. `ci_pipeline_number ` — 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/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). `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 | | `lib/build-graph.py` | Python tool: parses VISION.md, prerequisite-tree.md, AGENTS.md, formulas/*.toml, evidence/, and forge issues/labels into a NetworkX DiGraph. Runs structural analyses (orphaned objectives, stale prerequisites, thin evidence, circular deps) and outputs a JSON report. Used by `review-pr.sh` (per-PR changed-file analysis) and `predictor-run.sh` (full-project analysis) to provide structural context to Claude. | review-pr.sh, predictor-run.sh | | `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 | diff --git a/lib/agent-session.sh b/lib/agent-session.sh index 0738704..dbb1e2a 100644 --- a/lib/agent-session.sh +++ b/lib/agent-session.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 diff --git a/lib/env.sh b/lib/env.sh index b53979a..0d0aeb6 100755 --- a/lib/env.sh +++ b/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,77 +146,15 @@ wpdb() { -t "$@" 2>/dev/null } -# 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. -# 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 [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" +# Memory guard — exit 0 (skip) if available RAM is below MIN_MB. +# Usage: memory_guard [MIN_MB] (default 2000) +memory_guard() { + local min_mb="${1:-2000}" + local avail_mb + avail_mb=$(awk '/MemAvailable/{printf "%d", $2/1024}' /proc/meminfo) + if [ "${avail_mb:-0}" -lt "$min_mb" ]; then + log "SKIP: only ${avail_mb}MB available (need ${min_mb}MB)" + exit 0 fi } diff --git a/lib/formula-session.sh b/lib/formula-session.sh index 37c3274..0265c5d 100644 --- a/lib/formula-session.sh +++ b/lib/formula-session.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:-}" diff --git a/lib/hooks/on-stop-matrix.sh b/lib/hooks/on-stop-matrix.sh deleted file mode 100755 index e7999cc..0000000 --- a/lib/hooks/on-stop-matrix.sh +++ /dev/null @@ -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 diff --git a/lib/load-project.sh b/lib/load-project.sh index fc4e579..1caa4a9 100755 --- a/lib/load-project.sh +++ b/lib/load-project.sh @@ -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 diff --git a/lib/matrix_listener.service b/lib/matrix_listener.service deleted file mode 100644 index 1925155..0000000 --- a/lib/matrix_listener.service +++ /dev/null @@ -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 diff --git a/lib/matrix_listener.sh b/lib/matrix_listener.sh deleted file mode 100755 index 3c1b5c8..0000000 --- a/lib/matrix_listener.sh +++ /dev/null @@ -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 or REJECT , 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 diff --git a/planner/AGENTS.md b/planner/AGENTS.md index d8e530d..95263e6 100644 --- a/planner/AGENTS.md +++ b/planner/AGENTS.md @@ -76,4 +76,3 @@ all milestones" pattern that produced premature work in planner v1/v2. **Environment variables consumed**: - `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT` - `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to opus by planner-run.sh) -- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` diff --git a/predictor/AGENTS.md b/predictor/AGENTS.md index 28071ab..66ac75f 100644 --- a/predictor/AGENTS.md +++ b/predictor/AGENTS.md @@ -43,7 +43,6 @@ RAM < 2000 MB). **Environment variables consumed**: - `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT` - `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by predictor-run.sh) -- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Notifications (optional) **Lifecycle**: predictor-run.sh (daily 06:00 cron) → lock + memory guard → load formula + context (AGENTS.md, RESOURCES.md, VISION.md, prerequisite-tree.md) diff --git a/projects/disinto.toml.example b/projects/disinto.toml.example index c0a3003..3cdebc2 100644 --- a/projects/disinto.toml.example +++ b/projects/disinto.toml.example @@ -16,12 +16,6 @@ stale_minutes = 60 [services] containers = [] -[matrix] -room_id = "!your_room_id:matrix.example.org" -bot_user = "@disinto-factory:matrix.example.org" -token_env = "DISINTO_MATRIX_TOKEN" -mention_user = "@johba:matrix.allf.in" # Matrix user to @mention on escalations - [monitoring] check_prs = true check_dev_agent = true diff --git a/projects/harb.toml.example b/projects/harb.toml.example index 83bb1e1..7664486 100644 --- a/projects/harb.toml.example +++ b/projects/harb.toml.example @@ -16,12 +16,6 @@ stale_minutes = 60 [services] containers = ["ponder"] -[matrix] -room_id = "!your_room_id:matrix.example.org" -bot_user = "@harb-factory:matrix.example.org" -token_env = "MATRIX_TOKEN" -mention_user = "@johba:matrix.allf.in" # Matrix user to @mention on escalations - [monitoring] check_prs = true check_dev_agent = true diff --git a/review/AGENTS.md b/review/AGENTS.md index c6b6dda..58801bd 100644 --- a/review/AGENTS.md +++ b/review/AGENTS.md @@ -17,4 +17,3 @@ spawns `review-pr.sh `. - `FORGE_REVIEW_TOKEN` — Review-agent token for approvals (use human/admin account; branch protection: in approvals whitelist) - `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT` - `PRIMARY_BRANCH`, `WOODPECKER_REPO_ID` -- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` diff --git a/review/review-poll.sh b/review/review-poll.sh index ab341bb..bf02026 100755 --- a/review/review-poll.sh +++ b/review/review-poll.sh @@ -55,8 +55,6 @@ if [ -n "$REVIEW_SESSIONS" ]; then tmux kill-session -t "$session" 2>/dev/null || true rm -f "$phase_file" "/tmp/${PROJECT_NAME}-review-output-${pr_num}.json" \ "/tmp/review-injected-${PROJECT_NAME}-${pr_num}" - # Prune thread-map entries for this PR - sed -i "/\treview\t[^\t]*\t${pr_num}\t/d" "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true cd "$REPO_ROOT" git worktree remove "/tmp/${PROJECT_NAME}-review-${pr_num}" --force 2>/dev/null || true rm -rf "/tmp/${PROJECT_NAME}-review-${pr_num}" 2>/dev/null || true @@ -71,8 +69,6 @@ if [ -n "$REVIEW_SESSIONS" ]; then tmux kill-session -t "$session" 2>/dev/null || true rm -f "$phase_file" "/tmp/${PROJECT_NAME}-review-output-${pr_num}.json" \ "/tmp/review-injected-${PROJECT_NAME}-${pr_num}" - # Prune thread-map entries for this PR - sed -i "/\treview\t[^\t]*\t${pr_num}\t/d" "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true cd "$REPO_ROOT" git worktree remove "/tmp/${PROJECT_NAME}-review-${pr_num}" --force 2>/dev/null || true rm -rf "/tmp/${PROJECT_NAME}-review-${pr_num}" 2>/dev/null || true @@ -86,7 +82,6 @@ if [ -n "$REVIEW_SESSIONS" ]; then tmux kill-session -t "$session" 2>/dev/null || true rm -f "$phase_file" "/tmp/${PROJECT_NAME}-review-output-${pr_num}.json" \ "/tmp/review-injected-${PROJECT_NAME}-${pr_num}" - sed -i "/\treview\t[^\t]*\t${pr_num}\t/d" "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true cd "$REPO_ROOT" git worktree remove "/tmp/${PROJECT_NAME}-review-${pr_num}" --force 2>/dev/null || true rm -rf "/tmp/${PROJECT_NAME}-review-${pr_num}" 2>/dev/null || true @@ -217,7 +212,6 @@ if [ -n "${REVIEW_SESSIONS:-}" ]; then inject_review_into_dev_session "$pr_num" "${FRESH_SHA:-$current_sha}" "$pr_branch" else log " #${pr_num} re-review failed" - matrix_send "review" "❌ PR #${pr_num} re-review failed" 2>/dev/null || true fi [ "$REVIEWED" -lt "$MAX_REVIEWS" ] || break @@ -265,7 +259,6 @@ while IFS= read -r line; do inject_review_into_dev_session "$PR_NUM" "${FRESH_SHA:-$PR_SHA}" "$PR_BRANCH" else log " #${PR_NUM} review failed" - matrix_send "review" "❌ PR #${PR_NUM} review failed" 2>/dev/null || true fi if [ "$REVIEWED" -ge "$MAX_REVIEWS" ]; then diff --git a/review/review-pr.sh b/review/review-pr.sh index 7b84833..2a83573 100755 --- a/review/review-pr.sh +++ b/review/review-pr.sh @@ -160,7 +160,7 @@ if [ -z "$REVIEW_JSON" ]; then jq -n --arg b "## AI Review — Error\n\nReview failed.\n---\n*${PR_SHA:0:7}*" \ '{body: $b}' | curl -sf -o /dev/null -X POST -H "Authorization: token ${FORGE_TOKEN}" \ -H "Content-Type: application/json" "${API}/issues/${PR_NUMBER}/comments" -d @- || true - matrix_send "review" "PR #${PR_NUMBER} review failed" 2>/dev/null || true; exit 1 + exit 1 fi VERDICT=$(printf '%s' "$REVIEW_JSON" | jq -r '.verdict' | tr '[:lower:]' '[:upper:]' | tr '-' '_') REASON=$(printf '%s' "$REVIEW_JSON" | jq -r '.verdict_reason // ""') @@ -204,7 +204,6 @@ curl -s -o /dev/null -X POST -H "Authorization: token ${FORGE_REVIEW_TOKEN}" \ --data-binary @"${REVIEW_TMPDIR}/formal.json" >/dev/null 2>&1 || true log "formal ${REVENT} submitted" -matrix_send "review" "PR #${PR_NUMBER} ${RTYPE}: ${VERDICT} — ${PR_TITLE}" "" "$PR_NUMBER" >/dev/null 2>&1 || true case "$VERDICT" in REQUEST_CHANGES|DISCUSS) printf 'PHASE:awaiting_changes\nSHA:%s\n' "$PR_SHA" > "$PHASE_FILE" ;; *) rm -f "$PHASE_FILE" "$OUTPUT_FILE"; cd "${PROJECT_REPO_ROOT}" diff --git a/supervisor/AGENTS.md b/supervisor/AGENTS.md index 2ac9427..8258a0f 100644 --- a/supervisor/AGENTS.md +++ b/supervisor/AGENTS.md @@ -4,7 +4,7 @@ **Role**: Health monitoring and auto-remediation, executed as a formula-driven Claude agent. Collects system and project metrics via a bash pre-flight script, then runs an interactive Claude session (sonnet) that assesses health, auto-fixes -issues, reports via Matrix, and writes a daily journal. When blocked on external +issues, and writes a daily journal. When blocked on external resources or human decisions, files vault items instead of escalating directly. **Trigger**: `supervisor-run.sh` runs every 20 min via cron. Sources `lib/guard.sh` @@ -40,17 +40,11 @@ runs directly from cron like the planner and predictor. **Alert priorities**: P0 (memory crisis), P1 (disk), P2 (factory stopped/stalled), P3 (degraded PRs, circular deps, stale deps), P4 (housekeeping). -**Matrix integration**: The supervisor has its own Matrix thread. Posts health -summaries when there are changes, reports P0-P2 issues, and processes replies -from humans ("ignore disk warning", "kill that agent", "what's stuck?"). - **Environment variables consumed**: - `FORGE_TOKEN`, `FORGE_REPO`, `FORGE_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT` - `PRIMARY_BRANCH`, `CLAUDE_MODEL` (set to sonnet by supervisor-run.sh) - `WOODPECKER_TOKEN`, `WOODPECKER_SERVER`, `WOODPECKER_DB_PASSWORD`, `WOODPECKER_DB_USER`, `WOODPECKER_DB_HOST`, `WOODPECKER_DB_NAME` — CI database queries -- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Matrix notifications + human input **Lifecycle**: supervisor-run.sh (cron */20) → lock + memory guard → run -preflight.sh (collect metrics) → consume Matrix replies → load formula + -context → create tmux session → Claude assesses health, auto-fixes, posts -Matrix summary, writes journal → `PHASE:done`. +preflight.sh (collect metrics) → load formula + context → create tmux +session → Claude assesses health, auto-fixes, writes journal → `PHASE:done`. diff --git a/supervisor/PROMPT.md b/supervisor/PROMPT.md index 851453b..a7d1725 100644 --- a/supervisor/PROMPT.md +++ b/supervisor/PROMPT.md @@ -40,7 +40,6 @@ This gives you: - `$PROJECT_NAME` — short project name (for worktree prefixes, container names) - `$PRIMARY_BRANCH` — main branch (master or main) - `$FACTORY_ROOT` — path to the disinto repo -- `matrix_send ` — send notifications to the Matrix coordination room ## Handling Dependency Alerts diff --git a/supervisor/supervisor-poll.sh b/supervisor/supervisor-poll.sh index 5de5b0d..81494d3 100755 --- a/supervisor/supervisor-poll.sh +++ b/supervisor/supervisor-poll.sh @@ -134,11 +134,9 @@ if [ "${AVAIL_MB:-9999}" -lt 500 ] || { [ "${SWAP_USED_MB:-0}" -gt 3000 ] && [ " fi fi -# P0 is urgent — send immediately before per-project checks can crash the script +# P0 alerts already logged — clear so they are not duplicated in the final consolidated log if [ -n "$P0_ALERTS" ]; then - matrix_send "supervisor" "🚨 Supervisor P0 alerts: -$(printf '%b' "$P0_ALERTS")" 2>/dev/null || true - P0_ALERTS="" # clear so it is not duplicated in the final consolidated send + P0_ALERTS="" fi # ============================================================================= @@ -184,11 +182,9 @@ if [ "${DISK_PERCENT:-0}" -gt 80 ]; then fi fi -# P1 is urgent — send immediately before per-project checks can crash the script +# P1 alerts already logged — clear so they are not duplicated in the final consolidated log if [ -n "$P1_ALERTS" ]; then - matrix_send "supervisor" "⚠️ Supervisor P1 alerts: -$(printf '%b' "$P1_ALERTS")" 2>/dev/null || true - P1_ALERTS="" # clear so it is not duplicated in the final consolidated send + P1_ALERTS="" fi # Emit infra metric @@ -618,7 +614,6 @@ Instructions: _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 (escalate for ${_nh_age_h}h)" fi @@ -785,10 +780,6 @@ ALL_ALERTS="${P0_ALERTS}${P1_ALERTS}${P2_ALERTS}${P3_ALERTS}${P4_ALERTS}" if [ -n "$ALL_ALERTS" ]; then ALERT_TEXT=$(echo -e "$ALL_ALERTS") - # Notify Matrix - matrix_send "supervisor" "⚠️ Supervisor alerts: -${ALERT_TEXT}" 2>/dev/null || true - flog "Invoking claude -p for alerts" CLAUDE_PROMPT="$(cat "$PROMPT_FILE" 2>/dev/null || echo "You are a supervisor agent. Fix the issue below.") diff --git a/vault/AGENTS.md b/vault/AGENTS.md index bc020f6..6157ea1 100644 --- a/vault/AGENTS.md +++ b/vault/AGENTS.md @@ -6,12 +6,12 @@ **Pipeline A — Action Gating (*.json)**: Actions enter a pending queue and are classified by Claude via `vault-agent.sh`, which can auto-approve (call `vault-fire.sh` directly), auto-reject (call `vault-reject.sh`), or escalate -to a human by writing `PHASE:escalate` to a phase file and sending a Matrix -message — using the same unified escalation path as dev/action agents. +to a human by writing `PHASE:escalate` to a phase file — using the same +unified escalation path as dev/action agents. **Pipeline B — Procurement (*.md)**: The planner files resource requests as markdown files in `vault/pending/`. `vault-poll.sh` notifies the human via -Matrix. The human fulfills the request (creates accounts, provisions infra, +vault/forge. The human fulfills the request (creates accounts, provisions infra, adds secrets to `.env`) and moves the file to `vault/approved/`. `vault-fire.sh` then extracts the proposed entry and appends it to `RESOURCES.md`. @@ -20,7 +20,7 @@ adds secrets to `.env`) and moves the file to `vault/approved/`. `run-rent-a-human` formula (via an `action` issue) when a task requires a human touch — posting on Reddit, commenting on HN, signing up for a service, etc. Claude drafts copy-paste-ready content to `vault/outreach/{platform}/drafts/` -and notifies the human via Matrix for one-click execution. No vault approval +and notifies the human via vault/forge for one-click execution. No vault approval needed — the human reviews and publishes directly. **Trigger**: `vault-poll.sh` runs every 30 min via cron. @@ -31,15 +31,14 @@ needed — the human reviews and publishes directly. - `vault/PROMPT.md` — System prompt for the vault agent's Claude invocation - `vault/vault-fire.sh` — Executes an approved action (JSON) or writes RESOURCES.md entry (procurement MD) - `vault/vault-reject.sh` — Marks a JSON action as rejected -- `formulas/run-rent-a-human.toml` — Formula for human-action drafts: Claude researches target platform norms, drafts copy-paste content, writes to `vault/outreach/{platform}/drafts/`, notifies human via Matrix +- `formulas/run-rent-a-human.toml` — Formula for human-action drafts: Claude researches target platform norms, drafts copy-paste content, writes to `vault/outreach/{platform}/drafts/`, notifies human via vault/forge **Procurement flow**: 1. Planner drops `vault/pending/.md` with what/why/proposed RESOURCES.md entry -2. `vault-poll.sh` notifies human via Matrix +2. `vault-poll.sh` notifies human via vault/forge 3. Human fulfills: creates account, adds secrets to `.env`, moves file to `vault/approved/` 4. `vault-fire.sh` extracts proposed entry, appends to RESOURCES.md, moves to `vault/fired/` 5. Next planner run reads RESOURCES.md → new capability available → unblocks prerequisite tree **Environment variables consumed**: - All from `lib/env.sh` -- `MATRIX_TOKEN`, `MATRIX_ROOM_ID`, `MATRIX_HOMESERVER` — Escalation channel diff --git a/vault/PROMPT.md b/vault/PROMPT.md index c6fd57e..85dc669 100644 --- a/vault/PROMPT.md +++ b/vault/PROMPT.md @@ -29,8 +29,8 @@ For each pending JSON action, decide: **auto-approve**, **escalate**, or **rejec |----------|------------|---------------------------------------------| | low | true | auto-approve → fire immediately | | low | false | auto-approve → fire, log prominently | -| medium | true | auto-approve → fire, matrix notify | -| medium | false | escalate via matrix → wait for human reply | +| medium | true | auto-approve → fire, notify via vault/forge | +| medium | false | escalate via vault/forge → wait for human reply | | high | any | always escalate → wait for human reply | ## Rules @@ -94,18 +94,9 @@ source ${FACTORY_ROOT}/lib/env.sh bash ${FACTORY_ROOT}/vault/vault-fire.sh ``` -### Escalate via Matrix +### Escalate ```bash -matrix_send "vault" "🔒 VAULT — approval required - -Source: -Type: -Risk: / -Created: - - - -Reply APPROVE or REJECT " 2>/dev/null +echo "PHASE:escalate" > "$PHASE_FILE" ``` ### Reject @@ -125,8 +116,7 @@ ROUTE: - Process ALL pending JSON actions in the batch. Never skip silently. - For auto-approved actions, fire them immediately via `vault-fire.sh`. -- For escalated actions, move to `vault/approved/` only AFTER human approval - (vault-poll handles this via matrix_listener dispatch). +- For escalated actions, move to `vault/approved/` only AFTER human approval. - Read the action JSON carefully. Check the payload, not just the metadata. - Ignore `.md` files in pending/ — those are procurement requests handled separately by vault-poll.sh and the human. diff --git a/vault/vault-agent.sh b/vault/vault-agent.sh index 7c117ad..202474d 100755 --- a/vault/vault-agent.sh +++ b/vault/vault-agent.sh @@ -5,8 +5,8 @@ # builds a prompt with action summaries, and lets the LLM decide routing. # # The LLM can call vault-fire.sh (auto-approve) or vault-reject.sh (reject) -# directly. For escalations, it sends a Matrix message and marks the action -# as "escalated" in pending/ so vault-poll skips it on future runs. +# directly. For escalations, it writes a PHASE:escalate file and marks the +# action as "escalated" in pending/ so vault-poll skips it on future runs. set -euo pipefail @@ -69,7 +69,6 @@ ${ACTIONS_BATCH} - Vault directory: ${VAULT_DIR} - vault-fire.sh: bash ${VAULT_DIR}/vault-fire.sh - vault-reject.sh: bash ${VAULT_DIR}/vault-reject.sh \"\" -- matrix_send is available after: source ${FACTORY_ROOT}/lib/env.sh Process each action now. For auto-approve, fire immediately. For reject, call vault-reject.sh. @@ -77,7 +76,7 @@ For actions that need human approval (escalate), write a PHASE:escalate file to signal the unified escalation path: printf 'PHASE:escalate\nReason: vault procurement — %s\n' '' \\ > /tmp/vault-escalate-.phase -Then send a Matrix message with context about what needs approval." +Then STOP and wait — a human will review via the forge." CLAUDE_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$PROMPT" \ --model sonnet \ diff --git a/vault/vault-fire.sh b/vault/vault-fire.sh index cb807c2..18d3d90 100755 --- a/vault/vault-fire.sh +++ b/vault/vault-fire.sh @@ -83,7 +83,6 @@ if [ "$IS_PROCUREMENT" = true ]; then if [ -z "$ENTRY" ]; then log "ERROR: $ACTION_ID has no '## Proposed RESOURCES.md Entry' section" - matrix_send "vault" "❌ Procurement $ACTION_ID has no RESOURCES.md entry — cannot fire" 2>/dev/null || true exit 1 fi @@ -95,7 +94,6 @@ if [ "$IS_PROCUREMENT" = true ]; then mv "$ACTION_FILE" "${VAULT_DIR}/fired/${ACTION_ID}.md" rm -f "${LOCKS_DIR}/${ACTION_ID}.notified" log "$ACTION_ID: approved → fired (procurement)" - matrix_send "vault" "✅ Procurement fulfilled: ${ACTION_ID} — RESOURCES.md updated" 2>/dev/null || true exit 0 fi @@ -175,9 +173,7 @@ if [ "$FIRE_EXIT" -eq 0 ]; then && mv "$TMP" "${VAULT_DIR}/fired/${ACTION_ID}.json" rm -f "$ACTION_FILE" log "$ACTION_ID: approved → fired" - matrix_send "vault" "✅ Vault fired: ${ACTION_ID} (${ACTION_TYPE} from ${ACTION_SOURCE})" 2>/dev/null || true else log "ERROR: $ACTION_ID fire failed (exit $FIRE_EXIT) — stays in approved/ for retry" - matrix_send "vault" "❌ Vault fire failed: ${ACTION_ID} (${ACTION_TYPE}) — will retry" 2>/dev/null || true exit "$FIRE_EXIT" fi diff --git a/vault/vault-poll.sh b/vault/vault-poll.sh index 706e67a..5dbf06c 100755 --- a/vault/vault-poll.sh +++ b/vault/vault-poll.sh @@ -91,7 +91,6 @@ for action_file in "${VAULT_DIR}/approved/"*.json; do log "fired $ACTION_ID (retry)" else log "ERROR: fire failed for $ACTION_ID (retry)" - matrix_send "vault" "❌ Vault fire failed on retry: ${ACTION_ID}" 2>/dev/null || true fi unlock_action "$ACTION_ID" @@ -112,7 +111,6 @@ for req_file in "${VAULT_DIR}/approved/"*.md; do log "fired procurement $REQ_ID (retry)" else log "ERROR: fire failed for procurement $REQ_ID (retry)" - matrix_send "vault" "❌ Vault fire failed on retry: ${REQ_ID} (procurement)" 2>/dev/null || true fi unlock_action "$REQ_ID" @@ -143,7 +141,6 @@ for action_file in "${VAULT_DIR}/pending/"*.json; do AGE_HOURS=$((AGE_SECS / 3600)) log "timeout: $ACTION_ID escalated ${AGE_HOURS}h ago with no reply — auto-rejecting" bash "${VAULT_DIR}/vault-reject.sh" "$ACTION_ID" "timeout (${AGE_HOURS}h, no human reply)" >> "$LOGFILE" 2>&1 || true - matrix_send "vault" "⏰ Vault auto-rejected ${ACTION_ID} — no reply after ${AGE_HOURS}h" 2>/dev/null || true fi done @@ -184,7 +181,6 @@ if [ "$PENDING_COUNT" -gt 0 ]; then bash "${VAULT_DIR}/vault-agent.sh" >> "$LOGFILE" 2>&1 || { log "ERROR: vault-agent failed" - matrix_send "vault" "❌ vault-agent.sh failed — check vault.log" 2>/dev/null || true } fi @@ -216,15 +212,6 @@ for req_file in "${VAULT_DIR}/pending/"*.md; do log "new procurement request: $REQ_ID — $REQ_TITLE" - # Notify human via Matrix - matrix_send "vault" "🔑 PROCUREMENT REQUEST — ${REQ_TITLE} - -ID: ${REQ_ID} -Action: review vault/pending/${REQ_ID}.md -To approve: fulfill the request, add secrets to .env, move file to vault/approved/ - -$(head -20 "$req_file")" 2>/dev/null || true - # Mark as notified so we don't re-send mkdir -p "${VAULT_DIR}/.locks" touch "${VAULT_DIR}/.locks/${REQ_ID}.notified" diff --git a/vault/vault-reject.sh b/vault/vault-reject.sh index f5c15c1..95598bc 100755 --- a/vault/vault-reject.sh +++ b/vault/vault-reject.sh @@ -29,9 +29,6 @@ else exit 1 fi -ACTION_TYPE=$(jq -r '.type // "unknown"' < "$ACTION_FILE" 2>/dev/null) -ACTION_SOURCE=$(jq -r '.source // "unknown"' < "$ACTION_FILE" 2>/dev/null) - # Update with rejection metadata and move to rejected/ TMP=$(mktemp) jq --arg reason "$REASON" --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ @@ -43,4 +40,3 @@ rm -f "$ACTION_FILE" rm -f "${VAULT_DIR}/.locks/${ACTION_ID}.lock" log "$ACTION_ID: rejected — $REASON" -matrix_send "vault" "🚫 Vault rejected: ${ACTION_ID} (${ACTION_TYPE} from ${ACTION_SOURCE}) — ${REASON}" 2>/dev/null || true