Skip to content

Commit 7bf2081

Browse files
committed
feat(examples): add code review agent prototype
1 parent 56b8487 commit 7bf2081

18 files changed

Lines changed: 981 additions & 46 deletions

examples/skills_code_review_agent/README.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ execution, SQLite persistence, and dual-format reports.
2626
## Directory Map
2727

2828
- `agent/`: orchestration, config, and runtime helpers
29-
- `skills/code-review/`: formal skill package and deterministic scripts
29+
- repository `skills/code-review/`: canonical formal skill package and deterministic scripts
30+
- example-local `examples/skills_code_review_agent/skills/code-review/`: teaching copy kept in sync for the example
3031
- `src/`: parser, rules, filter policy, storage, report, telemetry, redaction
3132
- `tests/`: fixtures and automated tests
3233
- `DEVELOPMENT_PLAN.md`: phased implementation plan
@@ -124,16 +125,22 @@ Key method:
124125

125126
## Formal Skill Package
126127

127-
The reusable skill lives under:
128+
The canonical reusable skill lives under:
128129

129-
- [SKILL.md](file:///c:/Users/32349/trpc-agent-python-fork/examples/skills_code_review_agent/skills/code-review/SKILL.md)
130-
- [USAGE.md](file:///c:/Users/32349/trpc-agent-python-fork/examples/skills_code_review_agent/skills/code-review/USAGE.md)
131-
- [RULES.md](file:///c:/Users/32349/trpc-agent-python-fork/examples/skills_code_review_agent/skills/code-review/RULES.md)
132-
- [SCRIPT_CONTRACTS.md](file:///c:/Users/32349/trpc-agent-python-fork/examples/skills_code_review_agent/skills/code-review/SCRIPT_CONTRACTS.md)
130+
- [SKILL.md](file:///c:/Users/32349/trpc-agent-python-fork/skills/code-review/SKILL.md)
131+
- [USAGE.md](file:///c:/Users/32349/trpc-agent-python-fork/skills/code-review/USAGE.md)
132+
- [RULES.md](file:///c:/Users/32349/trpc-agent-python-fork/skills/code-review/RULES.md)
133+
- [SCRIPT_CONTRACTS.md](file:///c:/Users/32349/trpc-agent-python-fork/skills/code-review/SCRIPT_CONTRACTS.md)
133134

134-
The agent currently formalizes the skill package, its scripts, and its
135-
`SkillToolSet` entrypoints. The main pipeline still owns orchestration,
136-
governance, persistence, and final reporting.
135+
The example now resolves the repository-level `skills/code-review/` directory
136+
first for repository indexing, skill-script planning, and container skill mounts.
137+
The example-local copy remains as a fallback so the sample stays readable and
138+
self-contained.
139+
140+
The agent formalizes the skill package, its scripts, and its `SkillToolSet`
141+
entrypoints. The main pipeline still owns orchestration, governance,
142+
persistence, and final reporting so Filter, Storage, and Telemetry stay fully
143+
auditable inside the example.
137144

138145
## Test Coverage
139146

examples/skills_code_review_agent/agent/tools.py

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Any
1111

1212
from trpc_agent_sdk.code_executors import BaseWorkspaceRuntime
13+
from trpc_agent_sdk.code_executors import DEFAULT_SKILLS_CONTAINER
1314
from trpc_agent_sdk.code_executors import create_container_workspace_runtime
1415
from trpc_agent_sdk.code_executors import create_local_workspace_runtime
1516
from trpc_agent_sdk.skills import BaseSkillRepository
@@ -20,6 +21,7 @@
2021
from ..src.filter_policy import SkillScriptInvocation
2122
from ..src.review_types import SandboxRunRecord, SandboxRunStatus
2223

24+
SKILL_NAME = "code-review"
2325
SCRIPT_TIMEOUT_SECONDS = 20
2426
OUTPUT_LIMIT_CHARS = 4000
2527

@@ -36,9 +38,9 @@ def create_skill_tool_set(
3638
"omit_inline_content": False,
3739
}
3840
workspace_runtime = _create_workspace_runtime(workspace_runtime_type=workspace_runtime_type)
39-
skill_paths = _get_skill_paths()
41+
skill_paths = _get_skill_roots()
4042
repository = create_default_skill_repository(
41-
skill_paths,
43+
*skill_paths,
4244
workspace_runtime=workspace_runtime,
4345
use_cached_repository=use_cached_repository,
4446
)
@@ -52,7 +54,7 @@ def build_skill_script_plan(
5254
) -> list[SkillScriptInvocation]:
5355
"""Build the list of planned skill-script executions for the review."""
5456

55-
scripts_dir = project_root / "examples" / "skills_code_review_agent" / "skills" / "code-review" / "scripts"
57+
scripts_dir = resolve_code_review_skill_dir(project_root=project_root) / "scripts"
5658
return [
5759
SkillScriptInvocation(
5860
name="parse_diff",
@@ -126,8 +128,14 @@ def execute_skill_script(
126128
encoding="utf-8",
127129
timeout=timeout_seconds,
128130
)
129-
stdout, stdout_truncated = _truncate_output(completed.stdout, output_limit_chars)
130-
stderr, stderr_truncated = _truncate_output(completed.stderr, output_limit_chars)
131+
stdout, stdout_truncated = _truncate_output(
132+
_normalize_process_output(completed.stdout),
133+
output_limit_chars,
134+
)
135+
stderr, stderr_truncated = _truncate_output(
136+
_normalize_process_output(completed.stderr),
137+
output_limit_chars,
138+
)
131139
status = (
132140
SandboxRunStatus.SUCCEEDED
133141
if completed.returncode == 0
@@ -147,8 +155,14 @@ def execute_skill_script(
147155
blocked_by_filter=False,
148156
)
149157
except subprocess.TimeoutExpired as exc:
150-
stdout, stdout_truncated = _truncate_output(exc.stdout or "", output_limit_chars)
151-
stderr, stderr_truncated = _truncate_output(exc.stderr or "", output_limit_chars)
158+
stdout, stdout_truncated = _truncate_output(
159+
_normalize_process_output(exc.stdout),
160+
output_limit_chars,
161+
)
162+
stderr, stderr_truncated = _truncate_output(
163+
_normalize_process_output(exc.stderr),
164+
output_limit_chars,
165+
)
152166
return SandboxRunRecord(
153167
name=invocation.name,
154168
command=invocation.command,
@@ -195,13 +209,63 @@ def _truncate_output(text: str, limit: int) -> tuple[str, bool]:
195209
return text[:limit], True
196210

197211

198-
def _get_skill_paths() -> str:
199-
"""Get the skill root path for this example."""
212+
def _normalize_process_output(text: object) -> str:
213+
"""Normalize subprocess output for type-safe truncation and storage."""
214+
215+
if text is None:
216+
return ""
217+
if isinstance(text, bytes):
218+
return text.decode("utf-8", errors="replace")
219+
if isinstance(text, str):
220+
return text
221+
return str(text)
222+
223+
224+
def resolve_code_review_skill_dir(*, project_root: Path | None = None) -> Path:
225+
"""Resolve the canonical code-review skill directory.
226+
227+
The repository-level ``skills/code-review`` path is preferred so the example
228+
matches the issue's requested artifact layout. The example-local copy remains
229+
as a fallback to keep the sample self-contained.
230+
"""
231+
232+
for root in _get_skill_roots(project_root=project_root):
233+
candidate = Path(root).resolve() / SKILL_NAME
234+
if (candidate / "SKILL.md").is_file():
235+
return candidate
236+
raise FileNotFoundError(
237+
"Unable to locate the `code-review` skill under repository or example skill roots."
238+
)
239+
240+
241+
def _get_skill_roots(*, project_root: Path | None = None) -> tuple[str, ...]:
242+
"""Get ordered skill roots for repository scanning."""
243+
200244

201245
skills_root = os.getenv(ENV_SKILLS_ROOT)
202246
if skills_root:
203-
return skills_root
204-
return str(Path(__file__).resolve().parent.parent / "skills")
247+
return (skills_root,)
248+
249+
repo_root = _resolve_project_root(project_root)
250+
candidates = [
251+
repo_root / "skills",
252+
repo_root / "examples" / "skills_code_review_agent" / "skills",
253+
]
254+
255+
roots: list[str] = []
256+
for candidate in candidates:
257+
resolved = str(candidate.resolve())
258+
if candidate.is_dir() and resolved not in roots:
259+
roots.append(resolved)
260+
return tuple(roots)
261+
262+
263+
def _resolve_project_root(project_root: Path | None = None) -> Path:
264+
"""Resolve the repository root for the example."""
265+
266+
if project_root is not None:
267+
return project_root.expanduser().resolve()
268+
return Path(__file__).resolve().parents[3]
205269

206270

207271
def _create_workspace_runtime(
@@ -212,9 +276,8 @@ def _create_workspace_runtime(
212276
"""Create a workspace runtime for skill execution demos."""
213277

214278
if workspace_runtime_type == "container":
215-
skill_paths = _get_skill_paths()
216-
container_path = "/opt/trpc-agent/skills"
217-
host_config = {"Binds": [f"{skill_paths}:{container_path}:ro"]}
279+
skill_root = _get_skill_roots()[0]
280+
host_config = {"Binds": [f"{skill_root}:{DEFAULT_SKILLS_CONTAINER}:ro"]}
218281
kwargs["host_config"] = host_config
219282
kwargs["auto_inputs"] = True
220283
return create_container_workspace_runtime(**kwargs)

examples/skills_code_review_agent/src/deduper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def dedupe_and_classify_findings(findings: list[ReviewFinding]) -> list[ReviewFi
2929
finding.category.value,
3030
finding.file,
3131
finding.line,
32-
_normalize_evidence(finding.evidence),
32+
finding.title.strip().lower(),
3333
)
3434
existing = deduped.get(key)
3535
if existing is None or _should_replace(existing, finding):

0 commit comments

Comments
 (0)