monoco-toolkit 0.3.2__py3-none-any.whl → 0.3.5__py3-none-any.whl

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.
Files changed (36) hide show
  1. monoco/core/config.py +35 -0
  2. monoco/core/integrations.py +0 -6
  3. monoco/core/sync.py +6 -19
  4. monoco/features/issue/commands.py +24 -16
  5. monoco/features/issue/core.py +90 -39
  6. monoco/features/issue/domain/models.py +1 -0
  7. monoco/features/issue/domain_commands.py +47 -0
  8. monoco/features/issue/domain_service.py +69 -0
  9. monoco/features/issue/linter.py +153 -50
  10. monoco/features/issue/resolver.py +177 -0
  11. monoco/features/issue/resources/en/AGENTS.md +6 -4
  12. monoco/features/issue/resources/zh/AGENTS.md +6 -4
  13. monoco/features/issue/test_priority_integration.py +102 -0
  14. monoco/features/issue/test_resolver.py +83 -0
  15. monoco/features/issue/validator.py +97 -21
  16. monoco/features/scheduler/__init__.py +19 -0
  17. monoco/features/scheduler/cli.py +285 -0
  18. monoco/features/scheduler/config.py +68 -0
  19. monoco/features/scheduler/defaults.py +54 -0
  20. monoco/features/scheduler/engines.py +149 -0
  21. monoco/features/scheduler/manager.py +49 -0
  22. monoco/features/scheduler/models.py +24 -0
  23. monoco/features/scheduler/reliability.py +106 -0
  24. monoco/features/scheduler/session.py +87 -0
  25. monoco/features/scheduler/worker.py +133 -0
  26. monoco/main.py +5 -0
  27. {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/METADATA +37 -46
  28. {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/RECORD +31 -21
  29. monoco/core/agent/__init__.py +0 -3
  30. monoco/core/agent/action.py +0 -168
  31. monoco/core/agent/adapters.py +0 -133
  32. monoco/core/agent/protocol.py +0 -32
  33. monoco/core/agent/state.py +0 -106
  34. {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/WHEEL +0 -0
  35. {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/entry_points.txt +0 -0
  36. {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,7 @@ from monoco.features.i18n.core import detect_language
8
8
  from .models import IssueMetadata
9
9
  from .domain.parser import MarkdownParser
10
10
  from .domain.models import ContentBlock
11
+ from .resolver import ReferenceResolver, ResolutionContext
11
12
 
12
13
 
13
14
  class IssueValidator:
@@ -20,9 +21,19 @@ class IssueValidator:
20
21
  self.issue_root = issue_root
21
22
 
22
23
  def validate(
23
- self, meta: IssueMetadata, content: str, all_issue_ids: Set[str] = set()
24
+ self,
25
+ meta: IssueMetadata,
26
+ content: str,
27
+ all_issue_ids: Set[str] = set(),
28
+ current_project: Optional[str] = None,
29
+ workspace_root: Optional[str] = None,
24
30
  ) -> List[Diagnostic]:
31
+ """
32
+ Validate an issue and return diagnostics.
33
+ """
25
34
  diagnostics = []
35
+ self._current_project = current_project
36
+ self._workspace_root = workspace_root
26
37
 
27
38
  # Parse Content into Blocks (Domain Layer)
28
39
  # Handle case where content might be just body (from update_issue) or full file
@@ -311,7 +322,11 @@ class IssueValidator:
311
322
  if stripped == expected_header:
312
323
  header_found = True
313
324
 
314
- if stripped == "## Review Comments":
325
+ # Flexible matching for Review Comments header
326
+ if any(
327
+ kw in stripped
328
+ for kw in ["Review Comments", "评审备注", "评审记录", "Review"]
329
+ ):
315
330
  review_header_found = True
316
331
  review_header_index = i
317
332
 
@@ -406,6 +421,16 @@ class IssueValidator:
406
421
  ) -> List[Diagnostic]:
407
422
  diagnostics = []
408
423
 
424
+ # Initialize Resolver
425
+ resolver = None
426
+ if all_ids:
427
+ context = ResolutionContext(
428
+ current_project=self._current_project or "local",
429
+ workspace_root=self._workspace_root,
430
+ available_ids=all_ids,
431
+ )
432
+ resolver = ReferenceResolver(context)
433
+
409
434
  # Malformed ID Check
410
435
  if meta.parent and meta.parent.startswith("#"):
411
436
  line = self._get_field_line(content, "parent")
@@ -441,13 +466,25 @@ class IssueValidator:
441
466
  )
442
467
  )
443
468
 
444
- if not all_ids:
469
+ if not all_ids or not resolver:
445
470
  return diagnostics
446
471
 
472
+ # Logic: Epics must have a parent (unless it is the Sink Root EPIC-0000)
473
+ if meta.type == "epic" and meta.id != "EPIC-0000" and not meta.parent:
474
+ line = self._get_field_line(content, "parent")
475
+ diagnostics.append(
476
+ self._create_diagnostic(
477
+ "Hierarchy Violation: Epics must have a parent (e.g., 'EPIC-0000').",
478
+ DiagnosticSeverity.Error,
479
+ line=line,
480
+ )
481
+ )
482
+
447
483
  if (
448
484
  meta.parent
449
- and meta.parent not in all_ids
485
+ and meta.parent != "EPIC-0000"
450
486
  and not meta.parent.startswith("#")
487
+ and not resolver.is_valid_reference(meta.parent)
451
488
  ):
452
489
  line = self._get_field_line(content, "parent")
453
490
  diagnostics.append(
@@ -459,7 +496,7 @@ class IssueValidator:
459
496
  )
460
497
 
461
498
  for dep in meta.dependencies:
462
- if dep not in all_ids:
499
+ if not resolver.is_valid_reference(dep):
463
500
  line = self._get_field_line(content, "dependencies")
464
501
  diagnostics.append(
465
502
  self._create_diagnostic(
@@ -492,24 +529,27 @@ class IssueValidator:
492
529
  matches = re.finditer(r"\b((?:EPIC|FEAT|CHORE|FIX)-\d{4})\b", line)
493
530
  for match in matches:
494
531
  ref_id = match.group(1)
495
- if ref_id != meta.id and ref_id not in all_ids:
496
- # Check if it's a namespaced ID? The regex only catches local IDs.
497
- # If users use MON::FEAT-0001, the regex might catch FEAT-0001.
498
- # But all_ids contains full IDs (potentially namespaced).
499
- # Simple logic: if ref_id isn't in all_ids, check if any id ENDS with ref_id
500
-
501
- found_namespaced = any(
502
- known.endswith(f"::{ref_id}") for known in all_ids
503
- )
532
+ # Check for namespaced ID before this match?
533
+ # The regex above only catches the ID part.
534
+ # Let's adjust regex to optionally catch namespace::
535
+ full_match = re.search(
536
+ r"\b(?:([a-z0-9_-]+)::)?(" + re.escape(ref_id) + r")\b",
537
+ line[max(0, match.start() - 50) : match.end()],
538
+ )
504
539
 
505
- if not found_namespaced:
506
- diagnostics.append(
507
- self._create_diagnostic(
508
- f"Broken Reference: Issue '{ref_id}' not found.",
509
- DiagnosticSeverity.Warning,
510
- line=i,
511
- )
540
+ check_id = ref_id
541
+ if full_match and full_match.group(1):
542
+ check_id = f"{full_match.group(1)}::{ref_id}"
543
+
544
+ if ref_id != meta.id and not resolver.is_valid_reference(check_id):
545
+ diagnostics.append(
546
+ self._create_diagnostic(
547
+ f"Broken Reference: Issue '{check_id}' not found.",
548
+ DiagnosticSeverity.Warning,
549
+ line=i,
512
550
  )
551
+ )
552
+ return diagnostics
513
553
  return diagnostics
514
554
 
515
555
  def _validate_time_consistency(
@@ -575,6 +615,7 @@ class IssueValidator:
575
615
  has_domains_field = False
576
616
  lines = content.splitlines()
577
617
  in_fm = False
618
+ field_line = 0
578
619
  for i, line_content in enumerate(lines):
579
620
  stripped = line_content.strip()
580
621
  if stripped == "---":
@@ -585,6 +626,7 @@ class IssueValidator:
585
626
  elif in_fm:
586
627
  if stripped.startswith("domains:"):
587
628
  has_domains_field = True
629
+ field_line = i
588
630
  break
589
631
 
590
632
  # Governance Maturity Check
@@ -607,6 +649,40 @@ class IssueValidator:
607
649
  )
608
650
  )
609
651
 
652
+ # Domain Content Validation
653
+ from .domain_service import DomainService
654
+
655
+ service = DomainService()
656
+
657
+ if hasattr(meta, "domains") and meta.domains:
658
+ for domain in meta.domains:
659
+ if service.is_alias(domain):
660
+ canonical = service.get_canonical(domain)
661
+ diagnostics.append(
662
+ self._create_diagnostic(
663
+ f"Domain Alias: '{domain}' is an alias for '{canonical}'. Preference: Canonical.",
664
+ DiagnosticSeverity.Warning,
665
+ line=field_line,
666
+ )
667
+ )
668
+ elif not service.is_defined(domain):
669
+ if service.config.strict:
670
+ diagnostics.append(
671
+ self._create_diagnostic(
672
+ f"Unknown Domain: '{domain}' is not defined in domain ontology.",
673
+ DiagnosticSeverity.Error,
674
+ line=field_line,
675
+ )
676
+ )
677
+ else:
678
+ diagnostics.append(
679
+ self._create_diagnostic(
680
+ f"Unknown Domain: '{domain}' is not defined in domain ontology.",
681
+ DiagnosticSeverity.Warning,
682
+ line=field_line,
683
+ )
684
+ )
685
+
610
686
  return diagnostics
611
687
 
612
688
  def _validate_checkbox_logic_blocks(
@@ -0,0 +1,19 @@
1
+ from .models import RoleTemplate, SchedulerConfig
2
+ from .worker import Worker
3
+ from .config import load_scheduler_config
4
+ from .defaults import DEFAULT_ROLES
5
+ from .session import Session, RuntimeSession
6
+ from .manager import SessionManager
7
+ from .reliability import ApoptosisManager
8
+
9
+ __all__ = [
10
+ "RoleTemplate",
11
+ "SchedulerConfig",
12
+ "Worker",
13
+ "load_scheduler_config",
14
+ "DEFAULT_ROLES",
15
+ "Session",
16
+ "RuntimeSession",
17
+ "SessionManager",
18
+ "ApoptosisManager",
19
+ ]
@@ -0,0 +1,285 @@
1
+ import typer
2
+ import time
3
+ from pathlib import Path
4
+ from typing import Optional
5
+ from monoco.core.output import print_output
6
+ from monoco.core.config import get_config
7
+ from monoco.features.scheduler import SessionManager, load_scheduler_config
8
+
9
+ app = typer.Typer(name="agent", help="Manage agent sessions")
10
+ role_app = typer.Typer(name="role", help="Manage agent roles")
11
+
12
+
13
+ @role_app.command(name="list")
14
+ def list_roles():
15
+ """
16
+ List available agent roles and their sources.
17
+ """
18
+ from monoco.features.scheduler.config import RoleLoader
19
+
20
+ settings = get_config()
21
+ project_root = Path(settings.paths.root).resolve()
22
+
23
+ loader = RoleLoader(project_root)
24
+ roles = loader.load_all()
25
+
26
+ output = []
27
+ for name, role in roles.items():
28
+ output.append(
29
+ {
30
+ "role": name,
31
+ "engine": role.engine,
32
+ "source": loader.sources.get(name, "unknown"),
33
+ "description": role.description,
34
+ }
35
+ )
36
+
37
+ print_output(output, title="Agent Roles")
38
+
39
+
40
+ @app.command()
41
+ def draft(
42
+ desc: str = typer.Option(..., "--desc", "-d", help="Description of the task"),
43
+ type: str = typer.Option(
44
+ "feature", "--type", "-t", help="Issue type (feature/chore/fix)"
45
+ ),
46
+ ):
47
+ """
48
+ Draft a new issue based on a natural language description.
49
+ This creates a temporary 'drafter' agent session.
50
+ """
51
+ from monoco.core.output import print_error
52
+
53
+ settings = get_config()
54
+ project_root = Path(settings.paths.root).resolve()
55
+
56
+ # Load Roles
57
+ roles = load_scheduler_config(project_root)
58
+ # Use 'crafter' as the role for drafting (it handles new tasks)
59
+ role_name = "crafter"
60
+ selected_role = roles.get(role_name)
61
+
62
+ if not selected_role:
63
+ print_error(f"Role '{role_name}' not found.")
64
+ raise typer.Exit(code=1)
65
+
66
+ print_output(
67
+ f"Drafting {type} from description: '{desc}'",
68
+ title="Agent Drafter",
69
+ )
70
+
71
+ manager = SessionManager()
72
+ # We use a placeholder ID as we don't know the ID yet.
73
+ # The agent is expected to create the file, so the ID will be generated then.
74
+ session = manager.create_session("NEW_TASK", selected_role)
75
+
76
+ context = {"description": desc, "type": type}
77
+
78
+ try:
79
+ session.start(context=context)
80
+
81
+ # Monitoring Loop
82
+ while session.refresh_status() == "running":
83
+ time.sleep(1)
84
+
85
+ if session.model.status == "failed":
86
+ print_error("Drafting failed.")
87
+ else:
88
+ print_output("Drafting completed.", title="Agent Drafter")
89
+
90
+ except KeyboardInterrupt:
91
+ print("\nStopping...")
92
+ session.terminate()
93
+ print_output("Drafting cancelled.")
94
+
95
+
96
+ @app.command()
97
+ def run(
98
+ target: str = typer.Argument(
99
+ ..., help="Issue ID (e.g. FEAT-101) or a Task Description in quotes."
100
+ ),
101
+ role: Optional[str] = typer.Option(
102
+ None,
103
+ help="Specific role to use (crafter/builder/auditor). Default: intelligent selection.",
104
+ ),
105
+ detach: bool = typer.Option(
106
+ False, "--detach", "-d", help="Run in background (Daemon)"
107
+ ),
108
+ fail: bool = typer.Option(
109
+ False, "--fail", help="Simulate a crash for testing Apoptosis."
110
+ ),
111
+ ):
112
+ """
113
+ Start an agent session.
114
+ - If TARGET is an Issue ID: Work on that issue.
115
+ - If TARGET is a text description: Create a new issue (Crafter).
116
+ """
117
+ settings = get_config()
118
+ project_root = Path(settings.paths.root).resolve()
119
+
120
+ # 1. Smart Intent Recognition
121
+ import re
122
+
123
+ is_id = re.match(r"^[a-zA-Z]+-\d+$", target)
124
+
125
+ if is_id:
126
+ issue_id = target.upper()
127
+ role_name = role or "builder"
128
+ description = None
129
+ else:
130
+ # Implicit Draft Mode via run command
131
+ issue_id = "NEW_TASK"
132
+ role_name = role or "crafter"
133
+ description = target
134
+
135
+ # 2. Load Roles
136
+ roles = load_scheduler_config(project_root)
137
+ selected_role = roles.get(role_name)
138
+
139
+ if not selected_role:
140
+ from monoco.core.output import print_error
141
+
142
+ print_error(f"Role '{role_name}' not found. Available: {list(roles.keys())}")
143
+ raise typer.Exit(code=1)
144
+
145
+ print_output(
146
+ f"Starting Agent Session for '{target}' as {role_name}...",
147
+ title="Agent Scheduler",
148
+ )
149
+
150
+ # 3. Initialize Session
151
+ manager = SessionManager()
152
+ session = manager.create_session(issue_id, selected_role)
153
+
154
+ if detach:
155
+ print_output(
156
+ "Background mode not fully implemented yet. Running in foreground."
157
+ )
158
+
159
+ try:
160
+ # Pass description if it's a new task
161
+ context = {"description": description} if description else None
162
+
163
+ if fail:
164
+ from monoco.core.output import rprint
165
+
166
+ rprint("[bold yellow]DEBUG: Simulating immediate crash...[/bold yellow]")
167
+ session.model.status = "failed"
168
+ else:
169
+ session.start(context=context)
170
+
171
+ # Monitoring Loop
172
+ while session.refresh_status() == "running":
173
+ time.sleep(1)
174
+
175
+ if session.model.status == "failed":
176
+ from monoco.core.output import print_error
177
+
178
+ print_error(
179
+ f"Session {session.model.id} FAILED. Use 'monoco agent autopsy {session.model.id}' for analysis."
180
+ )
181
+ else:
182
+ print_output(
183
+ f"Session finished with status: {session.model.status}",
184
+ title="Agent Scheduler",
185
+ )
186
+
187
+ except KeyboardInterrupt:
188
+ print("\nStopping...")
189
+ session.terminate()
190
+ print_output("Session terminated.")
191
+
192
+
193
+ @app.command()
194
+ def kill(session_id: str):
195
+ """
196
+ Terminate a session.
197
+ """
198
+ manager = SessionManager()
199
+ session = manager.get_session(session_id)
200
+ if session:
201
+ session.terminate()
202
+ print_output(f"Session {session_id} terminated.")
203
+ else:
204
+ print_output(f"Session {session_id} not found.", style="red")
205
+
206
+
207
+ @app.command()
208
+ def autopsy(
209
+ target: str = typer.Argument(..., help="Session ID or Issue ID to analyze."),
210
+ ):
211
+ """
212
+ Execute Post-Mortem analysis on a failed session or target Issue.
213
+ """
214
+ from .reliability import ApoptosisManager
215
+
216
+ manager = SessionManager()
217
+
218
+ print_output(f"Initiating Autopsy for '{target}'...", title="Coroner")
219
+
220
+ # Try to find session
221
+ session = manager.get_session(target)
222
+ if not session:
223
+ # Fallback: Treat target as Issue ID and create a dummy failed session context
224
+ import re
225
+
226
+ if re.match(r"^[a-zA-Z]+-\d+$", target):
227
+ print_output(f"Session not in memory. Analyzing Issue {target} directly.")
228
+ # We create a transient session just to trigger the coroner
229
+ settings = get_config()
230
+ project_root = Path(settings.paths.root).resolve()
231
+ roles = load_scheduler_config(project_root)
232
+ builder_role = roles.get("builder")
233
+
234
+ if not builder_role:
235
+ print_output("Builder role not found.", style="red")
236
+ raise typer.Exit(code=1)
237
+
238
+ session = manager.create_session(target.upper(), builder_role)
239
+ session.model.status = "failed"
240
+ else:
241
+ print_output(
242
+ f"Could not find session or valid Issue ID for '{target}'", style="red"
243
+ )
244
+ raise typer.Exit(code=1)
245
+
246
+ apoptosis = ApoptosisManager(manager)
247
+ apoptosis.trigger_apoptosis(session.model.id)
248
+
249
+
250
+ @app.command(name="list")
251
+ def list_sessions():
252
+ """
253
+ List active agent sessions.
254
+ """
255
+ manager = SessionManager()
256
+ sessions = manager.list_sessions()
257
+
258
+ output = []
259
+ for s in sessions:
260
+ output.append(
261
+ {
262
+ "id": s.model.id,
263
+ "issue": s.model.issue_id,
264
+ "role": s.model.role_name,
265
+ "status": s.model.status,
266
+ "branch": s.model.branch_name,
267
+ }
268
+ )
269
+
270
+ print_output(
271
+ output
272
+ or "No active sessions found (Note: Persistence not implemented in CLI list yet).",
273
+ title="Active Sessions",
274
+ )
275
+
276
+
277
+ @app.command()
278
+ def logs(session_id: str):
279
+ """
280
+ Stream logs for a session.
281
+ """
282
+ print_output(f"Streaming logs for {session_id}...", title="Session Logs")
283
+ # Placeholder
284
+ print("[12:00:00] Session started")
285
+ print("[12:00:01] Worker initialized")
@@ -0,0 +1,68 @@
1
+ from typing import Dict, Optional
2
+ import yaml
3
+ from pathlib import Path
4
+ from .models import RoleTemplate, SchedulerConfig
5
+ from .defaults import DEFAULT_ROLES
6
+
7
+
8
+ class RoleLoader:
9
+ """
10
+ Tiered configuration loader for Agent Roles.
11
+ Level 1: Builtin Fallback
12
+ Level 2: Global (~/.monoco/roles.yaml)
13
+ Level 3: Project (./.monoco/roles.yaml)
14
+ """
15
+
16
+ def __init__(self, project_root: Optional[Path] = None):
17
+ self.project_root = project_root
18
+ self.user_home = Path.home()
19
+ self.roles: Dict[str, RoleTemplate] = {}
20
+ self.sources: Dict[str, str] = {} # role_name -> source description
21
+
22
+ def load_all(self) -> Dict[str, RoleTemplate]:
23
+ # Level 1: Defaults
24
+ for role in DEFAULT_ROLES:
25
+ self.roles[role.name] = role
26
+ self.sources[role.name] = "builtin"
27
+
28
+ # Level 2: Global
29
+ global_path = self.user_home / ".monoco" / "roles.yaml"
30
+ self._load_from_path(global_path, "global")
31
+
32
+ # Level 3: Project
33
+ if self.project_root:
34
+ project_path = self.project_root / ".monoco" / "roles.yaml"
35
+ self._load_from_path(project_path, "project")
36
+
37
+ return self.roles
38
+
39
+ def _load_from_path(self, path: Path, source_label: str):
40
+ if not path.exists():
41
+ return
42
+
43
+ try:
44
+ with open(path, "r") as f:
45
+ data = yaml.safe_load(f) or {}
46
+
47
+ if "roles" in data:
48
+ # Validate using SchedulerConfig
49
+ config = SchedulerConfig(roles=data["roles"])
50
+ for role in config.roles:
51
+ # Level 3 > Level 2 > Level 1 (名字相同的 Role 进行覆盖/Merge)
52
+ # Currently we do total replacement for same-named roles
53
+ self.roles[role.name] = role
54
+ self.sources[role.name] = str(path)
55
+ except Exception as e:
56
+ # We don't want to crash the whole tool if a config is malformed,
57
+ # but we should probably warn.
58
+ import sys
59
+
60
+ print(f"Warning: Failed to load roles from {path}: {e}", file=sys.stderr)
61
+
62
+
63
+ def load_scheduler_config(project_root: Path) -> Dict[str, RoleTemplate]:
64
+ """
65
+ Legacy compatibility wrapper for functional access.
66
+ """
67
+ loader = RoleLoader(project_root)
68
+ return loader.load_all()
@@ -0,0 +1,54 @@
1
+ from .models import RoleTemplate
2
+
3
+ DEFAULT_ROLES = [
4
+ RoleTemplate(
5
+ name="crafter",
6
+ description="Responsible for initial design, research, and drafting issues from descriptions.",
7
+ trigger="task.received",
8
+ goal="Produce a structured Issue file and/or detailed design document.",
9
+ tools=[
10
+ "create_issue_file",
11
+ "read_file",
12
+ "search_web",
13
+ "view_file_outline",
14
+ "write_to_file",
15
+ ],
16
+ system_prompt=(
17
+ "You are a Crafter agent. Your goal is to turn vague ideas into structured engineering plans.\n"
18
+ "If the user provides a description, use 'monoco issue create' and 'monoco issue update' to build the task.\n"
19
+ "If the user provides an existing Issue, analyze the context and provide a detailed design or implementation plan."
20
+ ),
21
+ engine="gemini",
22
+ ),
23
+ RoleTemplate(
24
+ name="builder",
25
+ description="Responsible for implementation.",
26
+ trigger="design.approved",
27
+ goal="Implement code and tests",
28
+ tools=["read_file", "write_to_file", "run_command", "git"],
29
+ system_prompt="You are a Builder agent. Your job is to implement the code based on the design.",
30
+ engine="gemini",
31
+ ),
32
+ RoleTemplate(
33
+ name="auditor",
34
+ description="Responsible for code review.",
35
+ trigger="implementation.submitted",
36
+ goal="Review code and provide feedback",
37
+ tools=[
38
+ "read_file",
39
+ "read_terminal",
40
+ "run_command",
41
+ ], # Assumed read_diff and lint are via run_command
42
+ system_prompt="You are an Auditor agent. Your job is to review the code for quality and correctness.",
43
+ engine="gemini",
44
+ ),
45
+ RoleTemplate(
46
+ name="coroner",
47
+ description="Responsible for analyzing failure root causes (Autopsy).",
48
+ trigger="session.crashed",
49
+ goal="Produce a post-mortem report",
50
+ tools=["read_file", "read_terminal", "git_log"],
51
+ system_prompt="You are a Coroner agent. Your job is to analyze why the previous session failed and write a post-mortem report.",
52
+ engine="gemini",
53
+ ),
54
+ ]