monoco-toolkit 0.3.11__py3-none-any.whl → 0.3.12__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/automation/__init__.py +51 -0
- monoco/core/automation/config.py +338 -0
- monoco/core/automation/field_watcher.py +296 -0
- monoco/core/automation/handlers.py +723 -0
- monoco/core/config.py +1 -1
- monoco/core/executor/__init__.py +38 -0
- monoco/core/executor/agent_action.py +254 -0
- monoco/core/executor/git_action.py +303 -0
- monoco/core/executor/im_action.py +309 -0
- monoco/core/executor/pytest_action.py +218 -0
- monoco/core/git.py +15 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/router/__init__.py +55 -0
- monoco/core/router/action.py +341 -0
- monoco/core/router/router.py +392 -0
- monoco/core/scheduler/__init__.py +63 -0
- monoco/core/scheduler/base.py +152 -0
- monoco/core/scheduler/engines.py +175 -0
- monoco/core/scheduler/events.py +171 -0
- monoco/core/scheduler/local.py +377 -0
- monoco/core/watcher/__init__.py +57 -0
- monoco/core/watcher/base.py +365 -0
- monoco/core/watcher/dropzone.py +152 -0
- monoco/core/watcher/issue.py +303 -0
- monoco/core/watcher/memo.py +200 -0
- monoco/core/watcher/task.py +238 -0
- monoco/daemon/events.py +34 -0
- monoco/daemon/scheduler.py +172 -201
- monoco/daemon/services.py +27 -243
- monoco/features/agent/__init__.py +25 -7
- monoco/features/agent/cli.py +91 -57
- monoco/features/agent/engines.py +31 -170
- monoco/features/agent/worker.py +1 -1
- monoco/features/issue/commands.py +90 -32
- monoco/features/issue/core.py +249 -4
- monoco/features/spike/commands.py +5 -3
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +1 -1
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/RECORD +41 -20
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -127
- monoco/features/agent/session.py +0 -169
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
monoco/features/agent/engines.py
CHANGED
|
@@ -1,175 +1,36 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Agent Engine Adapters for Monoco Scheduler.
|
|
3
3
|
|
|
4
|
-
This module
|
|
5
|
-
|
|
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 -p <prompt> -y
|
|
52
|
-
The -y flag enables "YOLO mode" (auto-approval of actions).
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
def build_command(self, prompt: str) -> List[str]:
|
|
56
|
-
# Based on Gemini CLI help: -p <prompt> for non-interactive
|
|
57
|
-
return ["gemini", "-p", prompt, "-y"]
|
|
58
|
-
|
|
59
|
-
@property
|
|
60
|
-
def name(self) -> str:
|
|
61
|
-
return "gemini"
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def supports_yolo_mode(self) -> bool:
|
|
65
|
-
return True
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class ClaudeAdapter(EngineAdapter):
|
|
69
|
-
"""
|
|
70
|
-
Adapter for Anthropic Claude CLI.
|
|
71
|
-
|
|
72
|
-
Command format: claude -p <prompt>
|
|
73
|
-
The -p/--print flag enables non-interactive mode.
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
def build_command(self, prompt: str) -> List[str]:
|
|
77
|
-
# Based on Claude CLI help: -p <prompt> is NOT standard, usually -p means print/non-interactive.
|
|
78
|
-
# But for one-shot execution, we do passing prompt as argument with -p flag.
|
|
79
|
-
return ["claude", "-p", prompt]
|
|
80
|
-
|
|
81
|
-
@property
|
|
82
|
-
def name(self) -> str:
|
|
83
|
-
return "claude"
|
|
4
|
+
DEPRECATED: This module has been moved to monoco.core.scheduler.
|
|
5
|
+
This file is kept for backward compatibility and re-exports from the new location.
|
|
84
6
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class QwenAdapter(EngineAdapter):
|
|
92
|
-
"""
|
|
93
|
-
Adapter for Qwen Code CLI.
|
|
94
|
-
|
|
95
|
-
Command format: qwen -p <prompt> -y
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
def build_command(self, prompt: str) -> List[str]:
|
|
99
|
-
# Assuming Qwen follows similar patterns (based on user feedback)
|
|
100
|
-
return ["qwen", "-p", prompt, "-y"]
|
|
101
|
-
|
|
102
|
-
@property
|
|
103
|
-
def name(self) -> str:
|
|
104
|
-
return "qwen"
|
|
105
|
-
|
|
106
|
-
@property
|
|
107
|
-
def supports_yolo_mode(self) -> bool:
|
|
108
|
-
return True
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
class KimiAdapter(EngineAdapter):
|
|
112
|
-
"""
|
|
113
|
-
Adapter for Kimi CLI (Moonshot AI).
|
|
114
|
-
|
|
115
|
-
Command format: kimi -p <prompt> --print
|
|
116
|
-
Note: --print implicitly adds --yolo.
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
def build_command(self, prompt: str) -> List[str]:
|
|
120
|
-
# Based on Kimi CLI help: -p, --prompt TEXT.
|
|
121
|
-
# Also using --print for non-interactive mode (which enables yolo).
|
|
122
|
-
return ["kimi", "-p", prompt, "--print"]
|
|
123
|
-
|
|
124
|
-
@property
|
|
125
|
-
def name(self) -> str:
|
|
126
|
-
return "kimi"
|
|
127
|
-
|
|
128
|
-
@property
|
|
129
|
-
def supports_yolo_mode(self) -> bool:
|
|
130
|
-
return True
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class EngineFactory:
|
|
134
|
-
"""
|
|
135
|
-
Factory for creating engine adapter instances.
|
|
136
|
-
|
|
137
|
-
Usage:
|
|
138
|
-
adapter = EngineFactory.create("gemini")
|
|
139
|
-
command = adapter.build_command("Write a test")
|
|
140
|
-
"""
|
|
141
|
-
|
|
142
|
-
_adapters = {
|
|
143
|
-
"gemini": GeminiAdapter,
|
|
144
|
-
"claude": ClaudeAdapter,
|
|
145
|
-
"qwen": QwenAdapter,
|
|
146
|
-
"kimi": KimiAdapter,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
@classmethod
|
|
150
|
-
def create(cls, engine_name: str) -> EngineAdapter:
|
|
151
|
-
"""
|
|
152
|
-
Create an adapter instance for the specified engine.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
engine_name: Name of the engine (e.g., "gemini", "claude")
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
An instance of the appropriate EngineAdapter
|
|
159
|
-
|
|
160
|
-
Raises:
|
|
161
|
-
ValueError: If the engine is not supported
|
|
162
|
-
"""
|
|
163
|
-
adapter_class = cls._adapters.get(engine_name.lower())
|
|
164
|
-
if not adapter_class:
|
|
165
|
-
supported = ", ".join(cls._adapters.keys())
|
|
166
|
-
raise ValueError(
|
|
167
|
-
f"Unsupported engine: '{engine_name}'. "
|
|
168
|
-
f"Supported engines: {supported}"
|
|
169
|
-
)
|
|
170
|
-
return adapter_class()
|
|
7
|
+
Migration:
|
|
8
|
+
Old: from monoco.features.agent.engines import EngineAdapter, EngineFactory
|
|
9
|
+
New: from monoco.core.scheduler import EngineAdapter, EngineFactory
|
|
10
|
+
"""
|
|
171
11
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
12
|
+
import warnings
|
|
13
|
+
from monoco.core.scheduler import (
|
|
14
|
+
EngineAdapter,
|
|
15
|
+
EngineFactory,
|
|
16
|
+
GeminiAdapter,
|
|
17
|
+
ClaudeAdapter,
|
|
18
|
+
QwenAdapter,
|
|
19
|
+
KimiAdapter,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
warnings.warn(
|
|
23
|
+
"monoco.features.agent.engines is deprecated. "
|
|
24
|
+
"Use monoco.core.scheduler instead.",
|
|
25
|
+
DeprecationWarning,
|
|
26
|
+
stacklevel=2
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"EngineAdapter",
|
|
31
|
+
"EngineFactory",
|
|
32
|
+
"GeminiAdapter",
|
|
33
|
+
"ClaudeAdapter",
|
|
34
|
+
"QwenAdapter",
|
|
35
|
+
"KimiAdapter",
|
|
36
|
+
]
|
monoco/features/agent/worker.py
CHANGED
|
@@ -2,6 +2,7 @@ import typer
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import Optional, List
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
import logging
|
|
5
6
|
from rich.console import Console
|
|
6
7
|
from rich.tree import Tree
|
|
7
8
|
from rich.panel import Panel
|
|
@@ -24,6 +25,7 @@ from . import domain_commands
|
|
|
24
25
|
|
|
25
26
|
app.add_typer(domain_commands.app, name="domain")
|
|
26
27
|
console = Console()
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
@app.command("create")
|
|
@@ -391,6 +393,13 @@ def submit(
|
|
|
391
393
|
)
|
|
392
394
|
|
|
393
395
|
try:
|
|
396
|
+
# FEAT-0163: Automatically sync files before submission to ensure manifest completeness
|
|
397
|
+
try:
|
|
398
|
+
core.sync_issue_files(issues_root, issue_id, project_root)
|
|
399
|
+
except Exception as se:
|
|
400
|
+
# Just log warning, don't fail submit if sync fails (defensive)
|
|
401
|
+
logger.warning(f"Auto-sync failed during submit for {issue_id}: {se}")
|
|
402
|
+
|
|
394
403
|
# Implicitly ensure status is Open
|
|
395
404
|
issue = core.update_issue(
|
|
396
405
|
issues_root,
|
|
@@ -445,7 +454,11 @@ def move_close(
|
|
|
445
454
|
),
|
|
446
455
|
json: AgentOutput = False,
|
|
447
456
|
):
|
|
448
|
-
"""Close issue.
|
|
457
|
+
"""Close issue with atomic transaction guarantee.
|
|
458
|
+
|
|
459
|
+
If any step fails, all changes made during the close operation will be
|
|
460
|
+
automatically rolled back to ensure the mainline remains clean.
|
|
461
|
+
"""
|
|
449
462
|
config = get_config()
|
|
450
463
|
issues_root = _resolve_issues_root(config, root)
|
|
451
464
|
project_root = _resolve_project_root(config)
|
|
@@ -472,19 +485,57 @@ def move_close(
|
|
|
472
485
|
|
|
473
486
|
# Handle force-prune logic
|
|
474
487
|
if force_prune:
|
|
475
|
-
#
|
|
476
|
-
|
|
488
|
+
# FEAT-0125: force-prune requires interactive confirmation to avoid accidental data loss
|
|
489
|
+
# unless in agent mode (which is handled by typer.confirm's default)
|
|
490
|
+
if not OutputManager.is_agent_mode():
|
|
477
491
|
confirm = typer.confirm(
|
|
478
|
-
"
|
|
479
|
-
default=False
|
|
492
|
+
"⚠ FORCE PRUNE will permanently delete the feature branch without checking its merge status. Continue?",
|
|
493
|
+
default=False
|
|
480
494
|
)
|
|
481
495
|
if not confirm:
|
|
482
|
-
|
|
496
|
+
console.print("[yellow]Aborted.[/yellow]")
|
|
497
|
+
raise typer.Exit(code=1)
|
|
498
|
+
|
|
483
499
|
prune = True
|
|
484
500
|
force = True
|
|
485
501
|
|
|
502
|
+
# ATOMIC TRANSACTION: Capture initial state for potential rollback
|
|
503
|
+
initial_head = None
|
|
504
|
+
transaction_commits = []
|
|
505
|
+
|
|
506
|
+
def rollback_transaction():
|
|
507
|
+
"""Rollback all changes made during the transaction."""
|
|
508
|
+
if initial_head and transaction_commits:
|
|
509
|
+
try:
|
|
510
|
+
git.git_reset_hard(project_root, initial_head)
|
|
511
|
+
if not OutputManager.is_agent_mode():
|
|
512
|
+
console.print(f"[yellow]↩ Rolled back to {initial_head[:7]}[/yellow]")
|
|
513
|
+
except Exception as rollback_error:
|
|
514
|
+
if not OutputManager.is_agent_mode():
|
|
515
|
+
console.print(f"[red]⚠ Rollback failed: {rollback_error}[/red]")
|
|
516
|
+
console.print(f"[red] Manual recovery may be required. Run: git reset --hard {initial_head[:7]}[/red]")
|
|
517
|
+
|
|
486
518
|
try:
|
|
487
|
-
#
|
|
519
|
+
# Capture initial HEAD before any modifications
|
|
520
|
+
initial_head = git.get_current_head(project_root)
|
|
521
|
+
|
|
522
|
+
# 0. Find issue across branches (FIX-0006)
|
|
523
|
+
# This will raise RuntimeError if issue found in multiple branches
|
|
524
|
+
found_path, source_branch = core.find_issue_path_across_branches(
|
|
525
|
+
issues_root, issue_id, project_root
|
|
526
|
+
)
|
|
527
|
+
if not found_path:
|
|
528
|
+
OutputManager.error(f"Issue {issue_id} not found in any branch.")
|
|
529
|
+
raise typer.Exit(code=1)
|
|
530
|
+
|
|
531
|
+
# If issue was found in a different branch, notify user
|
|
532
|
+
if source_branch and source_branch != git.get_current_branch(project_root):
|
|
533
|
+
if not OutputManager.is_agent_mode():
|
|
534
|
+
console.print(
|
|
535
|
+
f"[green]✔ Found {issue_id} in branch '{source_branch}', synced to working tree.[/green]"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# 1. Perform Smart Atomic Merge (FEAT-0154)
|
|
488
539
|
merged_files = []
|
|
489
540
|
try:
|
|
490
541
|
merged_files = core.merge_issue_changes(issues_root, issue_id, project_root)
|
|
@@ -498,7 +549,8 @@ def move_close(
|
|
|
498
549
|
if not no_commit:
|
|
499
550
|
commit_msg = f"feat: atomic merge changes from {issue_id}"
|
|
500
551
|
try:
|
|
501
|
-
git.git_commit(project_root, commit_msg)
|
|
552
|
+
commit_hash = git.git_commit(project_root, commit_msg)
|
|
553
|
+
transaction_commits.append(commit_hash)
|
|
502
554
|
if not OutputManager.is_agent_mode():
|
|
503
555
|
console.print(f"[green]✔ Committed merged changes.[/green]")
|
|
504
556
|
except Exception as e:
|
|
@@ -508,43 +560,42 @@ def move_close(
|
|
|
508
560
|
|
|
509
561
|
except Exception as e:
|
|
510
562
|
OutputManager.error(f"Merge Error: {e}")
|
|
563
|
+
rollback_transaction()
|
|
511
564
|
raise typer.Exit(code=1)
|
|
512
565
|
|
|
513
|
-
issue
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
566
|
+
# 2. Update issue status to closed
|
|
567
|
+
try:
|
|
568
|
+
issue = core.update_issue(
|
|
569
|
+
issues_root,
|
|
570
|
+
issue_id,
|
|
571
|
+
status="closed",
|
|
572
|
+
solution=solution,
|
|
573
|
+
no_commit=no_commit,
|
|
574
|
+
project_root=project_root,
|
|
575
|
+
)
|
|
576
|
+
# Track the auto-commit from update_issue if it occurred
|
|
577
|
+
if hasattr(issue, 'commit_result') and issue.commit_result:
|
|
578
|
+
transaction_commits.append(issue.commit_result)
|
|
579
|
+
except Exception as e:
|
|
580
|
+
OutputManager.error(f"Update Error: {e}")
|
|
581
|
+
rollback_transaction()
|
|
582
|
+
raise typer.Exit(code=1)
|
|
521
583
|
|
|
584
|
+
# 3. Prune issue resources (branch/worktree)
|
|
522
585
|
pruned_resources = []
|
|
523
586
|
if prune:
|
|
524
587
|
# Get isolation info for confirmation prompt
|
|
525
588
|
isolation_info = None
|
|
526
589
|
if issue.isolation:
|
|
527
|
-
isolation_type = issue.isolation.type
|
|
590
|
+
isolation_type = issue.isolation.type if issue.isolation.type else None
|
|
528
591
|
isolation_ref = issue.isolation.ref
|
|
529
592
|
isolation_info = (isolation_type, isolation_ref)
|
|
530
593
|
|
|
531
|
-
#
|
|
532
|
-
if not OutputManager.is_agent_mode() and isolation_info
|
|
594
|
+
# Auto-prune without confirmation (FEAT-0082 Update)
|
|
595
|
+
if not OutputManager.is_agent_mode() and isolation_info:
|
|
533
596
|
iso_type, iso_ref = isolation_info
|
|
534
597
|
if iso_ref:
|
|
535
|
-
console.print(f"
|
|
536
|
-
console.print(f"Issue [cyan]{issue_id}[/cyan] will be closed with the following action:")
|
|
537
|
-
console.print(f" • Delete {iso_type}: [bold]{iso_ref}[/bold]")
|
|
538
|
-
console.print(f"\n[dim]This operation will permanently remove the {iso_type}. "
|
|
539
|
-
f"Ensure all changes have been merged to main.[/dim]")
|
|
540
|
-
confirm = typer.confirm(
|
|
541
|
-
f"\nProceed with closing {issue_id} and deleting {iso_type}?",
|
|
542
|
-
default=True,
|
|
543
|
-
)
|
|
544
|
-
if not confirm:
|
|
545
|
-
console.print(f"[yellow]Close operation cancelled.[/yellow]")
|
|
546
|
-
console.print(f"[dim]Tip: Use --no-prune to close without deleting {iso_type}.[/dim]")
|
|
547
|
-
raise typer.Abort()
|
|
598
|
+
console.print(f"[dim]Cleaning up {iso_type}: {iso_ref}...[/dim]")
|
|
548
599
|
|
|
549
600
|
try:
|
|
550
601
|
pruned_resources = core.prune_issue_resources(
|
|
@@ -554,14 +605,21 @@ def move_close(
|
|
|
554
605
|
console.print(f"[green]✔ Cleaned up:[/green] {', '.join(pruned_resources)}")
|
|
555
606
|
except Exception as e:
|
|
556
607
|
OutputManager.error(f"Prune Error: {e}")
|
|
608
|
+
rollback_transaction()
|
|
557
609
|
raise typer.Exit(code=1)
|
|
558
610
|
|
|
611
|
+
# Success: Clear transaction state as all operations completed
|
|
559
612
|
OutputManager.print(
|
|
560
613
|
{"issue": issue, "status": "closed", "pruned": pruned_resources}
|
|
561
614
|
)
|
|
562
615
|
|
|
616
|
+
except typer.Abort:
|
|
617
|
+
# User cancelled, rollback already handled
|
|
618
|
+
raise
|
|
563
619
|
except Exception as e:
|
|
620
|
+
# Catch-all for unexpected errors
|
|
564
621
|
OutputManager.error(str(e))
|
|
622
|
+
rollback_transaction()
|
|
565
623
|
raise typer.Exit(code=1)
|
|
566
624
|
|
|
567
625
|
|