From 77427bd7d32fd70f52d0cc392fc5043d3ace600a Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Fri, 3 Apr 2026 18:13:19 +0000 Subject: [PATCH] chore(agents): add deep session introspection to system watchdog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance the system watchdog with OpenCode Server API session introspection to read actual supervisor conversations, tool calls, and todo lists. Upgrade Audit 6 (Zombie Detection) to use message-based analysis instead of only checking Forgejo activity — reads last 5 messages from each supervisor session to detect sleep-only patterns, error loops, and identical repeated tool calls. Add Audit 11 (Quick Session Spot-Check) running every 5-minute cycle: scans the 3 most recently active sessions for critical policy violations including force_merge usage, direct pushes to master, and type:ignore suppressions in written code. Add Audit 12 (Deep Session Introspection) running every 30 minutes: full analysis of all 16 supervisor sessions reading last 10 messages and todo lists. Detects misbehavior patterns, progress stalls via todo list analysis, conversation health metrics (error rates, sleep ratios), context exhaustion signals, and cross-agent conflicts (multiple agents touching the same PR or issue). Update action dispatch to handle new finding types: force_merge_detected, direct_push_to_master, stuck_supervisor, looping_supervisor, high_error_rate, context_exhaustion, and cross_agent_pr_conflict. --- .opencode/agents/ca-system-watchdog.md | 607 +++++++++++++++++++++++-- 1 file changed, 581 insertions(+), 26 deletions(-) diff --git a/.opencode/agents/ca-system-watchdog.md b/.opencode/agents/ca-system-watchdog.md index 933ecd18..7bdf1e62 100644 --- a/.opencode/agents/ca-system-watchdog.md +++ b/.opencode/agents/ca-system-watchdog.md @@ -6,8 +6,12 @@ description: > proper state transitions, priority ordering is correct (lower milestones first, critical bugs first), PRs are reviewed and merged promptly, supervisors are producing work (not zombies), dependency links and labels are correct, - and the system is on track to reach production readiness. Dispatches one-off - fix agents directly via curl/prompt_async for immediate corrections. Creates + and the system is on track to reach production readiness. Performs deep + session introspection via the OpenCode Server API — reads supervisor + conversations, tool calls, and todo lists to detect misbehavior (forbidden + API flags, policy violations), stuck agents (error loops, circular patterns), + context exhaustion, and cross-agent conflicts. Dispatches one-off fix agents + directly via curl/prompt_async for immediate corrections. Creates needs-feedback issues for systemic problems requiring agent definition changes. mode: subagent hidden: true @@ -136,6 +140,18 @@ LOOP FOREVER: # Verify that the system is generating improvement suggestions. findings += audit_improvement_generation() + # ── Audit 11: Quick Session Spot-Check (EVERY cycle) ───────── + # Fast check of the 3 most recently active supervisor sessions + # for obvious problems: forbidden API flags, error loops. + findings += audit_session_spot_check() + + # ── Audit 12: Deep Session Introspection (every 6th cycle) ─── + # Full analysis of ALL supervisor sessions: read recent messages, + # tool calls, todo lists. Detect misbehavior, stuck agents, + # context exhaustion, and cross-agent conflicts. + if cycle % 6 == 0: + findings += audit_deep_session_introspection() + # ── Take Action on Findings ────────────────────────────────── for finding in findings: take_action(finding) @@ -394,45 +410,133 @@ function audit_pr_pipeline(): return findings ``` -### Audit 6: Supervisor Health (Zombie Detection) +### Audit 6: Supervisor Health (Zombie Detection via Session Introspection) + +This audit uses the OpenCode Server API to check supervisor health at a +deeper level than just Forgejo activity. It reads actual session messages +to distinguish truly productive supervisors from zombies. + +**OpenCode Server API endpoints used:** +- `GET ${SERVER}/session` — list all sessions +- `GET ${SERVER}/session/status` — get status of all sessions +- `GET ${SERVER}/session/${ID}/message?limit=5` — read last 5 messages +- `GET ${SERVER}/session/${ID}/todo` — read the agent's todo list ``` function audit_supervisor_health(): findings = [] - # Get all supervisor sessions from the OpenCode server - sessions = curl GET ${SERVER}/session + # ── Step 1: Get all supervisor sessions ────────────────────── + sessions = curl -s GET "${SERVER}/session" | parse JSON + statuses = curl -s GET "${SERVER}/session/status" | parse JSON supervisor_sessions = [s for s in sessions if s.title.startswith("[CA-AUTO] supervisor:")] for session in supervisor_sessions: name = session.title.replace("[CA-AUTO] supervisor: ", "") + session_status = statuses.get(session.id) - # Check if the session is still active - # (The product-builder already checks for dead sessions; - # we check for ZOMBIE sessions — alive but not working) + # Skip sessions that are completed/errored (product-builder handles those) + if session_status in ("completed", "error"): + continue - # Check Forgejo for recent activity from this supervisor - # Look for comments, label changes, PR activity in the last 30 min - recent_comments = GET /repos/{owner}/{repo}/issues/comments - with since=(now - 30 minutes) - bot_comments = [c for c in recent_comments - if "CleverAgents Bot" in c.body - and name_matches_supervisor(c.body, name)] + # ── Step 2: Read last 5 messages from the session ──────── + messages = curl -s GET "${SERVER}/session/${session.id}/message?limit=5" + | parse JSON + # Each message is: { info: Message, parts: Part[] } + # Parts contain tool calls, text outputs, thinking, etc. - if not bot_comments: - # Supervisor is alive but no Forgejo activity in 30 min + if not messages: findings.append({ severity: "HIGH", type: "zombie_supervisor", - detail: f"Supervisor '{name}' (session {session.id}) has no " - f"Forgejo activity in the last 30 minutes — may be " - f"a zombie (context exhaustion or stuck in error loop)", + detail: f"Supervisor '{name}' (session {session.id}) has " + f"no messages at all — may have failed to start", session_id: session.id, supervisor_name: name }) + continue - # Check that ALL expected supervisors exist + # ── Step 3: Analyze message patterns for zombie signals ── + # Extract tool calls from recent message parts + recent_tool_calls = [] + recent_text_outputs = [] + sleep_only_count = 0 + error_count = 0 + + for msg in messages: + for part in msg.parts: + if part.type == "tool-invocation": + recent_tool_calls.append({ + tool: part.toolName, + args: part.input, + result: part.output, + error: part.isError + }) + if part.isError: + error_count += 1 + if part.toolName == "bash" and "sleep" in str(part.input): + sleep_only_count += 1 + elif part.type == "text": + recent_text_outputs.append(part.text) + + # ── Zombie signal: sleep-only pattern ──────────────────── + # If last 5 messages are ALL sleep calls with no productive + # tool calls between them, the agent is a zombie. + productive_calls = [tc for tc in recent_tool_calls + if tc.tool != "bash" + or "sleep" not in str(tc.args)] + if len(recent_tool_calls) >= 3 and not productive_calls: + findings.append({ + severity: "HIGH", + type: "zombie_supervisor", + detail: f"Supervisor '{name}' (session {session.id}): " + f"last {len(recent_tool_calls)} tool calls are ALL " + f"sleep commands with zero productive actions — " + f"agent is a zombie (likely context exhaustion)", + session_id: session.id, + supervisor_name: name, + evidence: "sleep-only pattern" + }) + + # ── Stuck signal: repeated error pattern ───────────────── + # If last 3+ tool calls all returned errors, agent is stuck + if error_count >= 3: + error_messages = [tc.result for tc in recent_tool_calls if tc.error] + findings.append({ + severity: "HIGH", + type: "stuck_supervisor", + detail: f"Supervisor '{name}' (session {session.id}): " + f"last {error_count} tool calls all returned errors. " + f"Agent is stuck in an error loop. " + f"Recent errors: {error_messages[:2]}", + session_id: session.id, + supervisor_name: name, + evidence: "error-loop pattern" + }) + + # ── Loop signal: identical repeated tool calls ─────────── + # If the same tool+args appears 3+ times in last 5 messages + call_signatures = [f"{tc.tool}:{str(tc.args)[:100]}" + for tc in recent_tool_calls if not tc.error] + from collections import Counter + sig_counts = Counter(call_signatures) + repeated = {sig: count for sig, count in sig_counts.items() + if count >= 3} + if repeated: + findings.append({ + severity: "HIGH", + type: "looping_supervisor", + detail: f"Supervisor '{name}' (session {session.id}): " + f"repeating the same tool call {list(repeated.values())[0]}+ " + f"times — agent is stuck in a loop. " + f"Repeated call: {list(repeated.keys())[0][:80]}", + session_id: session.id, + supervisor_name: name, + evidence: "identical-call loop" + }) + + # ── Step 4: Check that ALL expected supervisors exist ──────── EXPECTED = ["implementor-pool", "reviewer-pool", "tester-pool", "hunter-pool", "test-infra-pool", "architect", "epic-planner", "human-liaison", "agent-evolver", "arch-guard", "spec-updater", @@ -574,6 +678,393 @@ function audit_improvement_generation(): return findings ``` +### Audit 11: Quick Session Spot-Check (Every Cycle) + +**Purpose:** Fast scan of the 3 most recently active supervisor sessions +for obvious policy violations. Runs every cycle (~5 min) because catching +`force_merge` or direct master pushes quickly is critical. + +**OpenCode Server API endpoints used:** +- `GET ${SERVER}/session/status` — find active sessions +- `GET ${SERVER}/session/${ID}/message?limit=3` — read last 3 messages only + +``` +function audit_session_spot_check(): + findings = [] + + statuses = curl -s GET "${SERVER}/session/status" | parse JSON + sessions = curl -s GET "${SERVER}/session" | parse JSON + + # Find the 3 most recently active supervisor sessions + active_supervisors = [s for s in sessions + if s.title.startswith("[CA-AUTO]") + and statuses.get(s.id) not in ("completed", "error")] + # Sort by most recent activity (updatedAt or similar) + recent_3 = active_supervisors[:3] # already sorted by update time + + for session in recent_3: + name = session.title + + # Read only last 3 messages — this is a QUICK check + messages = curl -s GET "${SERVER}/session/${session.id}/message?limit=3" + | parse JSON + + for msg in messages: + for part in msg.parts: + if part.type != "tool-invocation": + continue + + # ── Check 1: force_merge usage (CRITICAL) ──────── + # Scan tool call arguments for force_merge: true + args_str = str(part.input).lower() if part.input else "" + result_str = str(part.output).lower() if part.output else "" + + if "force_merge" in args_str and "true" in args_str: + findings.append({ + severity: "CRITICAL", + type: "force_merge_detected", + detail: f"Session '{name}' ({session.id}) used " + f"force_merge: true in a tool call! " + f"This bypasses branch protection and is " + f"FORBIDDEN. Tool: {part.toolName}", + session_id: session.id, + evidence: args_str[:200] + }) + + # ── Check 2: Direct push to master (CRITICAL) ──── + if part.toolName == "bash": + cmd = str(part.input).lower() + if ("git push" in cmd + and ("master" in cmd or "main" in cmd) + and "origin" in cmd + and "feature/" not in cmd + and "improvement/" not in cmd + and "spec/" not in cmd + and "fix/" not in cmd): + findings.append({ + severity: "CRITICAL", + type: "direct_push_to_master", + detail: f"Session '{name}' ({session.id}) appears " + f"to be pushing directly to master! " + f"All changes must go through PRs. " + f"Command: {cmd[:150]}", + session_id: session.id, + evidence: cmd[:200] + }) + + # ── Check 3: type: ignore suppression ──────────── + if part.toolName in ("edit", "write", "filesystem_edit_file", + "filesystem_write_file"): + content = str(part.input) + if "type: ignore" in content or "type:ignore" in content: + findings.append({ + severity: "HIGH", + type: "type_ignore_suppression", + detail: f"Session '{name}' ({session.id}) wrote " + f"a 'type: ignore' suppression which is " + f"FORBIDDEN per CONTRIBUTING.md. " + f"Tool: {part.toolName}", + session_id: session.id + }) + + return findings +``` + +### Audit 12: Deep Session Introspection (Every 6th Cycle) + +**Purpose:** Comprehensive analysis of ALL supervisor sessions. Reads +recent messages, tool calls, and todo lists to understand what each agent +is actually doing, detect misbehavior patterns, identify stuck agents, +assess conversation health, and find cross-agent conflicts. + +This is expensive (reads messages from all 16 supervisors) so it runs +only every 6th cycle (~30 min). + +**OpenCode Server API endpoints used:** +- `GET ${SERVER}/session` — list all sessions +- `GET ${SERVER}/session/${ID}/message?limit=10` — read last 10 messages +- `GET ${SERVER}/session/${ID}/todo` — read the agent's todo list +- `GET ${SERVER}/session/${ID}/children` — list child worker sessions + +``` +function audit_deep_session_introspection(): + findings = [] + session_summaries = {} # name -> { what_doing, health, issues } + + sessions = curl -s GET "${SERVER}/session" | parse JSON + supervisor_sessions = [s for s in sessions + if s.title.startswith("[CA-AUTO] supervisor:")] + + for session in supervisor_sessions: + name = session.title.replace("[CA-AUTO] supervisor: ", "") + + # ── Read last 10 messages ──────────────────────────────── + messages = curl -s GET "${SERVER}/session/${session.id}/message?limit=10" + | parse JSON + + if not messages: + continue + + # ── Read todo list ─────────────────────────────────────── + todos = curl -s GET "${SERVER}/session/${session.id}/todo" + | parse JSON + + # ── Read child sessions (workers) ──────────────────────── + children = curl -s GET "${SERVER}/session/${session.id}/children" + | parse JSON + + # ══════════════════════════════════════════════════════════ + # ANALYSIS 1: Misbehavior Detection + # ══════════════════════════════════════════════════════════ + # Scan ALL tool calls in last 10 messages for policy violations + for msg in messages: + for part in msg.parts: + if part.type != "tool-invocation": + continue + + args_str = str(part.input) if part.input else "" + tool = part.toolName or "" + + # force_merge (already checked in spot-check but deeper here) + if "force_merge" in args_str.lower() and "true" in args_str.lower(): + findings.append({ + severity: "CRITICAL", + type: "force_merge_detected", + detail: f"Supervisor '{name}': used force_merge: true. " + f"Tool: {tool}. This is FORBIDDEN.", + session_id: session.id, + evidence: args_str[:300] + }) + + # Closing PRs as duplicates of their tracking issues + if tool == "forgejo_issue_state_change": + if "pull" in args_str.lower() and "close" in args_str.lower(): + findings.append({ + severity: "HIGH", + type: "closing_pr_as_duplicate", + detail: f"Supervisor '{name}': may be closing a " + f"PR instead of an issue. PRs must never " + f"be closed as duplicates of their issues.", + session_id: session.id + }) + + # Creating issues without required labels + if tool == "forgejo_create_issue": + if "State/" not in args_str and "Type/" not in args_str: + findings.append({ + severity: "MEDIUM", + type: "issue_missing_labels_at_creation", + detail: f"Supervisor '{name}': created an issue " + f"without State/ or Type/ labels in the " + f"creation call. Per CONTRIBUTING.md, all " + f"issues need State/Unverified and a Type/.", + session_id: session.id + }) + + # ══════════════════════════════════════════════════════════ + # ANALYSIS 2: Progress Assessment via Todo List + # ══════════════════════════════════════════════════════════ + if todos: + total = len(todos) + completed = len([t for t in todos if t.status == "completed"]) + in_progress = len([t for t in todos if t.status == "in_progress"]) + pending = len([t for t in todos if t.status == "pending"]) + + # All items stuck in in_progress for what seems like a long time + if in_progress > 0 and completed == 0 and total > 3: + findings.append({ + severity: "MEDIUM", + type: "stalled_progress", + detail: f"Supervisor '{name}': {in_progress} todo items " + f"in_progress, 0 completed out of {total} total. " + f"Agent may be stalled or spinning without " + f"making forward progress.", + session_id: session.id, + todos_summary: f"{completed}/{total} done, " + f"{in_progress} in progress" + }) + + session_summaries[name] = { + "todos": f"{completed}/{total} done", + "in_progress_items": [t.content for t in todos + if t.status == "in_progress"][:3] + } + + # ══════════════════════════════════════════════════════════ + # ANALYSIS 3: Conversation Health Metrics + # ══════════════════════════════════════════════════════════ + tool_call_count = 0 + text_output_count = 0 + error_count = 0 + sleep_count = 0 + + for msg in messages: + for part in msg.parts: + if part.type == "tool-invocation": + tool_call_count += 1 + if part.isError: + error_count += 1 + if part.toolName == "bash" and "sleep" in str(part.input): + sleep_count += 1 + elif part.type == "text": + text_output_count += 1 + + # High error rate = something is wrong + if tool_call_count > 0: + error_rate = error_count / tool_call_count + if error_rate > 0.5 and tool_call_count >= 4: + findings.append({ + severity: "HIGH", + type: "high_error_rate", + detail: f"Supervisor '{name}': {error_rate:.0%} of " + f"recent tool calls ({error_count}/{tool_call_count}) " + f"returned errors. Agent is likely fighting a " + f"persistent problem.", + session_id: session.id, + error_rate: error_rate + }) + + # Mostly sleeping = idle or zombie (supplements Audit 6) + if tool_call_count > 0: + sleep_ratio = sleep_count / tool_call_count + if sleep_ratio > 0.8 and tool_call_count >= 5: + findings.append({ + severity: "MEDIUM", + type: "mostly_sleeping", + detail: f"Supervisor '{name}': {sleep_ratio:.0%} of " + f"recent tool calls are sleep commands. Agent " + f"may be idle or approaching context exhaustion.", + session_id: session.id + }) + + # ══════════════════════════════════════════════════════════ + # ANALYSIS 4: Context Exhaustion Signals + # ══════════════════════════════════════════════════════════ + # Check if the agent's text outputs are getting shorter or + # less coherent (a sign of context window filling up). + # Also check if the agent mentions context limits. + for msg in messages: + for part in msg.parts: + if part.type == "text": + text = str(part.text).lower() + if any(phrase in text for phrase in [ + "context limit", "context window", "running out of", + "cannot process", "too long", "exceeded", + "token limit", "maximum length" + ]): + findings.append({ + severity: "HIGH", + type: "context_exhaustion", + detail: f"Supervisor '{name}': agent mentions " + f"context limits in its output. It is " + f"likely experiencing context exhaustion " + f"and should be re-launched with fresh " + f"context.", + session_id: session.id, + evidence: text[:200] + }) + break # One finding per session is enough + + # Record summary for cross-agent analysis + session_summaries[name] = session_summaries.get(name, {}) + session_summaries[name].update({ + "tool_calls": tool_call_count, + "errors": error_count, + "sleeps": sleep_count, + "children": len(children) if children else 0 + }) + + # ══════════════════════════════════════════════════════════════ + # ANALYSIS 5: Cross-Agent Conflict Detection + # ══════════════════════════════════════════════════════════════ + # Look for signs that agents are conflicting with each other: + # - Multiple agents claiming the same PR + # - Multiple agents modifying the same issue labels + # - Agent A's work being undone by agent B + + # Collect all PR numbers mentioned in recent tool calls + pr_mentions = {} # pr_number -> [session_names] + issue_modifications = {} # issue_number -> [session_names] + + for session in supervisor_sessions: + name = session.title.replace("[CA-AUTO] supervisor: ", "") + messages = curl -s GET "${SERVER}/session/${session.id}/message?limit=5" + | parse JSON + + for msg in messages: + for part in msg.parts: + if part.type != "tool-invocation": + continue + args_str = str(part.input) if part.input else "" + + # Track PR interactions + if part.toolName in ("forgejo_merge_pull_request", + "forgejo_create_pull_review", "forgejo_update_pull_request"): + pr_num = extract_pr_number(args_str) + if pr_num: + pr_mentions.setdefault(pr_num, []).append(name) + + # Track issue label modifications + if part.toolName in ("forgejo_add_issue_labels", + "forgejo_update_issue"): + issue_num = extract_issue_number(args_str) + if issue_num: + issue_modifications.setdefault(issue_num, []).append(name) + + # Flag PRs touched by 3+ different agents (possible conflict) + for pr_num, agents in pr_mentions.items(): + unique_agents = list(set(agents)) + if len(unique_agents) >= 3: + findings.append({ + severity: "MEDIUM", + type: "cross_agent_pr_conflict", + detail: f"PR #{pr_num} is being touched by {len(unique_agents)} " + f"different agents: {unique_agents}. This may indicate " + f"coordination problems or duplicate work.", + pr: pr_num, + agents: unique_agents + }) + + # Flag issues modified by 3+ different agents in same cycle + for issue_num, agents in issue_modifications.items(): + unique_agents = list(set(agents)) + if len(unique_agents) >= 3: + findings.append({ + severity: "MEDIUM", + type: "cross_agent_issue_conflict", + detail: f"Issue #{issue_num} is being modified by " + f"{len(unique_agents)} different agents: " + f"{unique_agents}. Check for conflicting label or " + f"state changes.", + issue: issue_num, + agents: unique_agents + }) + + # ── Post introspection summary ─────────────────────────────── + if session_summaries: + summary_lines = [] + for name, data in session_summaries.items(): + line = f" {name}: " + if "todos" in data: + line += f"todos={data['todos']}, " + line += f"calls={data.get('tool_calls', '?')}, " + line += f"errors={data.get('errors', '?')}, " + line += f"workers={data.get('children', '?')}" + summary_lines.append(line) + + post comment on session state issue: + "[WATCHDOG] Deep introspection — cycle : + Session health overview: + + Findings this pass: + + --- + **Automated by CleverAgents Bot** + Supervisor: System Watchdog | Agent: ca-system-watchdog" + + return findings +``` + --- ## Action Dispatch @@ -591,14 +1082,63 @@ function take_action(finding): # Also create a Priority/Critical bug issue create_bug_issue(finding) + elif finding.type == "force_merge_detected": + # An agent used the FORBIDDEN force_merge flag + # Create a Priority/Critical issue AND alert product-builder + create_bug_issue(finding) + post comment on session state issue: + f"[WATCHDOG] CRITICAL: force_merge detected! + {finding.detail} + Session: {finding.session_id} + Evidence: {finding.evidence[:200]} + Action: Created bug issue. The offending agent definition + may need to be corrected — consider creating a 'needs + feedback' issue for agent improvement. + + --- + **Automated by CleverAgents Bot** + Supervisor: System Watchdog | Agent: ca-system-watchdog" + + elif finding.type == "direct_push_to_master": + # An agent pushed directly to master, bypassing PR process + create_bug_issue(finding) + post comment on session state issue: + f"[WATCHDOG] CRITICAL: Direct push to master detected! + {finding.detail} + + --- + **Automated by CleverAgents Bot** + Supervisor: System Watchdog | Agent: ca-system-watchdog" + elif finding.severity == "HIGH": if finding.type == "closed_wrong_state": dispatch_one_off("ca-state-reconciler", finding) - elif finding.type == "zombie_supervisor": + elif finding.type in ("zombie_supervisor", "stuck_supervisor", + "looping_supervisor"): # Post alert on session state issue for product-builder + # Include session ID and evidence so product-builder can + # abort and re-launch the specific supervisor post_zombie_alert(finding) + elif finding.type == "context_exhaustion": + # Post alert — the supervisor should be re-launched with + # fresh context by the product-builder + post_zombie_alert(finding) + + elif finding.type == "high_error_rate": + # Post diagnostic on session state issue — the agent may + # need its configuration adjusted or its target fixed + post comment on session state issue: + f"[WATCHDOG] High error rate detected: + {finding.detail} + Recommend: check if the underlying resource (API, repo) + is accessible, or if the agent definition needs updating. + + --- + **Automated by CleverAgents Bot** + Supervisor: System Watchdog | Agent: ca-system-watchdog" + elif finding.type == "wrong_milestone_priority": # Post comment on the issue being worked on post_priority_warning(finding) @@ -606,6 +1146,10 @@ function take_action(finding): elif finding.type in ("in_review_but_merged",): dispatch_one_off("ca-state-reconciler", finding) + elif finding.type == "type_ignore_suppression": + # Create a Priority/High bug issue — this violates CONTRIBUTING.md + create_finding_issue(finding) + else: # Create an issue for the finding create_finding_issue(finding) @@ -651,8 +1195,13 @@ post comment on session state issue: - State label mismatches: - Priority ordering issues: - PR pipeline issues: - - Zombie supervisors: + - Zombie/stuck/looping supervisors: - Missing labels/links: + - Session introspection findings: + - Misbehavior (force_merge, direct push): + - Stuck/looping agents: + - Context exhaustion signals: + - Cross-agent conflicts: - One-off agents dispatched this period: - Issues created this period: @@ -716,10 +1265,16 @@ This agent should never voluntarily exit. If forced to exit: ``` CYCLES_COMPLETED: FINDINGS: - - Critical: (quality gate violations, broken master) - - High: (wrong states, zombies, priority issues) - - Medium: (missing labels, stale PRs) + - Critical: (quality gate violations, broken master, force_merge, direct push) + - High: (wrong states, zombies, stuck/looping agents, context exhaustion) + - Medium: (missing labels, stale PRs, cross-agent conflicts) - Low: (minor compliance gaps) +SESSION_INTROSPECTION: + - Sessions analyzed: + - Misbehavior detected: + - Zombie/stuck/looping: + - Context exhaustion: + - Cross-agent conflicts: ONE_OFF_AGENTS_DISPATCHED: ISSUES_CREATED: ```