From 9b24e2e5ed732f7c742873024be060cddbd2c0fd Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sat, 28 Feb 2026 01:51:46 +0100 Subject: [PATCH 1/5] scope support agentic context files --- src/codespy/agents/reviewer/models.py | 4 + .../reviewer/modules/agentic_extractor.py | 136 ++++++++++++++++++ .../agents/reviewer/modules/code_reviewer.py | 35 ++++- .../agents/reviewer/modules/doc_reviewer.py | 30 +++- .../reviewer/modules/scope_identifier.py | 12 ++ src/codespy/agents/reviewer/reviewer.py | 2 + 6 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 src/codespy/agents/reviewer/modules/agentic_extractor.py diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 0ab37ed..3297e7b 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -72,6 +72,10 @@ class ScopeResult(BaseModel): changed_files: list[ChangedFile] = Field( default_factory=list, description="Changed files belonging to this scope" ) + agentic_helpers: list[str] = Field( + default_factory=list, + description="Detected agentic helper file paths (AI agent prompts, instructions, configs) relative to repo root", + ) reason: str = Field(description="Explanation for why this scope was identified") model_config = {"arbitrary_types_allowed": True} diff --git a/src/codespy/agents/reviewer/modules/agentic_extractor.py b/src/codespy/agents/reviewer/modules/agentic_extractor.py new file mode 100644 index 0000000..0f571dd --- /dev/null +++ b/src/codespy/agents/reviewer/modules/agentic_extractor.py @@ -0,0 +1,136 @@ +"""Deterministic agentic helper detector — finds AI agent prompts, instructions, and configs.""" + +import logging +from pathlib import Path + +from codespy.tools.filesystem.client import FileSystem +from codespy.tools.filesystem.models import EntryType, TreeNode + +logger = logging.getLogger(__name__) + +# Single files at any depth that indicate agentic helpers (case-insensitive match). +_AGENTIC_SINGLE_FILES: set[str] = { + "claude.md", + "prompt.txt", + "system_prompt.txt", + "instructions.md", + "babyagi.md", + "agent_prompt.md", + "agent_instructions.md", + "task.md", + "memory.md", + "constraints.md", + "ai_settings.json", + "agent_config.yaml", +} + +# Folder-based patterns: directory name → set of allowed extensions. +_AGENTIC_FOLDER_PATTERNS: dict[str, set[str]] = { + "prompts": {".md"}, + "instructions": {".md"}, + "tools": {".md"}, + ".clinerules": {".md"}, + "config": {".json", ".yaml"}, +} + + +def _matches_single_file(name: str) -> bool: + """Check if a filename matches a known agentic single-file pattern.""" + return name.lower() in _AGENTIC_SINGLE_FILES + + +def _is_agentic_folder(dir_name: str) -> set[str] | None: + """Return allowed extensions if dir_name is a known agentic folder, else None.""" + return _AGENTIC_FOLDER_PATTERNS.get(dir_name.lower()) + + +def _collect_folder_files(node: TreeNode, prefix: str, allowed_exts: set[str]) -> list[str]: + """Collect files from a folder node that match allowed extensions.""" + paths: list[str] = [] + for child in node.children: + if child.entry_type == EntryType.FILE: + suffix = Path(child.name).suffix.lower() + if suffix in allowed_exts: + paths.append(f"{prefix}{child.name}") + return paths + + +def _scan_tree(node: TreeNode, prefix: str = "") -> list[str]: + """Recursively scan a tree for agentic helper files. + + Detects: + - Single files matching _AGENTIC_SINGLE_FILES at any depth + - Files inside known agentic folders matching _AGENTIC_FOLDER_PATTERNS + """ + paths: list[str] = [] + for child in node.children: + rel = f"{prefix}{child.name}" if prefix else child.name + if child.entry_type == EntryType.DIRECTORY: + allowed_exts = _is_agentic_folder(child.name) + if allowed_exts is not None: + # Collect matching files directly inside this folder + paths.extend(_collect_folder_files(child, f"{rel}/", allowed_exts)) + else: + # Recurse into non-agentic directories + paths.extend(_scan_tree(child, f"{rel}/")) + elif _matches_single_file(child.name): + paths.append(rel) + return paths + + +def detect_agentic_helpers(scope_root: Path) -> list[str]: + """Detect agentic helper files in a scope directory. + + Single tree scan at depth 3 to find AI agent prompts, instructions, + and configuration files. + + Args: + scope_root: Absolute path to the scope root directory. + + Returns: + List of relative file paths for detected agentic helpers. + """ + try: + fs = FileSystem(scope_root, create_if_missing=False) + except Exception: + logger.debug(f"Cannot access scope root for agentic detection: {scope_root}") + return [] + + tree = fs.get_tree(max_depth=3) + helpers = _scan_tree(tree) + + if helpers: + logger.info(f"Detected {len(helpers)} agentic helper(s) in {scope_root}: {helpers}") + + return sorted(helpers) + + +def extract_agentic_content(scope_root: Path, helper_paths: list[str]) -> str: + """Read and concatenate agentic helper file contents. + + Args: + scope_root: Absolute path to the scope root directory. + helper_paths: List of relative paths to agentic helper files. + + Returns: + Concatenated content with ``=== filename ===`` headers, + or empty string if no files or all reads fail. + """ + if not helper_paths: + return "" + + try: + fs = FileSystem(scope_root, create_if_missing=False) + except Exception: + logger.debug(f"Cannot access scope root for agentic extraction: {scope_root}") + return "" + + parts: list[str] = [] + for path in helper_paths: + try: + content = fs.read_file(path) + parts.append(f"=== {path} ===\n{content.content}") + except Exception as e: # noqa: BLE001 + logger.warning(f"Could not read agentic helper {path}: {e}") + + return "\n\n".join(parts) diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 337ccce..14f9ee8 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -9,11 +9,13 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult +from codespy.agents.reviewer.modules.agentic_extractor import extract_agentic_content from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, make_scope_relative, resolve_scope_root, restore_repo_paths, + strip_prefix, ) from codespy.config import get_settings from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server @@ -37,6 +39,12 @@ class CodeReviewSignature(dspy.Signature): Review each changed file's patch. For each file, check ALL categories (A, B, C) before moving to the next file. + AGENTIC CONTEXT: If agentic_context is non-empty, it contains AI agent + instruction/prompt/config files (e.g., claude.md, agent_config.yaml, + .clinerules/*.md) found in this scope. Use this as supporting context for + categories A, B, and C — it reveals the intended agent behavior, constraints, + and tool access patterns that may help you verify or dismiss findings. + TOOLS AVAILABLE: - find_function_definitions: check function signatures and implementations - find_function_calls: understand how functions are called, trace data flow @@ -130,6 +138,11 @@ class CodeReviewSignature(dspy.Signature): categories: list[IssueCategory] = dspy.InputField( desc="Allowed issue categories. Use only these values for the 'category' field on each issue." ) + agentic_context: str = dspy.InputField( + desc="Content of AI agent instruction/prompt/config files found in this scope " + "(e.g., claude.md, agent_config.yaml, .clinerules/*.md). " + "Empty string if none detected. Use as supporting context for all categories." + ) issues: list[Issue] = dspy.OutputField( desc="Verified issues. Category must be one of the provided categories. " @@ -224,14 +237,28 @@ async def aforward( max_iters=max_iters, ) scoped = make_scope_relative(scope) - logger.info( - f" Code review: scope {scope.subroot} " - f"({len(scope.changed_files)} files)" - ) + # Extract agentic helper content for this scope + # agentic_helpers are repo-root-relative; strip subroot prefix for scope-relative paths + scope_relative_helpers = [ + strip_prefix(h, scope.subroot) for h in scope.agentic_helpers + ] + agentic_ctx = extract_agentic_content(scope_root, scope_relative_helpers) + if agentic_ctx: + logger.info( + f" Code review: scope {scope.subroot} " + f"({len(scope.changed_files)} files, " + f"{len(scope.agentic_helpers)} agentic helpers)" + ) + else: + logger.info( + f" Code review: scope {scope.subroot} " + f"({len(scope.changed_files)} files)" + ) async with SignatureContext("code_review", self._cost_tracker): result = await agent.acall( scope=scoped, categories=categories, + agentic_context=agentic_ctx, ) issues = [ diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index b9530ff..d718b4f 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -9,12 +9,14 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult +from codespy.agents.reviewer.modules.agentic_extractor import extract_agentic_content from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, make_scope_relative, resolve_scope_root, restore_repo_paths, + strip_prefix, ) from codespy.config import get_settings @@ -30,9 +32,12 @@ class DocReviewSignature(dspy.Signature): You are given: 1. Code patches showing what changed 2. Current documentation content (README, .env.example, docs/, etc.) + 3. Optionally, agentic context (AI agent instruction/prompt/config files) Your job: identify documentation that is now WRONG or MISSING because of the code changes. Cross-reference the patches against the documentation. + If agentic_context is non-empty, also check whether code changes affect + AI agent behavior and whether agent instructions/prompts need updating. CHECK FOR: @@ -80,6 +85,10 @@ class DocReviewSignature(dspy.Signature): categories: list[IssueCategory] = dspy.InputField( desc="Allowed issue categories. Use only these values." ) + agentic_context: str = dspy.InputField( + desc="Content of AI agent instruction/prompt/config files found in this scope. " + "Empty string if none detected. Check if code changes make these stale." + ) issues: list[Issue] = dspy.OutputField( desc="Documentation issues. Category must be 'documentation'. " @@ -156,16 +165,29 @@ async def aforward( continue try: reviewer = dspy.ChainOfThought(DocReviewSignature) - logger.info( - f" Doc review: scope {scope.subroot} " - f"({len(scope.changed_files)} files)" - ) + # Extract agentic helper content for this scope + scope_relative_helpers = [ + strip_prefix(h, scope.subroot) for h in scope.agentic_helpers + ] + agentic_ctx = extract_agentic_content(scope_root, scope_relative_helpers) + if agentic_ctx: + logger.info( + f" Doc review: scope {scope.subroot} " + f"({len(scope.changed_files)} files, " + f"{len(scope.agentic_helpers)} agentic helpers)" + ) + else: + logger.info( + f" Doc review: scope {scope.subroot} " + f"({len(scope.changed_files)} files)" + ) async with SignatureContext("doc", self._cost_tracker): result = await asyncio.to_thread( reviewer, patches=patches, documentation=documentation, categories=[IssueCategory.DOCUMENTATION], + agentic_context=agentic_ctx, ) issues = [ issue for issue in (result.issues or []) diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 860fbd2..f142869 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -10,6 +10,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.reviewer.models import PackageManifest, ScopeResult, ScopeType +from codespy.agents.reviewer.modules.agentic_extractor import detect_agentic_helpers from codespy.config import get_settings from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server @@ -280,6 +281,17 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal )] finally: await cleanup_mcp_contexts(contexts) + # Detect agentic helpers in each scope + for scope in scopes: + scope_root = repo_path if scope.subroot == "." else repo_path / scope.subroot + helpers = detect_agentic_helpers(scope_root) + if helpers: + # Store paths relative to repo root (prefix with subroot) + if scope.subroot == ".": + scope.agentic_helpers = helpers + else: + scope.agentic_helpers = [f"{scope.subroot}/{h}" for h in helpers] + # Log results total_files = sum(len(s.changed_files) for s in scopes) logger.info(f"Identified {len(scopes)} scopes covering {total_files} files") diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 50f69cd..ca21e39 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -202,6 +202,8 @@ def forward(self, config: ReviewConfig) -> ReviewResult: logger.info(f" Lock file: {manifest.lock_file_path}") if manifest.dependencies_changed: logger.info(f" Dependencies changed: Yes") + if scope.agentic_helpers: + logger.info(f" Agentic helpers: {scope.agentic_helpers}") # Run review modules concurrently via asyncio.gather module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] From 3d0621d4ec5b2b259c3cdde858a5020ef3a01bc4 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 8 Mar 2026 17:24:00 +0100 Subject: [PATCH 2/5] rename --- src/codespy/agents/reviewer/models.py | 2 +- src/codespy/agents/reviewer/modules/agentic_extractor.py | 3 ++- src/codespy/agents/reviewer/modules/code_reviewer.py | 6 +++--- src/codespy/agents/reviewer/modules/doc_reviewer.py | 4 ++-- src/codespy/agents/reviewer/modules/scope_identifier.py | 8 ++++---- src/codespy/agents/reviewer/reviewer.py | 4 ++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 3297e7b..1d0a73d 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -72,7 +72,7 @@ class ScopeResult(BaseModel): changed_files: list[ChangedFile] = Field( default_factory=list, description="Changed files belonging to this scope" ) - agentic_helpers: list[str] = Field( + agentic_contexts: list[str] = Field( default_factory=list, description="Detected agentic helper file paths (AI agent prompts, instructions, configs) relative to repo root", ) diff --git a/src/codespy/agents/reviewer/modules/agentic_extractor.py b/src/codespy/agents/reviewer/modules/agentic_extractor.py index 0f571dd..c1f8c4e 100644 --- a/src/codespy/agents/reviewer/modules/agentic_extractor.py +++ b/src/codespy/agents/reviewer/modules/agentic_extractor.py @@ -30,6 +30,7 @@ "instructions": {".md"}, "tools": {".md"}, ".clinerules": {".md"}, + ".rules": {".md"}, "config": {".json", ".yaml"}, } @@ -78,7 +79,7 @@ def _scan_tree(node: TreeNode, prefix: str = "") -> list[str]: return paths -def detect_agentic_helpers(scope_root: Path) -> list[str]: +def detect_agentic_contexts(scope_root: Path) -> list[str]: """Detect agentic helper files in a scope directory. Single tree scan at depth 3 to find AI agent prompts, instructions, diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 14f9ee8..ee6c8e9 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -238,16 +238,16 @@ async def aforward( ) scoped = make_scope_relative(scope) # Extract agentic helper content for this scope - # agentic_helpers are repo-root-relative; strip subroot prefix for scope-relative paths + # agentic_contexts are repo-root-relative; strip subroot prefix for scope-relative paths scope_relative_helpers = [ - strip_prefix(h, scope.subroot) for h in scope.agentic_helpers + strip_prefix(h, scope.subroot) for h in scope.agentic_contexts ] agentic_ctx = extract_agentic_content(scope_root, scope_relative_helpers) if agentic_ctx: logger.info( f" Code review: scope {scope.subroot} " f"({len(scope.changed_files)} files, " - f"{len(scope.agentic_helpers)} agentic helpers)" + f"{len(scope.agentic_contexts)} agentic helpers)" ) else: logger.info( diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index d718b4f..cf53f01 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -167,14 +167,14 @@ async def aforward( reviewer = dspy.ChainOfThought(DocReviewSignature) # Extract agentic helper content for this scope scope_relative_helpers = [ - strip_prefix(h, scope.subroot) for h in scope.agentic_helpers + strip_prefix(h, scope.subroot) for h in scope.agentic_contexts ] agentic_ctx = extract_agentic_content(scope_root, scope_relative_helpers) if agentic_ctx: logger.info( f" Doc review: scope {scope.subroot} " f"({len(scope.changed_files)} files, " - f"{len(scope.agentic_helpers)} agentic helpers)" + f"{len(scope.agentic_contexts)} agentic helpers)" ) else: logger.info( diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index f142869..092cbe9 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -10,7 +10,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.reviewer.models import PackageManifest, ScopeResult, ScopeType -from codespy.agents.reviewer.modules.agentic_extractor import detect_agentic_helpers +from codespy.agents.reviewer.modules.agentic_extractor import detect_agentic_contexts from codespy.config import get_settings from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server @@ -284,13 +284,13 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal # Detect agentic helpers in each scope for scope in scopes: scope_root = repo_path if scope.subroot == "." else repo_path / scope.subroot - helpers = detect_agentic_helpers(scope_root) + helpers = detect_agentic_contexts(scope_root) if helpers: # Store paths relative to repo root (prefix with subroot) if scope.subroot == ".": - scope.agentic_helpers = helpers + scope.agentic_contexts = helpers else: - scope.agentic_helpers = [f"{scope.subroot}/{h}" for h in helpers] + scope.agentic_contexts = [f"{scope.subroot}/{h}" for h in helpers] # Log results total_files = sum(len(s.changed_files) for s in scopes) diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index ca21e39..d875cf7 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -202,8 +202,8 @@ def forward(self, config: ReviewConfig) -> ReviewResult: logger.info(f" Lock file: {manifest.lock_file_path}") if manifest.dependencies_changed: logger.info(f" Dependencies changed: Yes") - if scope.agentic_helpers: - logger.info(f" Agentic helpers: {scope.agentic_helpers}") + if scope.agentic_contexts: + logger.info(f" Agentic helpers: {scope.agentic_contexts}") # Run review modules concurrently via asyncio.gather module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] From 7cca81e04f7a41bbaf6feb5970b2927275a65b21 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 8 Mar 2026 17:24:47 +0100 Subject: [PATCH 3/5] rename --- src/codespy/agents/reviewer/models.py | 2 +- .../reviewer/modules/agentic_extractor.py | 18 +++++++++--------- .../agents/reviewer/modules/code_reviewer.py | 4 ++-- .../agents/reviewer/modules/doc_reviewer.py | 4 ++-- .../reviewer/modules/scope_identifier.py | 2 +- src/codespy/agents/reviewer/reviewer.py | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 1d0a73d..a2f6c07 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -74,7 +74,7 @@ class ScopeResult(BaseModel): ) agentic_contexts: list[str] = Field( default_factory=list, - description="Detected agentic helper file paths (AI agent prompts, instructions, configs) relative to repo root", + description="Detected agentic context file paths (AI agent prompts, instructions, configs) relative to repo root", ) reason: str = Field(description="Explanation for why this scope was identified") diff --git a/src/codespy/agents/reviewer/modules/agentic_extractor.py b/src/codespy/agents/reviewer/modules/agentic_extractor.py index c1f8c4e..05ce133 100644 --- a/src/codespy/agents/reviewer/modules/agentic_extractor.py +++ b/src/codespy/agents/reviewer/modules/agentic_extractor.py @@ -1,4 +1,4 @@ -"""Deterministic agentic helper detector — finds AI agent prompts, instructions, and configs.""" +"""Deterministic agentic context detector — finds AI agent prompts, instructions, and configs.""" import logging from pathlib import Path @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -# Single files at any depth that indicate agentic helpers (case-insensitive match). +# Single files at any depth that indicate agentic contexts (case-insensitive match). _AGENTIC_SINGLE_FILES: set[str] = { "claude.md", "prompt.txt", @@ -57,7 +57,7 @@ def _collect_folder_files(node: TreeNode, prefix: str, allowed_exts: set[str]) - def _scan_tree(node: TreeNode, prefix: str = "") -> list[str]: - """Recursively scan a tree for agentic helper files. + """Recursively scan a tree for agentic context files. Detects: - Single files matching _AGENTIC_SINGLE_FILES at any depth @@ -80,7 +80,7 @@ def _scan_tree(node: TreeNode, prefix: str = "") -> list[str]: def detect_agentic_contexts(scope_root: Path) -> list[str]: - """Detect agentic helper files in a scope directory. + """Detect agentic context files in a scope directory. Single tree scan at depth 3 to find AI agent prompts, instructions, and configuration files. @@ -89,7 +89,7 @@ def detect_agentic_contexts(scope_root: Path) -> list[str]: scope_root: Absolute path to the scope root directory. Returns: - List of relative file paths for detected agentic helpers. + List of relative file paths for detected agentic contexts. """ try: fs = FileSystem(scope_root, create_if_missing=False) @@ -101,17 +101,17 @@ def detect_agentic_contexts(scope_root: Path) -> list[str]: helpers = _scan_tree(tree) if helpers: - logger.info(f"Detected {len(helpers)} agentic helper(s) in {scope_root}: {helpers}") + logger.info(f"Detected {len(helpers)} agentic context(s) in {scope_root}: {helpers}") return sorted(helpers) def extract_agentic_content(scope_root: Path, helper_paths: list[str]) -> str: - """Read and concatenate agentic helper file contents. + """Read and concatenate agentic context file contents. Args: scope_root: Absolute path to the scope root directory. - helper_paths: List of relative paths to agentic helper files. + helper_paths: List of relative paths to agentic context files. Returns: Concatenated content with ``=== filename ===`` headers, @@ -132,6 +132,6 @@ def extract_agentic_content(scope_root: Path, helper_paths: list[str]) -> str: content = fs.read_file(path) parts.append(f"=== {path} ===\n{content.content}") except Exception as e: # noqa: BLE001 - logger.warning(f"Could not read agentic helper {path}: {e}") + logger.warning(f"Could not read agentic context {path}: {e}") return "\n\n".join(parts) diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index ee6c8e9..02e7103 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -237,7 +237,7 @@ async def aforward( max_iters=max_iters, ) scoped = make_scope_relative(scope) - # Extract agentic helper content for this scope + # Extract agentic context content for this scope # agentic_contexts are repo-root-relative; strip subroot prefix for scope-relative paths scope_relative_helpers = [ strip_prefix(h, scope.subroot) for h in scope.agentic_contexts @@ -247,7 +247,7 @@ async def aforward( logger.info( f" Code review: scope {scope.subroot} " f"({len(scope.changed_files)} files, " - f"{len(scope.agentic_contexts)} agentic helpers)" + f"{len(scope.agentic_contexts)} agentic contexts)" ) else: logger.info( diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index cf53f01..e008adc 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -165,7 +165,7 @@ async def aforward( continue try: reviewer = dspy.ChainOfThought(DocReviewSignature) - # Extract agentic helper content for this scope + # Extract agentic context content for this scope scope_relative_helpers = [ strip_prefix(h, scope.subroot) for h in scope.agentic_contexts ] @@ -174,7 +174,7 @@ async def aforward( logger.info( f" Doc review: scope {scope.subroot} " f"({len(scope.changed_files)} files, " - f"{len(scope.agentic_contexts)} agentic helpers)" + f"{len(scope.agentic_contexts)} agentic contexts)" ) else: logger.info( diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 092cbe9..fb9ebb7 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -281,7 +281,7 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal )] finally: await cleanup_mcp_contexts(contexts) - # Detect agentic helpers in each scope + # Detect agentic contexts in each scope for scope in scopes: scope_root = repo_path if scope.subroot == "." else repo_path / scope.subroot helpers = detect_agentic_contexts(scope_root) diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index d875cf7..f2ef849 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -203,7 +203,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: if manifest.dependencies_changed: logger.info(f" Dependencies changed: Yes") if scope.agentic_contexts: - logger.info(f" Agentic helpers: {scope.agentic_contexts}") + logger.info(f" agentic contexts: {scope.agentic_contexts}") # Run review modules concurrently via asyncio.gather module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] From 341e6177ab9c285834a6b41a7065e234453def4d Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 8 Mar 2026 17:45:55 +0100 Subject: [PATCH 4/5] rename --- .../agents/reviewer/modules/agentic_extractor.py | 16 ++++++++-------- .../agents/reviewer/modules/code_reviewer.py | 4 ++-- .../agents/reviewer/modules/doc_reviewer.py | 4 ++-- .../agents/reviewer/modules/scope_identifier.py | 9 +++++---- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/codespy/agents/reviewer/modules/agentic_extractor.py b/src/codespy/agents/reviewer/modules/agentic_extractor.py index 05ce133..afd1e01 100644 --- a/src/codespy/agents/reviewer/modules/agentic_extractor.py +++ b/src/codespy/agents/reviewer/modules/agentic_extractor.py @@ -98,26 +98,26 @@ def detect_agentic_contexts(scope_root: Path) -> list[str]: return [] tree = fs.get_tree(max_depth=3) - helpers = _scan_tree(tree) + agentic_files = _scan_tree(tree) - if helpers: - logger.info(f"Detected {len(helpers)} agentic context(s) in {scope_root}: {helpers}") + if agentic_files: + logger.info(f"Detected {len(agentic_files)} agentic context(s) in {scope_root}: {agentic_files}") - return sorted(helpers) + return sorted(agentic_files) -def extract_agentic_content(scope_root: Path, helper_paths: list[str]) -> str: +def extract_agentic_content(scope_root: Path, context_paths: list[str]) -> str: """Read and concatenate agentic context file contents. Args: scope_root: Absolute path to the scope root directory. - helper_paths: List of relative paths to agentic context files. + context_paths: List of relative paths to agentic context files. Returns: Concatenated content with ``=== filename ===`` headers, or empty string if no files or all reads fail. """ - if not helper_paths: + if not context_paths: return "" try: @@ -127,7 +127,7 @@ def extract_agentic_content(scope_root: Path, helper_paths: list[str]) -> str: return "" parts: list[str] = [] - for path in helper_paths: + for path in context_paths: try: content = fs.read_file(path) parts.append(f"=== {path} ===\n{content.content}") diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 02e7103..c670aad 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -239,10 +239,10 @@ async def aforward( scoped = make_scope_relative(scope) # Extract agentic context content for this scope # agentic_contexts are repo-root-relative; strip subroot prefix for scope-relative paths - scope_relative_helpers = [ + scope_relative_contexts = [ strip_prefix(h, scope.subroot) for h in scope.agentic_contexts ] - agentic_ctx = extract_agentic_content(scope_root, scope_relative_helpers) + agentic_ctx = extract_agentic_content(scope_root, scope_relative_contexts) if agentic_ctx: logger.info( f" Code review: scope {scope.subroot} " diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index e008adc..7099372 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -166,10 +166,10 @@ async def aforward( try: reviewer = dspy.ChainOfThought(DocReviewSignature) # Extract agentic context content for this scope - scope_relative_helpers = [ + scope_relative_contexts = [ strip_prefix(h, scope.subroot) for h in scope.agentic_contexts ] - agentic_ctx = extract_agentic_content(scope_root, scope_relative_helpers) + agentic_ctx = extract_agentic_content(scope_root, scope_relative_contexts) if agentic_ctx: logger.info( f" Doc review: scope {scope.subroot} " diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index fb9ebb7..83cf7f6 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -284,13 +284,14 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal # Detect agentic contexts in each scope for scope in scopes: scope_root = repo_path if scope.subroot == "." else repo_path / scope.subroot - helpers = detect_agentic_contexts(scope_root) - if helpers: + agentic_files = detect_agentic_contexts(scope_root) + if agentic_files: # Store paths relative to repo root (prefix with subroot) if scope.subroot == ".": - scope.agentic_contexts = helpers + scope.agentic_contexts = agentic_files else: - scope.agentic_contexts = [f"{scope.subroot}/{h}" for h in helpers] + scope.agentic_contexts = [f"{scope.subroot}/{h}" for h in agentic_files] + logger.info(f"Found {len(scope.agentic_contexts)} agentic context(s) for scope '{scope.subroot}': {scope.agentic_contexts}") # Log results total_files = sum(len(s.changed_files) for s in scopes) From e92ce857a0591ea693763e7831d32b025f1d0ab1 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 8 Mar 2026 18:09:57 +0100 Subject: [PATCH 5/5] recusrsively find context --- src/codespy/agents/reviewer/modules/agentic_extractor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/codespy/agents/reviewer/modules/agentic_extractor.py b/src/codespy/agents/reviewer/modules/agentic_extractor.py index afd1e01..1322b81 100644 --- a/src/codespy/agents/reviewer/modules/agentic_extractor.py +++ b/src/codespy/agents/reviewer/modules/agentic_extractor.py @@ -46,13 +46,16 @@ def _is_agentic_folder(dir_name: str) -> set[str] | None: def _collect_folder_files(node: TreeNode, prefix: str, allowed_exts: set[str]) -> list[str]: - """Collect files from a folder node that match allowed extensions.""" + """Recursively collect files from an agentic folder that match allowed extensions.""" paths: list[str] = [] for child in node.children: if child.entry_type == EntryType.FILE: suffix = Path(child.name).suffix.lower() if suffix in allowed_exts: paths.append(f"{prefix}{child.name}") + elif child.entry_type == EntryType.DIRECTORY: + # Recurse into subdirectories within the agentic folder + paths.extend(_collect_folder_files(child, f"{prefix}{child.name}/", allowed_exts)) return paths @@ -97,7 +100,7 @@ def detect_agentic_contexts(scope_root: Path) -> list[str]: logger.debug(f"Cannot access scope root for agentic detection: {scope_root}") return [] - tree = fs.get_tree(max_depth=3) + tree = fs.get_tree(max_depth=3, include_hidden=True) agentic_files = _scan_tree(tree) if agentic_files: