maestro-agents 0.1.0__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.
@@ -0,0 +1,46 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ *.egg
7
+ dist/
8
+ build/
9
+
10
+ # Virtual environments
11
+ .venv/
12
+
13
+ # Testing
14
+ .pytest_cache/
15
+ htmlcov/
16
+ .coverage
17
+ coverage.xml
18
+
19
+ # Type checking
20
+ .mypy_cache/
21
+ .pyright/
22
+
23
+ # Ruff
24
+ .ruff_cache/
25
+
26
+ # IDE
27
+ .idea/
28
+ *.swp
29
+ *.swo
30
+ *~
31
+
32
+ # OS
33
+ .DS_Store
34
+ Thumbs.db
35
+
36
+ # uv
37
+ uv.lock
38
+
39
+ # Project
40
+ *.db
41
+ *.sqlite3
42
+ .maestro/
43
+ src/api/
44
+
45
+ # env
46
+ .env
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: maestro-agents
3
+ Version: 0.1.0
4
+ Summary: Role-specific LLM agents with SSOT integration
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: litellm>=1.50
7
+ Requires-Dist: maestro-harness
8
+ Requires-Dist: maestro-ssot
9
+ Requires-Dist: pydantic-ai>=0.5
10
+ Requires-Dist: pydantic>=2.10
11
+ Requires-Dist: rich>=13.0
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "maestro-agents"
3
+ version = "0.1.0"
4
+ description = "Role-specific LLM agents with SSOT integration"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "pydantic>=2.10",
8
+ "pydantic-ai>=0.5",
9
+ "litellm>=1.50",
10
+ "rich>=13.0",
11
+ "maestro-ssot",
12
+ "maestro-harness",
13
+ ]
14
+
15
+ [tool.uv.sources]
16
+ maestro-ssot = { workspace = true }
17
+ maestro-harness = { workspace = true }
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["src/maestro_agents"]
@@ -0,0 +1,8 @@
1
+ """MAESTRO role agents package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .animation import ThinkingAnimation
6
+ from .base import AgentResult, MaestroAgent
7
+
8
+ __all__ = ["AgentResult", "MaestroAgent", "ThinkingAnimation"]
@@ -0,0 +1,79 @@
1
+ """Rich-based thinking animation and typewriter output for agent execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from rich.console import Console
10
+ from rich.live import Live
11
+
12
+
13
+ class ThinkingAnimation:
14
+ """Display a thinking spinner during LLM calls, then typewriter output.
15
+
16
+ Example::
17
+ anim = ThinkingAnimation("planner-1 planning")
18
+ anim.start()
19
+ # ... await LLM call ...
20
+ anim.stop()
21
+ anim.typewrite(result_text, prefix="[cyan]planner-1[/] ")
22
+ """
23
+
24
+ def __init__(self, label: str = "thinking", console: Console | None = None) -> None:
25
+ self.label = label
26
+ self._console = console
27
+ self._live: Live | None = None
28
+
29
+ def _get_console(self) -> Console:
30
+ if self._console is not None:
31
+ return self._console
32
+ from rich.console import Console
33
+
34
+ return Console()
35
+
36
+ def start(self) -> None:
37
+ """Start the thinking spinner."""
38
+ from rich.live import Live
39
+ from rich.spinner import Spinner
40
+
41
+ spinner = Spinner("dots", text=f"[dim]{self.label}[/]")
42
+ self._live = Live(
43
+ spinner, refresh_per_second=8, transient=True, console=self._get_console()
44
+ )
45
+ self._live.start()
46
+
47
+ def stop(self) -> None:
48
+ """Stop the spinner."""
49
+ if self._live is not None:
50
+ self._live.stop()
51
+ self._live = None
52
+
53
+ def typewrite(
54
+ self,
55
+ text: str,
56
+ prefix: str = "",
57
+ delay: float = 0.008,
58
+ max_chars: int = 2000,
59
+ ) -> None:
60
+ """Print text with a typewriter effect.
61
+
62
+ For long text (>max_chars), speeds up to avoid excessive wait time.
63
+ """
64
+ console = self._get_console()
65
+ text = text.strip()
66
+ if not text:
67
+ return
68
+
69
+ # Speed up for very long text
70
+ if len(text) > max_chars:
71
+ delay = max(0.001, delay * max_chars / len(text))
72
+
73
+ if prefix:
74
+ console.print(prefix, end="")
75
+
76
+ for char in text:
77
+ console.print(char, end="")
78
+ time.sleep(delay)
79
+ console.print() # newline
@@ -0,0 +1,33 @@
1
+ """BackendAgent: implements backend code consuming contracts from SSOT."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from maestro_agents.base import MaestroAgent
6
+
7
+
8
+ class BackendAgent(MaestroAgent):
9
+ """Implements backend modules by reading contracts and writing code.
10
+
11
+ Uses SSOT tools to:
12
+ 1. Claim a requirement assigned to the backend
13
+ 2. Read the relevant contracts
14
+ 3. Write implementation code via the harness sandbox
15
+ 4. Update requirement status to DONE when complete
16
+ """
17
+
18
+ role = "backend"
19
+
20
+ def system_prompt(self) -> str:
21
+ return (
22
+ "You are the Backend Agent in the MAESTRO multi-agent system. "
23
+ "Your job is to implement backend code based on requirements and "
24
+ "API contracts from the SSOT. "
25
+ "Read the assigned requirement and its contracts, then write "
26
+ "clean, well-structured Python code to actual files. "
27
+ "You have file_read, file_write, and shell_exec tools to interact "
28
+ "with the codebase. Use file_write to create/modify source files, "
29
+ "shell_exec to run tests and verify your implementation. "
30
+ "Do NOT use 'cd' in shell commands — the sandbox cwd is already "
31
+ "the project root. Use the SSOT tools to read contracts and requirements, "
32
+ "and update the requirement status when done."
33
+ )
@@ -0,0 +1,168 @@
1
+ """MaestroAgent base class wrapping PydanticAI with SSOT + Harness injection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from pydantic import BaseModel
9
+ from pydantic_ai import Agent
10
+ from pydantic_ai.settings import ModelSettings
11
+
12
+ from maestro_harness.harness import Harness
13
+ from maestro_ssot.hub import SSOTHub
14
+
15
+ from .animation import ThinkingAnimation
16
+ from .deps import AgentDeps
17
+ from .permissions import AgentPermissions
18
+ from .tools import register_tools
19
+
20
+ if TYPE_CHECKING:
21
+ from pydantic_ai import AgentEventStream
22
+
23
+
24
+ class AgentResult(BaseModel):
25
+ """Result of a single agent execution."""
26
+
27
+ output: str
28
+ success: bool = True
29
+ error: str | None = None
30
+ input_tokens: int = 0
31
+ output_tokens: int = 0
32
+
33
+
34
+ class MaestroAgent:
35
+ """Base class for all MAESTRO role agents.
36
+
37
+ Wraps a PydanticAI Agent with SSOT and Harness dependencies.
38
+ Subclasses override ``system_prompt`` and ``role`` to specialize behavior.
39
+ """
40
+
41
+ role: str = "generic"
42
+
43
+ def __init__(
44
+ self,
45
+ agent_id: str,
46
+ hub: SSOTHub,
47
+ harness: Harness,
48
+ model: str | object = "test",
49
+ permissions: AgentPermissions | None = None,
50
+ thinking: bool | None = None,
51
+ ) -> None:
52
+ self.agent_id = agent_id
53
+ self.hub = hub
54
+ self.harness = harness
55
+
56
+ deps = AgentDeps(
57
+ hub=hub,
58
+ harness=harness,
59
+ agent_id=agent_id,
60
+ role=self.role,
61
+ )
62
+
63
+ model_settings: ModelSettings | None = None
64
+ if thinking is not None:
65
+ model_settings = ModelSettings(thinking=thinking)
66
+
67
+ prompt = self.system_prompt() + "\n\n" + self._policy_context()
68
+ self._agent = Agent(
69
+ model, # type: ignore[arg-type]
70
+ deps_type=AgentDeps,
71
+ output_type=str,
72
+ system_prompt=prompt,
73
+ model_settings=model_settings,
74
+ )
75
+ register_tools(self._agent, permissions)
76
+ self._deps = deps
77
+
78
+ def _policy_context(self) -> str:
79
+ """Return a formatted description of the agent's Harness policy."""
80
+ lines: list[str] = []
81
+ policy = self.harness.get_agent_policy(self.agent_id)
82
+ if policy and policy.filesystem and policy.filesystem.allow:
83
+ lines.append(f"Allowed file paths: {', '.join(policy.filesystem.allow)}")
84
+ if policy and policy.filesystem and policy.filesystem.deny:
85
+ lines.append(f"Denied file paths: {', '.join(policy.filesystem.deny)}")
86
+ if policy and policy.commands and policy.commands.allow:
87
+ lines.append(f"Allowed shell commands: {', '.join(policy.commands.allow)}")
88
+ if policy and policy.commands and policy.commands.deny:
89
+ lines.append(f"Denied shell commands: {', '.join(policy.commands.deny)}")
90
+ return "\n".join(lines)
91
+
92
+ def system_prompt(self) -> str:
93
+ return "You are a MAESTRO software engineering agent."
94
+
95
+ async def run(
96
+ self,
97
+ prompt: str,
98
+ model: str | object | None = None,
99
+ animation: ThinkingAnimation | None = None,
100
+ timeout: float | None = None,
101
+ ) -> AgentResult:
102
+ """Execute the agent with optional thinking animation and timeout."""
103
+ if animation is not None:
104
+ animation.start()
105
+ try:
106
+ run_kwargs: dict[str, object] = {}
107
+ if model is not None:
108
+ run_kwargs["model"] = model
109
+
110
+ coro = self._agent.run(
111
+ prompt, deps=self._deps, **run_kwargs # type: ignore[call-overload]
112
+ )
113
+ if timeout is not None and timeout > 0:
114
+ coro = asyncio.wait_for(coro, timeout=timeout)
115
+ result = await coro
116
+ output = str(result.output)
117
+
118
+ # Extract token usage if available
119
+ input_tokens = 0
120
+ output_tokens = 0
121
+ try:
122
+ usage = result.usage
123
+ if usage is not None:
124
+ input_tokens = getattr(usage, "request_tokens", 0) or 0
125
+ output_tokens = getattr(usage, "response_tokens", 0) or 0
126
+ except Exception:
127
+ pass
128
+
129
+ if animation is not None:
130
+ animation.stop()
131
+ animation.typewrite(output, prefix=f"[cyan]{self.agent_id}[/] ")
132
+
133
+ return AgentResult(
134
+ output=output,
135
+ input_tokens=input_tokens,
136
+ output_tokens=output_tokens,
137
+ )
138
+ except TimeoutError:
139
+ if animation is not None:
140
+ animation.stop()
141
+ return AgentResult(
142
+ output="",
143
+ success=False,
144
+ error=f"Agent timed out after {timeout}s",
145
+ )
146
+ except Exception as e:
147
+ if animation is not None:
148
+ animation.stop()
149
+ return AgentResult(output="", success=False, error=str(e))
150
+
151
+ def run_stream(
152
+ self,
153
+ prompt: str,
154
+ model: str | object | None = None,
155
+ ) -> AgentEventStream[Any] | None:
156
+ """Return an event stream for real-time monitoring.
157
+
158
+ Returns ``None`` if the underlying model does not support streaming
159
+ (e.g. TestModel).
160
+ """
161
+ run_kwargs: dict[str, object] = {"deps": self._deps}
162
+ if model is not None:
163
+ run_kwargs["model"] = model
164
+ try:
165
+ return self._agent.run_stream_events(prompt, **run_kwargs) # type: ignore[call-overload, no-any-return]
166
+ except Exception:
167
+ # TestModel and some mock environments don't support streaming
168
+ return None
@@ -0,0 +1,94 @@
1
+ """Non-LLM contract validator: rule engine for consumer-producer consistency."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from maestro_ssot.hub import SSOTHub
8
+ from maestro_ssot.models import ContractStatus
9
+
10
+
11
+ @dataclass
12
+ class Violation:
13
+ contract_id: str
14
+ message: str
15
+ severity: str = "error" # error | warning
16
+
17
+
18
+ @dataclass
19
+ class ValidationResult:
20
+ valid: bool
21
+ violations: list[Violation] = field(default_factory=list)
22
+
23
+
24
+ def validate_contracts(hub: SSOTHub) -> ValidationResult:
25
+ """Check all contracts for consistency violations.
26
+
27
+ Rules:
28
+ 1. Stable/Deprecated contracts must have a non-empty interface_schema.
29
+ 2. Consumers of STABLE contracts must not reference DRAFT contracts.
30
+ 3. Contract version should be >= 1.
31
+ 4. All listed consumers should have known activity.
32
+ """
33
+ violations: list[Violation] = []
34
+ contracts = hub._persistence.list_contracts()
35
+
36
+ for contract in contracts:
37
+ # Rule 1: Stable/Deprecated contracts must have interface schema
38
+ if (
39
+ contract.status in (ContractStatus.STABLE, ContractStatus.DEPRECATED)
40
+ and not contract.interface_schema
41
+ ):
42
+ violations.append(
43
+ Violation(
44
+ contract_id=contract.id,
45
+ message=f"Contract '{contract.name}' is {contract.status.value} "
46
+ f"but has empty interface_schema",
47
+ severity="error",
48
+ )
49
+ )
50
+
51
+ # Rule 2: Check consumer references
52
+ for consumer in contract.consumers:
53
+ consumer_known = any(c.owner == consumer for c in contracts)
54
+ if not consumer_known:
55
+ log_entries = hub.query_log(agent_id=consumer, limit=1)
56
+ if not log_entries:
57
+ violations.append(
58
+ Violation(
59
+ contract_id=contract.id,
60
+ message=f"Consumer '{consumer}' of contract '{contract.name}' "
61
+ f"has no known activity",
62
+ severity="warning",
63
+ )
64
+ )
65
+
66
+ # Rule 3: Version check
67
+ if contract.version < 1:
68
+ violations.append(
69
+ Violation(
70
+ contract_id=contract.id,
71
+ message=f"Contract '{contract.name}' has invalid version {contract.version}",
72
+ severity="error",
73
+ )
74
+ )
75
+
76
+ # Rule 4: DRAFT contracts should not have STABLE consumers
77
+ if contract.status == ContractStatus.DRAFT:
78
+ for other in contracts:
79
+ if (
80
+ other.id != contract.id
81
+ and contract.id in other.consumers
82
+ and other.status == ContractStatus.STABLE
83
+ ):
84
+ violations.append(
85
+ Violation(
86
+ contract_id=other.id,
87
+ message=f"STABLE contract '{other.name}' depends on "
88
+ f"DRAFT contract '{contract.name}'",
89
+ severity="error",
90
+ )
91
+ )
92
+
93
+ errors = [v for v in violations if v.severity == "error"]
94
+ return ValidationResult(valid=len(errors) == 0, violations=violations)
@@ -0,0 +1,17 @@
1
+ """Agent dependency types shared between base and tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from maestro_harness.harness import Harness
8
+ from maestro_ssot.hub import SSOTHub
9
+
10
+
11
+ @dataclass
12
+ class AgentDeps:
13
+ hub: SSOTHub
14
+ harness: Harness
15
+ agent_id: str
16
+ role: str
17
+ memory: dict[str, str] = field(default_factory=dict)
@@ -0,0 +1,33 @@
1
+ """FrontendAgent: implements UI components consuming API contracts from SSOT."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from maestro_agents.base import MaestroAgent
6
+
7
+
8
+ class FrontendAgent(MaestroAgent):
9
+ """Implements frontend modules by reading contracts and writing client code.
10
+
11
+ Uses SSOT tools to:
12
+ 1. Claim a requirement assigned to the frontend
13
+ 2. Read the relevant API contracts
14
+ 3. Write implementation code (React, HTML, CSS, etc.) via the harness sandbox
15
+ 4. Update requirement status to DONE when complete
16
+ """
17
+
18
+ role = "frontend"
19
+
20
+ def system_prompt(self) -> str:
21
+ return (
22
+ "You are the Frontend Agent in the MAESTRO multi-agent system. "
23
+ "Your job is to implement frontend code based on requirements and "
24
+ "API contracts from the SSOT. "
25
+ "Read the assigned requirement and its contracts, then write "
26
+ "clean, well-structured client-side code (React, HTML, CSS, JavaScript) "
27
+ "to actual files. You have file_read, file_write, and shell_exec tools "
28
+ "to interact with the codebase. Use file_write to create/modify source files, "
29
+ "shell_exec to run build/lint commands and verify your implementation. "
30
+ "Do NOT use 'cd' in shell commands — the sandbox cwd is already "
31
+ "the project root. Use the SSOT tools to read contracts and requirements, "
32
+ "and update the requirement status when done."
33
+ )
@@ -0,0 +1,24 @@
1
+ """Agent permission model for SSOT tool access control."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class AgentPermissions(BaseModel):
9
+ """SSOT permissions for an agent — controls which tools are registered.
10
+
11
+ All permissions default to True for backward compatibility.
12
+ Set individual fields to False to restrict an agent's capabilities.
13
+ """
14
+
15
+ read_requirements: bool = True
16
+ write_requirements: bool = True
17
+ read_contracts: bool = True
18
+ write_contracts: bool = True
19
+ read_memory: bool = True
20
+ write_memory: bool = True
21
+ read_log: bool = True
22
+ read_files: bool = True
23
+ write_files: bool = True
24
+ execute_shell: bool = True
@@ -0,0 +1,40 @@
1
+ """PlanningAgent: decomposes requirements into a structured task tree."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from maestro_agents.base import MaestroAgent
6
+
7
+
8
+ class PlanningAgent(MaestroAgent):
9
+ """Decomposes high-level requirements into sub-tasks and registers contracts.
10
+
11
+ Uses SSOT tools to:
12
+ 1. Read the root requirement
13
+ 2. Break it into sub-requirements with dependencies
14
+ 3. Register interface contracts between modules
15
+ """
16
+
17
+ role = "planner"
18
+
19
+ def system_prompt(self) -> str:
20
+ return (
21
+ "You are the Planning Agent in the MAESTRO multi-agent system. "
22
+ "Your job is to decompose a high-level software requirement into "
23
+ "a tree of sub-requirements, each with clear acceptance criteria "
24
+ "and dependencies. You should also register API contracts between "
25
+ "modules that need to communicate. "
26
+ "Use the SSOT tools to add requirements and register contracts. "
27
+ "Be specific: each sub-requirement should be small enough for a "
28
+ "single agent to implement in one iteration.\n\n"
29
+ "CRITICAL RULES:\n"
30
+ "1. Every sub-requirement MUST specify a valid parent_id. "
31
+ " Pass the root requirement ID as parent_id for all sub-tasks.\n"
32
+ "2. Do NOT create duplicate tasks. Check existing requirements first.\n"
33
+ "3. Limit decomposition to 5-7 sub-tasks maximum. "
34
+ " If the requirement is simple, create fewer tasks or even just one.\n"
35
+ "4. Dependencies must reference task IDs that you have already created "
36
+ " in this same planning session. Do NOT reference tasks from previous sessions.\n"
37
+ "5. Only register contracts for actual API boundaries between modules.\n"
38
+ "6. Do NOT assign tasks to specific agents yourself — "
39
+ " the scheduler will handle assignment."
40
+ )
File without changes
@@ -0,0 +1,31 @@
1
+ """ReviewAgent: checks contract compliance, code quality, and security issues."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from maestro_agents.base import MaestroAgent
6
+
7
+
8
+ class ReviewAgent(MaestroAgent):
9
+ """Validates code and contract quality before COMMIT.
10
+
11
+ Uses SSOT tools to:
12
+ 1. Read all contracts and their consumers
13
+ 2. Read recent execution logs and code changes
14
+ 3. Check for contract violations, security issues, and code smells
15
+ 4. Annotate the execution log with review findings
16
+ """
17
+
18
+ role = "review"
19
+
20
+ def system_prompt(self) -> str:
21
+ return (
22
+ "You are the Review Agent in the MAESTRO multi-agent system. "
23
+ "Your job is to review code and contracts for quality, security, "
24
+ "and compliance before the system commits an epoch. "
25
+ "You have file_read and shell_exec tools. Use file_read to inspect "
26
+ "source files, and shell_exec to run linters (ruff, mypy) and static "
27
+ "analysis. Do NOT use 'cd' in shell commands — the sandbox cwd is already "
28
+ "the project root. Read the contracts and execution log from the SSOT. "
29
+ "Report any issues you find and suggest fixes. "
30
+ "Use the SSOT tools to read state and annotate the execution log."
31
+ )
@@ -0,0 +1,39 @@
1
+ """TestAgent: writes and runs tests, reports coverage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from maestro_agents.base import MaestroAgent
6
+
7
+
8
+ class TestAgent(MaestroAgent):
9
+ """Generates and executes tests for backend and frontend modules.
10
+
11
+ Note: ``__test__ = False`` prevents pytest from collecting this as a test class.
12
+ """
13
+
14
+ __test__ = False
15
+ """Generates and executes tests for backend and frontend modules.
16
+
17
+ Uses SSOT tools to:
18
+ 1. Claim testing-related requirements
19
+ 2. Read contracts to derive test cases
20
+ 3. Write pytest/jest tests via the harness sandbox
21
+ 4. Run tests and report results to the execution log
22
+ 5. Update requirement status to DONE when complete
23
+ """
24
+
25
+ role = "test"
26
+
27
+ def system_prompt(self) -> str:
28
+ return (
29
+ "You are the Test Agent in the MAESTRO multi-agent system. "
30
+ "Your job is to write and run tests for the codebase. "
31
+ "Read the requirements and contracts from the SSOT, then generate "
32
+ "comprehensive unit and integration tests. "
33
+ "You have file_read, file_write, and shell_exec tools. Use file_read "
34
+ "to inspect existing code, file_write to create test files, and "
35
+ "shell_exec to run pytest/jest and verify results. "
36
+ "Do NOT use 'cd' in shell commands — the sandbox cwd is already "
37
+ "the project root. Report test results via the SSOT execution log tools "
38
+ "and update the requirement status when done."
39
+ )
@@ -0,0 +1,266 @@
1
+ """PydanticAI function tools for SSOT read/write operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic_ai import Agent, RunContext
8
+
9
+ from maestro_harness.validator import Action
10
+
11
+ from .deps import AgentDeps
12
+ from .permissions import AgentPermissions
13
+
14
+
15
+ def register_tools(
16
+ agent: Agent[AgentDeps, str], permissions: AgentPermissions | None = None
17
+ ) -> None:
18
+ """Register SSOT tools onto a PydanticAI Agent, filtered by permissions.
19
+
20
+ Args:
21
+ agent: The PydanticAI agent to register tools on.
22
+ permissions: Permission bitmask controlling which tools are available.
23
+ If None, all tools are registered (backward-compatible default).
24
+ """
25
+ perms = permissions or AgentPermissions()
26
+
27
+ if perms.read_requirements:
28
+
29
+ @agent.tool
30
+ async def ssot_read_requirements(ctx: RunContext[AgentDeps]) -> str:
31
+ """Read all pending requirements from the SSOT."""
32
+ pending = ctx.deps.hub.list_pending()
33
+ if not pending:
34
+ return "No pending requirements."
35
+ lines = []
36
+ for r in pending:
37
+ assignee = r.assignee or "unassigned"
38
+ lines.append(
39
+ f"[{r.id}] {r.description} (status={r.status.value}, assignee={assignee})"
40
+ )
41
+ return "\n".join(lines)
42
+
43
+ @agent.tool
44
+ async def ssot_get_requirement(ctx: RunContext[AgentDeps], node_id: str) -> str:
45
+ """Get a specific requirement by ID."""
46
+ node = ctx.deps.hub.get_requirement(node_id)
47
+ if node is None:
48
+ return f"Requirement {node_id} not found."
49
+ lines = [
50
+ f"ID: {node.id}",
51
+ f"Description: {node.description}",
52
+ f"Status: {node.status.value}",
53
+ ]
54
+ if node.assignee:
55
+ lines.append(f"Assignee: {node.assignee}")
56
+ if node.acceptance_criteria:
57
+ lines.append("Acceptance criteria:")
58
+ for ac in node.acceptance_criteria:
59
+ lines.append(f" - {ac}")
60
+ if node.dependencies:
61
+ lines.append(f"Dependencies: {', '.join(node.dependencies)}")
62
+ return "\n".join(lines)
63
+
64
+ if perms.write_requirements:
65
+
66
+ @agent.tool
67
+ async def ssot_add_requirement(
68
+ ctx: RunContext[AgentDeps],
69
+ description: str,
70
+ parent_id: str,
71
+ acceptance_criteria: list[str] | None = None,
72
+ dependency_ids: list[str] | None = None,
73
+ ) -> str:
74
+ """Add a new requirement to the SSOT. parent_id is REQUIRED.
75
+
76
+ dependency_ids must be a list of existing requirement IDs (not descriptions)
77
+ that must be completed before this task can start.
78
+ """
79
+ node = ctx.deps.hub.add_requirement(
80
+ description=description,
81
+ parent_id=parent_id,
82
+ acceptance_criteria=acceptance_criteria,
83
+ dependencies=dependency_ids,
84
+ agent_id=ctx.deps.agent_id,
85
+ )
86
+ return f"Created requirement {node.id}: {node.description}"
87
+
88
+ @agent.tool
89
+ async def ssot_update_requirement_status(
90
+ ctx: RunContext[AgentDeps], node_id: str, status: str
91
+ ) -> str:
92
+ """Update a requirement's status. Status values: PENDING, IN_PROGRESS, DONE, BLOCKED."""
93
+ from maestro_ssot.models import ReqStatus
94
+
95
+ try:
96
+ req_status = ReqStatus(status)
97
+ except ValueError:
98
+ valid = ", ".join(s.value for s in ReqStatus)
99
+ return f"Invalid status '{status}'. Valid: {valid}"
100
+ ctx.deps.hub.update_requirement_status(node_id, req_status, ctx.deps.agent_id)
101
+ return f"Requirement {node_id} status updated to {status}"
102
+
103
+ @agent.tool
104
+ async def ssot_update_requirement_dependencies(
105
+ ctx: RunContext[AgentDeps], node_id: str, dependency_ids: list[str]
106
+ ) -> str:
107
+ """Update the dependencies of an existing requirement.
108
+
109
+ dependency_ids must be a list of existing requirement IDs.
110
+ """
111
+ node = ctx.deps.hub.update_requirement_dependencies(
112
+ node_id, dependency_ids, ctx.deps.agent_id
113
+ )
114
+ return f"Requirement {node_id} dependencies updated to {', '.join(node.dependencies)}"
115
+
116
+ if perms.read_contracts:
117
+
118
+ @agent.tool
119
+ async def ssot_read_contracts(ctx: RunContext[AgentDeps]) -> str:
120
+ """Read all contracts from the SSOT."""
121
+ contracts = ctx.deps.hub._persistence.list_contracts()
122
+ if not contracts:
123
+ return "No contracts registered."
124
+ lines = []
125
+ for c in contracts:
126
+ consumers = ", ".join(c.consumers) if c.consumers else "none"
127
+ lines.append(
128
+ f"[{c.id}] {c.name} (owner={c.owner}, v{c.version}, "
129
+ f"status={c.status.value}, consumers={consumers})"
130
+ )
131
+ return "\n".join(lines)
132
+
133
+ @agent.tool
134
+ async def ssot_get_contract(ctx: RunContext[AgentDeps], contract_id: str) -> str:
135
+ """Get a specific contract by ID."""
136
+ contract = ctx.deps.hub.get_contract(contract_id)
137
+ if contract is None:
138
+ return f"Contract {contract_id} not found."
139
+ import json
140
+
141
+ lines = [
142
+ f"ID: {contract.id}",
143
+ f"Name: {contract.name}",
144
+ f"Owner: {contract.owner}",
145
+ f"Version: {contract.version}",
146
+ f"Status: {contract.status.value}",
147
+ f"Consumers: {', '.join(contract.consumers) if contract.consumers else 'none'}",
148
+ f"Schema: {json.dumps(contract.interface_schema)}",
149
+ ]
150
+ return "\n".join(lines)
151
+
152
+ if perms.write_contracts:
153
+
154
+ @agent.tool
155
+ async def ssot_register_contract(
156
+ ctx: RunContext[AgentDeps],
157
+ name: str,
158
+ consumers: list[str] | None = None,
159
+ interface_schema: dict[str, Any] | None = None,
160
+ ) -> str:
161
+ """Register a new API contract in the SSOT.
162
+
163
+ The contract owner is automatically set to this agent.
164
+ """
165
+ schema = interface_schema if interface_schema is not None else {}
166
+ contract = ctx.deps.hub.register_contract(
167
+ name=name,
168
+ owner=ctx.deps.agent_id,
169
+ consumers=consumers,
170
+ interface_schema=schema,
171
+ )
172
+ return f"Registered contract {contract.id}: {contract.name} (v{contract.version})"
173
+
174
+ @agent.tool
175
+ async def ssot_update_contract_schema(
176
+ ctx: RunContext[AgentDeps], contract_id: str, new_schema: dict[str, Any]
177
+ ) -> str:
178
+ """Update a contract's interface schema. This bumps the version."""
179
+ contract = ctx.deps.hub.update_contract_schema(
180
+ contract_id, new_schema, ctx.deps.agent_id
181
+ )
182
+ return f"Contract {contract_id} updated to v{contract.version}"
183
+
184
+ if perms.read_memory:
185
+
186
+ @agent.tool
187
+ async def ssot_read_memory(ctx: RunContext[AgentDeps], key: str) -> str:
188
+ """Read a value from this agent's working memory."""
189
+ value = ctx.deps.hub.read_memory(ctx.deps.agent_id, key)
190
+ if value is None:
191
+ return f"No memory found for key '{key}'."
192
+ return value
193
+
194
+ if perms.write_memory:
195
+
196
+ @agent.tool
197
+ async def ssot_write_memory(ctx: RunContext[AgentDeps], key: str, value: str) -> str:
198
+ """Write a key-value pair to this agent's working memory."""
199
+ ctx.deps.hub.write_memory(ctx.deps.agent_id, key, value)
200
+ return f"Memory saved: {key}"
201
+
202
+ if perms.read_log:
203
+
204
+ @agent.tool
205
+ async def ssot_query_log(ctx: RunContext[AgentDeps], agent_id: str | None = None) -> str:
206
+ """Query the execution log, optionally filtered by agent_id."""
207
+ entries = ctx.deps.hub.query_log(agent_id=agent_id)
208
+ if not entries:
209
+ return "No log entries found."
210
+ lines = []
211
+ for e in entries[:20]:
212
+ status = e.status.value if hasattr(e.status, "value") else str(e.status)
213
+ lines.append(
214
+ f"[{e.timestamp}] {e.agent_id} {e.action_type} -> {e.target} ({status})"
215
+ )
216
+ return "\n".join(lines)
217
+
218
+ if perms.read_files:
219
+
220
+ @agent.tool
221
+ async def file_read(ctx: RunContext[AgentDeps], path: str) -> str:
222
+ """Read the contents of a file from the sandbox."""
223
+ action = Action(action_type="FILE_READ", target=path, agent_id=ctx.deps.agent_id)
224
+ result = ctx.deps.harness.execute_action(ctx.deps.agent_id, action)
225
+ if result.exit_code != 0:
226
+ return f"Error reading file: {result.stderr}"
227
+ content = result.stdout
228
+ if len(content) > 2000:
229
+ content = content[:2000] + f"\n... ({len(content) - 2000} chars truncated)"
230
+ return content
231
+
232
+ if perms.write_files:
233
+
234
+ @agent.tool
235
+ async def file_write(ctx: RunContext[AgentDeps], path: str, content: str) -> str:
236
+ """Write content to a file in the sandbox (creates parent dirs if needed)."""
237
+ action = Action(
238
+ action_type="FILE_WRITE",
239
+ target=path,
240
+ content=content,
241
+ agent_id=ctx.deps.agent_id,
242
+ )
243
+ result = ctx.deps.harness.execute_action(ctx.deps.agent_id, action)
244
+ if result.exit_code != 0:
245
+ return f"Error writing file: {result.stderr}"
246
+ return result.stdout or f"File written: {path}"
247
+
248
+ if perms.execute_shell:
249
+
250
+ @agent.tool
251
+ async def shell_exec(ctx: RunContext[AgentDeps], command: str) -> str:
252
+ """Execute a shell command in the sandbox and return stdout/stderr."""
253
+ action = Action(command=command, agent_id=ctx.deps.agent_id)
254
+ result = ctx.deps.harness.execute_action(ctx.deps.agent_id, action)
255
+ lines = [f"exit_code: {result.exit_code}"]
256
+ if result.stdout:
257
+ stdout = result.stdout
258
+ if len(stdout) > 2000:
259
+ stdout = stdout[:2000] + f"\n... ({len(stdout) - 2000} chars truncated)"
260
+ lines.append(f"stdout:\n{stdout}")
261
+ if result.stderr:
262
+ stderr = result.stderr
263
+ if len(stderr) > 1000:
264
+ stderr = stderr[:1000] + f"\n... ({len(stderr) - 1000} chars truncated)"
265
+ lines.append(f"stderr:\n{stderr}")
266
+ return "\n".join(lines)
File without changes
@@ -0,0 +1,16 @@
1
+ """Shared fixtures for maestro-agents tests."""
2
+
3
+ import pytest
4
+
5
+ from maestro_harness.harness import Harness
6
+ from maestro_ssot.hub import SSOTHub
7
+
8
+
9
+ @pytest.fixture
10
+ def hub() -> SSOTHub:
11
+ return SSOTHub(":memory:")
12
+
13
+
14
+ @pytest.fixture
15
+ def harness() -> Harness:
16
+ return Harness("configs/harness_policy.yaml", sandbox_root=".")
@@ -0,0 +1,63 @@
1
+ """Tests for MaestroAgent base class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from maestro_agents.base import AgentResult, MaestroAgent
6
+ from maestro_agents.deps import AgentDeps
7
+ from maestro_agents.permissions import AgentPermissions
8
+ from maestro_harness.harness import Harness
9
+ from maestro_ssot.hub import SSOTHub
10
+
11
+
12
+ class TestAgentResult:
13
+ def test_success_result(self) -> None:
14
+ r = AgentResult(output="done")
15
+ assert r.success
16
+ assert r.error is None
17
+
18
+ def test_error_result(self) -> None:
19
+ r = AgentResult(output="", success=False, error="boom")
20
+ assert not r.success
21
+ assert r.error == "boom"
22
+
23
+
24
+ class TestAgentDeps:
25
+ def test_deps_fields(self, hub: SSOTHub, harness: Harness) -> None:
26
+ deps = AgentDeps(hub=hub, harness=harness, agent_id="test", role="generic")
27
+ assert deps.agent_id == "test"
28
+ assert deps.role == "generic"
29
+ assert deps.memory == {}
30
+
31
+
32
+ class TestMaestroAgent:
33
+ def test_init(self, hub: SSOTHub, harness: Harness) -> None:
34
+ agent = MaestroAgent("test-agent", hub, harness)
35
+ assert agent.agent_id == "test-agent"
36
+ assert agent.hub is hub
37
+ assert agent.harness is harness
38
+ assert agent.role == "generic"
39
+
40
+ def test_default_system_prompt(self, hub: SSOTHub, harness: Harness) -> None:
41
+ agent = MaestroAgent("test-agent", hub, harness)
42
+ assert "MAESTRO" in agent.system_prompt()
43
+
44
+ def test_permissions_all_true_by_default(self, hub: SSOTHub, harness: Harness) -> None:
45
+ agent = MaestroAgent("test-agent", hub, harness)
46
+ toolset = agent._agent._function_toolset # type: ignore[attr-defined]
47
+ tool_names = {name for name in toolset.tools}
48
+ assert "file_read" in tool_names
49
+ assert "file_write" in tool_names
50
+ assert "shell_exec" in tool_names
51
+
52
+ def test_permissions_restrict_file_tools(self, hub: SSOTHub, harness: Harness) -> None:
53
+ restricted = AgentPermissions(
54
+ read_files=False, write_files=False, execute_shell=False
55
+ )
56
+ agent = MaestroAgent("restricted", hub, harness, permissions=restricted)
57
+ toolset = agent._agent._function_toolset # type: ignore[attr-defined]
58
+ tool_names = {name for name in toolset.tools}
59
+ assert "file_read" not in tool_names
60
+ assert "file_write" not in tool_names
61
+ assert "shell_exec" not in tool_names
62
+ # SSOT tools should still be present
63
+ assert "ssot_read_requirements" in tool_names
@@ -0,0 +1,23 @@
1
+ """Tests for BackendAgent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from maestro_agents.backend import BackendAgent
6
+ from maestro_harness.harness import Harness
7
+ from maestro_ssot.hub import SSOTHub
8
+
9
+
10
+ class TestBackendAgent:
11
+ def test_role(self, hub: SSOTHub, harness: Harness) -> None:
12
+ agent = BackendAgent("backend-1", hub, harness)
13
+ assert agent.role == "backend"
14
+
15
+ def test_system_prompt(self, hub: SSOTHub, harness: Harness) -> None:
16
+ agent = BackendAgent("backend-1", hub, harness)
17
+ prompt = agent.system_prompt()
18
+ assert "Backend Agent" in prompt
19
+ assert "implement" in prompt.lower()
20
+
21
+ def test_agent_id(self, hub: SSOTHub, harness: Harness) -> None:
22
+ agent = BackendAgent("backend-1", hub, harness)
23
+ assert agent.agent_id == "backend-1"
@@ -0,0 +1,49 @@
1
+ """Tests for the non-LLM contract validator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from maestro_agents.contract_validator import validate_contracts
6
+ from maestro_harness.harness import Harness
7
+ from maestro_ssot.hub import SSOTHub
8
+ from maestro_ssot.models import ContractStatus
9
+
10
+
11
+ class TestContractValidator:
12
+ def test_empty_contracts_valid(self, hub: SSOTHub, harness: Harness) -> None:
13
+ result = validate_contracts(hub)
14
+ assert result.valid
15
+ assert result.violations == []
16
+
17
+ def test_stable_contract_without_schema(self, hub: SSOTHub, harness: Harness) -> None:
18
+ contract = hub.register_contract("API", owner="backend_agent", interface_schema={})
19
+ hub.contracts.update_status(contract.id, ContractStatus.STABLE)
20
+ result = validate_contracts(hub)
21
+ assert not result.valid
22
+ assert any("empty interface_schema" in v.message for v in result.violations)
23
+
24
+ def test_draft_consumer_of_stable_contract(self, hub: SSOTHub, harness: Harness) -> None:
25
+ draft = hub.register_contract(
26
+ "DraftAPI", owner="backend_agent", interface_schema={"type": "object"}
27
+ )
28
+ active = hub.register_contract(
29
+ "ActiveAPI",
30
+ owner="frontend_agent",
31
+ consumers=[draft.id],
32
+ interface_schema={"type": "object"},
33
+ )
34
+ hub.contracts.update_status(active.id, ContractStatus.STABLE)
35
+
36
+ result = validate_contracts(hub)
37
+ assert not result.valid
38
+ assert any("DRAFT contract" in v.message for v in result.violations)
39
+
40
+ def test_valid_stable_contract_with_schema(self, hub: SSOTHub, harness: Harness) -> None:
41
+ contract = hub.register_contract(
42
+ "GoodAPI",
43
+ owner="backend_agent",
44
+ interface_schema={"type": "object", "properties": {"id": {"type": "string"}}},
45
+ )
46
+ hub.contracts.update_status(contract.id, ContractStatus.STABLE)
47
+
48
+ result = validate_contracts(hub)
49
+ assert result.valid
@@ -0,0 +1,23 @@
1
+ """Tests for PlanningAgent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from maestro_agents.planning import PlanningAgent
6
+ from maestro_harness.harness import Harness
7
+ from maestro_ssot.hub import SSOTHub
8
+
9
+
10
+ class TestPlanningAgent:
11
+ def test_role(self, hub: SSOTHub, harness: Harness) -> None:
12
+ agent = PlanningAgent("planner-1", hub, harness)
13
+ assert agent.role == "planner"
14
+
15
+ def test_system_prompt(self, hub: SSOTHub, harness: Harness) -> None:
16
+ agent = PlanningAgent("planner-1", hub, harness)
17
+ prompt = agent.system_prompt()
18
+ assert "Planning Agent" in prompt
19
+ assert "decompose" in prompt.lower()
20
+
21
+ def test_agent_id(self, hub: SSOTHub, harness: Harness) -> None:
22
+ agent = PlanningAgent("planner-1", hub, harness)
23
+ assert agent.agent_id == "planner-1"