diff --git a/examples/templates/README.md b/examples/templates/README.md index f289946563..13ec72ac65 100644 --- a/examples/templates/README.md +++ b/examples/templates/README.md @@ -44,3 +44,4 @@ uv run python -m exports.my_research_agent --input '{"topic": "..."}' |----------|-------------| | [deep_research_agent](deep_research_agent/) | Interactive research agent that searches diverse sources, evaluates findings with user checkpoints, and produces a cited HTML report | | [tech_news_reporter](tech_news_reporter/) | Researches the latest technology and AI news from the web and produces a well-organized report | +| [oss_contributor_accelerator](oss_contributor_accelerator/) | Discovers high-leverage GitHub issues for a contributor and generates an execution-ready contribution brief with PR drafts | diff --git a/examples/templates/oss_contributor_accelerator/DEMO.md b/examples/templates/oss_contributor_accelerator/DEMO.md new file mode 100644 index 0000000000..b6acd12e2c --- /dev/null +++ b/examples/templates/oss_contributor_accelerator/DEMO.md @@ -0,0 +1,41 @@ +# Demo Script (10-15 minutes) + +Use this to show the template's value quickly. + +## 1) Setup + +```bash +./quickstart.sh +``` + +## 2) Copy template to exports + +```bash +cp -r examples/templates/oss_contributor_accelerator exports/oss_contributor_accelerator +``` + +## 3) Run in TUI + +```bash +hive run exports/oss_contributor_accelerator --tui +``` + +## 4) Suggested demo input + +- Repo: `aden-hive/hive` +- Skills: `Python, agent frameworks, test automation, docs` +- Time: `6 hours/week` +- Preference: `quick win with meaningful impact` + +## 5) Success signal + +Agent should produce: +- ranked shortlist of 8 issues +- user-selected 1-3 issues +- `contribution_brief.md` with implementation + tests + PR draft text + +## 6) What to share with reviewers + +- Brief screenshot/video of issue ranking +- Final contribution_brief.md artifact +- One concrete issue picked and implemented from the brief diff --git a/examples/templates/oss_contributor_accelerator/README.md b/examples/templates/oss_contributor_accelerator/README.md new file mode 100644 index 0000000000..def94ce9b2 --- /dev/null +++ b/examples/templates/oss_contributor_accelerator/README.md @@ -0,0 +1,65 @@ +# OSS Contributor Accelerator + +**Version**: 1.0.0 +**Type**: Multi-node agent template + +## Overview + +Find high-leverage issues in an OSS repo and generate an execution-ready contribution brief. + +This template is designed for contributors who want to ship meaningful PRs quickly: + +1. Collect contributor context (skills, time, preferences) +2. Discover and rank issues from GitHub +3. Let the user pick 1-3 targets +4. Generate a markdown brief with implementation plan, test strategy, and PR drafts + +## Architecture + +Execution flow: + +```text +intake -> issue-scout -> selection -> contribution-pack +``` + +### Nodes + +1. **intake** (client-facing) + - Collects repo + contributor context + - Output: `repo_context` + +2. **issue-scout** + - Uses GitHub APIs to shortlist/rank 8 issues + - Output: `shortlisted_issues` + - Tools: `github_get_repo`, `github_list_issues`, `github_get_issue` + +3. **selection** (client-facing) + - Presents ranked issues and captures user selection + - Output: `selected_issues` + +4. **contribution-pack** (client-facing, terminal) + - Generates `contribution_brief.md` + - Includes implementation plan, tests, PR title/body drafts, maintainer update draft + - Tools: `save_data`, `append_data`, `serve_file_to_user` + +## Goal Criteria + +- High-impact issue shortlist +- Strong contributor-to-issue fit +- Actionable implementation and test plans +- PR-ready draft quality + +## Usage + +```bash +# Copy template to exports +cp -r examples/templates/oss_contributor_accelerator exports/my_oss_agent + +# Run +uv run python -m exports.my_oss_agent run +``` + +## Notes + +- Requires GitHub credentials for GitHub tools. +- Designed with human-in-the-loop selection before deep planning. diff --git a/examples/templates/oss_contributor_accelerator/__init__.py b/examples/templates/oss_contributor_accelerator/__init__.py new file mode 100644 index 0000000000..36ea7f7b70 --- /dev/null +++ b/examples/templates/oss_contributor_accelerator/__init__.py @@ -0,0 +1,5 @@ +"""OSS Contributor Accelerator template.""" + +from .agent import OSSContributorAcceleratorAgent, default_agent + +__all__ = ["OSSContributorAcceleratorAgent", "default_agent"] diff --git a/examples/templates/oss_contributor_accelerator/__main__.py b/examples/templates/oss_contributor_accelerator/__main__.py new file mode 100644 index 0000000000..2438308c8c --- /dev/null +++ b/examples/templates/oss_contributor_accelerator/__main__.py @@ -0,0 +1,90 @@ +"""CLI entry point for OSS Contributor Accelerator.""" + +import asyncio +import json +import logging +import sys + +import click + +from .agent import default_agent + + +def setup_logging(verbose=False, debug=False): + """Configure logging.""" + if debug: + level, fmt = logging.DEBUG, "%(asctime)s %(name)s: %(message)s" + elif verbose: + level, fmt = logging.INFO, "%(message)s" + else: + level, fmt = logging.WARNING, "%(levelname)s: %(message)s" + logging.basicConfig(level=level, format=fmt, stream=sys.stderr) + logging.getLogger("framework").setLevel(level) + + +@click.group() +@click.version_option(version="1.0.0") +def cli(): + """OSS Contributor Accelerator - turn issue backlog into shippable plans.""" + + +@cli.command() +@click.option("--mock", is_flag=True, help="Run in mock mode") +@click.option("--quiet", "-q", is_flag=True, help="Only output result JSON") +@click.option("--verbose", "-v", is_flag=True, help="Show execution details") +@click.option("--debug", is_flag=True, help="Show debug logging") +def run(mock, quiet, verbose, debug): + """Execute the workflow.""" + if not quiet: + setup_logging(verbose=verbose, debug=debug) + + context = {} + result = asyncio.run(default_agent.run(context, mock_mode=mock)) + + output_data = { + "success": result.success, + "steps_executed": result.steps_executed, + "output": result.output, + } + if result.error: + output_data["error"] = result.error + + click.echo(json.dumps(output_data, indent=2, default=str)) + sys.exit(0 if result.success else 1) + + +@cli.command() +@click.option("--json", "output_json", is_flag=True) +def info(output_json): + """Show agent information.""" + info_data = default_agent.info() + if output_json: + click.echo(json.dumps(info_data, indent=2)) + else: + click.echo(f"Agent: {info_data['name']}") + click.echo(f"Version: {info_data['version']}") + click.echo(f"Description: {info_data['description']}") + click.echo(f"\nNodes: {', '.join(info_data['nodes'])}") + click.echo(f"Client-facing: {', '.join(info_data['client_facing_nodes'])}") + click.echo(f"Entry: {info_data['entry_node']}") + click.echo(f"Terminal: {', '.join(info_data['terminal_nodes'])}") + + +@cli.command() +def validate(): + """Validate agent structure.""" + validation = default_agent.validate() + if validation["valid"]: + click.echo("Agent is valid") + if validation["warnings"]: + for warning in validation["warnings"]: + click.echo(f" WARNING: {warning}") + else: + click.echo("Agent has errors:") + for error in validation["errors"]: + click.echo(f" ERROR: {error}") + sys.exit(0 if validation["valid"] else 1) + + +if __name__ == "__main__": + cli() diff --git a/examples/templates/oss_contributor_accelerator/agent.json b/examples/templates/oss_contributor_accelerator/agent.json new file mode 100644 index 0000000000..997e11d2b9 --- /dev/null +++ b/examples/templates/oss_contributor_accelerator/agent.json @@ -0,0 +1,117 @@ +{ + "agent": { + "id": "oss_contributor_accelerator", + "name": "OSS Contributor Accelerator", + "version": "1.0.0", + "description": "Rank high-leverage OSS issues and generate execution-ready contribution briefs with PR drafts." + }, + "graph": { + "id": "oss-contributor-accelerator-graph", + "goal_id": "oss-contributor-accelerator", + "version": "1.0.0", + "entry_node": "intake", + "entry_points": { + "start": "intake" + }, + "pause_nodes": [], + "terminal_nodes": [ + "contribution-pack" + ], + "nodes": [ + { + "id": "intake", + "name": "Intake", + "node_type": "event_loop", + "client_facing": true, + "input_keys": [], + "output_keys": [ + "repo_context" + ] + }, + { + "id": "issue-scout", + "name": "Issue Scout", + "node_type": "event_loop", + "client_facing": false, + "input_keys": [ + "repo_context" + ], + "output_keys": [ + "shortlisted_issues" + ], + "tools": [ + "github_get_repo", + "github_list_issues", + "github_get_issue" + ] + }, + { + "id": "selection", + "name": "Selection", + "node_type": "event_loop", + "client_facing": true, + "input_keys": [ + "shortlisted_issues", + "repo_context" + ], + "output_keys": [ + "selected_issues" + ] + }, + { + "id": "contribution-pack", + "name": "Contribution Pack", + "node_type": "event_loop", + "client_facing": true, + "input_keys": [ + "selected_issues", + "repo_context" + ], + "output_keys": [ + "contribution_brief" + ], + "tools": [ + "save_data", + "append_data", + "serve_file_to_user" + ] + } + ], + "edges": [ + { + "id": "intake-to-issue-scout", + "source": "intake", + "target": "issue-scout", + "condition": "on_success", + "priority": 1 + }, + { + "id": "issue-scout-to-selection", + "source": "issue-scout", + "target": "selection", + "condition": "on_success", + "priority": 1 + }, + { + "id": "selection-to-contribution-pack", + "source": "selection", + "target": "contribution-pack", + "condition": "on_success", + "priority": 1 + } + ] + }, + "required_tools": [ + "github_get_repo", + "github_list_issues", + "github_get_issue", + "save_data", + "append_data", + "serve_file_to_user" + ], + "metadata": { + "created_by": "template", + "node_count": 4, + "edge_count": 3 + } +} diff --git a/examples/templates/oss_contributor_accelerator/agent.py b/examples/templates/oss_contributor_accelerator/agent.py new file mode 100644 index 0000000000..73cc9f9541 --- /dev/null +++ b/examples/templates/oss_contributor_accelerator/agent.py @@ -0,0 +1,302 @@ +"""Agent graph construction for OSS Contributor Accelerator.""" + +from pathlib import Path + +from framework.graph import Constraint, EdgeCondition, EdgeSpec, Goal, SuccessCriterion +from framework.graph.checkpoint_config import CheckpointConfig +from framework.graph.edge import GraphSpec +from framework.graph.executor import ExecutionResult +from framework.llm import LiteLLMProvider +from framework.runner.tool_registry import ToolRegistry +from framework.runtime.agent_runtime import AgentRuntime, create_agent_runtime +from framework.runtime.execution_stream import EntryPointSpec + +from .config import default_config, metadata +from .nodes import ( + contribution_pack_node, + intake_node, + issue_scout_node, + selection_node, +) + + +goal = Goal( + id="oss-contributor-accelerator", + name="OSS Contributor Accelerator", + description=( + "Identify high-leverage issues in an OSS repository, rank them based on " + "contributor fit and expected impact, and generate an execution-ready " + "contribution brief with PR-ready artifacts." + ), + success_criteria=[ + SuccessCriterion( + id="issue-quality", + description="Shortlist contains issues with meaningful user/project impact", + metric="impact_score", + target=">=0.8", + weight=0.25, + ), + SuccessCriterion( + id="fit-accuracy", + description="Selected issues match contributor skills and time budget", + metric="fit_score", + target=">=0.85", + weight=0.25, + ), + SuccessCriterion( + id="execution-clarity", + description="Contribution brief has concrete implementation and testing steps", + metric="plan_actionability", + target=">=0.9", + weight=0.3, + ), + SuccessCriterion( + id="shipping-readiness", + description="PR draft content is clear enough to submit with minor edits", + metric="pr_draft_quality", + target=">=0.85", + weight=0.2, + ), + ], + constraints=[ + Constraint( + id="no-fabrication", + description="Do not invent issue/repo facts not present in retrieved data", + constraint_type="accuracy", + category="integrity", + ), + Constraint( + id="human-choice", + description="Only generate deep plans for issues explicitly selected by user", + constraint_type="behavioral", + category="user_control", + ), + Constraint( + id="high-signal-output", + description="Outputs must stay concise, technical, and actionable", + constraint_type="quality", + category="communication", + ), + ], +) + +nodes = [ + intake_node, + issue_scout_node, + selection_node, + contribution_pack_node, +] + +edges = [ + EdgeSpec( + id="intake-to-issue-scout", + source="intake", + target="issue-scout", + condition=EdgeCondition.ON_SUCCESS, + priority=1, + ), + EdgeSpec( + id="issue-scout-to-selection", + source="issue-scout", + target="selection", + condition=EdgeCondition.ON_SUCCESS, + priority=1, + ), + EdgeSpec( + id="selection-to-contribution-pack", + source="selection", + target="contribution-pack", + condition=EdgeCondition.ON_SUCCESS, + priority=1, + ), +] + +entry_node = "intake" +entry_points = {"start": "intake"} +pause_nodes: list[str] = [] +terminal_nodes = ["contribution-pack"] + + +class OSSContributorAcceleratorAgent: + """4-node pipeline for OSS issue selection and contribution planning.""" + + def __init__(self, config=None): + self.config = config or default_config + self.goal = goal + self.nodes = nodes + self.edges = edges + self.entry_node = entry_node + self.entry_points = entry_points + self.pause_nodes = pause_nodes + self.terminal_nodes = terminal_nodes + self._graph: GraphSpec | None = None + self._agent_runtime: AgentRuntime | None = None + self._tool_registry: ToolRegistry | None = None + self._storage_path: Path | None = None + + def _build_graph(self) -> GraphSpec: + return GraphSpec( + id="oss-contributor-accelerator-graph", + goal_id=self.goal.id, + version="1.0.0", + entry_node=self.entry_node, + entry_points=self.entry_points, + terminal_nodes=self.terminal_nodes, + pause_nodes=self.pause_nodes, + nodes=self.nodes, + edges=self.edges, + default_model=self.config.model, + max_tokens=self.config.max_tokens, + loop_config={ + "max_iterations": 120, + "max_tool_calls_per_turn": 30, + "max_history_tokens": 32000, + }, + conversation_mode="continuous", + identity_prompt=( + "You are an OSS contribution closer. You prioritize high-impact work, " + "maintain factual accuracy, and turn vague issue queues into shippable plans." + ), + ) + + def _setup(self, mock_mode=False) -> None: + self._storage_path = Path.home() / ".hive" / "agents" / "oss_contributor_accelerator" + self._storage_path.mkdir(parents=True, exist_ok=True) + + self._tool_registry = ToolRegistry() + + mcp_config_path = Path(__file__).parent / "mcp_servers.json" + if mcp_config_path.exists(): + self._tool_registry.load_mcp_config(mcp_config_path) + + llm = None + if not mock_mode: + llm = LiteLLMProvider( + model=self.config.model, + api_key=self.config.api_key, + api_base=self.config.api_base, + ) + + tool_executor = self._tool_registry.get_executor() + tools = list(self._tool_registry.get_tools().values()) + + self._graph = self._build_graph() + + checkpoint_config = CheckpointConfig( + enabled=True, + checkpoint_on_node_start=False, + checkpoint_on_node_complete=True, + checkpoint_max_age_days=7, + async_checkpoint=True, + ) + + entry_point_specs = [ + EntryPointSpec( + id="default", + name="Default", + entry_node=self.entry_node, + trigger_type="manual", + isolation_level="shared", + ) + ] + + self._agent_runtime = create_agent_runtime( + graph=self._graph, + goal=self.goal, + storage_path=self._storage_path, + entry_points=entry_point_specs, + llm=llm, + tools=tools, + tool_executor=tool_executor, + checkpoint_config=checkpoint_config, + ) + + async def start(self, mock_mode=False) -> None: + if self._agent_runtime is None: + self._setup(mock_mode=mock_mode) + if not self._agent_runtime.is_running: + await self._agent_runtime.start() + + async def stop(self) -> None: + if self._agent_runtime and self._agent_runtime.is_running: + await self._agent_runtime.stop() + self._agent_runtime = None + + async def trigger_and_wait( + self, + entry_point: str = "default", + input_data: dict | None = None, + timeout: float | None = None, + session_state: dict | None = None, + ) -> ExecutionResult | None: + if self._agent_runtime is None: + raise RuntimeError("Agent not started. Call start() first.") + + return await self._agent_runtime.trigger_and_wait( + entry_point_id=entry_point, + input_data=input_data or {}, + session_state=session_state, + ) + + async def run( + self, context: dict, mock_mode=False, session_state=None + ) -> ExecutionResult: + await self.start(mock_mode=mock_mode) + try: + result = await self.trigger_and_wait( + "default", context, session_state=session_state + ) + return result or ExecutionResult(success=False, error="Execution timeout") + finally: + await self.stop() + + def info(self): + return { + "name": metadata.name, + "version": metadata.version, + "description": metadata.description, + "goal": { + "name": self.goal.name, + "description": self.goal.description, + }, + "nodes": [n.id for n in self.nodes], + "edges": [e.id for e in self.edges], + "entry_node": self.entry_node, + "entry_points": self.entry_points, + "pause_nodes": self.pause_nodes, + "terminal_nodes": self.terminal_nodes, + "client_facing_nodes": [n.id for n in self.nodes if n.client_facing], + } + + def validate(self): + errors: list[str] = [] + warnings: list[str] = [] + + node_ids = {node.id for node in self.nodes} + for edge in self.edges: + if edge.source not in node_ids: + errors.append(f"Edge {edge.id}: source '{edge.source}' not found") + if edge.target not in node_ids: + errors.append(f"Edge {edge.id}: target '{edge.target}' not found") + + if self.entry_node not in node_ids: + errors.append(f"Entry node '{self.entry_node}' not found") + + for terminal in self.terminal_nodes: + if terminal not in node_ids: + errors.append(f"Terminal node '{terminal}' not found") + + for ep_id, node_id in self.entry_points.items(): + if node_id not in node_ids: + errors.append( + f"Entry point '{ep_id}' references unknown node '{node_id}'" + ) + + return { + "valid": len(errors) == 0, + "errors": errors, + "warnings": warnings, + } + + +default_agent = OSSContributorAcceleratorAgent() diff --git a/examples/templates/oss_contributor_accelerator/config.py b/examples/templates/oss_contributor_accelerator/config.py new file mode 100644 index 0000000000..35065febc8 --- /dev/null +++ b/examples/templates/oss_contributor_accelerator/config.py @@ -0,0 +1,25 @@ +"""Runtime configuration for OSS Contributor Accelerator.""" + +from dataclasses import dataclass + +from framework.config import RuntimeConfig + + +default_config = RuntimeConfig() + + +@dataclass +class AgentMetadata: + name: str = "OSS Contributor Accelerator" + version: str = "1.0.0" + description: str = ( + "Find high-leverage issues in an OSS repo, rank them by impact/fit, and " + "generate an execution-ready contribution brief with PR drafts." + ) + intro_message: str = ( + "I help you land meaningful OSS contributions fast. Share a repo and I’ll " + "shortlist high-impact issues, then build a contribution brief you can execute." + ) + + +metadata = AgentMetadata() diff --git a/examples/templates/oss_contributor_accelerator/mcp_servers.json b/examples/templates/oss_contributor_accelerator/mcp_servers.json new file mode 100644 index 0000000000..18ba362f80 --- /dev/null +++ b/examples/templates/oss_contributor_accelerator/mcp_servers.json @@ -0,0 +1,9 @@ +{ + "hive-tools": { + "transport": "stdio", + "command": "uv", + "args": ["run", "python", "mcp_server.py", "--stdio"], + "cwd": "../../../tools", + "description": "Hive tools MCP server" + } +} diff --git a/examples/templates/oss_contributor_accelerator/nodes/__init__.py b/examples/templates/oss_contributor_accelerator/nodes/__init__.py new file mode 100644 index 0000000000..5e417be082 --- /dev/null +++ b/examples/templates/oss_contributor_accelerator/nodes/__init__.py @@ -0,0 +1,168 @@ +"""Node definitions for OSS Contributor Accelerator.""" + +from framework.graph import NodeSpec + + +intake_node = NodeSpec( + id="intake", + name="Intake", + description="Collect repository + contributor context", + node_type="event_loop", + client_facing=True, + max_node_visits=1, + input_keys=[], + output_keys=["repo_context"], + success_criteria=( + "Repository target and contributor preferences are collected and normalized." + ), + system_prompt="""\ +You are an elite OSS strategy partner helping contributors pick winning issues. + +**Objective:** collect enough context to identify the best 8 issues for this contributor. + +1) Ask the user for: +- target repository in `owner/repo` format +- their strongest skills/stack +- available time per week +- preferred issue types (bug/docs/feature/refactor/tests) +- whether they prefer quick wins or deeper architectural work + +2) Keep questions concise and practical. If the user already provided data, do NOT ask again. + +3) Once you have enough context, call: +set_output("repo_context", "") + +Do not over-interview. Get the essentials and move on. +""", + tools=[], +) + + +issue_scout_node = NodeSpec( + id="issue-scout", + name="Issue Scout", + description="Find and rank the best OSS issues for the contributor", + node_type="event_loop", + client_facing=False, + max_node_visits=1, + input_keys=["repo_context"], + output_keys=["shortlisted_issues"], + success_criteria=( + "At least 8 candidate issues are ranked with clear fit rationale and execution risk." + ), + system_prompt="""\ +You are an OSS issue triage specialist. + +INPUT: `repo_context` JSON. + +Use GitHub tools to discover and rank opportunities: +1) github_get_repo(owner, repo) for repo context +2) github_list_issues(owner, repo, state="open", limit=100) +3) For promising issues, call github_get_issue(owner, repo, issue_number) + +Selection policy: +- Prioritize issues labeled `good first issue`, `help wanted`, `bug`, `enhancement`, `documentation`, `tests`. +- Exclude stale/blocked issues when signals suggest no movement. +- Prefer issues with clear acceptance criteria and reproducible context. + +Create a ranked shortlist of **8 issues**. For each issue include: +- issue_number +- title +- url +- labels +- impact_score (1-10) +- effort_score (1-10) +- confidence_score (1-10) +- why_fit (specific to contributor skills + weekly hours) +- first_90_min_steps +- risk_flags + +Then call: +set_output("shortlisted_issues", "") + +Be concrete. No vague rankings. +""", + tools=["github_get_repo", "github_list_issues", "github_get_issue"], +) + + +selection_node = NodeSpec( + id="selection", + name="Selection", + description="Present ranked issues and let user choose targets", + node_type="event_loop", + client_facing=True, + max_node_visits=1, + input_keys=["shortlisted_issues", "repo_context"], + output_keys=["selected_issues"], + success_criteria="User explicitly selects 1-3 issues to pursue.", + system_prompt="""\ +You are a pragmatic OSS mentor. + +1) Present all shortlisted issues in a numbered list with: +- title + issue number +- impact/effort/confidence +- 1-line why-fit summary + +2) Ask user to choose 1-3 issue numbers. + +3) After selection, call: +set_output("selected_issues", "") + +Do not re-rank unless user asks. Keep it crisp. +""", + tools=[], +) + + +contribution_pack_node = NodeSpec( + id="contribution-pack", + name="Contribution Pack", + description="Generate execution-ready contribution brief for selected issues", + node_type="event_loop", + client_facing=True, + max_node_visits=1, + input_keys=["selected_issues", "repo_context"], + output_keys=["contribution_brief"], + success_criteria=( + "A structured markdown brief is generated and shared with implementation plans and PR drafts." + ), + system_prompt="""\ +You are a senior OSS execution coach. + +Create a single markdown file named `contribution_brief.md`. + +**IMPORTANT:** Build it incrementally to avoid token overflow: +1) save_data with the report header and summary table +2) append_data once per selected issue section +3) append_data for final checklist and closing +4) serve_file_to_user(filename="contribution_brief.md") + +For EACH selected issue include: +- Issue snapshot (title, url, labels) +- Why this is high leverage +- Implementation plan (5-8 steps) +- Test/verification plan +- PR title draft +- PR body draft (Summary, Changes, Testing, Risks) +- Maintainer update comment draft +- 90-minute kickoff checklist + +Constraints: +- Never invent repo facts not grounded in issue data. +- Keep claims realistic and actionable. +- Optimize for shipping first meaningful PR quickly. + +Finish with: +set_output("contribution_brief", "Created contribution_brief.md for {N} selected issues") +""", + tools=["save_data", "append_data", "serve_file_to_user"], +) + + +__all__ = [ + "intake_node", + "issue_scout_node", + "selection_node", + "contribution_pack_node", +]