chore(agents): add deep session introspection to system watchdog

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.
This commit is contained in:
2026-04-03 18:13:19 +00:00
parent 8c13e63c75
commit 77427bd7d3
+581 -26
View File
@@ -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 <cycle>:
Session health overview:
<newline-joined summary_lines>
Findings this pass: <len(findings)>
---
**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: <count>
- Priority ordering issues: <count>
- PR pipeline issues: <count>
- Zombie supervisors: <count>
- Zombie/stuck/looping supervisors: <count>
- Missing labels/links: <count>
- Session introspection findings: <count>
- Misbehavior (force_merge, direct push): <count>
- Stuck/looping agents: <count>
- Context exhaustion signals: <count>
- Cross-agent conflicts: <count>
- One-off agents dispatched this period: <count>
- Issues created this period: <count>
@@ -716,10 +1265,16 @@ This agent should never voluntarily exit. If forced to exit:
```
CYCLES_COMPLETED: <N>
FINDINGS:
- Critical: <N> (quality gate violations, broken master)
- High: <N> (wrong states, zombies, priority issues)
- Medium: <N> (missing labels, stale PRs)
- Critical: <N> (quality gate violations, broken master, force_merge, direct push)
- High: <N> (wrong states, zombies, stuck/looping agents, context exhaustion)
- Medium: <N> (missing labels, stale PRs, cross-agent conflicts)
- Low: <N> (minor compliance gaps)
SESSION_INTROSPECTION:
- Sessions analyzed: <N>
- Misbehavior detected: <N>
- Zombie/stuck/looping: <N>
- Context exhaustion: <N>
- Cross-agent conflicts: <N>
ONE_OFF_AGENTS_DISPATCHED: <N>
ISSUES_CREATED: <N>
```