revona 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
revona-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: revona
3
+ Version: 1.0.0
4
+ Summary: Autonomous AI Engineering Platform — built by LX Obsidian Labs
5
+ Author: LX Obsidian Labs
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/lx-obsidian-labs/revona-cli
8
+ Project-URL: Repository, https://github.com/lx-obsidian-labs/revona-cli
9
+ Project-URL: Issues, https://github.com/lx-obsidian-labs/revona-cli/issues
10
+ Keywords: ai,cli,coding,agent,engineering,automation,nvidia,revona
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Build Tools
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: openai>=1.30
24
+ Requires-Dist: click>=8.1
25
+ Requires-Dist: rich>=13.0
26
+ Requires-Dist: requests>=2.31
@@ -0,0 +1,45 @@
1
+ from pathlib import Path
2
+
3
+ AGENT_DIR = Path(".agent")
4
+ CONFIG_PATH = AGENT_DIR / "config.toml"
5
+ SESSIONS_DIR = AGENT_DIR / "sessions"
6
+ INDEX_PATH = AGENT_DIR / "index.txt"
7
+ MODELS_CACHE_PATH = AGENT_DIR / "models.json"
8
+
9
+ DEFAULT_MODEL = "deepseek-ai/deepseek-v4-pro"
10
+ BASE_URL = "https://integrate.api.nvidia.com/v1"
11
+
12
+ IGNORE_DIRS = {
13
+ ".git", "node_modules", "__pycache__", ".venv", "venv",
14
+ "dist", "build", ".mypy_cache", ".pytest_cache", "target",
15
+ }
16
+ TEXT_EXTS = {
17
+ ".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".toml", ".md",
18
+ ".txt", ".yaml", ".yml", ".html", ".css", ".sh", ".rs",
19
+ ".go", ".java", ".c", ".cpp", ".h", ".sql", ".cfg", ".ini",
20
+ }
21
+
22
+ # Brand name
23
+ APP_NAME = "Revona CLI"
24
+ COMPANY = "LX Obsidian Labs"
25
+
26
+ # Brand colours (Rich markup)
27
+ C_PRIMARY = "black"
28
+ C_ACCENT = "bright_blue"
29
+ C_SUCCESS = "bright_green"
30
+ C_WARNING = "bright_yellow"
31
+ C_ERROR = "bright_red"
32
+ C_DIM = "bright_black"
33
+
34
+ REVONA_ASCII = """\
35
+ ██████╗ ███████╗██╗ ██╗ ██████╗ ███╗ ██╗ █████╗
36
+ ██╔══██╗██╔════╝██║ ██║██╔═══██╗████╗ ██║██╔══██╗
37
+ ██████╔╝█████╗ ██║ ██║██║ ██║██╔██╗ ██║███████║
38
+ ██╔══██╗██╔══╝ ╚██╗ ██╔╝██║ ██║██║╚██╗██║██╔══██║
39
+ ██║ ██║███████╗ ╚████╔╝ ╚██████╔╝██║ ╚████║██║ ██║
40
+ ╚═╝ ╚═╝╚══════╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝"""
41
+
42
+
43
+ def ensure_dirs() -> None:
44
+ AGENT_DIR.mkdir(exist_ok=True)
45
+ SESSIONS_DIR.mkdir(exist_ok=True)
@@ -0,0 +1,462 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from copy import deepcopy
5
+ from typing import Callable
6
+
7
+ from rich.markdown import Markdown
8
+ from rich.panel import Panel
9
+
10
+ from . import DEFAULT_MODEL
11
+ from .agents import AGENTS, AgentSpec, get as get_agent, resolve_tools
12
+ from .memory import IntelligenceEngine
13
+ from .mission import Mission, Task, TaskStatus
14
+ from .progress import ProgressEngine
15
+ from .prompts import SYSTEM_PROMPT
16
+ from .tools import TOOL_SCHEMAS, execute_tool
17
+ from .terminal import console
18
+
19
+ _intel = IntelligenceEngine()
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Low-level agent loop
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def _run_agent_loop(
27
+ client,
28
+ model: str,
29
+ system_prompt: str,
30
+ user_prompt: str,
31
+ allowed_tools: list[dict],
32
+ max_iter: int = 30,
33
+ on_tool: Callable | None = None,
34
+ ) -> tuple[str, list[dict]]:
35
+ """Run a single agent with its tools. Returns (final_text, messages)."""
36
+ messages = [{"role": "system", "content": system_prompt}]
37
+ messages.append({"role": "user", "content": user_prompt})
38
+
39
+ for i in range(max_iter):
40
+ resp = client.chat.completions.create(
41
+ model=model,
42
+ messages=messages,
43
+ tools=allowed_tools or None,
44
+ stream=False,
45
+ temperature=0.2,
46
+ )
47
+ msg = resp.choices[0].message
48
+
49
+ if msg.tool_calls:
50
+ messages.append({
51
+ "role": "assistant",
52
+ "content": msg.content or "",
53
+ "tool_calls": [
54
+ {
55
+ "id": tc.id,
56
+ "type": "function",
57
+ "function": {"name": tc.function.name, "arguments": tc.function.arguments},
58
+ }
59
+ for tc in msg.tool_calls
60
+ ],
61
+ })
62
+ for tc in msg.tool_calls:
63
+ args = json.loads(tc.function.arguments or "{}")
64
+ if on_tool:
65
+ on_tool(tc.function.name, args)
66
+ result = execute_tool(tc.function.name, args)
67
+ messages.append({
68
+ "role": "tool",
69
+ "tool_call_id": tc.id,
70
+ "content": result[:12000],
71
+ })
72
+ continue
73
+
74
+ final = msg.content or ""
75
+ messages.append({"role": "assistant", "content": final})
76
+ return final, messages
77
+
78
+ return "(max iterations reached)", messages
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Orchestrator
83
+ # ---------------------------------------------------------------------------
84
+
85
+ def decompose_into_tasks(
86
+ client, model: str, request: str, context: str
87
+ ) -> list[dict]:
88
+ """Ask the planner to break a request into a structured task list."""
89
+ planner_spec = get_agent("planner")
90
+ if not planner_spec:
91
+ return _default_tasks(request)
92
+
93
+ prompt = (
94
+ f"## Repository context\n{context[:20000]}\n\n"
95
+ f"## Request\n{request}\n\n"
96
+ "Break this request into a list of numbered tasks that can be executed "
97
+ "sequentially. Each task should be a single, focused action. "
98
+ "Format as a JSON list:\n"
99
+ '[{"id": "1", "label": "short label", "description": "what to do", '
100
+ '"agent": "builder|tester|reviewer"}]\n'
101
+ "Output ONLY the JSON, no other text."
102
+ )
103
+
104
+ try:
105
+ text, _ = _run_agent_loop(
106
+ client, model,
107
+ planner_spec.system_prompt,
108
+ prompt,
109
+ resolve_tools(planner_spec.allowed_tools),
110
+ max_iter=5,
111
+ )
112
+ # Extract JSON from response
113
+ text = text.strip()
114
+ if "```json" in text:
115
+ text = text.split("```json")[1].split("```")[0].strip()
116
+ elif "```" in text:
117
+ text = text.split("```")[1].split("```")[0].strip()
118
+ tasks = json.loads(text)
119
+ if isinstance(tasks, list):
120
+ return tasks
121
+ except Exception:
122
+ pass
123
+ return _default_tasks(request)
124
+
125
+
126
+ def _default_tasks(request: str) -> list[dict]:
127
+ """Fallback when decomposition fails."""
128
+ return [{"id": "1", "label": "Implement", "description": request, "agent": "builder"}]
129
+
130
+
131
+ def verify_changes(
132
+ client, model: str, mission: Mission,
133
+ ) -> tuple[bool, str]:
134
+ """Run the reviewer agent over changed files."""
135
+ reviewer = get_agent("reviewer")
136
+ if not reviewer:
137
+ return True, "no reviewer configured"
138
+
139
+ changed = list(mission.edited_files)
140
+ if not changed:
141
+ return True, "no files changed"
142
+
143
+ prompt = (
144
+ "Review these changed files for correctness, security, and style:\n"
145
+ + "\n".join(f"- {f}" for f in changed)
146
+ + "\n\nRun relevant lint/type-check commands. Report any issues."
147
+ )
148
+
149
+ text, _ = _run_agent_loop(
150
+ client, model,
151
+ reviewer.system_prompt,
152
+ prompt,
153
+ resolve_tools(reviewer.allowed_tools),
154
+ max_iter=15,
155
+ )
156
+ approved = "APPROVED" in text
157
+ return approved, text
158
+
159
+
160
+ def run_tests(
161
+ client, model: str, mission: Mission,
162
+ ) -> tuple[bool, str]:
163
+ """Run the tester agent."""
164
+ tester = get_agent("tester")
165
+ if not tester:
166
+ return True, "no tester configured"
167
+
168
+ prompt = (
169
+ "Discover and run the project's tests. "
170
+ "If none exist, check a few key files for correctness manually."
171
+ )
172
+
173
+ text, _ = _run_agent_loop(
174
+ client, model,
175
+ tester.system_prompt,
176
+ prompt,
177
+ resolve_tools(tester.allowed_tools),
178
+ max_iter=15,
179
+ )
180
+ passing = "ALL TESTS PASSING" in text
181
+ return passing, text
182
+
183
+
184
+ def execute_mission(
185
+ client,
186
+ model: str,
187
+ mission: Mission,
188
+ progress: ProgressEngine,
189
+ max_global_iter: int = 3,
190
+ ) -> Mission:
191
+ """Execute a mission: run each task with the right agent, verify, retry."""
192
+ for global_pass in range(max_global_iter):
193
+ if mission.all_done():
194
+ break
195
+
196
+ # Execute ready tasks
197
+ for task in mission.get_ready_tasks():
198
+ if task.status != TaskStatus.PENDING:
199
+ continue
200
+
201
+ task.status = TaskStatus.RUNNING
202
+ progress.start(task.id)
203
+
204
+ # Pick agent and model
205
+ agent_spec = get_agent(task.agent) or get_agent("builder")
206
+ task_model = task.model or model
207
+
208
+ tools = resolve_tools(agent_spec.allowed_tools) if agent_spec else TOOL_SCHEMAS
209
+
210
+ context = mission.context if global_pass == 0 else ""
211
+ prompt = (
212
+ f"## Task: {task.label}\n{task.description}\n\n"
213
+ f"## Repository context\n{context[:20000]}\n\n"
214
+ "Execute this task. Use the available tools."
215
+ )
216
+
217
+ def _on_tool(name, args):
218
+ if name in ("write_file", "edit_file"):
219
+ fp = args.get("path", "")
220
+ if fp:
221
+ mission.edited_files.add(fp)
222
+ task.files_changed.append(fp)
223
+
224
+ try:
225
+ text, _ = _run_agent_loop(
226
+ client, task_model,
227
+ agent_spec.system_prompt,
228
+ prompt,
229
+ tools,
230
+ max_iter=30,
231
+ on_tool=_on_tool,
232
+ )
233
+ task.result = text[:2000]
234
+
235
+ # Auto-verify: run build/lint/test command
236
+ shell_result = execute_tool("run_shell", {"command": "python -m pytest --version 2>nul || pytest --version 2>nul || echo no test framework found"})
237
+ has_tests = "pytest" in shell_result.lower()
238
+
239
+ if has_tests:
240
+ test_out = execute_tool("run_shell", {"command": "python -m pytest -x -q 2>&1 || echo no tests to run"})
241
+ if "FAILED" in test_out or "ERROR" in test_out:
242
+ task.status = TaskStatus.FAILED
243
+ task.error = "Tests failed"
244
+ progress.fail(task.id, "tests failing")
245
+ else:
246
+ task.status = TaskStatus.DONE
247
+ progress.done(task.id)
248
+ else:
249
+ task.status = TaskStatus.DONE
250
+ progress.done(task.id)
251
+
252
+ except Exception as e:
253
+ task.status = TaskStatus.FAILED
254
+ task.error = str(e)[:500]
255
+ progress.fail(task.id, str(e)[:80])
256
+
257
+ # Handle failures: retry or re-plan
258
+ for task in mission.tasks.values():
259
+ if task.status == TaskStatus.FAILED and task.retries < task.max_retries:
260
+ task.retries += 1
261
+ task.status = TaskStatus.PENDING
262
+ progress.add_step(f"{task.id}-retry{task.retries}", f"Retry {task.label} ({task.retries}/{task.max_retries})")
263
+ console.print(f"[yellow]Retrying {task.label}...[/]")
264
+
265
+ # Final review pass
266
+ if mission.edited_files:
267
+ progress.add_step("review", "Reviewing changes")
268
+ progress.start("review")
269
+ approved, review_text = verify_changes(client, model, mission)
270
+ if approved:
271
+ progress.done("review")
272
+ else:
273
+ progress.fail("review")
274
+ console.print(Panel(review_text[:2000], title="Review Issues"))
275
+
276
+ return mission
277
+
278
+
279
+ def orchestrator_build(
280
+ client,
281
+ model: str,
282
+ request: str,
283
+ progress: ProgressEngine,
284
+ ) -> Mission:
285
+ """Full orchestrated build: decompose → execute → verify → reflect."""
286
+ global _intel
287
+
288
+ progress.add_step("brain", "Loading repository intelligence")
289
+ progress.start("brain")
290
+ enriched = _intel.load_all(request)
291
+ progress.done("brain")
292
+
293
+ progress.add_step("decompose", "Decomposing request into tasks")
294
+ progress.start("decompose")
295
+
296
+ task_list = decompose_into_tasks(client, model, request, enriched)
297
+ progress.done("decompose")
298
+
299
+ mission = Mission(request, enriched)
300
+ for t in task_list:
301
+ dep_ids = t.get("depends_on", [])
302
+ if isinstance(dep_ids, str):
303
+ dep_ids = [dep_ids]
304
+ mission.add_task(Task(
305
+ id=t["id"],
306
+ label=t.get("label", t["id"]),
307
+ description=t.get("description", ""),
308
+ agent=t.get("agent", "builder"),
309
+ model=model,
310
+ depends_on=dep_ids,
311
+ ))
312
+ progress.add_step(t["id"], t.get("label", t["id"]))
313
+
314
+ mission = execute_mission(client, model, mission, progress)
315
+ mission.save()
316
+ progress.summary(mission.summary())
317
+
318
+ # Reflection phase
319
+ progress.add_step("reflect", "Reflecting on mission — extracting lessons")
320
+ progress.start("reflect")
321
+ _intel.after_mission(client, model, request, list(mission.tasks.values()), mission.edited_files)
322
+ progress.done("reflect")
323
+
324
+ return mission
325
+
326
+
327
+ # ---------------------------------------------------------------------------
328
+ # Keep backward-compatible wrappers
329
+ # ---------------------------------------------------------------------------
330
+
331
+ def run_agent(client, model: str, prompt: str, messages: list | None = None, max_iter: int = 30, tui_state=None) -> list:
332
+ """Simple chat loop (backward compatible). Optionally stream to a TUI state."""
333
+ agent_spec = get_agent("builder")
334
+ system = agent_spec.system_prompt if agent_spec else SYSTEM_PROMPT
335
+ tools = resolve_tools(agent_spec.allowed_tools) if agent_spec else TOOL_SCHEMAS
336
+
337
+ if messages is None:
338
+ messages = [{"role": "system", "content": system}]
339
+ messages.append({"role": "user", "content": prompt})
340
+
341
+ for i in range(max_iter):
342
+ resp = client.chat.completions.create(
343
+ model=model, messages=messages, tools=tools or None,
344
+ stream=False, temperature=0.2,
345
+ )
346
+ msg = resp.choices[0].message
347
+
348
+ if msg.tool_calls:
349
+ messages.append({
350
+ "role": "assistant", "content": msg.content or "",
351
+ "tool_calls": [
352
+ {"id": tc.id, "type": "function",
353
+ "function": {"name": tc.function.name, "arguments": tc.function.arguments}}
354
+ for tc in msg.tool_calls
355
+ ],
356
+ })
357
+ for tc in msg.tool_calls:
358
+ args = json.loads(tc.function.arguments or "{}")
359
+ detail = f"> {tc.function.name}({json.dumps(args)[:200]})"
360
+ if tui_state:
361
+ tui_state.add_activity(detail)
362
+ else:
363
+ console.print(f"[dim]{detail}[/]")
364
+ result = execute_tool(tc.function.name, args)
365
+ messages.append({"role": "tool", "tool_call_id": tc.id, "content": result[:12000]})
366
+ continue
367
+
368
+ stream = client.chat.completions.create(
369
+ model=model,
370
+ messages=messages + [{"role": "assistant", "content": msg.content}],
371
+ stream=True, temperature=0.2,
372
+ )
373
+ if tui_state:
374
+ tui_state.status_message = "Generating..."
375
+ parts = []
376
+ for chunk in stream:
377
+ if chunk.choices and chunk.choices[0].delta.content:
378
+ parts.append(chunk.choices[0].delta.content)
379
+ tui_state.update_stream("".join(parts))
380
+ tui_state.status_message = "Ready"
381
+ else:
382
+ console.print("[bold cyan]agent>[/] ", end="")
383
+ parts = []
384
+ for chunk in stream:
385
+ if chunk.choices and chunk.choices[0].delta.content:
386
+ console.print(chunk.choices[0].delta.content, end="", highlight=False)
387
+ parts.append(chunk.choices[0].delta.content)
388
+ console.print()
389
+ messages.append({"role": "assistant", "content": "".join(parts)})
390
+ return messages
391
+
392
+ msg = "[yellow]Reached max iterations.[/]"
393
+ if tui_state:
394
+ tui_state.add_activity(msg)
395
+ else:
396
+ console.print(msg)
397
+ return messages
398
+
399
+
400
+ def plan(client, model: str, request: str, context: str) -> list:
401
+ """Generate a plan (backward compatible)."""
402
+ planner_spec = get_agent("planner")
403
+ system = planner_spec.system_prompt if planner_spec else SYSTEM_PROMPT
404
+ messages = [{"role": "system", "content": system}]
405
+ messages.append({"role": "user", "content": f"## Repository context\n{context[:40000]}\n\n## Request\n{request}\n\nProduce a plan."})
406
+ resp = client.chat.completions.create(
407
+ model=model, messages=messages, stream=False, temperature=0.2,
408
+ )
409
+ plan_text = resp.choices[0].message.content or ""
410
+ messages.append({"role": "assistant", "content": plan_text})
411
+ console.print(Panel(Markdown(plan_text), title="Plan", border_style="green"))
412
+ return messages
413
+
414
+
415
+ def build(client, model: str, plan_messages: list, user_request: str, max_iter: int = 50) -> list:
416
+ """Simple build loop (backward compatible)."""
417
+ agent_spec = get_agent("builder")
418
+ system = agent_spec.system_prompt if agent_spec else SYSTEM_PROMPT
419
+ tools = resolve_tools(agent_spec.allowed_tools) if agent_spec else TOOL_SCHEMAS
420
+ plan_text = plan_messages[-1]["content"] if plan_messages[-1]["role"] == "assistant" else ""
421
+ messages = [{"role": "system", "content": f"{system}\n\n## Plan\n{plan_text}\n\nExecute this plan."}]
422
+ messages.append({"role": "user", "content": user_request})
423
+
424
+ for i in range(max_iter):
425
+ console.print(f"\n[bold blue]--- Step {i + 1}/{max_iter} ---[/]")
426
+ resp = client.chat.completions.create(
427
+ model=model, messages=messages, tools=tools, stream=False, temperature=0.2,
428
+ )
429
+ msg = resp.choices[0].message
430
+
431
+ if not msg.tool_calls:
432
+ stream = client.chat.completions.create(
433
+ model=model,
434
+ messages=messages + [{"role": "assistant", "content": msg.content}],
435
+ stream=True, temperature=0.2,
436
+ )
437
+ console.print("[bold cyan]builder>[/] ", end="")
438
+ parts = []
439
+ for chunk in stream:
440
+ if chunk.choices and chunk.choices[0].delta.content:
441
+ console.print(chunk.choices[0].delta.content, end="", highlight=False)
442
+ parts.append(chunk.choices[0].delta.content)
443
+ console.print()
444
+ messages.append({"role": "assistant", "content": "".join(parts)})
445
+ return messages
446
+
447
+ messages.append({
448
+ "role": "assistant", "content": msg.content or "",
449
+ "tool_calls": [
450
+ {"id": tc.id, "type": "function",
451
+ "function": {"name": tc.function.name, "arguments": tc.function.arguments}}
452
+ for tc in msg.tool_calls
453
+ ],
454
+ })
455
+ for tc in msg.tool_calls:
456
+ args = json.loads(tc.function.arguments or "{}")
457
+ console.print(f"[dim]> {tc.function.name}({json.dumps(args)[:200]})[/]")
458
+ result = execute_tool(tc.function.name, args)
459
+ messages.append({"role": "tool", "tool_call_id": tc.id, "content": result[:12000]})
460
+
461
+ console.print("[yellow]Builder reached max iterations.[/]")
462
+ return messages
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass
7
+ class AgentSpec:
8
+ name: str
9
+ description: str
10
+ system_prompt: str
11
+ allowed_tools: list[str] = field(default_factory=list)
12
+ default_model: str | None = None # None = use mission default
13
+
14
+
15
+ AGENTS: dict[str, AgentSpec] = {}
16
+
17
+
18
+ def register(spec: AgentSpec) -> AgentSpec:
19
+ AGENTS[spec.name.lower()] = spec
20
+ return spec
21
+
22
+
23
+ def get(name: str) -> AgentSpec | None:
24
+ return AGENTS.get(name.lower())
25
+
26
+
27
+ def resolve_tools(allowed: list[str]) -> list[dict]:
28
+ """Return the tool schemas for an agent's allowed tools."""
29
+ from .tools import TOOL_SCHEMAS, TOOL_INDEX
30
+
31
+ return [TOOL_SCHEMAS[TOOL_INDEX[n]] for n in allowed if n in TOOL_INDEX]
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Agent definitions
36
+ # ---------------------------------------------------------------------------
37
+
38
+ register(AgentSpec(
39
+ name="Planner",
40
+ description="Analyzes requests and produces structured plans. Read-only.",
41
+ allowed_tools=["read_file", "list_files", "grep_files", "web_fetch"],
42
+ system_prompt="""You are a software planning agent. Given a request and repository context,
43
+ produce a concrete, ordered implementation plan.
44
+
45
+ Rules:
46
+ - Use tools to explore the repo before planning.
47
+ - Never write code — your output is a plan only.
48
+ - Each step should specify: what file(s) to change, the approach, and verification.
49
+ - End with a "Risks" section.
50
+ - You cannot edit files or run shell commands.""",
51
+ ))
52
+
53
+ register(AgentSpec(
54
+ name="Builder",
55
+ description="Writes and edits code to implement plans. Full tool access.",
56
+ allowed_tools=["read_file", "write_file", "edit_file", "list_files", "grep_files", "run_shell", "web_fetch"],
57
+ system_prompt="""You are a coding agent. You implement plans by writing and editing code.
58
+
59
+ Rules:
60
+ - Read files before editing them. Never assume content.
61
+ - Make focused, minimal edits.
62
+ - After each file change, run the appropriate build/lint/test command.
63
+ - If a step fails, diagnose and fix it before moving on.
64
+ - Do not expose secrets. Do not run destructive commands (rm -rf, git push --force).
65
+ - When done, summarize what was built.""",
66
+ ))
67
+
68
+ register(AgentSpec(
69
+ name="Reviewer",
70
+ description="Reviews code for bugs, security issues, and style problems. Read + git only.",
71
+ allowed_tools=["read_file", "list_files", "grep_files", "run_shell"],
72
+ system_prompt="""You are a code reviewer. You inspect changes and report issues.
73
+
74
+ Rules:
75
+ - Read the changed files and review for: correctness, security, performance, style.
76
+ - Run lint/type-check commands to find issues.
77
+ - Report findings with file:line references.
78
+ - Do not edit files.
79
+ - If no issues, say "APPROVED".
80
+ - If issues found, list them clearly so a Builder can fix them.""",
81
+ ))
82
+
83
+ register(AgentSpec(
84
+ name="Tester",
85
+ description="Writes and runs tests. Has test framework + shell access.",
86
+ allowed_tools=["read_file", "write_file", "edit_file", "list_files", "grep_files", "run_shell"],
87
+ system_prompt="""You are a testing agent. You write and run tests for the code in this repository.
88
+
89
+ Rules:
90
+ - Discover the test framework from existing tests or pyproject.toml.
91
+ - Write tests that cover the implemented functionality.
92
+ - Run tests after writing. Fix any failures.
93
+ - If tests pass, say "ALL TESTS PASSING".
94
+ - If tests fail, fix the code or tests until they pass.""",
95
+ ))
96
+
97
+ register(AgentSpec(
98
+ name="Researcher",
99
+ description="Searches the web for documentation, APIs, and solutions.",
100
+ allowed_tools=["web_fetch", "list_files", "read_file"],
101
+ system_prompt="""You are a research agent. You find documentation and solutions from the web.
102
+
103
+ Rules:
104
+ - Use web_fetch to search for and retrieve documentation.
105
+ - Summarize findings concisely.
106
+ - Provide code examples when relevant.
107
+ - Always cite the source URL.
108
+ - Do not edit any files.""",
109
+ ))
110
+
111
+ register(AgentSpec(
112
+ name="Learner",
113
+ description="Reflects on completed missions, extracts lessons, rates code quality, and updates organizational knowledge.",
114
+ allowed_tools=["read_file", "list_files", "grep_files", "run_shell"],
115
+ system_prompt="""You are a learning and reflection agent. After every mission, you analyze what happened.
116
+
117
+ Rules:
118
+ - Read the important files that were changed.
119
+ - Reflect on: what worked, what failed, why, and whether the solution can be reused.
120
+ - Rate code quality and identify security issues.
121
+ - Output a structured JSON reflection (never edit files).
122
+ - Focus on extracting *reusable* lessons, not just summaries.""",
123
+ ))