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.
- monoco/core/config.py +35 -0
- monoco/core/integrations.py +0 -6
- monoco/core/sync.py +6 -19
- monoco/features/issue/commands.py +24 -16
- monoco/features/issue/core.py +90 -39
- monoco/features/issue/domain/models.py +1 -0
- monoco/features/issue/domain_commands.py +47 -0
- monoco/features/issue/domain_service.py +69 -0
- monoco/features/issue/linter.py +153 -50
- monoco/features/issue/resolver.py +177 -0
- monoco/features/issue/resources/en/AGENTS.md +6 -4
- monoco/features/issue/resources/zh/AGENTS.md +6 -4
- monoco/features/issue/test_priority_integration.py +102 -0
- monoco/features/issue/test_resolver.py +83 -0
- monoco/features/issue/validator.py +97 -21
- monoco/features/scheduler/__init__.py +19 -0
- monoco/features/scheduler/cli.py +285 -0
- monoco/features/scheduler/config.py +68 -0
- monoco/features/scheduler/defaults.py +54 -0
- monoco/features/scheduler/engines.py +149 -0
- monoco/features/scheduler/manager.py +49 -0
- monoco/features/scheduler/models.py +24 -0
- monoco/features/scheduler/reliability.py +106 -0
- monoco/features/scheduler/session.py +87 -0
- monoco/features/scheduler/worker.py +133 -0
- monoco/main.py +5 -0
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/METADATA +37 -46
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/RECORD +31 -21
- monoco/core/agent/__init__.py +0 -3
- monoco/core/agent/action.py +0 -168
- monoco/core/agent/adapters.py +0 -133
- monoco/core/agent/protocol.py +0 -32
- monoco/core/agent/state.py +0 -106
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/entry_points.txt +0 -0
- {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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
+
]
|