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.
Files changed (44) hide show
  1. monoco/core/automation/__init__.py +51 -0
  2. monoco/core/automation/config.py +338 -0
  3. monoco/core/automation/field_watcher.py +296 -0
  4. monoco/core/automation/handlers.py +723 -0
  5. monoco/core/config.py +1 -1
  6. monoco/core/executor/__init__.py +38 -0
  7. monoco/core/executor/agent_action.py +254 -0
  8. monoco/core/executor/git_action.py +303 -0
  9. monoco/core/executor/im_action.py +309 -0
  10. monoco/core/executor/pytest_action.py +218 -0
  11. monoco/core/git.py +15 -0
  12. monoco/core/hooks/context.py +74 -13
  13. monoco/core/router/__init__.py +55 -0
  14. monoco/core/router/action.py +341 -0
  15. monoco/core/router/router.py +392 -0
  16. monoco/core/scheduler/__init__.py +63 -0
  17. monoco/core/scheduler/base.py +152 -0
  18. monoco/core/scheduler/engines.py +175 -0
  19. monoco/core/scheduler/events.py +171 -0
  20. monoco/core/scheduler/local.py +377 -0
  21. monoco/core/watcher/__init__.py +57 -0
  22. monoco/core/watcher/base.py +365 -0
  23. monoco/core/watcher/dropzone.py +152 -0
  24. monoco/core/watcher/issue.py +303 -0
  25. monoco/core/watcher/memo.py +200 -0
  26. monoco/core/watcher/task.py +238 -0
  27. monoco/daemon/events.py +34 -0
  28. monoco/daemon/scheduler.py +172 -201
  29. monoco/daemon/services.py +27 -243
  30. monoco/features/agent/__init__.py +25 -7
  31. monoco/features/agent/cli.py +91 -57
  32. monoco/features/agent/engines.py +31 -170
  33. monoco/features/agent/worker.py +1 -1
  34. monoco/features/issue/commands.py +90 -32
  35. monoco/features/issue/core.py +249 -4
  36. monoco/features/spike/commands.py +5 -3
  37. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +1 -1
  38. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/RECORD +41 -20
  39. monoco/features/agent/apoptosis.py +0 -44
  40. monoco/features/agent/manager.py +0 -127
  41. monoco/features/agent/session.py +0 -169
  42. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
  43. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
  44. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
@@ -1,175 +1,36 @@
1
1
  """
2
2
  Agent Engine Adapters for Monoco Scheduler.
3
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 -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
- @property
86
- def supports_yolo_mode(self) -> bool:
87
- # Claude uses -p for non-interactive mode, similar concept
88
- return True
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
- @classmethod
173
- def supported_engines(cls) -> List[str]:
174
- """Return a list of all supported engine names."""
175
- return list(cls._adapters.keys())
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
+ ]
@@ -1,6 +1,6 @@
1
1
  from typing import Optional
2
2
  from .models import RoleTemplate
3
- from .engines import EngineFactory
3
+ from monoco.core.scheduler import EngineFactory
4
4
 
5
5
 
6
6
  class Worker:
@@ -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
- # Use OutputManager to check mode, as `json` arg might not be reliable with Typer Annotated
476
- if not OutputManager.is_agent_mode() and not force:
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
- "⚠️ [Bold Red]Warning:[/Bold Red] You are about to FORCE prune issue resources. Git merge checks will be bypassed.\nAre you sure you want to proceed?",
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
- raise typer.Abort()
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
- # 0. Perform Smart Atomic Merge (FEAT-0154)
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 = core.update_issue(
514
- issues_root,
515
- issue_id,
516
- status="closed",
517
- solution=solution,
518
- no_commit=no_commit,
519
- project_root=project_root,
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.value if issue.isolation.type else None
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
- # Interactive confirmation before pruning (non-agent mode only)
532
- if not OutputManager.is_agent_mode() and isolation_info and not force:
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"\n[bold yellow]⚠️ Resource Cleanup Confirmation[/bold yellow]")
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