Coverage for src / core / protocols.py: 100%
282 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-04 04:43 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-04 04:43 +0000
1"""Protocol definitions for pipeline stage abstractions.
3This module defines Protocol classes that enable dependency injection and
4testability for the MalaOrchestrator's pipeline stages. Each protocol
5represents a stage boundary that the orchestrator interacts with.
7Design principles:
8- Protocols use structural typing (typing.Protocol) for flexibility
9- Methods match exactly what the orchestrator actually calls
10- Result types are defined as local Protocol types to avoid import-time dependencies
11- BeadsClient, ReviewRunner, and QualityGate conform to these protocols
13Usage:
14 These protocols enable:
15 1. Mock implementations for unit testing the orchestrator
16 2. Alternative implementations (e.g., in-memory issue tracker)
17 3. Clear contracts between orchestrator and its dependencies
18"""
20from __future__ import annotations
22from dataclasses import dataclass
23from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
25if TYPE_CHECKING:
26 from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
27 from pathlib import Path
28 from types import TracebackType
29 from typing import Self
32# =============================================================================
33# Local Protocol Types
34# =============================================================================
35# These Protocol types replace TYPE_CHECKING imports from domain/infra modules
36# to satisfy the "Layered Architecture" contract. They define the structural
37# shape that protocols.py needs without creating import-time dependencies.
38#
39# Each Protocol matches only the attributes/methods that protocols.py actually
40# uses, following the Interface Segregation Principle.
41#
42# Note: These protocols use plain attributes (not @property) to be compatible
43# with dataclass implementations. Protocol structural typing matches attributes
44# regardless of whether they're defined as properties or plain attributes.
45# =============================================================================
48@runtime_checkable
49class JsonlEntryProtocol(Protocol):
50 """Protocol for parsed JSONL log entries with byte offset tracking.
52 Matches the shape of session_log_parser.JsonlEntry for structural typing.
53 """
55 data: dict[str, Any]
56 """The parsed JSON object from this line."""
58 entry: object | None
59 """The typed LogEntry if successfully parsed, None otherwise."""
61 line_len: int
62 """Length of the raw line in bytes (for offset tracking)."""
64 offset: int
65 """Byte offset where this line started in the file."""
68@runtime_checkable
69class ValidationSpecProtocol(Protocol):
70 """Protocol for validation specification.
72 Matches the shape of validation.spec.ValidationSpec for structural typing.
73 Only includes attributes/methods that protocols.py method signatures use.
74 """
76 commands: Sequence[Any]
77 """List of validation commands to run."""
79 scope: Any
80 """The validation scope (per-issue or run-level)."""
83@runtime_checkable
84class ValidationEvidenceProtocol(Protocol):
85 """Protocol for validation evidence from agent runs.
87 Matches the shape of quality_gate.ValidationEvidence for structural typing.
88 """
90 commands_ran: dict[Any, bool]
91 """Mapping of CommandKind to whether it ran."""
93 failed_commands: list[str]
94 """List of validation commands that failed."""
96 def has_any_evidence(self) -> bool:
97 """Check if any validation command ran."""
98 ...
100 def to_evidence_dict(self) -> dict[str, bool]:
101 """Convert evidence to a serializable dict keyed by CommandKind value."""
102 ...
105@runtime_checkable
106class CommitResultProtocol(Protocol):
107 """Protocol for commit existence check results.
109 Matches the shape of quality_gate.CommitResult for structural typing.
110 """
112 exists: bool
113 """Whether a matching commit exists."""
115 commit_hash: str | None
116 """The commit hash if found."""
118 message: str | None
119 """The commit message if found."""
122@runtime_checkable
123class IssueResolutionProtocol(Protocol):
124 """Protocol for issue resolution records.
126 Matches the shape of models.IssueResolution for structural typing.
127 """
129 outcome: Any
130 """The resolution outcome (success, no_change, obsolete, etc.)."""
132 rationale: str
133 """Explanation for the resolution."""
136@runtime_checkable
137class GateResultProtocol(Protocol):
138 """Protocol for quality gate check results.
140 Matches the shape of quality_gate.GateResult for structural typing.
141 """
143 passed: bool
144 """Whether the quality gate passed."""
146 failure_reasons: list[str]
147 """List of reasons why the gate failed."""
149 commit_hash: str | None
150 """The commit hash if found."""
152 validation_evidence: ValidationEvidenceProtocol | None
153 """Evidence of validation commands executed."""
155 no_progress: bool
156 """Whether no progress was detected."""
158 resolution: IssueResolutionProtocol | None
159 """Issue resolution if applicable."""
162@runtime_checkable
163class ReviewIssueProtocol(Protocol):
164 """Protocol for review issues found during code review.
166 Matches the shape of cerberus_review.ReviewIssue for structural typing.
167 """
169 file: str
170 """File path where the issue was found."""
172 line_start: int
173 """Starting line number."""
175 line_end: int
176 """Ending line number."""
178 priority: int | None
179 """Issue priority (0=P0, 1=P1, etc.)."""
181 title: str
182 """Issue title."""
184 body: str
185 """Issue body/description."""
187 reviewer: str
188 """Which reviewer found this issue."""
191@runtime_checkable
192class ReviewResultProtocol(Protocol):
193 """Protocol for code review results.
195 Matches the shape of cerberus_review.ReviewResult for structural typing.
196 """
198 passed: bool
199 """Whether the review passed."""
201 issues: Sequence[ReviewIssueProtocol]
202 """List of issues found during review."""
204 parse_error: str | None
205 """Parse error message if JSON parsing failed."""
207 fatal_error: bool
208 """Whether this is a fatal error (should not retry)."""
210 review_log_path: Path | None
211 """Path to review session logs."""
214@runtime_checkable
215class UnmetCriterionProtocol(Protocol):
216 """Protocol for unmet criteria during epic verification.
218 Matches the shape of models.UnmetCriterion for structural typing.
219 """
221 criterion: str
222 """The acceptance criterion not met."""
224 evidence: str
225 """Why it's considered unmet."""
227 priority: int
228 """Issue priority matching Cerberus levels (0-3). P0/P1 blocking, P2/P3 informational."""
230 criterion_hash: str
231 """SHA256 of criterion text, for deduplication."""
234@runtime_checkable
235class EpicVerdictProtocol(Protocol):
236 """Protocol for epic verification verdicts.
238 Matches the shape of models.EpicVerdict for structural typing.
239 """
241 passed: bool
242 """Whether all acceptance criteria were met."""
244 unmet_criteria: Sequence[UnmetCriterionProtocol]
245 """List of criteria that were not satisfied."""
247 confidence: float
248 """Model confidence in the verdict (0.0 to 1.0)."""
250 reasoning: str
251 """Explanation of the verification outcome."""
254@runtime_checkable
255class DeadlockInfoProtocol(Protocol):
256 """Protocol for deadlock detection information.
258 Matches the shape of domain.deadlock.DeadlockInfo for structural typing.
259 """
261 cycle: list[str]
262 """List of agent IDs forming the deadlock cycle."""
264 victim_id: str
265 """Agent ID selected to be killed (youngest in cycle)."""
267 victim_issue_id: str | None
268 """Issue ID the victim was working on."""
270 blocked_on: str
271 """Lock path the victim was waiting for."""
273 blocker_id: str
274 """Agent ID holding the lock the victim needs."""
276 blocker_issue_id: str | None
277 """Issue ID the blocker was working on."""
280@runtime_checkable
281class LockEventProtocol(Protocol):
282 """Protocol for lock events.
284 Matches the shape of core.models.LockEvent for structural typing.
285 """
287 event_type: Any
288 """Type of lock event (LockEventType enum value)."""
290 agent_id: str
291 """ID of the agent that emitted this event."""
293 lock_path: str
294 """Path to the lock file."""
296 timestamp: float
297 """Unix timestamp when the event occurred."""
300@runtime_checkable
301class DeadlockMonitorProtocol(Protocol):
302 """Protocol for deadlock monitor.
304 Matches the interface of domain.deadlock.DeadlockMonitor for structural typing.
305 Only includes the handle_event method used by hooks.
306 """
308 async def handle_event(self, event: Any) -> Any: # noqa: ANN401
309 """Process a lock event and check for deadlocks.
311 Args:
312 event: The lock event to process (LockEvent).
314 Returns:
315 DeadlockInfo if a deadlock is detected, None otherwise.
316 """
317 ...
320@runtime_checkable
321class LogProvider(Protocol):
322 """Protocol for abstracting SDK log storage and schema.
324 Provides methods for accessing session logs without hardcoding filesystem
325 paths or Claude SDK's internal log format. This enables:
326 - Testing with mock log providers that return synthetic events
327 - Future support for remote log storage or SDK API access
328 - Isolation from SDK log format changes
330 The canonical implementation is FileSystemLogProvider, which reads JSONL
331 logs from the Claude SDK's ~/.claude/projects/{encoded-path}/ directory.
332 Test implementations can return in-memory events for isolation.
334 Methods:
335 get_log_path: Get the filesystem path for a session log.
336 iter_events: Iterate over typed log entries from a session.
337 get_end_offset: Get the byte offset at the end of a log file.
338 """
340 def get_log_path(self, repo_path: Path, session_id: str) -> Path:
341 """Get the filesystem path for a session's log file.
343 This method computes the expected log file path based on the repo
344 and session. The path may or may not exist yet.
346 Args:
347 repo_path: Path to the repository the session was run in.
348 session_id: Claude SDK session ID (UUID from ResultMessage).
350 Returns:
351 Path to the JSONL log file.
352 """
353 ...
355 def iter_events(
356 self, log_path: Path, offset: int = 0
357 ) -> Iterator[JsonlEntryProtocol]:
358 """Iterate over parsed JSONL entries from a log file.
360 Reads the file starting from the given byte offset and yields
361 structured entries. This enables incremental parsing across
362 retry attempts.
364 Args:
365 log_path: Path to the JSONL log file.
366 offset: Byte offset to start reading from (default 0).
368 Yields:
369 JsonlEntryProtocol objects for each successfully parsed JSON line.
370 The entry field contains the typed LogEntry if parsing succeeded.
372 Note:
373 - Lines that fail UTF-8 decoding are silently skipped
374 - Empty lines are silently skipped
375 - Lines that fail JSON parsing are silently skipped
376 - If file doesn't exist, yields nothing
377 """
378 ...
380 def get_end_offset(self, log_path: Path, start_offset: int = 0) -> int:
381 """Get the byte offset at the end of a log file.
383 This is a lightweight method for getting the current file position.
384 Use this when you only need the offset for retry scoping, not the
385 parsed entries themselves.
387 Args:
388 log_path: Path to the JSONL log file.
389 start_offset: Byte offset to start from (default 0).
391 Returns:
392 The byte offset at the end of the file, or start_offset if file
393 doesn't exist or can't be read.
394 """
395 ...
397 def extract_bash_commands(self, entry: JsonlEntryProtocol) -> list[tuple[str, str]]:
398 """Extract Bash tool_use commands from an entry.
400 Args:
401 entry: A JsonlEntryProtocol from iter_events.
403 Returns:
404 List of (tool_id, command) tuples for Bash tool_use blocks.
405 Returns empty list if entry is not an assistant message.
406 """
407 ...
409 def extract_tool_results(self, entry: JsonlEntryProtocol) -> list[tuple[str, bool]]:
410 """Extract tool_result entries from an entry.
412 Args:
413 entry: A JsonlEntryProtocol from iter_events.
415 Returns:
416 List of (tool_use_id, is_error) tuples for tool_result blocks.
417 Returns empty list if entry is not a user message.
418 """
419 ...
421 def extract_assistant_text_blocks(self, entry: JsonlEntryProtocol) -> list[str]:
422 """Extract text content from assistant message blocks.
424 Args:
425 entry: A JsonlEntryProtocol from iter_events.
427 Returns:
428 List of text strings from text blocks in assistant messages.
429 Returns empty list if entry is not an assistant message.
430 """
431 ...
434@runtime_checkable
435class IssueProvider(Protocol):
436 """Protocol for issue tracking operations.
438 Provides methods for fetching, claiming, closing, and marking issues.
439 The orchestrator uses this to manage issue lifecycle during parallel
440 processing.
442 The canonical implementation is BeadsClient, which wraps the bd CLI.
443 Test implementations can use in-memory state for isolation.
445 Methods match BeadsClient's async API exactly so BeadsClient conforms
446 to this protocol without adaptation.
447 """
449 async def get_ready_async(
450 self,
451 exclude_ids: set[str] | None = None,
452 epic_id: str | None = None,
453 only_ids: set[str] | None = None,
454 suppress_warn_ids: set[str] | None = None,
455 prioritize_wip: bool = False,
456 focus: bool = True,
457 orphans_only: bool = False,
458 ) -> list[str]:
459 """Get list of ready issue IDs, sorted by priority.
461 Args:
462 exclude_ids: Set of issue IDs to exclude from results.
463 epic_id: Optional epic ID to filter by - only return children.
464 only_ids: Optional set of issue IDs to include exclusively.
465 suppress_warn_ids: Set of issue IDs to suppress from warnings.
466 prioritize_wip: If True, sort in_progress issues first.
467 focus: If True, group tasks by parent epic.
468 orphans_only: If True, only return issues with no parent epic.
470 Returns:
471 List of issue IDs sorted by priority (lower = higher priority).
472 """
473 ...
475 async def claim_async(self, issue_id: str) -> bool:
476 """Claim an issue by setting status to in_progress.
478 Args:
479 issue_id: The issue ID to claim.
481 Returns:
482 True if successfully claimed, False otherwise.
483 """
484 ...
486 async def close_async(self, issue_id: str) -> bool:
487 """Close an issue by setting status to closed.
489 Args:
490 issue_id: The issue ID to close.
492 Returns:
493 True if successfully closed, False otherwise.
494 """
495 ...
497 async def mark_needs_followup_async(
498 self, issue_id: str, reason: str, log_path: Path | None = None
499 ) -> bool:
500 """Mark an issue as needing follow-up.
502 Called when the quality gate fails and the issue needs manual
503 intervention or a follow-up task.
505 Args:
506 issue_id: The issue ID to mark.
507 reason: Description of why the quality gate failed.
508 log_path: Optional path to the JSONL log file from the attempt.
510 Returns:
511 True if successfully marked, False otherwise.
512 """
513 ...
515 async def add_dependency_async(self, issue_id: str, depends_on_id: str) -> bool:
516 """Add a dependency between two issues.
518 Creates a "blocks" relationship where depends_on_id blocks issue_id.
519 Used by deadlock resolution to record that a victim issue depends on
520 the blocker's issue.
522 Args:
523 issue_id: The issue that depends on another.
524 depends_on_id: The issue that blocks issue_id.
526 Returns:
527 True if dependency added successfully, False otherwise.
528 """
529 ...
531 async def get_issue_description_async(self, issue_id: str) -> str | None:
532 """Get the description of an issue.
534 Args:
535 issue_id: The issue ID to get description for.
537 Returns:
538 The issue description string, or None if not found.
539 """
540 ...
542 async def close_eligible_epics_async(self) -> bool:
543 """Auto-close epics where all children are complete.
545 Returns:
546 True if any epics were closed, False otherwise.
547 """
548 ...
550 async def commit_issues_async(self) -> bool:
551 """Commit .beads/issues.jsonl if it has changes.
553 Returns:
554 True if commit succeeded, False otherwise.
555 """
556 ...
558 async def reset_async(
559 self, issue_id: str, log_path: Path | None = None, error: str | None = None
560 ) -> bool:
561 """Reset an issue back to ready status.
563 Called when an implementation attempt fails and the issue should be
564 made available for retry.
566 Args:
567 issue_id: The issue ID to reset.
568 log_path: Optional path to the JSONL log file from the attempt.
569 error: Optional error message describing the failure.
571 Returns:
572 True if successfully reset, False otherwise.
573 """
574 ...
576 async def get_epic_children_async(self, epic_id: str) -> set[str]:
577 """Get all child issue IDs of an epic.
579 Args:
580 epic_id: The epic ID to get children for.
582 Returns:
583 Set of child issue IDs, or empty set if not found or on error.
584 """
585 ...
587 async def get_parent_epic_async(self, issue_id: str) -> str | None:
588 """Get the parent epic ID for an issue.
590 Args:
591 issue_id: The issue ID to find the parent epic for.
593 Returns:
594 The parent epic ID, or None if no parent epic exists (orphan).
595 """
596 ...
598 async def create_issue_async(
599 self,
600 title: str,
601 description: str,
602 priority: str,
603 tags: list[str] | None = None,
604 parent_id: str | None = None,
605 ) -> str | None:
606 """Create a new issue for tracking.
608 Used to create tracking issues for low-priority review findings (P2/P3)
609 that should be addressed later but don't block the current work.
611 Args:
612 title: Issue title.
613 description: Issue description (supports markdown).
614 priority: Priority string (P1, P2, P3, etc.).
615 tags: Optional list of tags to apply.
616 parent_id: Optional parent epic ID to attach this issue to.
618 Returns:
619 Created issue ID, or None on failure.
620 """
621 ...
623 async def find_issue_by_tag_async(self, tag: str) -> str | None:
624 """Find an existing issue with the given tag.
626 Used for deduplication when creating tracking issues.
628 Args:
629 tag: The tag to search for.
631 Returns:
632 Issue ID if found, None otherwise.
633 """
634 ...
636 async def update_issue_description_async(
637 self, issue_id: str, description: str
638 ) -> bool:
639 """Update an issue's description.
641 Used for appending new findings to existing tracking issues.
643 Args:
644 issue_id: The issue ID to update.
645 description: New description content (replaces existing).
647 Returns:
648 True if successfully updated, False otherwise.
649 """
650 ...
652 async def update_issue_async(
653 self,
654 issue_id: str,
655 *,
656 title: str | None = None,
657 priority: str | None = None,
658 ) -> bool:
659 """Update an issue's title and/or priority.
661 Used for updating tracking issues when new findings change
662 the count or highest priority.
664 Args:
665 issue_id: The issue ID to update.
666 title: New title (optional).
667 priority: New priority string like "P2" (optional).
669 Returns:
670 True if successfully updated, False otherwise.
671 """
672 ...
675@runtime_checkable
676class CodeReviewer(Protocol):
677 """Protocol for code review operations.
679 Provides a callable interface for reviewing commits and returning
680 structured results. The orchestrator uses this to run post-commit
681 code reviews via the Cerberus review-gate CLI.
683 The canonical implementation is DefaultReviewer in cerberus_review.py.
684 Test implementations can return predetermined results for isolation.
685 """
687 async def __call__(
688 self,
689 diff_range: str,
690 context_file: Path | None = None,
691 timeout: int = 300,
692 claude_session_id: str | None = None,
693 *,
694 commit_shas: Sequence[str] | None = None,
695 ) -> ReviewResultProtocol:
696 """Run code review on a diff range.
698 Args:
699 diff_range: Git diff range to review (e.g., "baseline..HEAD").
700 context_file: Optional path to file with issue description context.
701 timeout: Timeout in seconds for the review operation.
702 claude_session_id: Optional Claude session ID for review attribution.
703 commit_shas: Optional list of commit SHAs to review directly.
704 When provided, reviewers should scope to these commits only.
706 Returns:
707 ReviewResultProtocol with review outcome. On parse failure,
708 returns passed=False with parse_error set.
709 """
710 ...
713@runtime_checkable
714class GateChecker(Protocol):
715 """Protocol for quality gate checking.
717 Provides methods for verifying agent work meets quality requirements.
718 The orchestrator uses this after each agent attempt to determine if
719 the issue was successfully resolved.
721 The canonical implementation is QualityGate, which conforms to this
722 protocol. Test implementations can verify specific conditions for isolation.
724 Methods match QualityGate's API exactly so QualityGate conforms to this
725 protocol without adaptation.
726 """
728 def check_with_resolution(
729 self,
730 issue_id: str,
731 log_path: Path,
732 baseline_timestamp: int | None = None,
733 log_offset: int = 0,
734 spec: ValidationSpecProtocol | None = None,
735 ) -> GateResultProtocol:
736 """Run quality gate check with support for no-op/obsolete resolutions.
738 This method is scope-aware and handles special resolution outcomes:
739 - ISSUE_NO_CHANGE: Issue already addressed, no commit needed
740 - ISSUE_OBSOLETE: Issue no longer relevant, no commit needed
741 - ISSUE_ALREADY_COMPLETE: Work done in previous run
743 Args:
744 issue_id: The issue ID to verify.
745 log_path: Path to the JSONL log file from agent session.
746 baseline_timestamp: Unix timestamp for commit freshness check.
747 log_offset: Byte offset to start parsing from.
748 spec: ValidationSpec for scope-aware evidence checking (required).
750 Returns:
751 GateResultProtocol with pass/fail, failure reasons, and resolution.
753 Raises:
754 ValueError: If spec is not provided.
755 """
756 ...
758 def get_log_end_offset(self, log_path: Path, start_offset: int = 0) -> int:
759 """Get the byte offset at the end of a log file.
761 This is a lightweight method for getting the current file position
762 after reading from a given offset. Use this when you only need the
763 offset for retry scoping, not the evidence itself.
765 Args:
766 log_path: Path to the JSONL log file.
767 start_offset: Byte offset to start from (default 0).
769 Returns:
770 The byte offset at the end of the file, or start_offset if file
771 doesn't exist or can't be read.
772 """
773 ...
775 def check_no_progress(
776 self,
777 log_path: Path,
778 log_offset: int,
779 previous_commit_hash: str | None,
780 current_commit_hash: str | None,
781 spec: ValidationSpecProtocol | None = None,
782 check_validation_evidence: bool = True,
783 ) -> bool:
784 """Check if no progress was made since the last attempt.
786 No progress is detected when ALL of these are true:
787 - The commit hash hasn't changed (or both are None)
788 - No uncommitted changes in the working tree
789 - (Optionally) No new validation evidence was found after the log offset
791 Args:
792 log_path: Path to the JSONL log file from agent session.
793 log_offset: Byte offset marking the end of the previous attempt.
794 previous_commit_hash: Commit hash from the previous attempt.
795 current_commit_hash: Commit hash from this attempt.
796 spec: Optional ValidationSpec for spec-driven evidence detection.
797 check_validation_evidence: If True (default), also check for new validation
798 evidence. Set to False for review retries where only commit/working-tree
799 changes should gate progress.
801 Returns:
802 True if no progress was made, False if progress was detected.
803 """
804 ...
806 def parse_validation_evidence_with_spec(
807 self, log_path: Path, spec: ValidationSpecProtocol, offset: int = 0
808 ) -> ValidationEvidenceProtocol:
809 """Parse JSONL log for validation evidence using spec-defined patterns.
811 Args:
812 log_path: Path to the JSONL log file.
813 spec: ValidationSpec defining detection patterns.
814 offset: Byte offset to start parsing from (default 0).
816 Returns:
817 ValidationEvidenceProtocol with flags indicating which validations ran.
818 """
819 ...
821 def check_commit_exists(
822 self, issue_id: str, baseline_timestamp: int | None = None
823 ) -> CommitResultProtocol:
824 """Check if a commit with bd-<issue_id> exists in recent history.
826 Searches commits from the last 30 days to accommodate long-running
827 work that may span multiple days.
829 Args:
830 issue_id: The issue ID to search for (without bd- prefix).
831 baseline_timestamp: Unix timestamp. If provided, only accepts
832 commits created after this time.
834 Returns:
835 CommitResultProtocol indicating whether a matching commit exists.
836 """
837 ...
840@runtime_checkable
841class EpicVerificationModel(Protocol):
842 """Protocol for model-agnostic epic verification.
844 Provides an interface for verifying whether code changes satisfy
845 an epic's acceptance criteria. The initial implementation uses
846 Claude via SDK, but this protocol allows swapping to other models
847 (Codex, Gemini, local models) without changing the verifier.
849 The canonical implementation is ClaudeEpicVerificationModel in
850 src/epic_verifier.py. Test implementations can return predetermined
851 verdicts for isolation.
852 """
854 async def verify(
855 self,
856 epic_criteria: str,
857 commit_range: str,
858 commit_list: str,
859 spec_content: str | None,
860 ) -> EpicVerdictProtocol:
861 """Verify if the commit scope satisfies the epic's acceptance criteria.
863 Args:
864 epic_criteria: The epic's acceptance criteria text.
865 commit_range: Commit range hint covering child issue commits.
866 commit_list: Authoritative list of commit SHAs to inspect.
867 spec_content: Optional content of linked spec file.
869 Returns:
870 Structured verdict with pass/fail and unmet criteria details.
871 """
872 ...
875# =============================================================================
876# SDK Client Protocol
877# =============================================================================
880@runtime_checkable
881class SDKClientProtocol(Protocol):
882 """Protocol for Claude SDK client abstraction.
884 Enables the pipeline layer to use SDK clients without importing
885 claude_agent_sdk directly. The canonical implementation is
886 ClaudeSDKClient, wrapped by SDKClientFactory in infra.
888 This protocol captures the async context manager and streaming
889 interface used by AgentSessionRunner.
890 """
892 async def __aenter__(self) -> Self:
893 """Enter async context."""
894 ...
896 async def __aexit__(
897 self,
898 exc_type: type[BaseException] | None,
899 exc_val: BaseException | None,
900 exc_tb: TracebackType | None,
901 ) -> None:
902 """Exit async context."""
903 ...
905 async def query(self, prompt: str, session_id: str | None = None) -> None:
906 """Send a query to the agent.
908 Args:
909 prompt: The prompt text to send.
910 session_id: Optional session ID for continuation.
911 """
912 ...
914 def receive_response(self) -> AsyncIterator[object]:
915 """Get an async iterator of response messages.
917 Returns:
918 AsyncIterator yielding AssistantMessage, ResultMessage, etc.
919 """
920 ...
922 async def disconnect(self) -> None:
923 """Disconnect the client."""
924 ...
927@runtime_checkable
928class SDKClientFactoryProtocol(Protocol):
929 """Protocol for SDK client factory.
931 Enables dependency injection of the factory into pipeline components,
932 allowing tests to provide mock factories.
933 """
935 def create(self, options: object) -> SDKClientProtocol:
936 """Create a new SDK client with the given options.
938 Args:
939 options: ClaudeAgentOptions for the client.
941 Returns:
942 SDKClientProtocol instance.
943 """
944 ...
946 def create_options(
947 self,
948 *,
949 cwd: str,
950 permission_mode: str = "bypassPermissions",
951 model: str = "opus",
952 system_prompt: dict[str, str] | None = None,
953 setting_sources: list[str] | None = None,
954 mcp_servers: object | None = None,
955 disallowed_tools: list[str] | None = None,
956 env: dict[str, str] | None = None,
957 hooks: dict[str, list[object]] | None = None,
958 ) -> object:
959 """Create SDK options without requiring SDK import in caller.
961 Args:
962 cwd: Working directory for the agent.
963 permission_mode: Permission mode.
964 model: Model to use.
965 system_prompt: System prompt configuration.
966 setting_sources: List of setting sources.
967 mcp_servers: List of MCP server configurations.
968 disallowed_tools: List of tools to disallow.
969 env: Environment variables for the agent.
970 hooks: Hook configurations keyed by event type.
972 Returns:
973 ClaudeAgentOptions instance.
974 """
975 ...
977 def create_hook_matcher(
978 self,
979 matcher: object | None,
980 hooks: list[object],
981 ) -> object:
982 """Create a HookMatcher for SDK hook registration.
984 Args:
985 matcher: Optional matcher configuration.
986 hooks: List of hook callables.
988 Returns:
989 HookMatcher instance.
990 """
991 ...
994# =============================================================================
995# Command Runner Protocols
996# =============================================================================
999@runtime_checkable
1000class CommandResultProtocol(Protocol):
1001 """Protocol for command execution results.
1003 Matches the interface of src.infra.tools.command_runner.CommandResult
1004 for structural typing without import-time dependencies.
1005 """
1007 ok: bool
1008 returncode: int
1009 stdout: str
1010 stderr: str
1011 timed_out: bool
1012 duration_seconds: float
1014 def stdout_tail(self, max_chars: int = 800, max_lines: int = 20) -> str:
1015 """Get truncated stdout.
1017 Args:
1018 max_chars: Maximum number of characters.
1019 max_lines: Maximum number of lines.
1021 Returns:
1022 Truncated stdout string.
1023 """
1024 ...
1026 def stderr_tail(self, max_chars: int = 800, max_lines: int = 20) -> str:
1027 """Get truncated stderr.
1029 Args:
1030 max_chars: Maximum number of characters.
1031 max_lines: Maximum number of lines.
1033 Returns:
1034 Truncated stderr string.
1035 """
1036 ...
1039@runtime_checkable
1040class CommandRunnerPort(Protocol):
1041 """Protocol for abstracting command execution.
1043 Enables dependency injection of command runners into domain modules,
1044 allowing the core layer to define the interface without depending on
1045 the infrastructure implementation.
1047 The canonical implementation is CommandRunner in
1048 src/infra/tools/command_runner.py.
1049 """
1051 def run(
1052 self,
1053 cmd: list[str] | str,
1054 env: Mapping[str, str] | None = None,
1055 timeout: float | None = None,
1056 use_process_group: bool | None = None,
1057 shell: bool = False,
1058 cwd: Path | None = None,
1059 ) -> CommandResultProtocol:
1060 """Run a command synchronously.
1062 Args:
1063 cmd: Command to run. Can be a list of strings or a shell string.
1064 env: Environment variables to set (merged with os.environ).
1065 timeout: Timeout for command execution in seconds.
1066 use_process_group: Whether to use process group for termination.
1067 shell: If True, run command through shell.
1068 cwd: Override working directory for this command.
1070 Returns:
1071 CommandResultProtocol with execution details.
1072 """
1073 ...
1075 async def run_async(
1076 self,
1077 cmd: list[str] | str,
1078 env: Mapping[str, str] | None = None,
1079 timeout: float | None = None,
1080 use_process_group: bool | None = None,
1081 shell: bool = False,
1082 cwd: Path | None = None,
1083 ) -> CommandResultProtocol:
1084 """Run a command asynchronously.
1086 Args:
1087 cmd: Command to run. Can be a list of strings or a shell string.
1088 env: Environment variables to set (merged with os.environ).
1089 timeout: Timeout for command execution in seconds.
1090 use_process_group: Whether to use process group for termination.
1091 shell: If True, run command through shell.
1092 cwd: Override working directory for this command.
1094 Returns:
1095 CommandResultProtocol with execution details.
1096 """
1097 ...
1100@runtime_checkable
1101class EnvConfigPort(Protocol):
1102 """Protocol for abstracting environment configuration.
1104 Enables dependency injection of environment config into domain modules,
1105 allowing the core layer to define the interface without depending on
1106 the infrastructure implementation.
1108 The canonical implementation is in src/infra/tools/env.py.
1109 """
1111 @property
1112 def scripts_dir(self) -> Path:
1113 """Path to the scripts directory (e.g., test-mutex.sh)."""
1114 ...
1116 @property
1117 def cache_dir(self) -> Path:
1118 """Path to the mala cache directory."""
1119 ...
1121 @property
1122 def lock_dir(self) -> Path:
1123 """Path to the lock directory for multi-agent coordination."""
1124 ...
1126 def find_cerberus_bin_path(self) -> Path | None:
1127 """Find the cerberus plugin bin directory.
1129 Returns:
1130 Path to cerberus bin directory, or None if not found.
1131 """
1132 ...
1135@runtime_checkable
1136class LockManagerPort(Protocol):
1137 """Protocol for abstracting file-based locking operations.
1139 Enables dependency injection of lock managers into domain modules,
1140 allowing the core layer to define the interface without depending on
1141 the infrastructure implementation.
1143 The canonical implementation is in src/infra/tools/locking.py.
1144 """
1146 def lock_path(self, filepath: str, repo_namespace: str | None = None) -> Path:
1147 """Get the lock file path for a given filepath.
1149 Args:
1150 filepath: Path to the file to lock.
1151 repo_namespace: Optional repo namespace for cross-repo disambiguation.
1153 Returns:
1154 Path to the lock file.
1155 """
1156 ...
1158 def try_lock(
1159 self, filepath: str, agent_id: str, repo_namespace: str | None = None
1160 ) -> bool:
1161 """Try to acquire a lock without blocking.
1163 Args:
1164 filepath: Path to the file to lock.
1165 agent_id: Identifier of the agent requesting the lock.
1166 repo_namespace: Optional repo namespace for cross-repo disambiguation.
1168 Returns:
1169 True if lock was acquired, False if already held by another agent.
1170 """
1171 ...
1173 def wait_for_lock(
1174 self,
1175 filepath: str,
1176 agent_id: str,
1177 repo_namespace: str | None = None,
1178 timeout_seconds: float = 30.0,
1179 poll_interval_ms: int = 100,
1180 ) -> bool:
1181 """Wait for and acquire a lock on a file.
1183 Args:
1184 filepath: Path to the file to lock.
1185 agent_id: Identifier of the agent requesting the lock.
1186 repo_namespace: Optional repo namespace for cross-repo disambiguation.
1187 timeout_seconds: Maximum time to wait for the lock in seconds.
1188 poll_interval_ms: Polling interval in milliseconds.
1190 Returns:
1191 True if lock was acquired, False if timeout.
1192 """
1193 ...
1195 def release_lock(
1196 self, filepath: str, agent_id: str, repo_namespace: str | None = None
1197 ) -> bool:
1198 """Release a lock on a file.
1200 Only releases the lock if it is held by the specified agent_id.
1201 This prevents accidental or malicious release of locks held by
1202 other agents.
1204 Args:
1205 filepath: Path to the file to unlock.
1206 agent_id: Identifier of the agent releasing the lock.
1207 repo_namespace: Optional repo namespace for cross-repo disambiguation.
1209 Returns:
1210 True if lock was released, False if lock was not held by agent_id.
1211 """
1212 ...
1215@runtime_checkable
1216class LoggerPort(Protocol):
1217 """Protocol for console/terminal logging with colored output.
1219 Enables dependency injection of loggers into domain modules,
1220 allowing the core layer to define the interface without depending on
1221 the infrastructure implementation.
1223 The canonical implementation is in src/infra/io/log_output/console.py.
1224 """
1226 def log(
1227 self,
1228 message: str,
1229 *,
1230 level: str = "info",
1231 color: str | None = None,
1232 ) -> None:
1233 """Log a message to the console.
1235 Args:
1236 message: The message to log.
1237 level: Log level (e.g., "info", "debug", "error").
1238 color: Optional color name (e.g., "cyan", "green", "red").
1239 """
1240 ...
1243# =============================================================================
1244# Event Sink Protocol
1245# =============================================================================
1248@dataclass
1249class EventRunConfig:
1250 """Configuration snapshot for a run, passed to on_run_started.
1252 Mirrors the relevant fields from MalaOrchestrator for event reporting.
1253 """
1255 repo_path: str
1256 max_agents: int | None
1257 timeout_minutes: int | None
1258 max_issues: int | None
1259 max_gate_retries: int
1260 max_review_retries: int
1261 epic_id: str | None = None
1262 only_ids: list[str] | None = None
1263 braintrust_enabled: bool = False
1264 braintrust_disabled_reason: str | None = None # e.g., "add BRAINTRUST_API_KEY..."
1265 review_enabled: bool = True # Cerberus code review enabled
1266 review_disabled_reason: str | None = None
1267 prioritize_wip: bool = False
1268 orphans_only: bool = False
1269 cli_args: dict[str, object] | None = None
1272@runtime_checkable
1273class MalaEventSink(Protocol):
1274 """Protocol for receiving orchestrator events.
1276 Implementations handle presentation (console, logging, metrics) while
1277 the orchestrator focuses on coordination logic. Each method corresponds
1278 to a semantic event in the orchestration flow.
1280 All methods are synchronous and should be non-blocking. Implementations
1281 that need async behavior should queue events internally.
1282 """
1284 # -------------------------------------------------------------------------
1285 # Run lifecycle
1286 # -------------------------------------------------------------------------
1288 def on_run_started(self, config: EventRunConfig) -> None:
1289 """Called when the orchestrator run begins.
1291 Args:
1292 config: Run configuration snapshot.
1293 """
1294 ...
1296 def on_run_completed(
1297 self,
1298 success_count: int,
1299 total_count: int,
1300 run_validation_passed: bool,
1301 abort_reason: str | None = None,
1302 ) -> None:
1303 """Called when the orchestrator run completes.
1305 Args:
1306 success_count: Number of issues completed successfully.
1307 total_count: Total number of issues processed.
1308 run_validation_passed: Whether Gate 4 (run-level validation) passed.
1309 abort_reason: If run was aborted, the reason string.
1310 """
1311 ...
1313 def on_ready_issues(self, issue_ids: list[str]) -> None:
1314 """Called when ready issues are fetched.
1316 Args:
1317 issue_ids: List of issue IDs ready for processing.
1318 """
1319 ...
1321 def on_waiting_for_agents(self, count: int) -> None:
1322 """Called when waiting for agents to complete.
1324 Args:
1325 count: Number of active agents being waited on.
1326 """
1327 ...
1329 def on_no_more_issues(self, reason: str) -> None:
1330 """Called when there are no more issues to process.
1332 Args:
1333 reason: Reason string (e.g., "limit_reached", "none_ready").
1334 """
1335 ...
1337 # -------------------------------------------------------------------------
1338 # Agent lifecycle
1339 # -------------------------------------------------------------------------
1341 def on_agent_started(self, agent_id: str, issue_id: str) -> None:
1342 """Called when an agent is spawned for an issue.
1344 Args:
1345 agent_id: Unique agent identifier.
1346 issue_id: Issue being worked on.
1347 """
1348 ...
1350 def on_agent_completed(
1351 self,
1352 agent_id: str,
1353 issue_id: str,
1354 success: bool,
1355 duration_seconds: float,
1356 summary: str,
1357 ) -> None:
1358 """Called when an agent completes (success or failure).
1360 Args:
1361 agent_id: Unique agent identifier.
1362 issue_id: Issue that was worked on.
1363 success: Whether the agent succeeded.
1364 duration_seconds: Total execution time.
1365 summary: Result summary or error message.
1366 """
1367 ...
1369 def on_claim_failed(self, agent_id: str, issue_id: str) -> None:
1370 """Called when claiming an issue fails.
1372 Args:
1373 agent_id: Agent that attempted the claim.
1374 issue_id: Issue that could not be claimed.
1375 """
1376 ...
1378 # -------------------------------------------------------------------------
1379 # SDK message streaming
1380 # -------------------------------------------------------------------------
1382 def on_tool_use(
1383 self,
1384 agent_id: str,
1385 tool_name: str,
1386 description: str = "",
1387 arguments: dict[str, Any] | None = None,
1388 ) -> None:
1389 """Called when an agent invokes a tool.
1391 Args:
1392 agent_id: Agent invoking the tool.
1393 tool_name: Name of the tool being called.
1394 description: Brief description of the action.
1395 arguments: Tool arguments (may be truncated for display).
1396 """
1397 ...
1399 def on_agent_text(self, agent_id: str, text: str) -> None:
1400 """Called when an agent emits text output.
1402 Args:
1403 agent_id: Agent emitting text.
1404 text: Text content (may be truncated for display).
1405 """
1406 ...
1408 # -------------------------------------------------------------------------
1409 # Quality gate events
1410 # -------------------------------------------------------------------------
1412 def on_gate_started(
1413 self,
1414 agent_id: str | None,
1415 attempt: int,
1416 max_attempts: int,
1417 issue_id: str | None = None,
1418 ) -> None:
1419 """Called when a quality gate check begins.
1421 Args:
1422 agent_id: Agent ID (None for run-level gate).
1423 attempt: Current attempt number (1-indexed).
1424 max_attempts: Maximum retry attempts.
1425 issue_id: Issue being validated (for display).
1426 """
1427 ...
1429 def on_gate_passed(
1430 self,
1431 agent_id: str | None,
1432 issue_id: str | None = None,
1433 ) -> None:
1434 """Called when a quality gate passes.
1436 Args:
1437 agent_id: Agent ID (None for run-level gate).
1438 issue_id: Issue being validated (for display).
1439 """
1440 ...
1442 def on_gate_failed(
1443 self,
1444 agent_id: str | None,
1445 attempt: int,
1446 max_attempts: int,
1447 issue_id: str | None = None,
1448 ) -> None:
1449 """Called when a quality gate fails after all retries.
1451 Args:
1452 agent_id: Agent ID (None for run-level gate).
1453 attempt: Final attempt number.
1454 max_attempts: Maximum retry attempts.
1455 issue_id: Issue being validated (for display).
1456 """
1457 ...
1459 def on_gate_retry(
1460 self,
1461 agent_id: str,
1462 attempt: int,
1463 max_attempts: int,
1464 issue_id: str | None = None,
1465 ) -> None:
1466 """Called when retrying a quality gate after failure.
1468 Args:
1469 agent_id: Agent being retried.
1470 attempt: Current retry attempt number.
1471 max_attempts: Maximum retry attempts.
1472 issue_id: Issue being validated (for display).
1473 """
1474 ...
1476 def on_gate_result(
1477 self,
1478 agent_id: str | None,
1479 passed: bool,
1480 failure_reasons: list[str] | None = None,
1481 issue_id: str | None = None,
1482 ) -> None:
1483 """Called when a quality gate check completes with its result.
1485 This provides the detailed gate result including failure reasons,
1486 complementing the simpler on_gate_passed/on_gate_failed events.
1488 Args:
1489 agent_id: Agent ID (None for run-level gate).
1490 passed: Whether the gate passed.
1491 failure_reasons: List of failure reasons (if failed).
1492 issue_id: Issue being validated (for display).
1493 """
1494 ...
1496 # -------------------------------------------------------------------------
1497 # Codex review events
1498 # -------------------------------------------------------------------------
1500 def on_review_started(
1501 self,
1502 agent_id: str,
1503 attempt: int,
1504 max_attempts: int,
1505 issue_id: str | None = None,
1506 ) -> None:
1507 """Called when a Codex review begins.
1509 Args:
1510 agent_id: Agent being reviewed.
1511 attempt: Current attempt number.
1512 max_attempts: Maximum review attempts.
1513 issue_id: Issue being reviewed (for display).
1514 """
1515 ...
1517 def on_review_passed(
1518 self,
1519 agent_id: str,
1520 issue_id: str | None = None,
1521 ) -> None:
1522 """Called when a Codex review passes.
1524 Args:
1525 agent_id: Agent that passed review.
1526 issue_id: Issue being reviewed (for display).
1527 """
1528 ...
1530 def on_review_retry(
1531 self,
1532 agent_id: str,
1533 attempt: int,
1534 max_attempts: int,
1535 error_count: int | None = None,
1536 parse_error: str | None = None,
1537 issue_id: str | None = None,
1538 ) -> None:
1539 """Called when retrying a Codex review after issues found.
1541 Args:
1542 agent_id: Agent being retried.
1543 attempt: Current retry attempt number.
1544 max_attempts: Maximum review attempts.
1545 error_count: Number of errors found (if available).
1546 parse_error: Parse error message (if review failed to parse).
1547 issue_id: Issue being reviewed (for display).
1548 """
1549 ...
1551 def on_review_warning(
1552 self,
1553 message: str,
1554 agent_id: str | None = None,
1555 issue_id: str | None = None,
1556 ) -> None:
1557 """Called for review-related warnings (e.g., verdict mismatch).
1559 Args:
1560 message: Warning message.
1561 agent_id: Associated agent (if any).
1562 issue_id: Issue being reviewed (for display).
1563 """
1564 ...
1566 # -------------------------------------------------------------------------
1567 # Fixer agent events
1568 # -------------------------------------------------------------------------
1570 def on_fixer_started(
1571 self,
1572 attempt: int,
1573 max_attempts: int,
1574 ) -> None:
1575 """Called when a fixer agent is spawned.
1577 Args:
1578 attempt: Current fixer attempt number.
1579 max_attempts: Maximum fixer attempts.
1580 """
1581 ...
1583 def on_fixer_completed(self, result: str) -> None:
1584 """Called when a fixer agent completes.
1586 Args:
1587 result: Brief result description.
1588 """
1589 ...
1591 def on_fixer_failed(self, reason: str) -> None:
1592 """Called when a fixer agent fails.
1594 Args:
1595 reason: Failure reason (e.g., "timeout", "error").
1596 """
1597 ...
1599 # -------------------------------------------------------------------------
1600 # Issue lifecycle
1601 # -------------------------------------------------------------------------
1603 def on_issue_closed(self, agent_id: str, issue_id: str) -> None:
1604 """Called when an issue is closed after successful completion.
1606 Args:
1607 agent_id: Agent that completed the issue.
1608 issue_id: Issue that was closed.
1609 """
1610 ...
1612 def on_issue_completed(
1613 self,
1614 agent_id: str,
1615 issue_id: str,
1616 success: bool,
1617 duration_seconds: float,
1618 summary: str,
1619 ) -> None:
1620 """Called when an issue implementation completes (success or failure).
1622 This is the primary issue completion event, distinct from on_agent_completed
1623 which tracks the agent lifecycle. Use this for issue-level tracking.
1625 Args:
1626 agent_id: Agent that worked on the issue.
1627 issue_id: Issue that was completed.
1628 success: Whether the issue was successfully implemented.
1629 duration_seconds: Total time spent on the issue.
1630 summary: Result summary or error message.
1631 """
1632 ...
1634 def on_epic_closed(self, agent_id: str) -> None:
1635 """Called when a parent epic is auto-closed.
1637 Args:
1638 agent_id: Agent that triggered the epic closure.
1639 """
1640 ...
1642 def on_validation_started(
1643 self,
1644 agent_id: str,
1645 issue_id: str | None = None,
1646 ) -> None:
1647 """Called when per-issue validation begins.
1649 Args:
1650 agent_id: Agent being validated.
1651 issue_id: Issue being validated (for display).
1652 """
1653 ...
1655 def on_validation_result(
1656 self,
1657 agent_id: str,
1658 passed: bool,
1659 issue_id: str | None = None,
1660 ) -> None:
1661 """Called when per-issue validation completes.
1663 Args:
1664 agent_id: Agent that was validated.
1665 passed: Whether validation passed.
1666 issue_id: Issue being validated (for display).
1667 """
1668 ...
1670 def on_validation_step_running(
1671 self,
1672 step_name: str,
1673 agent_id: str | None = None,
1674 ) -> None:
1675 """Called when a validation step starts.
1677 Args:
1678 step_name: Name of the validation step (e.g., "ruff", "pytest").
1679 agent_id: Associated agent (if any).
1680 """
1681 ...
1683 def on_validation_step_skipped(
1684 self,
1685 step_name: str,
1686 reason: str,
1687 agent_id: str | None = None,
1688 ) -> None:
1689 """Called when a validation step is skipped.
1691 Args:
1692 step_name: Name of the validation step.
1693 reason: Reason for skipping (e.g., "cache hit", "no changes").
1694 agent_id: Associated agent (if any).
1695 """
1696 ...
1698 def on_validation_step_passed(
1699 self,
1700 step_name: str,
1701 duration_seconds: float,
1702 agent_id: str | None = None,
1703 ) -> None:
1704 """Called when a validation step succeeds.
1706 Args:
1707 step_name: Name of the validation step.
1708 duration_seconds: Time taken to complete the step.
1709 agent_id: Associated agent (if any).
1710 """
1711 ...
1713 def on_validation_step_failed(
1714 self,
1715 step_name: str,
1716 exit_code: int,
1717 agent_id: str | None = None,
1718 ) -> None:
1719 """Called when a validation step fails.
1721 Args:
1722 step_name: Name of the validation step.
1723 exit_code: Exit code from the step.
1724 agent_id: Associated agent (if any).
1725 """
1726 ...
1728 # -------------------------------------------------------------------------
1729 # Warnings and diagnostics
1730 # -------------------------------------------------------------------------
1732 def on_warning(self, message: str, agent_id: str | None = None) -> None:
1733 """Called for warning conditions.
1735 Args:
1736 message: Warning message.
1737 agent_id: Associated agent (if any).
1738 """
1739 ...
1741 def on_log_timeout(self, agent_id: str, log_path: str) -> None:
1742 """Called when waiting for a log file times out.
1744 Args:
1745 agent_id: Agent waiting for the log.
1746 log_path: Path to the missing log file.
1747 """
1748 ...
1750 def on_locks_cleaned(self, agent_id: str, count: int) -> None:
1751 """Called when stale locks are cleaned up for an agent.
1753 Args:
1754 agent_id: Agent whose locks were cleaned.
1755 count: Number of locks cleaned.
1756 """
1757 ...
1759 def on_locks_released(self, count: int) -> None:
1760 """Called when remaining locks are released at run end.
1762 Args:
1763 count: Number of locks released.
1764 """
1765 ...
1767 def on_issues_committed(self) -> None:
1768 """Called when .beads/issues.jsonl is committed."""
1769 ...
1771 def on_run_metadata_saved(self, path: str) -> None:
1772 """Called when run metadata is saved.
1774 Args:
1775 path: Path to the saved metadata file.
1776 """
1777 ...
1779 def on_run_level_validation_disabled(self) -> None:
1780 """Called when run-level validation is disabled."""
1781 ...
1783 def on_abort_requested(self, reason: str) -> None:
1784 """Called when a fatal error triggers a run abort.
1786 Args:
1787 reason: Description of the fatal error.
1788 """
1789 ...
1791 def on_tasks_aborting(self, count: int, reason: str) -> None:
1792 """Called when active tasks are being aborted.
1794 Args:
1795 count: Number of active tasks being aborted.
1796 reason: Reason for the abort.
1797 """
1798 ...
1800 # -------------------------------------------------------------------------
1801 # Epic verification lifecycle
1802 # -------------------------------------------------------------------------
1804 def on_epic_verification_started(self, epic_id: str) -> None:
1805 """Called when epic verification begins.
1807 Args:
1808 epic_id: The epic being verified.
1809 """
1810 ...
1812 def on_epic_verification_passed(self, epic_id: str, confidence: float) -> None:
1813 """Called when epic verification passes.
1815 Args:
1816 epic_id: The epic that passed verification.
1817 confidence: Confidence score (0.0 to 1.0).
1818 """
1819 ...
1821 def on_epic_verification_failed(
1822 self,
1823 epic_id: str,
1824 unmet_count: int,
1825 remediation_ids: list[str],
1826 ) -> None:
1827 """Called when epic verification fails with unmet criteria.
1829 Args:
1830 epic_id: The epic that failed verification.
1831 unmet_count: Number of unmet criteria.
1832 remediation_ids: IDs of created remediation issues.
1833 """
1834 ...
1836 def on_epic_remediation_created(
1837 self,
1838 epic_id: str,
1839 issue_id: str,
1840 criterion: str,
1841 ) -> None:
1842 """Called when a remediation issue is created for an unmet criterion.
1844 Args:
1845 epic_id: The epic the remediation is for.
1846 issue_id: The created issue ID.
1847 criterion: The unmet criterion text (may be truncated).
1848 """
1849 ...
1851 # -------------------------------------------------------------------------
1852 # Pipeline module events
1853 # -------------------------------------------------------------------------
1855 def on_lifecycle_state(self, agent_id: str, state: str) -> None:
1856 """Called when lifecycle state changes (verbose/debug).
1858 Args:
1859 agent_id: Agent whose lifecycle changed.
1860 state: New lifecycle state name.
1861 """
1862 ...
1864 def on_log_waiting(self, agent_id: str) -> None:
1865 """Called when waiting for session log file.
1867 Args:
1868 agent_id: Agent waiting for log.
1869 """
1870 ...
1872 def on_log_ready(self, agent_id: str) -> None:
1873 """Called when session log file is ready.
1875 Args:
1876 agent_id: Agent whose log is ready.
1877 """
1878 ...
1880 def on_review_skipped_no_progress(self, agent_id: str) -> None:
1881 """Called when review is skipped due to no progress.
1883 Args:
1884 agent_id: Agent whose review was skipped.
1885 """
1886 ...
1888 def on_fixer_text(self, attempt: int, text: str) -> None:
1889 """Called when fixer agent emits text output.
1891 Args:
1892 attempt: Current fixer attempt number.
1893 text: Text content.
1894 """
1895 ...
1897 def on_fixer_tool_use(
1898 self,
1899 attempt: int,
1900 tool_name: str,
1901 arguments: dict[str, Any] | None = None,
1902 ) -> None:
1903 """Called when fixer agent invokes a tool.
1905 Args:
1906 attempt: Current fixer attempt number.
1907 tool_name: Name of the tool being called.
1908 arguments: Tool arguments.
1909 """
1910 ...
1912 def on_deadlock_detected(self, info: DeadlockInfoProtocol) -> None:
1913 """Called when a deadlock is detected and resolved.
1915 Args:
1916 info: Information about the detected deadlock, including the cycle
1917 of agents, the victim selected for cancellation, and the
1918 blocker holding the needed resource.
1919 """
1920 ...