CLIO-helper Architecture
March 2, 2026 · View on GitHub
This document describes the architecture, data flows, and design decisions for CLIO-helper.
System Overview
CLIO-helper is a polling daemon that monitors GitHub repositories and uses CLIO AI for automated analysis. It runs as a systemd user service (or launchd on macOS) and communicates with GitHub via the gh CLI.
graph TB
subgraph "CLIO-helper Daemon"
CLI[clio-helper CLI]
DM[Discussion Monitor]
IM[Issue Monitor]
PM[PR Monitor]
SM[Stale Monitor]
RM[Release Monitor]
GR[Guardrails]
AZ[Analyzer]
ST[(State DB<br/>SQLite)]
end
subgraph "External Services"
GH[GitHub API]
CLIO[CLIO AI Agent]
REPO[Local Repo Clones]
end
CLI --> DM & IM & PM & SM & RM
DM & IM & PM --> GR
DM & IM & PM --> AZ
AZ --> CLIO
CLIO --> REPO
DM & IM & PM & SM & RM --> GH
DM & IM & PM & SM & RM --> ST
Main Daemon Loop
The daemon runs a continuous poll cycle. Each iteration checks all enabled monitors, sleeps for the configured interval, then repeats.
flowchart TD
Start([clio-helper start]) --> LoadConfig[Load config<br/>~/.clio/helper-config.json]
LoadConfig --> InitState[Initialize State DB]
InitState --> InitMonitors[Initialize enabled monitors]
InitMonitors --> SyncRepos[Clone/pull repos<br/>for code context]
SyncRepos --> PollLoop{Poll cycle}
PollLoop --> Disc{Discussions<br/>enabled?}
Disc -- Yes --> RunDisc[DiscussionMonitor.poll_cycle]
Disc -- No --> Issue
RunDisc --> Issue{Issues<br/>enabled?}
Issue -- Yes --> RunIssue[IssueMonitor.poll_cycle]
Issue -- No --> PR
RunIssue --> PR{PRs<br/>enabled?}
PR -- Yes --> RunPR[PRMonitor.poll_cycle]
PR -- No --> Stale
RunPR --> Stale{Stale<br/>enabled?}
Stale -- Yes --> RunStale[StaleMonitor.poll_cycle]
Stale -- No --> Rel
RunStale --> Rel{Releases<br/>enabled?}
Rel -- Yes --> RunRel[ReleaseMonitor.poll_cycle]
Rel -- No --> Stats
RunRel --> Stats[Update stats]
Stats --> Once{--once flag?}
Once -- Yes --> Exit([Exit])
Once -- No --> Sleep[Sleep poll_interval_seconds]
Sleep --> PollLoop
Discussion Monitor Flow
The Discussion Monitor handles community Q&A in GitHub Discussions.
flowchart TD
Start([poll_cycle]) --> FetchRepo["For each repo"]
FetchRepo --> Fetch["Fetch 30 most recent<br/>updated discussions<br/>(gh API - GraphQL)"]
Fetch --> Filter{Filter}
Filter --> Locked{Locked?}
Locked -- Yes --> Skip1([Skip])
Locked -- No --> Answered{Answered?}
Answered -- Yes --> Skip2([Skip])
Answered -- No --> Bot{Bot author?}
Bot -- Yes --> Skip3([Skip])
Bot -- No --> Maintainer{Maintainer<br/>author?}
Maintainer -- Yes --> Skip4([Skip])
Maintainer -- No --> Age{Too old?}
Age -- Yes --> Skip5([Skip])
Age -- No --> Cooldown{In cooldown?}
Cooldown -- Yes --> Skip6([Skip])
Cooldown -- No --> MaxResp{Hit max<br/>responses?}
MaxResp -- Yes --> Handoff[Post handoff message<br/>to maintainer]
MaxResp -- No --> RateLimit{User rate<br/>limited?}
RateLimit -- Yes --> Skip7([Skip])
RateLimit -- No --> Process
Process[Process discussion] --> Guards[Guardrails pre-filter]
Guards --> Flagged{Flagged?}
Flagged -- Auto-moderate --> Close[Close + comment]
Flagged -- Flag only --> Alert[Log to alert file]
Alert --> Analyze
Flagged -- Clean --> Analyze
Analyze[Analyzer: run CLIO<br/>with full thread context] --> Action{Action?}
Action -- respond --> Post[Post comment<br/>via GraphQL]
Action -- skip --> RecordSkip[Record skip]
Action -- moderate --> Moderate[Close discussion]
Action -- flag --> Flag[Alert maintainer]
Post --> Record[Record in State DB]
RecordSkip --> Record
Moderate --> Record
Flag --> Record
Issue Triage Flow
The Issue Monitor performs deep codebase investigation for root cause analysis.
flowchart TD
Start([poll_cycle]) --> FetchRepo["For each repo"]
FetchRepo --> Fetch["Fetch open issues<br/>sorted by updated<br/>(REST API)"]
Fetch --> FilterPR{Is PR?<br/>filter out}
FilterPR -- PR --> Skip0([Skip])
FilterPR -- Issue --> Cooldown{In cooldown?}
Cooldown -- Yes --> Skip1([Skip])
Cooldown -- No --> Labels{Has triage<br/>labels?}
Labels -- Yes --> Skip2([Skip - already triaged])
Labels -- No --> Bot{Bot created?}
Bot -- Yes --> Skip3([Skip])
Bot -- No --> Triage
Triage[Build issue context] --> Context["Gather:<br/>• Issue title, body, author<br/>• Comments<br/>• Timeline events<br/>• Current labels<br/>• Linked commits"]
Context --> Analyze["Analyzer: run CLIO<br/>in repo directory"]
Analyze --> CLIO["CLIO investigates:<br/>• Searches codebase<br/>• Reads relevant files<br/>• Traces code paths<br/>• Returns JSON"]
CLIO --> Extract[Extract triage JSON]
Extract --> Apply{Apply triage}
Apply --> Comment["Post triage comment:<br/>• Classification table<br/>• Root cause analysis<br/>• Affected areas"]
Apply --> ApplyLabels["Apply labels:<br/>• bug/enhancement/etc.<br/>• priority:high/medium/etc.<br/>• area labels"]
Apply --> Assign["Assign to maintainer"]
Apply --> NeedsInfo{"needs-info?"}
NeedsInfo -- Yes --> RequestInfo[Post comment requesting<br/>specific missing info]
Apply --> CloseCheck{"close?"}
CloseCheck -- Yes --> CloseIssue[Close with reason]
Apply --> Addressed{"already-addressed?"}
Addressed -- Yes --> AddressedComment[Post addressed comment]
Comment --> Record[Record in State DB]
Pull Request Review Flow
The PR Monitor performs thorough code review with full source context.
flowchart TD
Start([poll_cycle]) --> FetchRepo["For each repo"]
FetchRepo --> Fetch["Fetch open PRs<br/>sorted by updated<br/>(REST API)"]
Fetch --> SHA{Same HEAD SHA<br/>as last review?}
SHA -- Yes --> Skip1([Skip - no new commits])
SHA -- No --> Cooldown{In cooldown?}
Cooldown -- Yes --> Skip2([Skip])
Cooldown -- No --> Draft{Draft PR?}
Draft -- Yes --> Skip3([Skip])
Draft -- No --> Bot{Bot author?}
Bot -- Yes --> Skip4([Skip])
Bot -- No --> Review
Review[Build PR context] --> Context["Gather:<br/>• PR title, description, author<br/>• Full diff (up to max_diff_size)<br/>• Changed files list<br/>• Existing comments"]
Context --> IsUpdate{Previous review<br/>exists?}
IsUpdate -- Yes --> MarkUpdate[Mark as re-review]
IsUpdate -- No --> Analyze
MarkUpdate --> Analyze["Analyzer: run CLIO<br/>in repo directory"]
Analyze --> CLIO["CLIO reviews:<br/>• Reads full source files<br/>• Checks logic & correctness<br/>• Evaluates naming & clarity<br/>• Finds missing checks<br/>• Reviews architecture<br/>• Checks style compliance<br/>• Scans for security issues<br/>• Detects malware patterns<br/>• Returns JSON with file_comments"]
CLIO --> Extract[Extract review JSON]
Extract --> Format["Format review comment:<br/>• Verdict banner<br/>• Summary & metrics<br/>• Security concerns<br/>• Per-file findings<br/>• Style & doc issues<br/>• Overall feedback"]
Format --> Post["Post review comment<br/>(marked 'Updated' if re-review)"]
Post --> Labels[Apply suggested labels]
Labels --> Record["Record in State DB<br/>(with HEAD SHA)"]
Stale Management Flow
The Stale Monitor uses graduated warnings before closing inactive items.
flowchart TD
Start([poll_cycle]) --> FetchRepo["For each repo"]
FetchRepo --> Issues["Fetch open issues<br/>sorted by least<br/>recently updated"]
FetchRepo --> PRs["Fetch open PRs<br/>sorted by least<br/>recently updated"]
Issues --> ForIssue["For each issue"]
ForIssue --> Protected{Protected?<br/>• keep-open label<br/>• pinned<br/>• milestoned<br/>• assigned<br/>• priority:critical/high}
Protected -- Yes --> Skip1([Skip])
Protected -- No --> IssueAge{"Age > close_days<br/>(default: 60)?"}
IssueAge -- Yes --> WasWarned{Was warned<br/>previously?}
WasWarned -- Yes --> Close["Close issue:<br/>• Post closure comment<br/>• Close as 'not planned'"]
WasWarned -- No --> Warn
IssueAge -- No --> WarnAge{"Age > warning_days<br/>(default: 30)?"}
WarnAge -- Yes --> AlreadyWarned{Already<br/>warned?}
AlreadyWarned -- Yes --> Skip2([Skip])
AlreadyWarned -- No --> Warn["Warn:<br/>• Post stale notice<br/>• Add 'stale' label<br/>• Note days remaining"]
WarnAge -- No --> Skip3([Skip])
PRs --> ForPR["For each PR"]
ForPR --> DraftPR{Draft?}
DraftPR -- Yes --> Skip4([Skip])
DraftPR -- No --> PRAge{"Age > pr_warning_days<br/>(default: 14)?"}
PRAge -- Yes --> PRWarned{Already<br/>warned?}
PRWarned -- No --> WarnPR["Post stale PR notice"]
PRWarned -- Yes --> Skip5([Skip])
PRAge -- No --> Skip6([Skip])
Close --> Record[Record in State DB]
Warn --> Record
WarnPR --> Record
Release Notes Flow
The Release Monitor generates categorized changelogs from conventional commits.
flowchart TD
Start([poll_cycle]) --> FetchRepo["For each repo"]
FetchRepo --> Fetch["Fetch latest release<br/>(REST API)"]
Fetch --> Processed{Already<br/>processed?}
Processed -- Yes --> Skip1([Skip])
Processed -- No --> HasNotes{Has manual notes<br/>> 100 chars?}
HasNotes -- Yes --> Skip2([Skip - respect manual notes])
HasNotes -- No --> Generate
Generate --> PrevTag["Find previous release tag"]
PrevTag --> Commits["Fetch commits between<br/>prev_tag...current_tag<br/>(Compare API)"]
Commits --> Parse["Parse conventional commits"]
Parse --> Categorize["Categorize:<br/>• feat → New Features<br/>• fix → Bug Fixes<br/>• refactor → Refactoring<br/>• docs → Documentation<br/>• test → Tests<br/>• chore → Maintenance<br/>• other → Other Changes"]
Categorize --> Breaking["Detect BREAKING CHANGE<br/>or ! prefix"]
Breaking --> Format["Format changelog markdown"]
Format --> Update["Update release body<br/>via PATCH API"]
Update --> Record[Record in State DB]
Analyzer Pipeline
The Analyzer is the bridge between monitors and CLIO AI. It handles prompt construction, CLIO execution, and response parsing.
flowchart TD
Monitor["Monitor calls<br/>analyzer.analyze(context)"] --> BuildPrompt["Build prompt:<br/>1. Load prompt template<br/>(file or built-in default)<br/>2. Append context data<br/>3. Strip invisible chars"]
BuildPrompt --> WriteTemp["Write prompt to<br/>temp file (UTF-8)"]
WriteTemp --> DetermineRepo["Determine repo path<br/>for code context"]
DetermineRepo --> RunCLIO["Run CLIO:<br/>cd repo_dir &&<br/>cat prompt.md |<br/>clio --new --model X --exit"]
RunCLIO --> Capture["Capture stdout<br/>(stderr discarded)"]
Capture --> StripANSI["Strip ANSI escape codes"]
StripANSI --> ExtractJSON{"Extract JSON"}
ExtractJSON --> CodeFence["Try: ```json ... ```"]
CodeFence -- Found --> Parse
CodeFence -- Not found --> Balanced["Try: balanced brace<br/>extraction (nested JSON)"]
Balanced -- Found --> Parse
Balanced -- Not found --> Simple["Try: simple<br/>{...} match"]
Simple -- Found --> Parse
Simple -- Not found --> Fallback["Return raw message<br/>as respond action"]
Parse["Parse JSON"] --> Detect{"Detect format"}
Detect -- "Has 'action' field" --> Discussion["Discussion format:<br/>{action, message, reason}"]
Detect -- "Has 'classification'" --> Triage["Issue triage format:<br/>{classification, root_cause, ...}"]
Detect -- "Has 'recommendation'" --> Review["PR review format:<br/>{recommendation, file_comments, ...}"]
Detect -- "Neither" --> GenericRespond["Generic respond:<br/>wrap in {action: respond}"]
Discussion --> Return["Return to monitor"]
Triage --> Return
Review --> Return
GenericRespond --> Return
Fallback --> Return
Guardrails Pipeline
Content safety checks run before AI analysis for discussions.
flowchart TD
Input["User content"] --> Check["Guardrails.check(content)"]
Check --> Injection["Prompt injection scan:<br/>• 'ignore previous instructions'<br/>• 'new system prompt'<br/>• 'you are now'<br/>• Role-play patterns<br/>• Jailbreak patterns<br/>(Score: 3+ = flag, 5+ = block)"]
Check --> Invisible["Invisible character scan:<br/>• Zero-width joiners/spaces<br/>• Unicode tags (U+E0000+)<br/>• Directional overrides<br/>• Variation selectors<br/>• Punycode IDN domains"]
Check --> Encoded["Encoded content scan:<br/>• Base64 strings (40+ chars)<br/>• Hex-encoded sequences<br/>• Excessive URL encoding"]
Check --> Harmful["Harmful content scan:<br/>• 'how to hack'<br/>• 'create malware'<br/>• Exploitation patterns"]
Check --> Social["Social engineering scan:<br/>• 'I'm the admin'<br/>• 'this is urgent'<br/>• Authority claims"]
Check --> Spam["Spam scan:<br/>• Shortened URLs<br/>• Commercial keywords"]
Injection & Invisible & Encoded & Harmful & Social & Spam --> Combine["Combine results"]
Combine --> Result{"Verdict"}
Result -- "No flags" --> Clean["✅ Clean<br/>Proceed to Analyzer"]
Result -- "Flags detected<br/>(advisory)" --> Flagged["⚠️ Flagged<br/>Log alert, proceed with caution"]
Result -- "Auto-moderate<br/>(high severity)" --> Blocked["🛑 Blocked<br/>Auto-moderate: close thread"]
State Database Schema
erDiagram
discussion_checks {
TEXT discussion_id PK "e.g. 'issue:owner/repo#42'"
INTEGER last_checked "Unix timestamp"
TEXT last_action "e.g. 'reviewed:sha:abc12345'"
INTEGER check_count "Number of checks"
}
responses {
INTEGER id PK "Auto-increment"
TEXT discussion_id FK
TEXT action "respond/skip/moderate/flag"
TEXT message "Posted response text"
INTEGER posted_at "Unix timestamp"
}
users {
TEXT username PK "GitHub username"
INTEGER first_seen "Unix timestamp"
INTEGER response_count "Total responses to user"
INTEGER last_interaction "Unix timestamp"
}
user_responses {
INTEGER id PK "Auto-increment"
TEXT username FK
TEXT discussion_id
INTEGER responded_at "Unix timestamp"
}
error_log {
INTEGER id PK "Auto-increment"
TEXT error_type
TEXT message
INTEGER occurred_at "Unix timestamp"
}
schema_info {
TEXT key PK "e.g. 'version'"
TEXT value "e.g. '3'"
}
discussion_checks ||--o{ responses : "discussion_id"
users ||--o{ user_responses : "username"
ID Conventions:
- Discussions:
disc:owner/repo#number - Issues:
issue:owner/repo#number - Pull Requests:
pr:owner/repo#number - Stale items:
stale:owner/repo#numberorstale:owner/repo#prN - Releases:
release:owner/repo:tag
Module Dependency Graph
graph LR
CLI[clio-helper] --> DM[DiscussionMonitor]
CLI --> IM[IssueMonitor]
CLI --> PM[PRMonitor]
CLI --> SM[StaleMonitor]
CLI --> RM[ReleaseMonitor]
DM --> AZ[Analyzer]
DM --> GR[Guardrails]
DM --> ST[State]
IM --> AZ
IM --> ST
PM --> AZ
PM --> ST
SM --> ST
RM --> ST
AZ --> CLIO["CLIO (external)"]
DM & IM & PM & SM & RM --> GH["gh CLI (external)"]
Deployment
graph TB
subgraph "Host Machine"
subgraph "systemd user service"
Daemon["clio-helper daemon"]
end
subgraph "~/.clio/"
Config["helper-config.json"]
StateDB["helper-state.db"]
Log["helper-daemon.log"]
Alerts["helper-alerts.log"]
subgraph "repos/"
Repo1["org/project-1/"]
Repo2["org/project-2/"]
RepoN["org/project-N/"]
end
end
CLIO["CLIO installation"]
GH["gh CLI"]
end
subgraph "GitHub"
API["GitHub API"]
Repos["Repositories"]
Disc["Discussions"]
Issues["Issues"]
PRs["Pull Requests"]
Releases["Releases"]
end
Daemon --> Config
Daemon --> StateDB
Daemon --> Log
Daemon --> Alerts
Daemon --> CLIO
Daemon --> GH
GH --> API
API --> Disc & Issues & PRs & Releases
CLIO --> Repo1 & Repo2 & RepoN
Design Decisions
Why CLIO instead of direct API calls?
CLIO provides tool-calling capabilities (file search, file read, semantic search) that allow the AI to investigate the codebase during analysis. A direct API call would only see the prompt text - CLIO can actually explore the code.
Why gh CLI instead of raw HTTP?
The gh CLI handles authentication, pagination, and GraphQL complexity. It's a well-maintained tool that simplifies GitHub API interactions significantly, especially for the Discussions GraphQL API which requires complex query construction.
Why SQLite for state?
- Zero configuration (no server to run)
- Atomic writes (no corruption on crash)
- Single file (easy to backup/inspect)
- Concurrent reads (daemon is single-threaded, but safe if queried externally)
- Built-in to Perl via DBD::SQLite
Why poll instead of webhooks?
- No inbound network requirements (works behind NAT/firewall)
- No public endpoint to secure
- Simpler deployment (just a service, no web server)
- Resilient to missed events (polls catch everything, webhooks can miss)
- Configurable frequency (can run less often to save API quota)
Why separate posting_token?
Allows using a bot account for posting comments while using a personal token for reading. This provides clearer attribution (comments come from "clio-bot" not your personal account) and allows different permission scopes.