monoco-toolkit 0.3.3__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/features/issue/commands.py +0 -15
- monoco/features/issue/linter.py +35 -40
- 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 +50 -21
- monoco/features/scheduler/cli.py +94 -13
- monoco/features/scheduler/config.py +51 -15
- monoco/features/scheduler/engines.py +149 -0
- monoco/features/scheduler/reliability.py +11 -4
- monoco/features/scheduler/worker.py +9 -5
- monoco/main.py +1 -0
- {monoco_toolkit-0.3.3.dist-info → monoco_toolkit-0.3.5.dist-info}/METADATA +37 -46
- {monoco_toolkit-0.3.3.dist-info → monoco_toolkit-0.3.5.dist-info}/RECORD +19 -15
- {monoco_toolkit-0.3.3.dist-info → monoco_toolkit-0.3.5.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.3.dist-info → monoco_toolkit-0.3.5.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.3.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,7 +466,7 @@ 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
|
|
|
447
472
|
# Logic: Epics must have a parent (unless it is the Sink Root EPIC-0000)
|
|
@@ -457,8 +482,9 @@ class IssueValidator:
|
|
|
457
482
|
|
|
458
483
|
if (
|
|
459
484
|
meta.parent
|
|
460
|
-
and meta.parent
|
|
485
|
+
and meta.parent != "EPIC-0000"
|
|
461
486
|
and not meta.parent.startswith("#")
|
|
487
|
+
and not resolver.is_valid_reference(meta.parent)
|
|
462
488
|
):
|
|
463
489
|
line = self._get_field_line(content, "parent")
|
|
464
490
|
diagnostics.append(
|
|
@@ -470,7 +496,7 @@ class IssueValidator:
|
|
|
470
496
|
)
|
|
471
497
|
|
|
472
498
|
for dep in meta.dependencies:
|
|
473
|
-
if
|
|
499
|
+
if not resolver.is_valid_reference(dep):
|
|
474
500
|
line = self._get_field_line(content, "dependencies")
|
|
475
501
|
diagnostics.append(
|
|
476
502
|
self._create_diagnostic(
|
|
@@ -503,24 +529,27 @@ class IssueValidator:
|
|
|
503
529
|
matches = re.finditer(r"\b((?:EPIC|FEAT|CHORE|FIX)-\d{4})\b", line)
|
|
504
530
|
for match in matches:
|
|
505
531
|
ref_id = match.group(1)
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
known.endswith(f"::{ref_id}") for known in all_ids
|
|
514
|
-
)
|
|
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
|
+
)
|
|
515
539
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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,
|
|
523
550
|
)
|
|
551
|
+
)
|
|
552
|
+
return diagnostics
|
|
524
553
|
return diagnostics
|
|
525
554
|
|
|
526
555
|
def _validate_time_consistency(
|
monoco/features/scheduler/cli.py
CHANGED
|
@@ -7,6 +7,90 @@ from monoco.core.config import get_config
|
|
|
7
7
|
from monoco.features.scheduler import SessionManager, load_scheduler_config
|
|
8
8
|
|
|
9
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.")
|
|
10
94
|
|
|
11
95
|
|
|
12
96
|
@app.command()
|
|
@@ -43,6 +127,7 @@ def run(
|
|
|
43
127
|
role_name = role or "builder"
|
|
44
128
|
description = None
|
|
45
129
|
else:
|
|
130
|
+
# Implicit Draft Mode via run command
|
|
46
131
|
issue_id = "NEW_TASK"
|
|
47
132
|
role_name = role or "crafter"
|
|
48
133
|
description = target
|
|
@@ -84,7 +169,7 @@ def run(
|
|
|
84
169
|
session.start(context=context)
|
|
85
170
|
|
|
86
171
|
# Monitoring Loop
|
|
87
|
-
while session.
|
|
172
|
+
while session.refresh_status() == "running":
|
|
88
173
|
time.sleep(1)
|
|
89
174
|
|
|
90
175
|
if session.model.status == "failed":
|
|
@@ -141,9 +226,15 @@ def autopsy(
|
|
|
141
226
|
if re.match(r"^[a-zA-Z]+-\d+$", target):
|
|
142
227
|
print_output(f"Session not in memory. Analyzing Issue {target} directly.")
|
|
143
228
|
# We create a transient session just to trigger the coroner
|
|
144
|
-
|
|
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)
|
|
145
237
|
|
|
146
|
-
builder_role = next(r for r in DEFAULT_ROLES if r.name == "builder")
|
|
147
238
|
session = manager.create_session(target.upper(), builder_role)
|
|
148
239
|
session.model.status = "failed"
|
|
149
240
|
else:
|
|
@@ -192,13 +283,3 @@ def logs(session_id: str):
|
|
|
192
283
|
# Placeholder
|
|
193
284
|
print("[12:00:00] Session started")
|
|
194
285
|
print("[12:00:01] Worker initialized")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
@app.command()
|
|
198
|
-
def kill(session_id: str):
|
|
199
|
-
"""
|
|
200
|
-
Terminate a session.
|
|
201
|
-
"""
|
|
202
|
-
print_output(f"Killing session {session_id}...", title="Kill Session")
|
|
203
|
-
# Placeholder
|
|
204
|
-
print("Signal sent.")
|
|
@@ -1,32 +1,68 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
1
2
|
import yaml
|
|
2
3
|
from pathlib import Path
|
|
3
|
-
from typing import Dict
|
|
4
4
|
from .models import RoleTemplate, SchedulerConfig
|
|
5
5
|
from .defaults import DEFAULT_ROLES
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
class RoleLoader:
|
|
9
9
|
"""
|
|
10
|
-
|
|
11
|
-
|
|
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)
|
|
12
14
|
"""
|
|
13
|
-
roles = {role.name: role for role in DEFAULT_ROLES}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
|
|
17
43
|
try:
|
|
18
|
-
with open(
|
|
44
|
+
with open(path, "r") as f:
|
|
19
45
|
data = yaml.safe_load(f) or {}
|
|
20
46
|
|
|
21
|
-
# Use Pydantic to validate the whole config if possible, or just the roles list
|
|
22
|
-
# Depending on file structure. Assuming the file has a 'roles' key.
|
|
23
47
|
if "roles" in data:
|
|
24
|
-
#
|
|
48
|
+
# Validate using SchedulerConfig
|
|
25
49
|
config = SchedulerConfig(roles=data["roles"])
|
|
26
50
|
for role in config.roles:
|
|
27
|
-
|
|
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)
|
|
28
55
|
except Exception as e:
|
|
29
|
-
#
|
|
30
|
-
|
|
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
|
+
|
|
31
62
|
|
|
32
|
-
|
|
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,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Engine Adapters for Monoco Scheduler.
|
|
3
|
+
|
|
4
|
+
This module provides a unified interface for different AI agent execution engines,
|
|
5
|
+
allowing the Worker to seamlessly switch between Gemini, Claude, and future engines.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EngineAdapter(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Abstract base class for agent engine adapters.
|
|
15
|
+
|
|
16
|
+
Each adapter is responsible for:
|
|
17
|
+
1. Constructing the correct CLI command for its engine
|
|
18
|
+
2. Handling engine-specific error scenarios
|
|
19
|
+
3. Providing metadata about the engine's capabilities
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
24
|
+
"""
|
|
25
|
+
Build the CLI command to execute the agent with the given prompt.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
prompt: The instruction/context to send to the agent
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
List of command arguments (e.g., ["gemini", "-y", "prompt text"])
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def name(self) -> str:
|
|
38
|
+
"""Return the canonical name of this engine."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def supports_yolo_mode(self) -> bool:
|
|
43
|
+
"""Whether this engine supports auto-approval mode."""
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GeminiAdapter(EngineAdapter):
|
|
48
|
+
"""
|
|
49
|
+
Adapter for Google Gemini CLI.
|
|
50
|
+
|
|
51
|
+
Command format: gemini -y <prompt>
|
|
52
|
+
The -y flag enables "YOLO mode" (auto-approval of actions).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
56
|
+
return ["gemini", "-y", prompt]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def name(self) -> str:
|
|
60
|
+
return "gemini"
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def supports_yolo_mode(self) -> bool:
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ClaudeAdapter(EngineAdapter):
|
|
68
|
+
"""
|
|
69
|
+
Adapter for Anthropic Claude CLI.
|
|
70
|
+
|
|
71
|
+
Command format: claude -p <prompt>
|
|
72
|
+
The -p/--print flag enables non-interactive mode (print response and exit).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
76
|
+
return ["claude", "-p", prompt]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def name(self) -> str:
|
|
80
|
+
return "claude"
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def supports_yolo_mode(self) -> bool:
|
|
84
|
+
# Claude uses -p for non-interactive mode, similar concept
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class QwenAdapter(EngineAdapter):
|
|
89
|
+
"""
|
|
90
|
+
Adapter for Qwen Code CLI.
|
|
91
|
+
|
|
92
|
+
Command format: qwen -y <prompt>
|
|
93
|
+
The -y flag enables "YOLO mode" (auto-approval of actions).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
97
|
+
return ["qwen", "-y", prompt]
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def name(self) -> str:
|
|
101
|
+
return "qwen"
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def supports_yolo_mode(self) -> bool:
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class EngineFactory:
|
|
109
|
+
"""
|
|
110
|
+
Factory for creating engine adapter instances.
|
|
111
|
+
|
|
112
|
+
Usage:
|
|
113
|
+
adapter = EngineFactory.create("gemini")
|
|
114
|
+
command = adapter.build_command("Write a test")
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
_adapters = {
|
|
118
|
+
"gemini": GeminiAdapter,
|
|
119
|
+
"claude": ClaudeAdapter,
|
|
120
|
+
"qwen": QwenAdapter,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def create(cls, engine_name: str) -> EngineAdapter:
|
|
125
|
+
"""
|
|
126
|
+
Create an adapter instance for the specified engine.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
engine_name: Name of the engine (e.g., "gemini", "claude")
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
An instance of the appropriate EngineAdapter
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If the engine is not supported
|
|
136
|
+
"""
|
|
137
|
+
adapter_class = cls._adapters.get(engine_name.lower())
|
|
138
|
+
if not adapter_class:
|
|
139
|
+
supported = ", ".join(cls._adapters.keys())
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Unsupported engine: '{engine_name}'. "
|
|
142
|
+
f"Supported engines: {supported}"
|
|
143
|
+
)
|
|
144
|
+
return adapter_class()
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def supported_engines(cls) -> List[str]:
|
|
148
|
+
"""Return a list of all supported engine names."""
|
|
149
|
+
return list(cls._adapters.keys())
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from monoco.core.config import get_config
|
|
1
3
|
from .manager import SessionManager
|
|
2
4
|
from .session import RuntimeSession
|
|
3
|
-
from .
|
|
5
|
+
from .config import load_scheduler_config
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class ApoptosisManager:
|
|
@@ -11,10 +13,15 @@ class ApoptosisManager:
|
|
|
11
13
|
|
|
12
14
|
def __init__(self, session_manager: SessionManager):
|
|
13
15
|
self.session_manager = session_manager
|
|
16
|
+
|
|
17
|
+
# Load roles dynamically based on current project context
|
|
18
|
+
settings = get_config()
|
|
19
|
+
project_root = Path(settings.paths.root).resolve()
|
|
20
|
+
roles = load_scheduler_config(project_root)
|
|
21
|
+
|
|
14
22
|
# Find coroner role
|
|
15
|
-
self.coroner_role =
|
|
16
|
-
|
|
17
|
-
)
|
|
23
|
+
self.coroner_role = roles.get("coroner")
|
|
24
|
+
|
|
18
25
|
if not self.coroner_role:
|
|
19
26
|
raise ValueError("Coroner role not defined!")
|
|
20
27
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
from .models import RoleTemplate
|
|
3
|
+
from .engines import EngineFactory
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class Worker:
|
|
@@ -37,7 +38,8 @@ class Worker:
|
|
|
37
38
|
import sys
|
|
38
39
|
|
|
39
40
|
# Prepare the prompt
|
|
40
|
-
|
|
41
|
+
# We treat 'crafter' as a drafter when context is provided (Draft Mode)
|
|
42
|
+
if (self.role.name == "drafter" or self.role.name == "crafter") and context:
|
|
41
43
|
issue_type = context.get("type", "feature")
|
|
42
44
|
description = context.get("description", "No description")
|
|
43
45
|
prompt = (
|
|
@@ -64,10 +66,9 @@ class Worker:
|
|
|
64
66
|
print(f"[{self.role.name}] Goal: {self.role.goal}")
|
|
65
67
|
|
|
66
68
|
try:
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
69
|
+
# Use factory to get the appropriate engine adapter
|
|
70
|
+
adapter = EngineFactory.create(engine)
|
|
71
|
+
engine_args = adapter.build_command(prompt)
|
|
71
72
|
|
|
72
73
|
self._process = subprocess.Popen(
|
|
73
74
|
engine_args, stdout=sys.stdout, stderr=sys.stderr, text=True
|
|
@@ -77,6 +78,9 @@ class Worker:
|
|
|
77
78
|
# DO NOT WAIT HERE.
|
|
78
79
|
# The scheduler/monitoring loop is responsible for checking status.
|
|
79
80
|
|
|
81
|
+
except ValueError as e:
|
|
82
|
+
# Engine not supported by factory
|
|
83
|
+
raise RuntimeError(f"Unsupported engine '{engine}'. {str(e)}")
|
|
80
84
|
except FileNotFoundError:
|
|
81
85
|
raise RuntimeError(
|
|
82
86
|
f"Agent engine '{engine}' not found. Please ensure it is installed and in PATH."
|
monoco/main.py
CHANGED
|
@@ -169,6 +169,7 @@ app.add_typer(workspace_cmd.app, name="workspace", help="Manage workspace")
|
|
|
169
169
|
from monoco.features.scheduler import cli as scheduler_cmd
|
|
170
170
|
|
|
171
171
|
app.add_typer(scheduler_cmd.app, name="agent", help="Manage agent sessions")
|
|
172
|
+
app.add_typer(scheduler_cmd.role_app, name="role", help="Manage agent roles")
|
|
172
173
|
|
|
173
174
|
|
|
174
175
|
from monoco.daemon.commands import serve
|