Implement AGENT-002 Governance Analyst as running TypeScript code#15
Implement AGENT-002 Governance Analyst as running TypeScript code#15glandua wants to merge 1 commit intoregen-network:mainfrom
Conversation
First working agent in the Regen Agentic Tokenomics framework. Implements all 3 governance analyst workflows from Phase 2.2 specs: - WF-GA-01: Proposal Analysis & Summarization - WF-GA-02: Voting Outcome Prediction & Alerts - WF-GA-03: Post-Vote Analysis & Reporting Architecture: - Direct Cosmos LCD client (no MCP dependency) - OODA loop engine matching Phase 2.4 spec - Claude-powered analysis (Orient + Decide phases only) - SQLite local state (swappable to PostgreSQL) - Console + Discord webhook output - Layer 1 only: read-only, informational, no on-chain actions Standalone Node.js — no ElizaOS dependency. Can be wrapped as ElizaOS plugin later without changing core logic. Typechecks clean. Zero runtime dependencies beyond @anthropic-ai/sdk and better-sqlite3. https://claude.ai/code/session_01HApBbQuEP529cmiPijDsaU
Summary of ChangesHello @glandua, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request delivers the initial implementation of AGENT-002, a governance analyst agent for the Regen Network. The agent automates the analysis of governance proposals, monitors voting progress, and generates post-vote reports, providing critical informational insights. It leverages an OODA loop architecture and integrates with Claude AI for sophisticated data interpretation, operating as a read-only Layer 1 component to enhance transparency and stakeholder awareness within the network. Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces the AGENT-002 Governance Analyst, a well-architected, standalone TypeScript agent designed to monitor and analyze Regen Network governance proposals using the Anthropic Claude LLM. It follows the OODA loop pattern and uses a local SQLite store for state. A medium-severity vulnerability related to Indirect Prompt Injection was identified, where untrusted data from blockchain proposals is directly embedded into LLM prompts, potentially allowing an attacker to manipulate the agent's output and broadcast malicious content to Discord. Remediation involves using structured prompts with clear delimiters for untrusted data. Furthermore, a critical bug related to the handling of bondedTokens affects all three workflows, causing incorrect data to be used for analysis. Suggestions are also provided to improve maintainability and correctness by addressing hardcoded values in the LLM system prompt, magic numbers for proposal statuses, and an invalid default model name in the configuration.
| const report = await generatePostVoteReport( | ||
| proposal, | ||
| tally, | ||
| "0", | ||
| simpleVotes | ||
| ); |
There was a problem hiding this comment.
The generatePostVoteReport function is called with a hardcoded "0" for bondedTokens. This will cause the turnout calculation within the function to be incorrect (likely resulting in 0%), leading to a flawed post-vote analysis.
The bondedTokens value is fetched correctly in the observe phase but is not propagated to the decide phase. You should pass this value through the orient phase to make it available here.
| const report = await generatePostVoteReport( | |
| proposal, | |
| tally, | |
| "0", | |
| simpleVotes | |
| ); | |
| const report = await generatePostVoteReport( | |
| proposal, | |
| tally, | |
| orientation.bondedTokens, | |
| simpleVotes | |
| ); | |
| const analysis = await analyzeProposal( | ||
| proposal, | ||
| tally, | ||
| "0" // bondedTokens passed separately | ||
| ); |
There was a problem hiding this comment.
The analyzeProposal function is called with a hardcoded "0" for the bondedTokens parameter. This will provide incorrect information to the LLM, as the prompt for analysis includes the Bonded tokens value.
The bondedTokens value is correctly fetched in the observe phase but is not passed down to the decide phase. This value should be passed through the orient phase to be used here.
| const analysis = await analyzeProposal( | |
| proposal, | |
| tally, | |
| "0" // bondedTokens passed separately | |
| ); | |
| const analysis = await analyzeProposal( | |
| proposal, | |
| tally, | |
| orientation.bondedTokens | |
| ); | |
| const statusReport = await assessVotingStatus( | ||
| item.proposal, | ||
| item.tally, | ||
| "0", // passed via tally context | ||
| item.previousSnapshot | ||
| ); |
There was a problem hiding this comment.
The assessVotingStatus function is called with a hardcoded "0" for bondedTokens. This is incorrect and will lead to an inaccurate turnout calculation within assessVotingStatus, as well as providing wrong information in the LLM prompt.
The bondedTokens value is fetched in the observe phase and should be passed down through the orient phase to be available here.
| const statusReport = await assessVotingStatus( | |
| item.proposal, | |
| item.tally, | |
| "0", // passed via tally context | |
| item.previousSnapshot | |
| ); | |
| const statusReport = await assessVotingStatus( | |
| item.proposal, | |
| item.tally, | |
| item.bondedTokens, | |
| item.previousSnapshot | |
| ); | |
| Regen Network Context: | ||
| - Cosmos SDK-based blockchain for ecological assets (eco-credits) | ||
| - ~224M REGEN total supply, ~3.2M in community pool | ||
| - 75 active validators, 33.4% quorum, 50% pass threshold | ||
| - 61+ historical governance proposals | ||
| - Core modules: x/ecocredit, x/gov, x/staking, x/marketplace |
There was a problem hiding this comment.
The SYSTEM_PROMPT contains several hardcoded values (total supply, community pool size, validator count, proposal count) that will become stale over time. Providing outdated context to the LLM can lead to inaccurate analyses. These values should be dynamic.
Consider converting the SYSTEM_PROMPT into a function that accepts an object with up-to-date chain state, which can be fetched during the observe phase of the workflows. This ensures the agent always operates with the most current information.
| tally: TallyResult, | ||
| bondedTokens: string | ||
| ): Promise<string> { | ||
| const prompt = buildAnalysisPrompt(proposal, tally, bondedTokens); |
There was a problem hiding this comment.
The agent constructs prompts for the Anthropic Claude LLM by directly concatenating untrusted data from blockchain governance proposals (e.g., proposal.content.description, proposal.content.title). Since anyone can submit a proposal to the Regen Network blockchain, an attacker can craft a malicious proposal containing instructions designed to override the agent's system prompt (Indirect Prompt Injection).
Because the agent is fully automated and its analysis is automatically posted to a Discord webhook (in src/output.ts), a successful prompt injection could allow an attacker to broadcast arbitrary messages, phishing links, or misinformation to the community Discord channel, appearing as if they were generated by the trusted analyst agent.
To remediate this, use clear delimiters (e.g., XML tags like <proposal_description>...</proposal_description>) to wrap untrusted input and instruct the LLM in the system prompt to treat content within those tags as data only, not instructions. Additionally, consider implementing a secondary check or sanitization step for the LLM's output before it is posted to external channels.
| const prompt = `Provide a voting status update for this Regen Network governance proposal. | ||
|
|
||
| ## Proposal | ||
| - ID: #${proposal.id} | ||
| - Title: ${proposal.content.title} | ||
| - Type: ${proposal.content["@type"]} | ||
| - Status: ${proposal.status} | ||
| - Voting ends: ${proposal.voting_end_time} (${hoursRemaining.toFixed(1)} hours remaining) | ||
|
|
||
| ## Current Tally (uregen) | ||
| - Yes: ${tally.yes} (${pct(tally.yes, totalVoted)}%) | ||
| - No: ${tally.no} (${pct(tally.no, totalVoted)}%) | ||
| - Abstain: ${tally.abstain} (${pct(tally.abstain, totalVoted)}%) | ||
| - No with Veto: ${tally.no_with_veto} (${pct(tally.no_with_veto, totalVoted)}%) | ||
|
|
||
| ## Participation | ||
| - Total voted: ${totalVoted.toString()} uregen | ||
| - Bonded tokens: ${bondedTokens} uregen | ||
| - Current turnout: ${turnout.toFixed(1)}% | ||
| - Quorum required: 33.4% | ||
| - Quorum met: ${turnout >= 33.4 ? "YES" : "NO"} | ||
|
|
||
| ${previousSnapshot ? `## Previous Snapshot\n${previousSnapshot}` : "## No previous snapshot available."} | ||
|
|
||
| Provide: | ||
| 1. A 2-sentence summary of current state | ||
| 2. Whether quorum is at risk | ||
| 3. Projected outcome (PASS/FAIL/UNCERTAIN) with confidence (0.0-1.0) | ||
| 4. Alert level (NORMAL/HIGH/CRITICAL) based on quorum risk, close vote, or time pressure | ||
| 5. Key changes since last snapshot (if available) | ||
|
|
||
| Format as a concise markdown report.`; |
| const prompt = `Generate a post-vote analysis report for this finalized Regen Network governance proposal. | ||
|
|
||
| ## Proposal | ||
| - ID: #${proposal.id} | ||
| - Title: ${proposal.content.title} | ||
| - Type: ${proposal.content["@type"]} | ||
| - Outcome: ${outcome} | ||
|
|
||
| ## Description | ||
| ${proposal.content.description.slice(0, 2000)} | ||
|
|
||
| ## Final Tally (uregen) | ||
| - Yes: ${tally.yes} (${pct(tally.yes, totalVoted)}%) | ||
| - No: ${tally.no} (${pct(tally.no, totalVoted)}%) | ||
| - Abstain: ${tally.abstain} (${pct(tally.abstain, totalVoted)}%) | ||
| - No with Veto: ${tally.no_with_veto} (${pct(tally.no_with_veto, totalVoted)}%) | ||
| - Total turnout: ${turnout.toFixed(1)}% | ||
|
|
||
| ## Vote Count | ||
| - Total distinct voters: ${votes.length} | ||
|
|
||
| Provide: | ||
| 1. Executive summary (2-3 sentences) | ||
| 2. Analysis of the result — was it decisive or contested? | ||
| 3. Turnout analysis — high/low compared to typical Regen proposals | ||
| 4. Implications — what does this mean for the network going forward? | ||
| 5. Notable patterns (e.g., high veto %, low turnout, etc.) | ||
|
|
||
| Format as a structured markdown report with ## headings.`; |
| function buildAnalysisPrompt( | ||
| proposal: Proposal, | ||
| tally: TallyResult, | ||
| bondedTokens: string | ||
| ): string { | ||
| const totalVoted = | ||
| BigInt(tally.yes) + | ||
| BigInt(tally.no) + | ||
| BigInt(tally.abstain) + | ||
| BigInt(tally.no_with_veto); | ||
|
|
||
| return `Analyze this Regen Network governance proposal in detail. | ||
|
|
||
| ## Proposal Data | ||
| - ID: #${proposal.id} | ||
| - Title: ${proposal.content.title} | ||
| - Type: ${proposal.content["@type"]} | ||
| - Status: ${proposal.status} | ||
| - Submitted: ${proposal.submit_time} | ||
| - Voting Start: ${proposal.voting_start_time} | ||
| - Voting End: ${proposal.voting_end_time} | ||
| - Deposit: ${proposal.total_deposit.map((d) => `${d.amount} ${d.denom}`).join(", ")} | ||
|
|
||
| ## Description | ||
| ${proposal.content.description.slice(0, 3000)} | ||
|
|
||
| ## Current Tally (uregen) | ||
| - Yes: ${tally.yes} (${pct(tally.yes, totalVoted)}%) | ||
| - No: ${tally.no} (${pct(tally.no, totalVoted)}%) | ||
| - Abstain: ${tally.abstain} (${pct(tally.abstain, totalVoted)}%) | ||
| - No with Veto: ${tally.no_with_veto} (${pct(tally.no_with_veto, totalVoted)}%) | ||
| - Bonded tokens: ${bondedTokens} | ||
|
|
||
| Provide a comprehensive analysis with: | ||
| 1. **TL;DR** — One-sentence summary | ||
| 2. **Category** — parameter_change | software_upgrade | community_pool_spend | credit_class | currency_allowlist | text_signaling | other | ||
| 3. **Impact Assessment** (Technical / Economic / Governance — rate each Low/Medium/High with explanation) | ||
| 4. **Risk Factors** — What could go wrong? | ||
| 5. **Historical Context** — How does this compare to previous Regen proposals? | ||
| 6. **Stakeholder Impacts** — Who benefits, who bears risk? | ||
|
|
||
| Format as a structured markdown document.`; | ||
| } |
| POLL_INTERVAL_SECONDS=300 | ||
|
|
||
| # Optional: Claude model to use (default: claude-sonnet-4-5-20250929) | ||
| ANTHROPIC_MODEL=claude-sonnet-4-5-20250929 |
There was a problem hiding this comment.
The default model name claude-sonnet-4-5-20250929 appears to be invalid. The versioning and future date suggest it might be a placeholder or a typo. To prevent runtime errors for users setting up the agent, it would be better to use a known valid model. For example, the latest Sonnet model is claude-3.5-sonnet-20240620.
ANTHROPIC_MODEL=claude-3.5-sonnet-20240620
| async getVotingProposals(): Promise<Proposal[]> { | ||
| return this.listProposals("2"); // PROPOSAL_STATUS_VOTING_PERIOD | ||
| } | ||
|
|
||
| async getPassedProposals(): Promise<Proposal[]> { | ||
| return this.listProposals("3"); // PROPOSAL_STATUS_PASSED | ||
| } | ||
|
|
||
| async getRejectedProposals(): Promise<Proposal[]> { | ||
| return this.listProposals("4"); // PROPOSAL_STATUS_REJECTED | ||
| } | ||
|
|
||
| async getDepositProposals(): Promise<Proposal[]> { | ||
| return this.listProposals("1"); // PROPOSAL_STATUS_DEPOSIT_PERIOD | ||
| } |
There was a problem hiding this comment.
The methods for fetching proposals by status (e.g., getVotingProposals) use magic strings like "2", "3", etc. This reduces code readability and maintainability, as the meaning of these numbers isn't immediately clear and they could change in future API versions.
It's recommended to define these statuses as a constant enum-like object and reference them by name.
First working agent in the Regen Agentic Tokenomics framework. Implements all 3 governance analyst workflows from Phase 2.2 specs:
Architecture:
Standalone Node.js — no ElizaOS dependency. Can be wrapped as ElizaOS plugin later without changing core logic.
Typechecks clean. Zero runtime dependencies beyond @anthropic-ai/sdk and better-sqlite3.
https://claude.ai/code/session_01HApBbQuEP529cmiPijDsaU