gemcode 0.3.31__tar.gz → 0.3.32__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.
- {gemcode-0.3.31/src/gemcode.egg-info → gemcode-0.3.32}/PKG-INFO +1 -1
- {gemcode-0.3.31 → gemcode-0.3.32}/pyproject.toml +1 -1
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/agent.py +7 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/callbacks.py +22 -0
- gemcode-0.3.32/src/gemcode/permissions.py +206 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/repl_commands.py +28 -9
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/repl_slash.py +125 -4
- gemcode-0.3.32/src/gemcode/review_agent.py +142 -0
- gemcode-0.3.32/src/gemcode/session_store.py +195 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/__init__.py +3 -1
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/bash.py +146 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tui/input_handler.py +2 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tui/scrollback.py +13 -0
- {gemcode-0.3.31 → gemcode-0.3.32/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode.egg-info/SOURCES.txt +2 -0
- gemcode-0.3.31/src/gemcode/permissions.py +0 -5
- {gemcode-0.3.31 → gemcode-0.3.32}/LICENSE +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/MANIFEST.in +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/README.md +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/setup.cfg +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/config.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/version.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_credentials.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_paths.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_permissions.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_tools.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.31 → gemcode-0.3.32}/tests/test_workspace_hints.py +0 -0
|
@@ -449,6 +449,13 @@ You have native deep thinking capability — use it actively:
|
|
|
449
449
|
- For **dev servers**: `bash("npm run dev", background=True, cwd_subdir="frontend")`
|
|
450
450
|
- For **subfolders**: `bash("cargo build --release", cwd_subdir="backend")`
|
|
451
451
|
|
|
452
|
+
- **`bash_stream`** — streaming version of bash: yields stdout line-by-line in real-time. Use for long-running commands where the user wants to see live progress:
|
|
453
|
+
- `bash_stream("pytest -v tests/")` — see each test result as it runs
|
|
454
|
+
- `bash_stream("npm run build")` — watch build progress in real-time
|
|
455
|
+
- `bash_stream("pip install -r requirements.txt")` — see install progress
|
|
456
|
+
- `bash_stream("tail -f logs/app.log", timeout_seconds=60)` — live log watching
|
|
457
|
+
- Prefer `bash_stream` over `bash` whenever the command takes > 5 seconds and progress visibility matters
|
|
458
|
+
|
|
452
459
|
- **`run_command`** — simple single-executable calls without shell features:
|
|
453
460
|
- `run_command("npm", args=["install", "--legacy-peer-deps"])` — clean npm install
|
|
454
461
|
- `run_command("python3", args=["-m", "pytest", "--version"])` — version check
|
|
@@ -157,6 +157,24 @@ def make_before_tool_callback(cfg: GemCodeConfig):
|
|
|
157
157
|
except Exception:
|
|
158
158
|
pass
|
|
159
159
|
|
|
160
|
+
# ── Permission rules (.gemcode/settings.json allow/deny) ──────────────
|
|
161
|
+
# Evaluated before the normal permission flow so explicit allow/deny rules
|
|
162
|
+
# override cfg.permission_mode and interactive prompts.
|
|
163
|
+
try:
|
|
164
|
+
from gemcode.permissions import check_rules
|
|
165
|
+
rule_result = check_rules(name, args or {}, cfg.project_root)
|
|
166
|
+
if rule_result == "deny":
|
|
167
|
+
return {
|
|
168
|
+
"error": f"Tool call blocked by .gemcode/settings.json deny rule for '{name}'.",
|
|
169
|
+
"error_kind": _ERROR_KIND_PERMISSION_DENIED,
|
|
170
|
+
}
|
|
171
|
+
if rule_result == "allow":
|
|
172
|
+
# Explicit allow — skip the normal permission prompt entirely
|
|
173
|
+
# but still run post-tool hook (logging/audit).
|
|
174
|
+
pass # fall through to tool execution
|
|
175
|
+
except Exception:
|
|
176
|
+
rule_result = None
|
|
177
|
+
|
|
160
178
|
streak = 0
|
|
161
179
|
if tool_context is not None:
|
|
162
180
|
try:
|
|
@@ -181,6 +199,10 @@ def make_before_tool_callback(cfg: GemCodeConfig):
|
|
|
181
199
|
"error_kind": _ERROR_KIND_CIRCUIT_BREAKER,
|
|
182
200
|
}
|
|
183
201
|
|
|
202
|
+
# If permission rules explicitly allowed this tool call, skip the gate entirely.
|
|
203
|
+
if rule_result == "allow" and (name in MUTATING_TOOLS or is_computer_tool):
|
|
204
|
+
return None # allow without prompting
|
|
205
|
+
|
|
184
206
|
if name in MUTATING_TOOLS or is_computer_tool:
|
|
185
207
|
if cfg.permission_mode == "strict":
|
|
186
208
|
if is_computer_tool:
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Permission rules engine — inspired by Claude Code's allow/deny pattern system.
|
|
3
|
+
|
|
4
|
+
Users define rules in `.gemcode/settings.json` (project) or `~/.gemcode/settings.json` (global).
|
|
5
|
+
Rules are evaluated for every tool call: deny first, then allow, then default.
|
|
6
|
+
|
|
7
|
+
Schema:
|
|
8
|
+
{
|
|
9
|
+
"permissions": {
|
|
10
|
+
"allow": [
|
|
11
|
+
"bash(git *)",
|
|
12
|
+
"bash(npm run *)",
|
|
13
|
+
"bash(pytest *)",
|
|
14
|
+
"read_file(*)",
|
|
15
|
+
"write_file(src/**)"
|
|
16
|
+
],
|
|
17
|
+
"deny": [
|
|
18
|
+
"bash(rm -rf *)",
|
|
19
|
+
"bash(curl *)",
|
|
20
|
+
"bash(wget *)",
|
|
21
|
+
"read_file(.env)",
|
|
22
|
+
"read_file(.env.*)",
|
|
23
|
+
"write_file(secrets/**)"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Pattern syntax (same as Claude Code's permission rules):
|
|
29
|
+
- "bash" — matches ALL bash calls
|
|
30
|
+
- "bash(*)" — same as above
|
|
31
|
+
- "bash(git *)" — bash calls whose command starts with "git "
|
|
32
|
+
- "write_file(src/**)" — write_file calls where path matches src/**
|
|
33
|
+
- "read_file(.env)" — exact path match for .env
|
|
34
|
+
- "*" — matches all tool calls
|
|
35
|
+
|
|
36
|
+
Evaluation order: deny → allow → default (ask/yes_to_all/strict from cfg)
|
|
37
|
+
First matching rule wins.
|
|
38
|
+
|
|
39
|
+
Files (merged in order — later rules take precedence):
|
|
40
|
+
1. ~/.gemcode/settings.json (user-global)
|
|
41
|
+
2. .gemcode/settings.json (project-specific, can override global)
|
|
42
|
+
|
|
43
|
+
Reloaded on every tool call (file is stat-cached to avoid repeated disk reads).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import fnmatch
|
|
49
|
+
import json
|
|
50
|
+
import os
|
|
51
|
+
import time
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
from typing import Any
|
|
54
|
+
|
|
55
|
+
# ── Cache so we don't re-read files every millisecond ────────────────────────
|
|
56
|
+
_file_cache: dict[str, tuple[float, float, list]] = {} # path → (mtime, stat_time, rules)
|
|
57
|
+
_CACHE_TTL = 2.0 # seconds
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _read_settings_rules(path: Path) -> list[dict]:
|
|
61
|
+
"""Read and cache permission rules from a settings file."""
|
|
62
|
+
path_str = str(path)
|
|
63
|
+
now = time.monotonic()
|
|
64
|
+
if path_str in _file_cache:
|
|
65
|
+
cached_mtime, cached_time, cached_rules = _file_cache[path_str]
|
|
66
|
+
if now - cached_time < _CACHE_TTL:
|
|
67
|
+
return cached_rules
|
|
68
|
+
|
|
69
|
+
if not path.is_file():
|
|
70
|
+
_file_cache[path_str] = (0.0, now, [])
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
mtime = path.stat().st_mtime
|
|
75
|
+
if path_str in _file_cache and _file_cache[path_str][0] == mtime:
|
|
76
|
+
# File hasn't changed — just refresh the TTL
|
|
77
|
+
old = _file_cache[path_str]
|
|
78
|
+
_file_cache[path_str] = (old[0], now, old[2])
|
|
79
|
+
return old[2]
|
|
80
|
+
|
|
81
|
+
data = json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
|
82
|
+
perms = data.get("permissions") or {}
|
|
83
|
+
rules: list[dict] = []
|
|
84
|
+
for pattern in perms.get("deny") or []:
|
|
85
|
+
rules.append({"action": "deny", "pattern": str(pattern)})
|
|
86
|
+
for pattern in perms.get("allow") or []:
|
|
87
|
+
rules.append({"action": "allow", "pattern": str(pattern)})
|
|
88
|
+
_file_cache[path_str] = (mtime, now, rules)
|
|
89
|
+
return rules
|
|
90
|
+
except Exception:
|
|
91
|
+
_file_cache[path_str] = (0.0, now, [])
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def load_rules(project_root: Path) -> list[dict]:
|
|
96
|
+
"""
|
|
97
|
+
Load merged rules from user-global and project settings.
|
|
98
|
+
Order: global first (lower priority), project second (higher priority / overrides).
|
|
99
|
+
"""
|
|
100
|
+
global_rules = _read_settings_rules(Path.home() / ".gemcode" / "settings.json")
|
|
101
|
+
project_rules = _read_settings_rules(project_root / ".gemcode" / "settings.json")
|
|
102
|
+
# Deny rules always evaluated first regardless of file order, so combine and re-sort
|
|
103
|
+
all_rules = global_rules + project_rules
|
|
104
|
+
# Stable sort: deny rules before allow rules (deny has priority)
|
|
105
|
+
deny_rules = [r for r in all_rules if r["action"] == "deny"]
|
|
106
|
+
allow_rules = [r for r in all_rules if r["action"] == "allow"]
|
|
107
|
+
return deny_rules + allow_rules
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _parse_pattern(pattern: str) -> tuple[str, str | None]:
|
|
111
|
+
"""
|
|
112
|
+
Parse a rule pattern into (tool_glob, arg_glob | None).
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
"bash" → ("bash", None)
|
|
116
|
+
"bash(*)" → ("bash", "*")
|
|
117
|
+
"bash(git *)" → ("bash", "git *")
|
|
118
|
+
"write_file(src/*)→ ("write_file", "src/*")
|
|
119
|
+
"*" → ("*", None)
|
|
120
|
+
"""
|
|
121
|
+
pattern = pattern.strip()
|
|
122
|
+
paren_open = pattern.find("(")
|
|
123
|
+
if paren_open == -1:
|
|
124
|
+
return (pattern, None)
|
|
125
|
+
tool_glob = pattern[:paren_open].strip()
|
|
126
|
+
arg_part = pattern[paren_open + 1:]
|
|
127
|
+
if arg_part.endswith(")"):
|
|
128
|
+
arg_part = arg_part[:-1]
|
|
129
|
+
return (tool_glob, arg_part.strip() or None)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_primary_arg(tool_name: str, args: dict[str, Any]) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Extract the primary argument to match against the pattern's arg_glob.
|
|
135
|
+
For bash: the command string.
|
|
136
|
+
For file tools: the path argument.
|
|
137
|
+
For everything else: concatenate all string args.
|
|
138
|
+
"""
|
|
139
|
+
if tool_name in ("bash", "run_command"):
|
|
140
|
+
return (
|
|
141
|
+
args.get("command") or
|
|
142
|
+
args.get("cmd") or
|
|
143
|
+
args.get("args", [""])[0] if isinstance(args.get("args"), list) else args.get("args", "")
|
|
144
|
+
) or ""
|
|
145
|
+
# File tools — use first path-like arg
|
|
146
|
+
for key in ("path", "file_path", "file", "filename", "dest", "src"):
|
|
147
|
+
val = args.get(key)
|
|
148
|
+
if val and isinstance(val, str):
|
|
149
|
+
return val
|
|
150
|
+
# Fallback: join all string values
|
|
151
|
+
return " ".join(str(v) for v in args.values() if isinstance(v, str))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def check_rules(
|
|
155
|
+
tool_name: str,
|
|
156
|
+
args: dict[str, Any],
|
|
157
|
+
project_root: Path,
|
|
158
|
+
) -> str | None:
|
|
159
|
+
"""
|
|
160
|
+
Evaluate permission rules for a tool call.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
"allow" — explicit allow rule matched → skip normal permission prompts
|
|
164
|
+
"deny" — explicit deny rule matched → block the tool call
|
|
165
|
+
None — no rule matched → use default behavior (cfg.permission_mode)
|
|
166
|
+
"""
|
|
167
|
+
rules = load_rules(project_root)
|
|
168
|
+
if not rules:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
primary_arg = _get_primary_arg(tool_name, args)
|
|
172
|
+
|
|
173
|
+
for rule in rules:
|
|
174
|
+
tool_glob, arg_glob = _parse_pattern(rule["pattern"])
|
|
175
|
+
|
|
176
|
+
# Match tool name
|
|
177
|
+
if tool_glob != "*" and not fnmatch.fnmatch(tool_name, tool_glob):
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Match argument (if pattern specifies one)
|
|
181
|
+
if arg_glob is not None and arg_glob != "*":
|
|
182
|
+
if not fnmatch.fnmatch(primary_arg, arg_glob):
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
return rule["action"] # "allow" or "deny"
|
|
186
|
+
|
|
187
|
+
return None # no rule matched
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def describe_rules(project_root: Path) -> list[str]:
|
|
191
|
+
"""Human-readable rule listing for /permissions output."""
|
|
192
|
+
rules = load_rules(project_root)
|
|
193
|
+
if not rules:
|
|
194
|
+
return [" (no rules — using default permission_mode)"]
|
|
195
|
+
lines: list[str] = []
|
|
196
|
+
deny_rules = [r for r in rules if r["action"] == "deny"]
|
|
197
|
+
allow_rules = [r for r in rules if r["action"] == "allow"]
|
|
198
|
+
if deny_rules:
|
|
199
|
+
lines.append(" deny:")
|
|
200
|
+
for r in deny_rules:
|
|
201
|
+
lines.append(f" ✗ {r['pattern']}")
|
|
202
|
+
if allow_rules:
|
|
203
|
+
lines.append(" allow:")
|
|
204
|
+
for r in allow_rules:
|
|
205
|
+
lines.append(f" ✓ {r['pattern']}")
|
|
206
|
+
return lines
|
|
@@ -70,6 +70,10 @@ def format_model_lines(cfg: GemCodeConfig) -> list[str]:
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def format_permissions_lines(cfg: GemCodeConfig) -> list[str]:
|
|
73
|
+
from gemcode.permissions import describe_rules
|
|
74
|
+
rules_lines = describe_rules(cfg.project_root)
|
|
75
|
+
settings_file = cfg.project_root / ".gemcode" / "settings.json"
|
|
76
|
+
global_settings = os.path.expanduser("~/.gemcode/settings.json")
|
|
73
77
|
ac = cfg.allow_commands
|
|
74
78
|
if ac is None:
|
|
75
79
|
preview = "(default allowlist)"
|
|
@@ -78,14 +82,23 @@ def format_permissions_lines(cfg: GemCodeConfig) -> list[str]:
|
|
|
78
82
|
preview = ", ".join(names[:16])
|
|
79
83
|
if len(names) > 16:
|
|
80
84
|
preview += f", … (+{len(names) - 16} more)"
|
|
81
|
-
|
|
85
|
+
lines = [
|
|
82
86
|
"Permissions:",
|
|
83
|
-
f" permission_mode: {cfg.permission_mode}",
|
|
84
|
-
f" yes_to_all: {cfg.yes_to_all}",
|
|
85
|
-
f"
|
|
86
|
-
f"
|
|
87
|
-
f" allow_commands: {preview}",
|
|
87
|
+
f" permission_mode : {cfg.permission_mode}",
|
|
88
|
+
f" yes_to_all : {cfg.yes_to_all}",
|
|
89
|
+
f" interactive_permission: {getattr(cfg, 'interactive_permission_ask', False)}",
|
|
90
|
+
f" hitl_sticky_session : {getattr(cfg, 'interactive_hitl_sticky_session', True)}",
|
|
91
|
+
f" allow_commands : {preview}",
|
|
92
|
+
"",
|
|
93
|
+
" Settings-based rules (deny first, then allow):",
|
|
94
|
+
f" Project: {settings_file} ({'exists' if settings_file.exists() else 'not found'})",
|
|
95
|
+
f" Global : {global_settings} ({'exists' if os.path.exists(global_settings) else 'not found'})",
|
|
88
96
|
]
|
|
97
|
+
lines.extend(rules_lines)
|
|
98
|
+
lines.append("")
|
|
99
|
+
lines.append(" To add rules, create .gemcode/settings.json:")
|
|
100
|
+
lines.append(' { "permissions": { "allow": ["bash(git *)"], "deny": ["bash(rm -rf *)"] } }')
|
|
101
|
+
return lines
|
|
89
102
|
|
|
90
103
|
|
|
91
104
|
def format_memory_lines(cfg: GemCodeConfig) -> list[str]:
|
|
@@ -201,10 +214,16 @@ def slash_help_lines() -> list[str]:
|
|
|
201
214
|
" /help Show this help",
|
|
202
215
|
" /status Full status: model, capabilities, thinking, limits",
|
|
203
216
|
" /config All active config fields (model, caps, context, thinking)",
|
|
204
|
-
" /session Print current session id",
|
|
205
|
-
" /session
|
|
217
|
+
" /session Print current session id and name",
|
|
218
|
+
" /session list List all sessions (most recent first)",
|
|
219
|
+
" /session name <n> Name the current session",
|
|
220
|
+
" /session resume <n> Resume session by name, ID prefix, or full UUID",
|
|
221
|
+
" /session new Start a fresh session (history reset)",
|
|
206
222
|
" /clear Alias for /session new",
|
|
207
|
-
" /compact Force
|
|
223
|
+
" /compact Force context compaction now (summarize history)",
|
|
224
|
+
" /compact <focus> Compact with custom focus, e.g. /compact test output",
|
|
225
|
+
" /review Parallel code review: security + style + correctness",
|
|
226
|
+
" /review <path> Review a specific file or directory",
|
|
208
227
|
" /context Show context pressure + last prompt tokens",
|
|
209
228
|
" /audit [N] Tail of .gemcode/audit.log (default 40 lines)",
|
|
210
229
|
" /tools List tool inventory for this config",
|
|
@@ -155,6 +155,39 @@ async def process_repl_slash(
|
|
|
155
155
|
return ReplSlashResult(skip_model_turn=True)
|
|
156
156
|
|
|
157
157
|
# ── /init ─────────────────────────────────────────────────────────────────
|
|
158
|
+
# ── /review ───────────────────────────────────────────────────────────────
|
|
159
|
+
if name == "review":
|
|
160
|
+
scope = (sc.args or "").strip()
|
|
161
|
+
# Build a prompt that includes the diff/files as context for the pipeline.
|
|
162
|
+
# We dispatch this as a model_prompt so the main agent runs the review pipeline.
|
|
163
|
+
if scope:
|
|
164
|
+
review_prompt = (
|
|
165
|
+
f"Run a parallel code review on: {scope}\n\n"
|
|
166
|
+
"Use the review_code tool if available, or:\n"
|
|
167
|
+
"1. If it's a file/directory, read the relevant files with read_file/list_directory\n"
|
|
168
|
+
"2. Run `bash('git diff HEAD -- " + scope + "')` to see recent changes\n"
|
|
169
|
+
"3. Produce a parallel code review using your SecurityReviewer, StyleReviewer, "
|
|
170
|
+
"and CorrectnessReviewer sub-agents (via run_subtask with focus='security'/'style'/'correctness'), "
|
|
171
|
+
"then synthesize the findings into a structured report with: Critical Issues, Suggestions, Verdict."
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
review_prompt = (
|
|
175
|
+
"Run a parallel code review on the current changes.\n\n"
|
|
176
|
+
"Steps:\n"
|
|
177
|
+
"1. Run `bash('git diff HEAD')` to see unstaged changes, and "
|
|
178
|
+
"`bash('git diff --cached')` for staged changes\n"
|
|
179
|
+
"2. If no git diff, run `bash('git diff HEAD~1')` for the last commit\n"
|
|
180
|
+
"3. Run THREE parallel sub-reviews using run_subtask:\n"
|
|
181
|
+
" - Security review (auth, injections, secrets, validation)\n"
|
|
182
|
+
" - Style review (readability, naming, DRY, docs)\n"
|
|
183
|
+
" - Correctness review (logic, error handling, edge cases, tests)\n"
|
|
184
|
+
"4. Synthesize into a final report: Critical Issues / Suggestions / Verdict\n\n"
|
|
185
|
+
"Run all three sub-reviews simultaneously using run_subtask for maximum speed."
|
|
186
|
+
)
|
|
187
|
+
out("Running parallel code review (security + style + correctness)…")
|
|
188
|
+
out()
|
|
189
|
+
return ReplSlashResult(model_prompt=review_prompt)
|
|
190
|
+
|
|
158
191
|
# ── /notes ────────────────────────────────────────────────────────────────
|
|
159
192
|
if name == "notes":
|
|
160
193
|
sub = (sc.args or "").strip().lower()
|
|
@@ -366,12 +399,76 @@ async def process_repl_slash(
|
|
|
366
399
|
return ReplSlashResult(skip_model_turn=True)
|
|
367
400
|
|
|
368
401
|
if name in ("session", "clear"):
|
|
369
|
-
|
|
402
|
+
args_lower = (sc.args or "").strip().lower()
|
|
403
|
+
args_raw = (sc.args or "").strip()
|
|
404
|
+
|
|
405
|
+
# /clear or /session new — start fresh session
|
|
406
|
+
if name == "clear" or args_lower in ("new", "reset"):
|
|
370
407
|
new_id = str(uuid.uuid4())
|
|
371
408
|
out(f"new session_id: {new_id}")
|
|
372
409
|
out()
|
|
373
410
|
return ReplSlashResult(skip_model_turn=True, new_session_id=new_id)
|
|
411
|
+
|
|
412
|
+
# /session list — show recent sessions
|
|
413
|
+
if args_lower in ("list", "ls", "history"):
|
|
414
|
+
from gemcode.session_store import list_sessions, format_session_list
|
|
415
|
+
sessions = list_sessions(cfg.project_root)
|
|
416
|
+
out("Recent sessions (most recent first):")
|
|
417
|
+
out("─" * 55)
|
|
418
|
+
for line in format_session_list(sessions):
|
|
419
|
+
out(line)
|
|
420
|
+
out()
|
|
421
|
+
out(" /session resume <id|name> — resume a session")
|
|
422
|
+
out(" /session name <name> — name current session")
|
|
423
|
+
out()
|
|
424
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
425
|
+
|
|
426
|
+
# /session name <name> — give a name to the current session
|
|
427
|
+
if args_lower.startswith("name "):
|
|
428
|
+
new_name = args_raw[5:].strip()
|
|
429
|
+
if not new_name:
|
|
430
|
+
out("Usage: /session name <name>")
|
|
431
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
432
|
+
from gemcode.session_store import name_session
|
|
433
|
+
name_session(cfg.project_root, session_id, new_name)
|
|
434
|
+
out(f"Session named: '{new_name}'")
|
|
435
|
+
out(f"Session id : {session_id[:8]}…")
|
|
436
|
+
out()
|
|
437
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
438
|
+
|
|
439
|
+
# /session resume <id|name> — switch to a different session
|
|
440
|
+
if args_lower.startswith("resume ") or args_lower.startswith("r "):
|
|
441
|
+
query = args_raw.split(None, 1)[1].strip() if " " in args_raw else ""
|
|
442
|
+
if not query:
|
|
443
|
+
out("Usage: /session resume <session-id|name|prefix>")
|
|
444
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
445
|
+
from gemcode.session_store import find_session
|
|
446
|
+
found = find_session(cfg.project_root, query)
|
|
447
|
+
if found is None:
|
|
448
|
+
out(f"No session found matching '{query}'.")
|
|
449
|
+
out("Use /session list to see available sessions.")
|
|
450
|
+
out()
|
|
451
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
452
|
+
if found == session_id:
|
|
453
|
+
out(f"Already in session {found[:8]}.")
|
|
454
|
+
out()
|
|
455
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
456
|
+
out(f"Resuming session {found[:8]}…")
|
|
457
|
+
out()
|
|
458
|
+
return ReplSlashResult(skip_model_turn=True, new_session_id=found)
|
|
459
|
+
|
|
460
|
+
# Default: show current session info
|
|
461
|
+
from gemcode.session_store import get_session_name, touch_session
|
|
462
|
+
touch_session(cfg.project_root, session_id)
|
|
463
|
+
session_name = get_session_name(cfg.project_root, session_id)
|
|
374
464
|
out(f"session_id: {session_id}")
|
|
465
|
+
if session_name:
|
|
466
|
+
out(f"name : {session_name}")
|
|
467
|
+
out()
|
|
468
|
+
out(" /session list — list all sessions")
|
|
469
|
+
out(" /session name <name> — name this session")
|
|
470
|
+
out(" /session resume <id|name> — resume another session")
|
|
471
|
+
out(" /session new — start a fresh session")
|
|
375
472
|
out()
|
|
376
473
|
return ReplSlashResult(skip_model_turn=True)
|
|
377
474
|
|
|
@@ -418,12 +515,36 @@ async def process_repl_slash(
|
|
|
418
515
|
return ReplSlashResult(skip_model_turn=True)
|
|
419
516
|
|
|
420
517
|
if name == "compact":
|
|
518
|
+
focus = (sc.args or "").strip()
|
|
421
519
|
os.environ["GEMCODE_AUTOCOMPACT_FORCE"] = "1"
|
|
520
|
+
if focus:
|
|
521
|
+
compact_prompt = (
|
|
522
|
+
f"Compact the conversation history now. Focus on preserving: {focus}\n\n"
|
|
523
|
+
"Create a concise summary that retains:\n"
|
|
524
|
+
"- All decisions, conclusions, and code changes made\n"
|
|
525
|
+
f"- Special attention to: {focus}\n"
|
|
526
|
+
"- Any open tasks or blockers\n"
|
|
527
|
+
"- Key file paths and architecture insights\n\n"
|
|
528
|
+
"After summarizing, reply with a brief confirmation of what was preserved."
|
|
529
|
+
)
|
|
530
|
+
else:
|
|
531
|
+
compact_prompt = (
|
|
532
|
+
"Compact the conversation history now.\n\n"
|
|
533
|
+
"Create a concise summary preserving:\n"
|
|
534
|
+
"- All decisions, conclusions, and code changes made\n"
|
|
535
|
+
"- Any open tasks or next steps\n"
|
|
536
|
+
"- Key file paths, commands, and architecture insights\n"
|
|
537
|
+
"- Error messages and their resolutions\n\n"
|
|
538
|
+
"After summarizing, reply with a brief confirmation. "
|
|
539
|
+
"Tip: use `/compact <focus>` to specify what to prioritize, e.g. `/compact test output and error messages`"
|
|
540
|
+
)
|
|
541
|
+
out("Compacting context history…")
|
|
542
|
+
if focus:
|
|
543
|
+
out(f"Focus: {focus}")
|
|
544
|
+
out()
|
|
422
545
|
return ReplSlashResult(
|
|
423
546
|
skip_model_turn=False,
|
|
424
|
-
model_prompt=
|
|
425
|
-
"Compact the conversation history now. Reply with: Compacted."
|
|
426
|
-
),
|
|
547
|
+
model_prompt=compact_prompt,
|
|
427
548
|
)
|
|
428
549
|
|
|
429
550
|
if name in ("exit", "quit"):
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parallel code-review pipeline using ADK SequentialAgent + ParallelAgent.
|
|
3
|
+
|
|
4
|
+
Inspired by the ADK community financial-advisor pattern:
|
|
5
|
+
ParallelAgent (3 specialist reviewers run simultaneously)
|
|
6
|
+
→ SecurityReviewer (writes to state["security_findings"])
|
|
7
|
+
→ StyleReviewer (writes to state["style_findings"])
|
|
8
|
+
→ CorrectnessReviewer(writes to state["correctness_findings"])
|
|
9
|
+
→ SynthesisAgent (reads all three state keys and produces final report)
|
|
10
|
+
|
|
11
|
+
The pipeline is wrapped in a SequentialAgent:
|
|
12
|
+
ParallelReviewers → Synthesizer
|
|
13
|
+
|
|
14
|
+
Called from the /review slash command with an optional scope argument:
|
|
15
|
+
/review — review recent git diff (staged + unstaged changes)
|
|
16
|
+
/review src/auth/ — review a specific directory
|
|
17
|
+
/review file.py — review a specific file
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_GEMINI_FLASH = "gemini-2.5-flash"
|
|
25
|
+
_GEMINI_PRO = "gemini-2.5-pro"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_review_pipeline(model: str | None = None) -> Any:
|
|
29
|
+
"""
|
|
30
|
+
Build and return a SequentialAgent [ParallelReviewers → Synthesizer].
|
|
31
|
+
|
|
32
|
+
The pipeline is stateless — pass the diff/files as the initial prompt.
|
|
33
|
+
Uses flash for the three parallel reviewers (fast + cheap) and the
|
|
34
|
+
provided model (or flash) for the final synthesis.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
from google.adk.agents import LlmAgent, SequentialAgent, ParallelAgent
|
|
38
|
+
except ImportError as e:
|
|
39
|
+
raise ImportError("google-adk is required for /review") from e
|
|
40
|
+
|
|
41
|
+
synthesizer_model = model or _GEMINI_FLASH
|
|
42
|
+
|
|
43
|
+
security_reviewer = LlmAgent(
|
|
44
|
+
name="SecurityReviewer",
|
|
45
|
+
model=_GEMINI_FLASH,
|
|
46
|
+
include_contents="none", # Stateless — only sees what's injected via state
|
|
47
|
+
instruction="""You are a security-focused code reviewer.
|
|
48
|
+
Review the code changes provided in the message. Focus exclusively on:
|
|
49
|
+
- Authentication/authorization bypasses
|
|
50
|
+
- Injection vulnerabilities (SQL, command, path traversal)
|
|
51
|
+
- Secrets or credentials in code
|
|
52
|
+
- Insecure deserialization or parsing
|
|
53
|
+
- Missing input validation or sanitization
|
|
54
|
+
- Race conditions affecting security
|
|
55
|
+
- Dangerous use of eval/exec/subprocess
|
|
56
|
+
|
|
57
|
+
For each issue: SEVERITY (Critical/High/Medium/Low), FILE+LINE if visible, description, fix suggestion.
|
|
58
|
+
If no security issues found, say "No security issues found."
|
|
59
|
+
Be concise — max 300 words. Output ONLY your security findings.""",
|
|
60
|
+
output_key="security_findings",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
style_reviewer = LlmAgent(
|
|
64
|
+
name="StyleReviewer",
|
|
65
|
+
model=_GEMINI_FLASH,
|
|
66
|
+
include_contents="none",
|
|
67
|
+
instruction="""You are a code style and readability reviewer.
|
|
68
|
+
Review the code changes provided in the message. Focus exclusively on:
|
|
69
|
+
- Code readability and clarity
|
|
70
|
+
- Naming conventions (variables, functions, classes)
|
|
71
|
+
- DRY violations and unnecessary duplication
|
|
72
|
+
- Function/method length and complexity
|
|
73
|
+
- Missing or poor documentation/comments
|
|
74
|
+
- Inconsistent formatting or style
|
|
75
|
+
- Dead code or unused imports
|
|
76
|
+
|
|
77
|
+
For each issue: SEVERITY (High/Medium/Low), FILE+LINE if visible, description, suggestion.
|
|
78
|
+
If no style issues found, say "No style issues found."
|
|
79
|
+
Be concise — max 300 words. Output ONLY your style findings.""",
|
|
80
|
+
output_key="style_findings",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
correctness_reviewer = LlmAgent(
|
|
84
|
+
name="CorrectnessReviewer",
|
|
85
|
+
model=_GEMINI_FLASH,
|
|
86
|
+
include_contents="none",
|
|
87
|
+
instruction="""You are a correctness-focused code reviewer.
|
|
88
|
+
Review the code changes provided in the message. Focus exclusively on:
|
|
89
|
+
- Logic errors and off-by-one bugs
|
|
90
|
+
- Null/undefined pointer dereferences
|
|
91
|
+
- Incorrect error handling or exception swallowing
|
|
92
|
+
- Type mismatches or coercion issues
|
|
93
|
+
- Missing edge case handling
|
|
94
|
+
- Resource leaks (file handles, connections, memory)
|
|
95
|
+
- Incorrect algorithm implementation
|
|
96
|
+
- Broken tests or missing test coverage
|
|
97
|
+
|
|
98
|
+
For each issue: SEVERITY (Critical/High/Medium/Low), FILE+LINE if visible, description, fix suggestion.
|
|
99
|
+
If no correctness issues found, say "No correctness issues found."
|
|
100
|
+
Be concise — max 300 words. Output ONLY your correctness findings.""",
|
|
101
|
+
output_key="correctness_findings",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
synthesizer = LlmAgent(
|
|
105
|
+
name="ReviewSynthesizer",
|
|
106
|
+
model=synthesizer_model,
|
|
107
|
+
include_contents="none",
|
|
108
|
+
instruction="""You are a senior engineering lead synthesizing a code review.
|
|
109
|
+
|
|
110
|
+
You have three specialist review reports in state:
|
|
111
|
+
- Security findings: {security_findings}
|
|
112
|
+
- Style findings: {style_findings}
|
|
113
|
+
- Correctness: {correctness_findings}
|
|
114
|
+
|
|
115
|
+
Produce a clear, actionable code review report:
|
|
116
|
+
|
|
117
|
+
## Code Review Summary
|
|
118
|
+
|
|
119
|
+
### Critical / Must Fix
|
|
120
|
+
[List only Critical and High severity issues from all reviewers, grouped by file if possible]
|
|
121
|
+
|
|
122
|
+
### Suggestions
|
|
123
|
+
[List Medium/Low severity issues worth addressing, grouped by category]
|
|
124
|
+
|
|
125
|
+
### Verdict
|
|
126
|
+
[One of: APPROVE / APPROVE WITH MINOR CHANGES / REQUEST CHANGES — with 1-2 sentence justification]
|
|
127
|
+
|
|
128
|
+
Keep the report under 500 words. Be direct and developer-friendly.""",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
parallel_reviewers = ParallelAgent(
|
|
132
|
+
name="ParallelReviewers",
|
|
133
|
+
sub_agents=[security_reviewer, style_reviewer, correctness_reviewer],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
pipeline = SequentialAgent(
|
|
137
|
+
name="CodeReviewPipeline",
|
|
138
|
+
description="Parallel code review: security + style + correctness, then synthesis.",
|
|
139
|
+
sub_agents=[parallel_reviewers, synthesizer],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return pipeline
|