scripts-context-isolation
#!/usr/bin/env python3 """ CODITECT Subagent Context Isolation (ADR-078)
Implements Superpowers' fresh-context-per-task pattern ensuring subagents receive clean context without pollution from parent or sibling tasks.
Key principle: "Subagent should complete its task without knowing what other tasks exist."
Based on analysis of submodules/superpowers. """
from dataclasses import dataclass, field from typing import Dict, List, Optional, Any from pathlib import Path import json
@dataclass class ArchitecturalContext: """ Essential project context provided to all subagents.
This is the ONLY shared context across subagents - it provides
project-wide information without task-specific details.
Attributes:
project_structure: Directory tree of key project directories
key_patterns: Design patterns used in the project
relevant_adrs: ADR titles and summaries (not full text)
dependencies: Key dependencies and versions
naming_conventions: Code style and naming rules
track_nomenclature: Task ID format (e.g., A.9.1.3)
"""
project_structure: str
key_patterns: List[str]
relevant_adrs: List[str]
dependencies: Dict[str, str]
naming_conventions: str
track_nomenclature: str
@classmethod
def from_project(cls, project_root: Path) -> "ArchitecturalContext":
"""Build architectural context from project directory.
Args:
project_root: Path to project root directory
Returns:
ArchitecturalContext populated from project
"""
return cls(
project_structure=cls._get_structure(project_root),
key_patterns=cls._extract_patterns(project_root),
relevant_adrs=cls._summarize_adrs(project_root),
dependencies=cls._get_dependencies(project_root),
naming_conventions=cls._get_conventions(project_root),
track_nomenclature="Format: Track.Section.Task[.Subtask] (e.g., A.9.1.3)"
)
@staticmethod
def _get_structure(root: Path) -> str:
"""Get project directory structure.
Returns a tree-like representation of key directories.
"""
structure_lines = [f"{root.name}/"]
# List key directories (up to 2 levels)
key_dirs = ["src", "scripts", "tests", "agents", "skills", "config", "docs"]
for dir_name in key_dirs:
dir_path = root / dir_name
if dir_path.exists() and dir_path.is_dir():
structure_lines.append(f"├── {dir_name}/")
# List immediate children
try:
children = sorted(dir_path.iterdir())[:5] # Limit to 5
for child in children:
if child.is_dir():
structure_lines.append(f"│ ├── {child.name}/")
elif child.suffix in [".py", ".md", ".json"]:
structure_lines.append(f"│ ├── {child.name}")
except PermissionError:
pass
return "\n".join(structure_lines)
@staticmethod
def _extract_patterns(root: Path) -> List[str]:
"""Extract key patterns from codebase.
Looks for common patterns in file structure and naming.
"""
patterns = []
# Check for common pattern indicators
if (root / "scripts" / "core").exists():
patterns.append("Core module pattern")
if (root / "tests").exists():
patterns.append("TDD/Test-driven development")
if (root / "agents").exists():
patterns.append("Agent-based architecture")
if (root / "skills").exists():
patterns.append("Skill composition pattern")
if (root / "config").exists():
patterns.append("Configuration-driven design")
return patterns if patterns else ["Standard Python project"]
@staticmethod
def _summarize_adrs(root: Path) -> List[str]:
"""Get ADR titles and summaries.
Returns list of ADR summaries without full content.
"""
adrs = []
adr_dirs = [
root / "internal" / "architecture" / "adrs",
root / "docs" / "adrs"
]
for adr_dir in adr_dirs:
if adr_dir.exists():
adr_files = sorted(adr_dir.glob("ADR-*.md"))[:10] # Limit to 10
for adr_file in adr_files:
# Just get the filename as summary
name = adr_file.stem.replace("-", " ").replace("_", " ")
adrs.append(name)
return adrs if adrs else ["No ADRs found"]
@staticmethod
def _get_dependencies(root: Path) -> Dict[str, str]:
"""Get key dependencies from project.
Looks at pyproject.toml, requirements.txt, or package.json.
"""
deps = {}
# Try pyproject.toml
pyproject = root / "pyproject.toml"
if pyproject.exists():
try:
content = pyproject.read_text()
if "python" in content.lower():
deps["python"] = "3.12+"
# Could parse more, but keep it simple
except Exception:
pass
# Try requirements.txt
requirements = root / "requirements.txt"
if requirements.exists():
try:
lines = requirements.read_text().strip().split("\n")[:5]
for line in lines:
if "==" in line:
pkg, ver = line.split("==")[:2]
deps[pkg.strip()] = ver.strip()
elif line.strip() and not line.startswith("#"):
deps[line.strip()] = "latest"
except Exception:
pass
return deps if deps else {"python": "3.12+"}
@staticmethod
def _get_conventions(root: Path) -> str:
"""Get naming/style conventions.
Infers from project structure and common patterns.
"""
conventions = []
# Check for Python style
if list(root.glob("**/*.py"))[:1]:
conventions.append("Python: snake_case for functions/variables")
conventions.append("Python: PascalCase for classes")
# Check for markdown
if list(root.glob("**/*.md"))[:1]:
conventions.append("Documentation: Markdown format")
# Check for YAML/JSON config
if list(root.glob("**/*.json"))[:1] or list(root.glob("**/*.yaml"))[:1]:
conventions.append("Configuration: JSON/YAML files")
return "\n".join(conventions) if conventions else "Standard Python conventions"
@dataclass class IsolatedContext: """ Context provided to a single subagent.
This context is ISOLATED - the subagent cannot see other tasks,
parent conversation history, or sibling task results.
Attributes:
task_id: PILOT plan task ID (e.g., A.9.1.3)
task_description: Full task text (NOT file reference)
architectural_context: Shared project context
file_contents: Map of file paths to their contents
constraints: Constraints specific to this task
clarifications: Clarifications added during execution
"""
task_id: str
task_description: str
architectural_context: ArchitecturalContext
file_contents: Dict[str, str] = field(default_factory=dict)
constraints: List[str] = field(default_factory=list)
clarifications: List[str] = field(default_factory=list)
def to_prompt(self) -> str:
"""Convert to subagent prompt.
Generates a complete prompt for the subagent that includes
only this task's information, without any reference to other tasks.
Returns:
Formatted prompt string for subagent
"""
# Build file contents section
files_section = ""
if self.file_contents:
file_blocks = []
for path, content in self.file_contents.items():
file_blocks.append(f"### {path}\n```\n{content}\n```")
files_section = "\n\n".join(file_blocks)
# Build constraints section
constraints_section = ""
if self.constraints:
constraints_section = "\n".join(f"- {c}" for c in self.constraints)
else:
constraints_section = "None specified."
# Build clarifications section
clarifications_section = ""
if self.clarifications:
clarifications_section = "\n\n## Clarifications\n\n" + "\n".join(
f"- {c}" for c in self.clarifications
)
# Build patterns list
patterns_list = ""
if self.architectural_context.key_patterns:
patterns_list = "\n".join(
f"- {p}" for p in self.architectural_context.key_patterns
)
else:
patterns_list = "- Standard patterns"
# Build ADRs list
adrs_list = ""
if self.architectural_context.relevant_adrs:
adrs_list = "\n".join(
f"- {a}" for a in self.architectural_context.relevant_adrs
)
else:
adrs_list = "- No ADRs specified"
return f"""## Your Task
Task ID: {self.task_id}
{self.task_description}
Architectural Context
Project Structure:
{self.architectural_context.project_structure}
Key Patterns: {patterns_list}
Relevant ADRs: {adrs_list}
Naming Conventions: {self.architectural_context.naming_conventions}
Task ID Format: {self.architectural_context.track_nomenclature}
File Contents
{files_section if files_section else "No files provided. Request if needed using Read tool."}
Constraints
{constraints_section} {clarifications_section}
Important Notes
- You are working on ONE task only
- You do not need to know about other tasks
- Include task ID in all tool descriptions:
{self.task_id}: action - Ask clarifying questions if requirements are unclear
- Complete self-review before marking task complete
- Follow the project's naming conventions and patterns """
class ContextIsolationManager: """ Manages context isolation for subagent dispatch.
Implements Superpowers' fresh-context-per-task pattern, ensuring
each subagent receives clean, task-specific context.
Usage:
manager = ContextIsolationManager(project_root)
# Create isolated context for a task
ctx = manager.create_isolated_context(
task_id="A.9.1.3",
task_description="Implement UserService...",
required_files=["models/user.py"],
constraints=["Do not modify existing fields"]
)
# Dispatch subagent
dispatch_code = manager.dispatch_subagent(ctx, "backend-specialist")
Attributes:
project_root: Path to project root directory
arch_context: Shared architectural context
task_cache: Cache of task contexts for re-dispatch
"""
def __init__(self, project_root: Path):
"""Initialize context isolation manager.
Args:
project_root: Path to project root directory
"""
self.project_root = Path(project_root)
self.arch_context = ArchitecturalContext.from_project(self.project_root)
self.task_cache: Dict[str, IsolatedContext] = {}
def create_isolated_context(
self,
task_id: str,
task_description: str,
required_files: Optional[List[str]] = None,
constraints: Optional[List[str]] = None
) -> IsolatedContext:
"""
Create isolated context for a subagent.
Args:
task_id: PILOT plan task ID (e.g., A.9.1.3)
task_description: Full task text (NOT file reference)
required_files: Paths to files subagent needs (relative to project root)
constraints: Constraints for this task
Returns:
IsolatedContext ready for subagent dispatch
"""
# Load file contents (actual content, not references)
file_contents = {}
if required_files:
for rel_path in required_files:
full_path = self.project_root / rel_path
if full_path.exists() and full_path.is_file():
try:
file_contents[rel_path] = full_path.read_text()
except Exception:
pass # Skip files that can't be read
context = IsolatedContext(
task_id=task_id,
task_description=task_description,
architectural_context=self.arch_context,
file_contents=file_contents,
constraints=constraints or []
)
# Cache for potential re-dispatch or clarification
self.task_cache[task_id] = context
return context
def dispatch_subagent(
self,
context: IsolatedContext,
agent_type: str = "general"
) -> str:
"""
Generate Task tool invocation for isolated subagent.
Args:
context: IsolatedContext for the task
agent_type: Type of subagent to dispatch
Returns:
Ready-to-execute Task() call string
"""
prompt = context.to_prompt()
# Escape for inclusion in Python string
escaped_prompt = prompt.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n')
return f'''Task(
subagent_type="{agent_type}",
prompt="{escaped_prompt}",
description="{context.task_id}: Execute isolated task"
)'''
def add_clarification(
self,
task_id: str,
clarification: str
) -> None:
"""Add clarification to task context.
Used when subagent asks a question and controller provides answer.
Args:
task_id: Task ID to add clarification to
clarification: Clarification text
"""
if task_id in self.task_cache:
self.task_cache[task_id].clarifications.append(clarification)
def add_file_content(
self,
task_id: str,
file_path: str
) -> None:
"""Add file content to task context.
Used when subagent requests additional file contents.
Args:
task_id: Task ID to add file to
file_path: Path to file (relative to project root)
"""
if task_id in self.task_cache:
full_path = self.project_root / file_path
if full_path.exists() and full_path.is_file():
try:
self.task_cache[task_id].file_contents[file_path] = full_path.read_text()
except Exception:
pass # Skip files that can't be read
def get_cached_context(self, task_id: str) -> Optional[IsolatedContext]:
"""Get cached context for a task.
Args:
task_id: Task ID to look up
Returns:
Cached IsolatedContext or None if not found
"""
return self.task_cache.get(task_id)
def clear_cache(self, task_id: Optional[str] = None) -> None:
"""Clear context cache.
Args:
task_id: Specific task to clear, or None to clear all
"""
if task_id:
self.task_cache.pop(task_id, None)
else:
self.task_cache.clear()
Convenience functions
def create_isolation_manager(project_root: Optional[Path] = None) -> ContextIsolationManager: """Create a ContextIsolationManager with auto-detected project root.
Args:
project_root: Optional project root, auto-detected if None
Returns:
Configured ContextIsolationManager
"""
if project_root is None:
# Try to detect project root
project_root = Path.cwd()
return ContextIsolationManager(project_root)
def isolate_and_dispatch( task_id: str, task_description: str, project_root: Path, agent_type: str = "general", required_files: Optional[List[str]] = None, constraints: Optional[List[str]] = None ) -> str: """Quick function to create isolated context and generate dispatch.
Args:
task_id: PILOT plan task ID
task_description: Full task text
project_root: Path to project root
agent_type: Type of subagent
required_files: Files needed by subagent
constraints: Task constraints
Returns:
Task() invocation string
"""
manager = ContextIsolationManager(project_root)
context = manager.create_isolated_context(
task_id=task_id,
task_description=task_description,
required_files=required_files,
constraints=constraints
)
return manager.dispatch_subagent(context, agent_type)