1010from typing import Any
1111
1212from trpc_agent_sdk .code_executors import BaseWorkspaceRuntime
13+ from trpc_agent_sdk .code_executors import DEFAULT_SKILLS_CONTAINER
1314from trpc_agent_sdk .code_executors import create_container_workspace_runtime
1415from trpc_agent_sdk .code_executors import create_local_workspace_runtime
1516from trpc_agent_sdk .skills import BaseSkillRepository
2021from ..src .filter_policy import SkillScriptInvocation
2122from ..src .review_types import SandboxRunRecord , SandboxRunStatus
2223
24+ SKILL_NAME = "code-review"
2325SCRIPT_TIMEOUT_SECONDS = 20
2426OUTPUT_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
207271def _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 )
0 commit comments