k-cli-for-devs 1.0.0__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 (75) hide show
  1. k_cli/__init__.py +77 -0
  2. k_cli/agents/__init__.py +0 -0
  3. k_cli/agents/adversarial_swarm.py +338 -0
  4. k_cli/agents/agent_core.py +255 -0
  5. k_cli/agents/background_daemon.py +141 -0
  6. k_cli/agents/orchestrator.py +376 -0
  7. k_cli/agents/persona.py +649 -0
  8. k_cli/agents/scaffold_engine.py +121 -0
  9. k_cli/agents/strands_agent.py +832 -0
  10. k_cli/agents/subagents.py +1496 -0
  11. k_cli/cli.py +3297 -0
  12. k_cli/core/__init__.py +0 -0
  13. k_cli/core/airgap.py +95 -0
  14. k_cli/core/credentials.py +548 -0
  15. k_cli/core/intent_sensor.py +177 -0
  16. k_cli/core/llm_driver.py +1028 -0
  17. k_cli/core/model_manager.py +1109 -0
  18. k_cli/core/models_hub.py +913 -0
  19. k_cli/core/prompting.py +41 -0
  20. k_cli/core/sdk.py +322 -0
  21. k_cli/core/session.py +826 -0
  22. k_cli/core/smart_router.py +230 -0
  23. k_cli/core/storage_manager.py +176 -0
  24. k_cli/core/viewport_engine.py +117 -0
  25. k_cli/demo/demo_runner.py +579 -0
  26. k_cli/git/__init__.py +0 -0
  27. k_cli/git/ai_bisect.py +208 -0
  28. k_cli/git/conflict_resolver.py +1039 -0
  29. k_cli/git/git_guard.py +417 -0
  30. k_cli/git/patcher.py +1175 -0
  31. k_cli/git/repo_map.py +1780 -0
  32. k_cli/git/smart_git.py +928 -0
  33. k_cli/git/verifier.py +969 -0
  34. k_cli/github/__init__.py +0 -0
  35. k_cli/github/dedup_engine.py +787 -0
  36. k_cli/github/github_client.py +1702 -0
  37. k_cli/github/github_engine.py +641 -0
  38. k_cli/github/local_hub.py +209 -0
  39. k_cli/github/pr_watcher.py +129 -0
  40. k_cli/github/trending.py +205 -0
  41. k_cli/tools/__init__.py +0 -0
  42. k_cli/tools/audit.py +79 -0
  43. k_cli/tools/chaos_immunity.py +377 -0
  44. k_cli/tools/codebase_qa.py +106 -0
  45. k_cli/tools/command_runner.py +256 -0
  46. k_cli/tools/diagram_generator.py +547 -0
  47. k_cli/tools/doc_retriever.py +1332 -0
  48. k_cli/tools/feature.py +105 -0
  49. k_cli/tools/ghost_daemon.py +122 -0
  50. k_cli/tools/incident_triage.py +1365 -0
  51. k_cli/tools/mcp_client.py +1846 -0
  52. k_cli/tools/repo_gardener.py +142 -0
  53. k_cli/tools/rules.py +109 -0
  54. k_cli/tools/security.py +52 -0
  55. k_cli/tools/security_healer.py +999 -0
  56. k_cli/tools/synapse_graph.py +155 -0
  57. k_cli/tui/__init__.py +0 -0
  58. k_cli/tui/diff_viewer.py +223 -0
  59. k_cli/tui/tui.py +1145 -0
  60. k_cli/tui/tui_animations.py +648 -0
  61. k_cli/tui/tui_app.py +2788 -0
  62. k_cli/ui/__init__.py +10 -0
  63. k_cli/ui/simple_repl.py +315 -0
  64. k_cli/web/__init__.py +7 -0
  65. k_cli/web/server.py +624 -0
  66. k_cli/web/static/app.js +830 -0
  67. k_cli/web/static/index.html +495 -0
  68. k_cli/web/static/monitor.html +189 -0
  69. k_cli/web/static/style.css +838 -0
  70. k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
  71. k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
  72. k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
  73. k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
  74. k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
  75. k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1496 @@
1
+ """
2
+ subagents.py - Native Subagent Task Spawner & Multi-Agent Orchestrator for K-CLI
3
+ Project Bankai Engine v1.0.0
4
+
5
+ Enables K-CLI to decompose complex user prompts into parallel subtasks:
6
+ - [EXPLORER] : Inspects workspace structure, AST symbol maps, locates files.
7
+ - [RESEARCHER] : Investigates offline DevDocs, API signatures, dependencies.
8
+ - [REFACTORER] : Generates code modifications and SEARCH/REPLACE surgical patch blocks.
9
+ - [TESTER] : Formulates test suites and runs ground-truth verification guard.
10
+
11
+ Features:
12
+ 1. DAG Task Decomposition (LLM-based with deterministic fallback).
13
+ 2. Multi-threaded background execution with structured JSON messaging.
14
+ 3. PatchAggregator to merge SEARCH/REPLACE blocks into unified patches.
15
+ 4. Rich CLI tree visualization with live progress bars and status dashboards.
16
+ 5. Memory-budgeted execution (< 1.0 GB RAM constraint).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import ast
22
+ import gc
23
+ import json
24
+ import os
25
+ import psutil
26
+ import queue
27
+ import re
28
+ import sys
29
+ import threading
30
+ import time
31
+ import uuid
32
+ from dataclasses import dataclass, field
33
+ from enum import Enum
34
+ from pathlib import Path
35
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
36
+
37
+ try:
38
+ from k_cli.tools.doc_retriever import DocRetriever
39
+ from k_cli.core.llm_driver import LLMDriver
40
+ from k_cli.git.patcher import Patcher
41
+ from k_cli.git.repo_map import RepoMap
42
+ from k_cli.git.verifier import CodeExtractor, VerificationResult, Verifier
43
+ from k_cli.git.conflict_resolver import ConflictResolver, ConflictBlock, ConflictSummary, FileResolutionResult
44
+ from k_cli.github.github_client import GitHubClient, PRLifecycleManager, PRReviewResult, PRFixResult
45
+ from k_cli.tools.mcp_client import MCPManager, MCPClient, MCPTool, MCPToolResult
46
+ from k_cli.github.dedup_engine import DedupEngine, DedupMatch
47
+ except ModuleNotFoundError:
48
+ from doc_retriever import DocRetriever
49
+ from k_cli.core.llm_driver import LLMDriver
50
+ from patcher import Patcher
51
+ from repo_map import RepoMap
52
+ from verifier import CodeExtractor, VerificationResult, Verifier
53
+ try:
54
+ from conflict_resolver import ConflictResolver, ConflictBlock, ConflictSummary, FileResolutionResult
55
+ except (ModuleNotFoundError, ImportError):
56
+ ConflictResolver = None # type: ignore
57
+ ConflictBlock = None # type: ignore
58
+ ConflictSummary = None # type: ignore
59
+ FileResolutionResult = None # type: ignore
60
+ try:
61
+ from github_client import GitHubClient, PRLifecycleManager, PRReviewResult, PRFixResult
62
+ except (ModuleNotFoundError, ImportError):
63
+ GitHubClient = None # type: ignore
64
+ PRLifecycleManager = None # type: ignore
65
+ PRReviewResult = None # type: ignore
66
+ PRFixResult = None # type: ignore
67
+ try:
68
+ from mcp_client import MCPManager, MCPClient, MCPTool, MCPToolResult
69
+ except (ModuleNotFoundError, ImportError):
70
+ MCPManager = None # type: ignore
71
+ MCPClient = None # type: ignore
72
+ MCPTool = None # type: ignore
73
+ MCPToolResult = None # type: ignore
74
+ try:
75
+ from dedup_engine import DedupEngine, DedupMatch
76
+ except (ModuleNotFoundError, ImportError):
77
+ DedupEngine = None # type: ignore
78
+ DedupMatch = None # type: ignore
79
+
80
+ from rich.console import Console
81
+ from rich.live import Live
82
+ from rich.panel import Panel
83
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
84
+ from rich.syntax import Syntax
85
+ from rich.table import Table
86
+ from rich.tree import Tree
87
+
88
+
89
+ def _resolve_driver(driver: Optional[LLMDriver] = None, mock_mode: bool = False) -> LLMDriver:
90
+ """Helper to ensure safe offline fallback during test runs or mock environments."""
91
+ if driver is not None:
92
+ return driver
93
+ is_mock = mock_mode or os.getenv("KCLI_MOCK_MODE", "").lower() in ("true", "1") or ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("K_CLI_REAL_LLM"))
94
+ return LLMDriver(mock_mode=is_mock)
95
+
96
+
97
+ # ==============================================================================
98
+ # 1. Enums & Structured Data Models
99
+ # ==============================================================================
100
+
101
+ class SubagentRole(str, Enum):
102
+ """Specialized worker roles in the multi-agent orchestration tree."""
103
+ EXPLORER = "EXPLORER"
104
+ RESEARCHER = "RESEARCHER"
105
+ REFACTORER = "REFACTORER"
106
+ TESTER = "TESTER"
107
+ CODER = "CODER"
108
+ CRITIC = "CRITIC"
109
+ ARCHITECT = "ARCHITECT"
110
+ CONFLICT_RESOLVER = "CONFLICT_RESOLVER"
111
+ PR_REVIEWER = "PR_REVIEWER"
112
+ MCP_OPERATOR = "MCP_OPERATOR"
113
+
114
+ @classmethod
115
+ def from_str(cls, val: str) -> "SubagentRole":
116
+ val_upper = str(val).upper().strip()
117
+ for role in cls:
118
+ if role.value == val_upper or role.name == val_upper:
119
+ return role
120
+ if "CONFLICT" in val_upper or "MERGE" in val_upper:
121
+ return cls.CONFLICT_RESOLVER
122
+ if "PR" in val_upper or "PULL_REQUEST" in val_upper:
123
+ return cls.PR_REVIEWER
124
+ if "MCP" in val_upper or "TOOL_OPERATOR" in val_upper or "OPERATOR" in val_upper:
125
+ return cls.MCP_OPERATOR
126
+ if "EXPLOR" in val_upper:
127
+ return cls.EXPLORER
128
+ if "RESEARCH" in val_upper or "DOC" in val_upper:
129
+ return cls.RESEARCHER
130
+ if "TEST" in val_upper or "VERIF" in val_upper:
131
+ return cls.TESTER
132
+ if "REFACTOR" in val_upper or "PATCH" in val_upper or "EDIT" in val_upper:
133
+ return cls.REFACTORER
134
+ if "CRITIC" in val_upper:
135
+ return cls.CRITIC
136
+ return cls.CODER
137
+
138
+
139
+ class SubagentStatus(str, Enum):
140
+ """Lifecycle states of subagent tasks."""
141
+ PENDING = "PENDING"
142
+ RUNNING = "RUNNING"
143
+ COMPLETED = "COMPLETED"
144
+ FAILED = "FAILED"
145
+ CANCELLED = "CANCELLED"
146
+
147
+
148
+ class SubagentMessageType(str, Enum):
149
+ """Structured messaging protocols between orchestrator and subagents."""
150
+ TASK_INIT = "TASK_INIT"
151
+ PROGRESS = "PROGRESS"
152
+ LOG = "LOG"
153
+ SEARCH_REPLACE_PATCH = "SEARCH_REPLACE_PATCH"
154
+ CODE_OUTPUT = "CODE_OUTPUT"
155
+ TEST_RESULT = "TEST_RESULT"
156
+ RESEARCH_FINDING = "RESEARCH_FINDING"
157
+ EXPLORATION_MAP = "EXPLORATION_MAP"
158
+ CONFLICT_RESOLVED = "CONFLICT_RESOLVED"
159
+ PR_REVIEWED = "PR_REVIEWED"
160
+ MCP_TOOL_RESULT = "MCP_TOOL_RESULT"
161
+ DEDUP_WARNING = "DEDUP_WARNING"
162
+ TASK_COMPLETE = "TASK_COMPLETE"
163
+ TASK_FAILED = "TASK_FAILED"
164
+ HEARTBEAT = "HEARTBEAT"
165
+
166
+
167
+ @dataclass
168
+ class SubagentMessage:
169
+ """Structured JSON message exchanged during multi-agent execution."""
170
+ message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
171
+ sender_id: str = "orchestrator"
172
+ recipient_id: str = "broadcast"
173
+ msg_type: SubagentMessageType = SubagentMessageType.LOG
174
+ payload: Dict[str, Any] = field(default_factory=dict)
175
+ timestamp: float = field(default_factory=time.time)
176
+
177
+ def to_dict(self) -> Dict[str, Any]:
178
+ return {
179
+ "message_id": self.message_id,
180
+ "sender_id": self.sender_id,
181
+ "recipient_id": self.recipient_id,
182
+ "msg_type": self.msg_type.value if hasattr(self.msg_type, "value") else str(self.msg_type),
183
+ "payload": self.payload,
184
+ "timestamp": self.timestamp,
185
+ }
186
+
187
+ def to_json(self) -> str:
188
+ return json.dumps(self.to_dict())
189
+
190
+ @classmethod
191
+ def from_dict(cls, data: Dict[str, Any]) -> "SubagentMessage":
192
+ msg_type_str = data.get("msg_type", SubagentMessageType.LOG.value)
193
+ try:
194
+ mtype = SubagentMessageType(msg_type_str)
195
+ except ValueError:
196
+ mtype = SubagentMessageType.LOG
197
+
198
+ return cls(
199
+ message_id=data.get("message_id", str(uuid.uuid4())),
200
+ sender_id=data.get("sender_id", "unknown"),
201
+ recipient_id=data.get("recipient_id", "broadcast"),
202
+ msg_type=mtype,
203
+ payload=data.get("payload", {}),
204
+ timestamp=data.get("timestamp", time.time()),
205
+ )
206
+
207
+ @classmethod
208
+ def from_json(cls, json_str: str) -> "SubagentMessage":
209
+ return cls.from_dict(json.loads(json_str))
210
+
211
+
212
+ @dataclass
213
+ class SubagentTask:
214
+ """Represents a unit of work assigned to a subagent."""
215
+ task_id: str
216
+ name: str
217
+ role: SubagentRole
218
+ prompt: str
219
+ parent_id: Optional[str] = None
220
+ dependencies: List[str] = field(default_factory=list)
221
+ context: Dict[str, Any] = field(default_factory=dict)
222
+ status: SubagentStatus = SubagentStatus.PENDING
223
+ progress: float = 0.0
224
+ status_message: str = "Queued"
225
+ output_text: str = ""
226
+ patch_blocks: List[Tuple[str, str]] = field(default_factory=list)
227
+ raw_patch: str = ""
228
+ verification_result: Optional[VerificationResult] = None
229
+ logs: List[str] = field(default_factory=list)
230
+ metadata: Dict[str, Any] = field(default_factory=dict)
231
+ duration_seconds: float = 0.0
232
+ ram_mb: float = 0.0
233
+ error_trace: str = ""
234
+
235
+ def to_dict(self) -> Dict[str, Any]:
236
+ return {
237
+ "task_id": self.task_id,
238
+ "name": self.name,
239
+ "role": self.role.value if hasattr(self.role, "value") else str(self.role),
240
+ "prompt": self.prompt,
241
+ "parent_id": self.parent_id,
242
+ "dependencies": list(self.dependencies),
243
+ "context": self.context,
244
+ "status": self.status.value if hasattr(self.status, "value") else str(self.status),
245
+ "progress": self.progress,
246
+ "status_message": self.status_message,
247
+ "output_text": self.output_text,
248
+ "patch_blocks": self.patch_blocks,
249
+ "raw_patch": self.raw_patch,
250
+ "verification_result": self.verification_result.to_dict() if self.verification_result else None,
251
+ "logs": list(self.logs),
252
+ "metadata": self.metadata,
253
+ "duration_seconds": self.duration_seconds,
254
+ "ram_mb": self.ram_mb,
255
+ "error_trace": self.error_trace,
256
+ }
257
+
258
+ @classmethod
259
+ def from_dict(cls, data: Dict[str, Any]) -> "SubagentTask":
260
+ role_str = data.get("role", SubagentRole.CODER.value)
261
+ status_str = data.get("status", SubagentStatus.PENDING.value)
262
+ return cls(
263
+ task_id=data.get("task_id", str(uuid.uuid4())),
264
+ name=data.get("name", "Unnamed Task"),
265
+ role=SubagentRole.from_str(role_str),
266
+ prompt=data.get("prompt", ""),
267
+ parent_id=data.get("parent_id"),
268
+ dependencies=data.get("dependencies", []),
269
+ context=data.get("context", {}),
270
+ status=SubagentStatus(status_str) if status_str in SubagentStatus._value2member_map_ else SubagentStatus.PENDING,
271
+ progress=float(data.get("progress", 0.0)),
272
+ status_message=data.get("status_message", ""),
273
+ output_text=data.get("output_text", ""),
274
+ patch_blocks=data.get("patch_blocks", []),
275
+ raw_patch=data.get("raw_patch", ""),
276
+ logs=data.get("logs", []),
277
+ metadata=data.get("metadata", {}),
278
+ duration_seconds=float(data.get("duration_seconds", 0.0)),
279
+ ram_mb=float(data.get("ram_mb", 0.0)),
280
+ error_trace=data.get("error_trace", ""),
281
+ )
282
+
283
+
284
+ @dataclass
285
+ class SubagentRunResult:
286
+ """Unified result container returned after executing a multi-agent plan."""
287
+ success: bool
288
+ tasks: List[SubagentTask]
289
+ aggregated_patch: str
290
+ patches_by_file: Dict[str, str]
291
+ final_code: str
292
+ verification: Optional[VerificationResult]
293
+ summary: str
294
+ total_ram_mb: float
295
+ total_duration_sec: float
296
+ history: List[Dict[str, Any]] = field(default_factory=list)
297
+ dedup_warning: Optional[str] = None
298
+ dedup_match: Optional[Dict[str, Any]] = None
299
+
300
+ def to_dict(self) -> Dict[str, Any]:
301
+ return {
302
+ "success": self.success,
303
+ "tasks": [t.to_dict() for t in self.tasks],
304
+ "aggregated_patch": self.aggregated_patch,
305
+ "patches_by_file": self.patches_by_file,
306
+ "final_code": self.final_code,
307
+ "verification": self.verification.to_dict() if self.verification else None,
308
+ "summary": self.summary,
309
+ "total_ram_mb": self.total_ram_mb,
310
+ "total_duration_sec": self.total_duration_sec,
311
+ "history": self.history,
312
+ "dedup_warning": self.dedup_warning,
313
+ "dedup_match": self.dedup_match,
314
+ }
315
+
316
+
317
+ # ==============================================================================
318
+ # 2. Task Decomposer (Prompt -> Subtask DAG)
319
+ # ==============================================================================
320
+
321
+ class TaskDecomposer:
322
+ """
323
+ Decomposes complex user prompts into parallel/dependent subtasks.
324
+ Supports LLM-based intelligent planning with robust fallback pipelines.
325
+ """
326
+
327
+ DECOMPOSITION_SYSTEM_PROMPT = (
328
+ "You are [TASK_DECOMPOSER] for K-CLI multi-agent engine. "
329
+ "Decompose the user coding prompt into a directed task list. "
330
+ "Valid roles: EXPLORER, RESEARCHER, REFACTORER, TESTER. "
331
+ "Return ONLY a valid JSON array of objects with keys: "
332
+ "'id', 'name', 'role', 'prompt', 'dependencies' (list of IDs). "
333
+ "Do NOT include any text outside the JSON array."
334
+ )
335
+
336
+ def __init__(self, driver: Optional[LLMDriver] = None):
337
+ self.driver = _resolve_driver(driver)
338
+
339
+ def decompose(
340
+ self,
341
+ prompt: str,
342
+ context_files: Optional[List[str]] = None,
343
+ target_roles: Optional[List[SubagentRole]] = None,
344
+ use_llm: bool = True,
345
+ ) -> List[SubagentTask]:
346
+ """
347
+ Decomposes a user prompt into a list of SubagentTask instances.
348
+ """
349
+ cleaned_prompt = (prompt or "").strip()
350
+ if not cleaned_prompt:
351
+ return []
352
+
353
+ # If specific roles were explicitly requested, build matching pipeline
354
+ if target_roles:
355
+ return self._build_pipeline_for_roles(cleaned_prompt, target_roles, context_files)
356
+
357
+ # Attempt LLM decomposition if requested and driver available
358
+ if use_llm:
359
+ try:
360
+ llm_tasks = self._decompose_with_llm(cleaned_prompt, context_files)
361
+ if llm_tasks:
362
+ return llm_tasks
363
+ except Exception:
364
+ pass
365
+
366
+ # Fallback: Deterministic intelligent decomposition
367
+ return self._decompose_deterministic(cleaned_prompt, context_files)
368
+
369
+ def _decompose_with_llm(self, prompt: str, context_files: Optional[List[str]]) -> Optional[List[SubagentTask]]:
370
+ """Invokes LLM to decompose prompt into structured JSON task list."""
371
+ context_str = f"\nContext files: {', '.join(context_files)}" if context_files else ""
372
+ full_user_prompt = f"User Request: {prompt}{context_str}\nDecompose into subagent tasks:"
373
+
374
+ raw_output = self.driver.generate(
375
+ prompt=full_user_prompt,
376
+ system_prompt=self.DECOMPOSITION_SYSTEM_PROMPT,
377
+ temperature=0.1,
378
+ )
379
+
380
+ # Extract JSON array from LLM output
381
+ json_text = raw_output.strip()
382
+ m = re.search(r"\[\s*\{.*\}\s*\]", json_text, re.DOTALL)
383
+ if m:
384
+ json_text = m.group(0)
385
+
386
+ data = json.loads(json_text)
387
+ if not isinstance(data, list) or not data:
388
+ return None
389
+
390
+ tasks: List[SubagentTask] = []
391
+ valid_ids: Set[str] = set()
392
+
393
+ for idx, item in enumerate(data, start=1):
394
+ if not isinstance(item, dict):
395
+ continue
396
+ tid = str(item.get("id", f"subtask_{idx}")).strip()
397
+ name = str(item.get("name", f"Subtask {idx}")).strip()
398
+ role_str = str(item.get("role", "CODER")).strip()
399
+ sub_prompt = str(item.get("prompt", prompt)).strip()
400
+ deps = [str(d).strip() for d in item.get("dependencies", []) if str(d).strip() in valid_ids]
401
+
402
+ role = SubagentRole.from_str(role_str)
403
+ task = SubagentTask(
404
+ task_id=tid,
405
+ name=name,
406
+ role=role,
407
+ prompt=sub_prompt,
408
+ dependencies=deps,
409
+ context={"context_files": context_files or []},
410
+ )
411
+ tasks.append(task)
412
+ valid_ids.add(tid)
413
+
414
+ return tasks if tasks else None
415
+
416
+ def _decompose_deterministic(
417
+ self,
418
+ prompt: str,
419
+ context_files: Optional[List[str]] = None,
420
+ ) -> List[SubagentTask]:
421
+ """
422
+ Constructs deterministic pipeline based on user request keywords:
423
+ - Conflicts: [CONFLICT_RESOLVER] -> [TESTER]
424
+ - PR Review: [PR_REVIEWER]
425
+ - MCP Operator: [MCP_OPERATOR]
426
+ - Standard: [EXPLORER] + [RESEARCHER] -> [REFACTORER] -> [TESTER]
427
+ """
428
+ files = context_files or []
429
+ files_hint = f" Focus on files: {', '.join(files)}." if files else ""
430
+ prompt_lower = prompt.lower()
431
+
432
+ if "conflict" in prompt_lower or "merge conflict" in prompt_lower:
433
+ task_conflict = SubagentTask(
434
+ task_id="task_conflict_resolver",
435
+ name="Resolve Git Merge Conflicts",
436
+ role=SubagentRole.CONFLICT_RESOLVER,
437
+ prompt=f"Inspect and resolve git merge conflicts in workspace for: '{prompt}'.{files_hint}",
438
+ dependencies=[],
439
+ context={"context_files": files},
440
+ )
441
+ task_tester = SubagentTask(
442
+ task_id="task_tester",
443
+ name="Verify Resolved Files",
444
+ role=SubagentRole.TESTER,
445
+ prompt=f"Verify syntax and compiler correctness of resolved files for: '{prompt}'.",
446
+ dependencies=["task_conflict_resolver"],
447
+ context={"context_files": files},
448
+ )
449
+ return [task_conflict, task_tester]
450
+
451
+ if "review pr" in prompt_lower or "pr review" in prompt_lower or "pull request review" in prompt_lower:
452
+ task_pr = SubagentTask(
453
+ task_id="task_pr_reviewer",
454
+ name="Review GitHub Pull Request",
455
+ role=SubagentRole.PR_REVIEWER,
456
+ prompt=f"Perform AI code review and diff analysis for: '{prompt}'.",
457
+ dependencies=[],
458
+ context={"context_files": files},
459
+ )
460
+ return [task_pr]
461
+
462
+ if "mcp tool" in prompt_lower or "call mcp" in prompt_lower or "mcp operator" in prompt_lower:
463
+ task_mcp = SubagentTask(
464
+ task_id="task_mcp_operator",
465
+ name="Execute MCP Tool Operations",
466
+ role=SubagentRole.MCP_OPERATOR,
467
+ prompt=f"Execute Model Context Protocol tools to fulfill: '{prompt}'.",
468
+ dependencies=[],
469
+ context={"context_files": files},
470
+ )
471
+ return [task_mcp]
472
+
473
+ # Subagent 1: Explorer
474
+ task_explorer = SubagentTask(
475
+ task_id="task_explorer",
476
+ name="Explore Workspace & AST Map",
477
+ role=SubagentRole.EXPLORER,
478
+ prompt=f"Inspect repository structure, locate relevant files, and extract AST symbols for: '{prompt}'.{files_hint}",
479
+ dependencies=[],
480
+ context={"context_files": files},
481
+ )
482
+
483
+ # Subagent 2: Researcher
484
+ task_researcher = SubagentTask(
485
+ task_id="task_researcher",
486
+ name="Research DevDocs & API Contracts",
487
+ role=SubagentRole.RESEARCHER,
488
+ prompt=f"Identify required libraries, API signatures, imports, and edge cases for: '{prompt}'.",
489
+ dependencies=[],
490
+ context={"context_files": files},
491
+ )
492
+
493
+ # Subagent 3: Refactorer / Coder
494
+ task_refactorer = SubagentTask(
495
+ task_id="task_refactorer",
496
+ name="Synthesize Code & Surgical Patches",
497
+ role=SubagentRole.REFACTORER,
498
+ prompt=f"Generate Python implementation or SEARCH/REPLACE surgical patch blocks to fulfill: '{prompt}'.",
499
+ dependencies=["task_explorer", "task_researcher"],
500
+ context={"context_files": files},
501
+ )
502
+
503
+ # Subagent 4: Tester / Verifier
504
+ task_tester = SubagentTask(
505
+ task_id="task_tester",
506
+ name="Verify AST & Validate Tests",
507
+ role=SubagentRole.TESTER,
508
+ prompt=f"Perform AST syntax validation, create unit tests, and verify compiler correctness for the implementation of '{prompt}'.",
509
+ dependencies=["task_refactorer"],
510
+ context={"context_files": files},
511
+ )
512
+
513
+ return [task_explorer, task_researcher, task_refactorer, task_tester]
514
+
515
+ def _build_pipeline_for_roles(
516
+ self,
517
+ prompt: str,
518
+ roles: List[SubagentRole],
519
+ context_files: Optional[List[str]],
520
+ ) -> List[SubagentTask]:
521
+ """Builds custom pipeline for a specific list of roles."""
522
+ tasks: List[SubagentTask] = []
523
+ prev_id: Optional[str] = None
524
+
525
+ for idx, role in enumerate(roles, start=1):
526
+ tid = f"task_{role.value.lower()}_{idx}"
527
+ deps = [prev_id] if prev_id and role in (SubagentRole.REFACTORER, SubagentRole.TESTER, SubagentRole.CRITIC) else []
528
+ task = SubagentTask(
529
+ task_id=tid,
530
+ name=f"{role.value.capitalize()} Worker",
531
+ role=role,
532
+ prompt=f"Execute {role.value} operations for: '{prompt}'",
533
+ dependencies=deps,
534
+ context={"context_files": context_files or []},
535
+ )
536
+ tasks.append(task)
537
+ prev_id = tid
538
+
539
+ return tasks
540
+
541
+
542
+ # ==============================================================================
543
+ # 3. Subagent Worker Implementation
544
+ # ==============================================================================
545
+
546
+ class SubagentWorker:
547
+ """
548
+ Executes a single SubagentTask inside an isolated thread.
549
+ Communicates via thread-safe JSON messages and updates task status in real time.
550
+ """
551
+
552
+ ROLE_PROMPTS = {
553
+ SubagentRole.EXPLORER: (
554
+ "You are [EXPLORER] subagent for K-CLI. "
555
+ "Inspect workspace AST maps and locate symbols and files related to the user request. "
556
+ "Output concise findings and list target file paths."
557
+ ),
558
+ SubagentRole.RESEARCHER: (
559
+ "You are [RESEARCHER] subagent for K-CLI. "
560
+ "Extract documentation signatures, standard library imports, and edge cases. "
561
+ "Be technical and concise. Zero conversational fluff."
562
+ ),
563
+ SubagentRole.REFACTORER: (
564
+ "You are [REFACTORER] subagent for K-CLI. "
565
+ "Generate production-ready code or SEARCH/REPLACE surgical patch blocks. "
566
+ "Enclose all code in markdown blocks. Zero chatter outside code blocks."
567
+ ),
568
+ SubagentRole.CODER: (
569
+ "You are [CODER] subagent for K-CLI. "
570
+ "Generate clean, memory-efficient implementation code inside markdown blocks."
571
+ ),
572
+ SubagentRole.TESTER: (
573
+ "You are [TESTER] subagent for K-CLI. "
574
+ "Formulate pytest test cases and verify syntax integrity for the generated solution."
575
+ ),
576
+ SubagentRole.CRITIC: (
577
+ "You are [CRITIC] subagent for K-CLI. "
578
+ "Review candidate code for memory bloat, null checks, boundary bugs, and performance. "
579
+ "Output VALIDATED or CRITIQUE: <reasons>."
580
+ ),
581
+ SubagentRole.CONFLICT_RESOLVER: (
582
+ "You are [CONFLICT_RESOLVER] subagent for K-CLI. "
583
+ "Inspect git merge conflict markers (<<<<<<<, =======, >>>>>>>) and AST scope context. "
584
+ "Synthesize correct 3-way conflict resolutions that preserve semantic logic from both branches and maintain syntactic validity."
585
+ ),
586
+ SubagentRole.PR_REVIEWER: (
587
+ "You are [PR_REVIEWER] subagent for K-CLI. "
588
+ "Analyze Pull Request diffs, inspect CI check statuses and security/performance implications. "
589
+ "Provide structured verdicts, identified bugs, security issues, and concrete code improvements."
590
+ ),
591
+ SubagentRole.MCP_OPERATOR: (
592
+ "You are [MCP_OPERATOR] subagent for K-CLI. "
593
+ "Inspect available Model Context Protocol (MCP) server tools, construct valid JSON-RPC tool parameters, execute remote MCP tools, and interpret tool results."
594
+ ),
595
+ }
596
+
597
+ def __init__(
598
+ self,
599
+ task: SubagentTask,
600
+ message_queue: Optional[queue.Queue] = None,
601
+ driver: Optional[LLMDriver] = None,
602
+ verifier: Optional[Verifier] = None,
603
+ patcher: Optional[Patcher] = None,
604
+ repo_map: Optional[RepoMap] = None,
605
+ doc_retriever: Optional[DocRetriever] = None,
606
+ workspace_dir: Optional[Union[str, Path]] = None,
607
+ mcp_manager: Optional[Any] = None,
608
+ conflict_resolver: Optional[Any] = None,
609
+ pr_manager: Optional[Any] = None,
610
+ dedup_engine: Optional[Any] = None,
611
+ ):
612
+ self.task = task
613
+ self.msg_queue = message_queue or queue.Queue()
614
+ self.driver = _resolve_driver(driver)
615
+ self.verifier = verifier or Verifier()
616
+ self.patcher = patcher or Patcher()
617
+ self.workspace_dir = Path(workspace_dir or ".").resolve()
618
+ self.repo_map = repo_map or RepoMap(root_dir=str(self.workspace_dir))
619
+ self.doc_retriever = doc_retriever or DocRetriever()
620
+ self.mcp_manager = mcp_manager
621
+ self.conflict_resolver = conflict_resolver
622
+ self.pr_manager = pr_manager
623
+ self.dedup_engine = dedup_engine
624
+
625
+ def _send_message(self, msg_type: SubagentMessageType, payload: Dict[str, Any]) -> None:
626
+ """Publishes a structured message to the orchestrator message bus."""
627
+ msg = SubagentMessage(
628
+ sender_id=self.task.task_id,
629
+ recipient_id="orchestrator",
630
+ msg_type=msg_type,
631
+ payload=payload,
632
+ )
633
+ self.msg_queue.put(msg)
634
+
635
+ def _update_progress(self, progress: float, status_msg: str) -> None:
636
+ """Updates internal task progress and sends notification."""
637
+ self.task.progress = min(1.0, max(0.0, progress))
638
+ self.task.status_message = status_msg
639
+ self.task.logs.append(f"[{time.strftime('%H:%M:%S')}] ({int(self.task.progress*100)}%) {status_msg}")
640
+ self._send_message(
641
+ SubagentMessageType.PROGRESS,
642
+ {
643
+ "task_id": self.task.task_id,
644
+ "role": self.task.role.value,
645
+ "progress": self.task.progress,
646
+ "status_message": status_msg,
647
+ },
648
+ )
649
+
650
+ def execute(self, dependency_results: Optional[Dict[str, SubagentTask]] = None) -> SubagentTask:
651
+ """
652
+ Main worker execution entrypoint.
653
+ """
654
+ start_time = time.time()
655
+ self.task.status = SubagentStatus.RUNNING
656
+ self._send_message(
657
+ SubagentMessageType.TASK_INIT,
658
+ {"task_id": self.task.task_id, "name": self.task.name, "role": self.task.role.value},
659
+ )
660
+ self._update_progress(0.1, f"Started {self.task.role.value} execution")
661
+
662
+ dep_results = dependency_results or {}
663
+
664
+ try:
665
+ # Dispatch to role-specific worker logic
666
+ if self.task.role == SubagentRole.EXPLORER:
667
+ self._execute_explorer(dep_results)
668
+ elif self.task.role == SubagentRole.RESEARCHER:
669
+ self._execute_researcher(dep_results)
670
+ elif self.task.role in (SubagentRole.REFACTORER, SubagentRole.CODER):
671
+ self._execute_refactorer(dep_results)
672
+ elif self.task.role == SubagentRole.TESTER:
673
+ self._execute_tester(dep_results)
674
+ elif self.task.role == SubagentRole.CRITIC:
675
+ self._execute_critic(dep_results)
676
+ elif self.task.role == SubagentRole.CONFLICT_RESOLVER:
677
+ self._execute_conflict_resolver(dep_results)
678
+ elif self.task.role == SubagentRole.PR_REVIEWER:
679
+ self._execute_pr_reviewer(dep_results)
680
+ elif self.task.role == SubagentRole.MCP_OPERATOR:
681
+ self._execute_mcp_operator(dep_results)
682
+ else:
683
+ self._execute_generic(dep_results)
684
+
685
+ self.task.status = SubagentStatus.COMPLETED
686
+ self._update_progress(1.0, f"{self.task.role.value} completed successfully")
687
+ self._send_message(
688
+ SubagentMessageType.TASK_COMPLETE,
689
+ {
690
+ "task_id": self.task.task_id,
691
+ "output_length": len(self.task.output_text),
692
+ "patch_blocks_count": len(self.task.patch_blocks),
693
+ },
694
+ )
695
+
696
+ except Exception as exc:
697
+ self.task.status = SubagentStatus.FAILED
698
+ self.task.error_trace = str(exc)
699
+ self._update_progress(1.0, f"Error: {exc}")
700
+ self._send_message(
701
+ SubagentMessageType.TASK_FAILED,
702
+ {"task_id": self.task.task_id, "error": str(exc)},
703
+ )
704
+
705
+ finally:
706
+ self.task.duration_seconds = time.time() - start_time
707
+ self.task.ram_mb = self._get_current_ram()
708
+
709
+ return self.task
710
+
711
+ def _get_current_ram(self) -> float:
712
+ try:
713
+ return psutil.Process().memory_info().rss / (1024 * 1024)
714
+ except Exception:
715
+ return 0.0
716
+
717
+ # --------------------------------------------------------------------------
718
+ # Role-Specific Worker Implementations
719
+ # --------------------------------------------------------------------------
720
+
721
+ def _execute_explorer(self, dep_results: Dict[str, SubagentTask]) -> None:
722
+ """EXPLORER: Scans AST repository map and locates candidate source files."""
723
+ self._update_progress(0.3, "Analyzing AST repository map...")
724
+ focus_files = self.task.context.get("context_files", [])
725
+
726
+ repo_tree = ""
727
+ try:
728
+ repo_tree = self.repo_map.get_repo_map(max_tokens=500, focus_files=focus_files)
729
+ except Exception:
730
+ repo_tree = ""
731
+
732
+ self._update_progress(0.6, "Scanning workspace symbol index...")
733
+ matched_files = []
734
+ try:
735
+ for p in self.workspace_dir.glob("*.py"):
736
+ if p.is_file():
737
+ matched_files.append(p.name)
738
+ except Exception:
739
+ pass
740
+
741
+ findings = []
742
+ if repo_tree.strip():
743
+ findings.append(f"AST Repository Map:\n{repo_tree.strip()}")
744
+ if matched_files:
745
+ findings.append(f"Workspace Files: {', '.join(matched_files[:10])}")
746
+
747
+ output_text = "\n\n".join(findings) if findings else "Workspace inspected (clean baseline)."
748
+ self.task.output_text = output_text
749
+ self.task.metadata["repo_tree"] = repo_tree
750
+ self.task.metadata["matched_files"] = matched_files
751
+
752
+ self._send_message(
753
+ SubagentMessageType.EXPLORATION_MAP,
754
+ {"task_id": self.task.task_id, "files": matched_files, "tree_len": len(repo_tree)},
755
+ )
756
+
757
+ def _execute_researcher(self, dep_results: Dict[str, SubagentTask]) -> None:
758
+ """RESEARCHER: Queries DevDocs SQLite index and extracts API signatures."""
759
+ self._update_progress(0.3, "Searching DevDocs FTS5 offline database...")
760
+ snippets = ""
761
+ try:
762
+ snippets = self.doc_retriever.format_context_snippets(self.task.prompt, max_tokens=300)
763
+ except Exception:
764
+ snippets = ""
765
+
766
+ self._update_progress(0.7, "Analyzing API contracts and constraints...")
767
+ prompt_with_docs = f"User Request: {self.task.prompt}\n\nDevDocs Snippets:\n{snippets}"
768
+ sys_prompt = self.ROLE_PROMPTS.get(SubagentRole.RESEARCHER, "")
769
+
770
+ llm_out = self.driver.generate(
771
+ prompt=prompt_with_docs,
772
+ system_prompt=sys_prompt,
773
+ temperature=0.2,
774
+ )
775
+
776
+ self.task.output_text = f"{llm_out}\n\n{snippets}".strip()
777
+ self.task.metadata["doc_snippets"] = snippets
778
+
779
+ self._send_message(
780
+ SubagentMessageType.RESEARCH_FINDING,
781
+ {"task_id": self.task.task_id, "output_preview": llm_out[:100]},
782
+ )
783
+
784
+ def _execute_refactorer(self, dep_results: Dict[str, SubagentTask]) -> None:
785
+ """REFACTORER: Synthesizes implementation code and SEARCH/REPLACE blocks."""
786
+ self._update_progress(0.3, "Gathering upstream Explorer & Researcher intelligence...")
787
+
788
+ context_blocks = []
789
+ for dep_id, dep_task in dep_results.items():
790
+ if dep_task.output_text:
791
+ context_blocks.append(f"[{dep_task.role.value} Context ({dep_task.name})]:\n{dep_task.output_text}")
792
+
793
+ # Inject context files if available
794
+ context_files = self.task.context.get("context_files", [])
795
+ for cf in context_files:
796
+ fp = self.workspace_dir / cf
797
+ if fp.exists() and fp.is_file():
798
+ try:
799
+ content = fp.read_text(encoding="utf-8")
800
+ context_blocks.append(f"File {cf}:\n```\n{content}\n```")
801
+ except Exception:
802
+ pass
803
+
804
+ self._update_progress(0.6, "Generating surgical patches & implementation code...")
805
+ composed_prompt = f"User Request: {self.task.prompt}\n\n" + "\n\n".join(context_blocks)
806
+ sys_prompt = self.ROLE_PROMPTS.get(SubagentRole.REFACTORER, self.ROLE_PROMPTS[SubagentRole.CODER])
807
+
808
+ raw_code = self.driver.generate(
809
+ prompt=composed_prompt,
810
+ system_prompt=sys_prompt,
811
+ temperature=0.1,
812
+ )
813
+
814
+ # Parse potential SEARCH/REPLACE blocks
815
+ blocks = self.patcher.parse_search_replace_blocks(raw_code)
816
+ self.task.patch_blocks = blocks
817
+ self.task.raw_patch = raw_code
818
+ self.task.output_text = raw_code
819
+
820
+ self._update_progress(0.9, f"Generated code ({len(blocks)} SEARCH/REPLACE blocks)")
821
+
822
+ if blocks:
823
+ self._send_message(
824
+ SubagentMessageType.SEARCH_REPLACE_PATCH,
825
+ {"task_id": self.task.task_id, "block_count": len(blocks)},
826
+ )
827
+ else:
828
+ self._send_message(
829
+ SubagentMessageType.CODE_OUTPUT,
830
+ {"task_id": self.task.task_id, "code_len": len(raw_code)},
831
+ )
832
+
833
+ def _execute_tester(self, dep_results: Dict[str, SubagentTask]) -> None:
834
+ """TESTER: Performs AST syntax validation and executes test suite."""
835
+ self._update_progress(0.3, "Extracting candidate code from upstream workers...")
836
+
837
+ code_to_test = ""
838
+ for dep_id, dep_task in dep_results.items():
839
+ if dep_task.output_text:
840
+ _, primary_code = CodeExtractor.extract_primary_code(dep_task.output_text)
841
+ if primary_code.strip():
842
+ code_to_test = primary_code
843
+ break
844
+
845
+ if not code_to_test:
846
+ code_to_test = "def solution():\n return True\n"
847
+
848
+ self._update_progress(0.6, "Executing ground-truth AST & compilation verifier...")
849
+ v_res = self.verifier.verify(code_to_test, language="python")
850
+ self.task.verification_result = v_res
851
+
852
+ status_str = "PASSED" if v_res.success else f"FAILED (line {v_res.line_number or '?'})"
853
+ self.task.output_text = f"Verification {status_str}:\n{v_res.error_trace or 'All syntax checks passed.'}"
854
+
855
+ self._send_message(
856
+ SubagentMessageType.TEST_RESULT,
857
+ {"task_id": self.task.task_id, "success": v_res.success, "type": v_res.verification_type},
858
+ )
859
+
860
+ def _execute_critic(self, dep_results: Dict[str, SubagentTask]) -> None:
861
+ """CRITIC: Evaluates candidate code for edge cases and memory bloat."""
862
+ self._update_progress(0.4, "Reviewing candidate implementation...")
863
+ code_snippets = [dt.output_text for dt in dep_results.values() if dt.output_text]
864
+ full_code = "\n".join(code_snippets)
865
+
866
+ critique_prompt = f"Review the following code for memory safety, edge cases, and correctness:\n{full_code}"
867
+ sys_prompt = self.ROLE_PROMPTS[SubagentRole.CRITIC]
868
+
869
+ critique_out = self.driver.generate(
870
+ prompt=critique_prompt,
871
+ system_prompt=sys_prompt,
872
+ temperature=0.2,
873
+ )
874
+
875
+ self.task.output_text = critique_out
876
+ self._send_message(
877
+ SubagentMessageType.LOG,
878
+ {"task_id": self.task.task_id, "critique": critique_out[:80]},
879
+ )
880
+
881
+ def invoke_mcp_tool(
882
+ self,
883
+ tool_name: str,
884
+ arguments: Optional[Dict[str, Any]] = None,
885
+ server_name: Optional[str] = None,
886
+ ) -> Any:
887
+ """Invokes an MCP tool in the subagent's execution context."""
888
+ mgr = self.mcp_manager
889
+ if mgr is None and MCPManager is not None:
890
+ mgr = MCPManager()
891
+ self.mcp_manager = mgr
892
+
893
+ if mgr is None:
894
+ raise RuntimeError("MCPManager is not available in subagent execution context.")
895
+
896
+ return mgr.call_tool(tool_name, arguments=arguments or {}, server_name=server_name)
897
+
898
+ def list_mcp_tools(self, server_name: Optional[str] = None) -> List[Any]:
899
+ """Lists available MCP tools in the subagent's execution context."""
900
+ mgr = self.mcp_manager
901
+ if mgr is None and MCPManager is not None:
902
+ mgr = MCPManager()
903
+ self.mcp_manager = mgr
904
+
905
+ if mgr is None:
906
+ return []
907
+
908
+ return mgr.list_tools(server_name=server_name)
909
+
910
+ def _execute_conflict_resolver(self, dep_results: Dict[str, SubagentTask]) -> None:
911
+ """CONFLICT_RESOLVER: Analyzes and resolves git merge conflict markers with compiler verification."""
912
+ self._update_progress(0.3, "Detecting and analyzing merge conflicts...")
913
+ resolver = self.conflict_resolver or (ConflictResolver() if ConflictResolver else None)
914
+ if resolver is None:
915
+ self.task.output_text = "ConflictResolver is not available."
916
+ return
917
+
918
+ target_file = self.task.context.get("file_path") or (self.task.context.get("context_files", [None])[0] if self.task.context.get("context_files") else None)
919
+ auto_stage = self.task.context.get("auto_accept", False) or self.task.context.get("auto_stage", True)
920
+
921
+ self._update_progress(0.6, "Performing AI 3-way conflict resolution with verification...")
922
+ if target_file and os.path.exists(str(target_file)):
923
+ res = resolver.resolve_file(
924
+ file_path=str(target_file),
925
+ llm_driver=self.driver,
926
+ verifier=self.verifier,
927
+ auto_stage=auto_stage,
928
+ )
929
+ self.task.output_text = f"Resolved {res.resolved_conflicts}/{res.total_conflicts} conflicts in {res.file_path}"
930
+ self.task.metadata["file_resolution"] = res.to_dict()
931
+ if not res.success:
932
+ self.task.error_trace = res.error_message or "Failed to resolve conflicts"
933
+ else:
934
+ summary = resolver.resolve_all_conflicts(
935
+ repo_path=str(self.workspace_dir),
936
+ llm_driver=self.driver,
937
+ verifier=self.verifier,
938
+ auto_stage=auto_stage,
939
+ )
940
+ self.task.output_text = f"Resolved {summary.resolved_files}/{summary.total_files} conflicted files."
941
+ self.task.metadata["conflict_summary"] = summary.to_dict()
942
+ if not summary.success:
943
+ self.task.error_trace = f"Failed to resolve {summary.failed_files} files."
944
+
945
+ self._send_message(
946
+ SubagentMessageType.CONFLICT_RESOLVED,
947
+ {"task_id": self.task.task_id, "output": self.task.output_text},
948
+ )
949
+
950
+ def _execute_pr_reviewer(self, dep_results: Dict[str, SubagentTask]) -> None:
951
+ """PR_REVIEWER: Inspects PR diffs, CI status, and generates compiler-grade code reviews."""
952
+ self._update_progress(0.3, "Fetching PR diff and CI check status...")
953
+ pr_mgr = self.pr_manager
954
+ if pr_mgr is None and PRLifecycleManager is not None:
955
+ pr_mgr = PRLifecycleManager(repo_dir=self.workspace_dir)
956
+ self.pr_manager = pr_mgr
957
+
958
+ if pr_mgr is None:
959
+ self.task.output_text = "PRLifecycleManager is not available."
960
+ return
961
+
962
+ pr_num = self.task.context.get("pr_number")
963
+ if pr_num is None:
964
+ m = re.search(r"#?(\d+)", self.task.prompt)
965
+ pr_num = int(m.group(1)) if m else 1
966
+
967
+ self._update_progress(0.6, f"Analyzing PR #{pr_num} diff for bugs and security...")
968
+ post_comment = self.task.context.get("post_comment", False)
969
+ review = pr_mgr.review_pr(
970
+ pr_number=pr_num,
971
+ llm_driver=self.driver,
972
+ post_comment=post_comment,
973
+ )
974
+
975
+ md_output = review.format_markdown() if hasattr(review, "format_markdown") else str(review)
976
+ self.task.output_text = md_output
977
+ self.task.metadata["pr_review"] = review.to_dict() if hasattr(review, "to_dict") else {}
978
+
979
+ self._send_message(
980
+ SubagentMessageType.PR_REVIEWED,
981
+ {"task_id": self.task.task_id, "verdict": getattr(review, "verdict", "COMMENT"), "pr_number": pr_num},
982
+ )
983
+
984
+ def _execute_mcp_operator(self, dep_results: Dict[str, SubagentTask]) -> None:
985
+ """MCP_OPERATOR: Executes Model Context Protocol tools and queries."""
986
+ self._update_progress(0.3, "Connecting to MCP servers and discovering tools...")
987
+ mgr = self.mcp_manager
988
+ if mgr is None and MCPManager is not None:
989
+ mgr = MCPManager()
990
+ self.mcp_manager = mgr
991
+
992
+ if mgr is None:
993
+ self.task.output_text = "MCPManager is not available."
994
+ return
995
+
996
+ tool_name = self.task.context.get("tool_name")
997
+ tool_args = self.task.context.get("arguments") or self.task.context.get("args") or {}
998
+
999
+ if tool_name:
1000
+ self._update_progress(0.6, f"Executing MCP tool '{tool_name}'...")
1001
+ try:
1002
+ result = mgr.call_tool(tool_name, arguments=tool_args)
1003
+ self.task.output_text = result.text or json.dumps(result.raw, indent=2)
1004
+ self.task.metadata["mcp_result"] = result.to_dict() if hasattr(result, "to_dict") else {"text": result.text}
1005
+ self._send_message(
1006
+ SubagentMessageType.MCP_TOOL_RESULT,
1007
+ {"task_id": self.task.task_id, "tool_name": tool_name, "success": not getattr(result, "is_error", False)},
1008
+ )
1009
+ except Exception as e:
1010
+ self.task.output_text = f"Error executing tool '{tool_name}': {e}"
1011
+ self.task.error_trace = str(e)
1012
+ else:
1013
+ tools = mgr.list_tools()
1014
+ tool_names = [t.name for t in tools]
1015
+ self.task.output_text = f"Discovered {len(tools)} MCP tools: {', '.join(tool_names)}"
1016
+ self.task.metadata["tools"] = [t.to_dict() if hasattr(t, "to_dict") else {"name": t.name} for t in tools]
1017
+
1018
+ def _execute_generic(self, dep_results: Dict[str, SubagentTask]) -> None:
1019
+ """Generic fallback executor using LLM driver."""
1020
+ self._update_progress(0.5, "Executing task...")
1021
+ sys_prompt = self.ROLE_PROMPTS.get(self.task.role, "You are a K-CLI AI assistant.")
1022
+ out = self.driver.generate(
1023
+ prompt=self.task.prompt,
1024
+ system_prompt=sys_prompt,
1025
+ temperature=0.2,
1026
+ )
1027
+ self.task.output_text = out
1028
+
1029
+
1030
+ # ==============================================================================
1031
+ # 4. Result & Patch Aggregator
1032
+ # ==============================================================================
1033
+
1034
+ class PatchAggregator:
1035
+ """
1036
+ Collects outputs from all subagent tasks, aggregates SEARCH/REPLACE blocks,
1037
+ validates merged code syntax, and produces a unified SubagentRunResult.
1038
+ """
1039
+
1040
+ def __init__(self, patcher: Optional[Patcher] = None, verifier: Optional[Verifier] = None):
1041
+ self.patcher = patcher or Patcher()
1042
+ self.verifier = verifier or Verifier()
1043
+
1044
+ def aggregate(
1045
+ self,
1046
+ tasks: List[SubagentTask],
1047
+ total_duration: float = 0.0,
1048
+ ) -> SubagentRunResult:
1049
+ """
1050
+ Merges subagent outputs into a coherent patch and final verified code.
1051
+ """
1052
+ all_patch_blocks: List[Tuple[str, str]] = []
1053
+ raw_patches: List[str] = []
1054
+ patches_by_file: Dict[str, str] = {}
1055
+ primary_code_candidates: List[str] = []
1056
+ verification: Optional[VerificationResult] = None
1057
+ summaries: List[str] = []
1058
+
1059
+ for task in tasks:
1060
+ if task.patch_blocks:
1061
+ all_patch_blocks.extend(task.patch_blocks)
1062
+ if task.raw_patch:
1063
+ raw_patches.append(task.raw_patch)
1064
+
1065
+ if task.output_text:
1066
+ _, extracted = CodeExtractor.extract_primary_code(task.output_text)
1067
+ if extracted.strip() and not task.patch_blocks:
1068
+ primary_code_candidates.append(extracted)
1069
+
1070
+ if task.verification_result:
1071
+ verification = task.verification_result
1072
+
1073
+ status_glyph = "✔" if task.status == SubagentStatus.COMPLETED else "✘"
1074
+ summaries.append(f"{status_glyph} [{task.role.value}] {task.name} ({task.duration_seconds:.2f}s): {task.status_message}")
1075
+
1076
+ # Assemble unified patch text
1077
+ unified_patch = ""
1078
+ if raw_patches:
1079
+ unified_patch = "\n\n".join(raw_patches)
1080
+ elif all_patch_blocks:
1081
+ patch_chunks = []
1082
+ for s, r in all_patch_blocks:
1083
+ patch_chunks.append(f"<<<<<<< SEARCH\n{s}\n=======\n{r}\n>>>>>>>")
1084
+ unified_patch = "\n\n".join(patch_chunks)
1085
+
1086
+ # Determine primary final code
1087
+ final_code = ""
1088
+ if primary_code_candidates:
1089
+ final_code = primary_code_candidates[-1]
1090
+ elif unified_patch:
1091
+ final_code = unified_patch
1092
+ elif tasks:
1093
+ final_code = tasks[-1].output_text
1094
+
1095
+ # If no verification was explicitly recorded by a TESTER subagent, run quick verification
1096
+ if verification is None and primary_code_candidates:
1097
+ _, code_only = CodeExtractor.extract_primary_code(primary_code_candidates[-1])
1098
+ if code_only.strip():
1099
+ verification = self.verifier.verify(code_only, language="python")
1100
+ elif verification is None and all_patch_blocks:
1101
+ all_valid = True
1102
+ err_msg = ""
1103
+ for _, replace_code in all_patch_blocks:
1104
+ v = self.verifier.verify(replace_code, language="python")
1105
+ if not v.success:
1106
+ all_valid = False
1107
+ err_msg = v.error_trace
1108
+ break
1109
+ verification = VerificationResult(
1110
+ success=all_valid,
1111
+ error_trace=err_msg,
1112
+ code=unified_patch,
1113
+ language="python",
1114
+ verification_type="patch_syntax",
1115
+ )
1116
+
1117
+ overall_success = all(t.status == SubagentStatus.COMPLETED for t in tasks)
1118
+ if verification and not verification.success:
1119
+ overall_success = False
1120
+
1121
+ total_ram = max((t.ram_mb for t in tasks), default=0.0)
1122
+ if total_ram == 0.0:
1123
+ try:
1124
+ total_ram = psutil.Process().memory_info().rss / (1024 * 1024)
1125
+ except Exception:
1126
+ total_ram = 0.0
1127
+
1128
+ return SubagentRunResult(
1129
+ success=overall_success,
1130
+ tasks=tasks,
1131
+ aggregated_patch=unified_patch,
1132
+ patches_by_file=patches_by_file,
1133
+ final_code=final_code,
1134
+ verification=verification,
1135
+ summary="\n".join(summaries),
1136
+ total_ram_mb=total_ram,
1137
+ total_duration_sec=total_duration,
1138
+ history=[t.to_dict() for t in tasks],
1139
+ )
1140
+
1141
+
1142
+ # ==============================================================================
1143
+ # 5. Multi-Agent Orchestrator & Dispatcher
1144
+ # ==============================================================================
1145
+
1146
+ class SubagentDispatcher:
1147
+ """
1148
+ Schedules and executes SubagentTasks across parallel background worker threads.
1149
+ Respects task dependencies (DAG) and optimizes thread resource usage.
1150
+ """
1151
+
1152
+ def __init__(
1153
+ self,
1154
+ driver: Optional[LLMDriver] = None,
1155
+ verifier: Optional[Verifier] = None,
1156
+ patcher: Optional[Patcher] = None,
1157
+ repo_map: Optional[RepoMap] = None,
1158
+ doc_retriever: Optional[DocRetriever] = None,
1159
+ workspace_dir: Optional[Union[str, Path]] = None,
1160
+ max_workers: int = 4,
1161
+ ram_budget_mb: float = 1024.0,
1162
+ mcp_manager: Optional[Any] = None,
1163
+ dedup_engine: Optional[Any] = None,
1164
+ ):
1165
+ self.driver = _resolve_driver(driver)
1166
+ self.verifier = verifier or Verifier()
1167
+ self.patcher = patcher or Patcher()
1168
+ self.workspace_dir = Path(workspace_dir or ".").resolve()
1169
+ self.repo_map = repo_map or RepoMap(root_dir=str(self.workspace_dir))
1170
+ self.doc_retriever = doc_retriever or DocRetriever()
1171
+ self.max_workers = max(1, max_workers)
1172
+ self.ram_budget_mb = ram_budget_mb
1173
+ self.mcp_manager = mcp_manager
1174
+ self.dedup_engine = dedup_engine
1175
+
1176
+ self.decomposer = TaskDecomposer(driver=self.driver)
1177
+ self.aggregator = PatchAggregator(patcher=self.patcher, verifier=self.verifier)
1178
+ self.msg_queue: queue.Queue = queue.Queue()
1179
+
1180
+ def check_ram_budget(self) -> float:
1181
+ """Monitors and enforces RAM consumption budget."""
1182
+ try:
1183
+ ram_mb = psutil.Process().memory_info().rss / (1024 * 1024)
1184
+ if ram_mb > self.ram_budget_mb * 0.85:
1185
+ gc.collect()
1186
+ ram_mb = psutil.Process().memory_info().rss / (1024 * 1024)
1187
+ return ram_mb
1188
+ except Exception:
1189
+ return 0.0
1190
+
1191
+ def dispatch(
1192
+ self,
1193
+ tasks: List[SubagentTask],
1194
+ event_callback: Optional[Callable[[SubagentMessage], None]] = None,
1195
+ ) -> SubagentRunResult:
1196
+ """
1197
+ Executes tasks according to their dependency graph (DAG).
1198
+ Runs ready tasks in parallel background threads.
1199
+ """
1200
+ start_time = time.time()
1201
+ task_map: Dict[str, SubagentTask] = {t.task_id: t for t in tasks}
1202
+ completed_tasks: Dict[str, SubagentTask] = {}
1203
+ active_threads: Dict[str, threading.Thread] = {}
1204
+ lock = threading.Lock()
1205
+
1206
+ # Start listener thread for message queue if callback is provided
1207
+ stop_listener = threading.Event()
1208
+
1209
+ def _message_listener():
1210
+ while not stop_listener.is_set() or not self.msg_queue.empty():
1211
+ try:
1212
+ msg = self.msg_queue.get(timeout=0.05)
1213
+ if event_callback:
1214
+ event_callback(msg)
1215
+ except queue.Empty:
1216
+ continue
1217
+
1218
+ listener_thread = threading.Thread(target=_message_listener, daemon=True)
1219
+ listener_thread.start()
1220
+
1221
+ def _worker_wrapper(worker_task: SubagentTask, deps: Dict[str, SubagentTask]):
1222
+ worker = SubagentWorker(
1223
+ task=worker_task,
1224
+ message_queue=self.msg_queue,
1225
+ driver=self.driver,
1226
+ verifier=self.verifier,
1227
+ patcher=self.patcher,
1228
+ repo_map=self.repo_map,
1229
+ doc_retriever=self.doc_retriever,
1230
+ workspace_dir=self.workspace_dir,
1231
+ mcp_manager=self.mcp_manager,
1232
+ dedup_engine=self.dedup_engine,
1233
+ )
1234
+ worker.execute(dependency_results=deps)
1235
+ with lock:
1236
+ completed_tasks[worker_task.task_id] = worker_task
1237
+
1238
+ # Main scheduling loop
1239
+ while len(completed_tasks) < len(tasks):
1240
+ self.check_ram_budget()
1241
+
1242
+ # Identify tasks ready to run
1243
+ with lock:
1244
+ ready_tasks = [
1245
+ t for t in tasks
1246
+ if t.status == SubagentStatus.PENDING
1247
+ and t.task_id not in active_threads
1248
+ and all(dep in completed_tasks and completed_tasks[dep].status == SubagentStatus.COMPLETED for dep in t.dependencies)
1249
+ ]
1250
+
1251
+ # Check if any dependencies failed, causing downstream tasks to cancel
1252
+ for t in tasks:
1253
+ if t.status == SubagentStatus.PENDING and t.task_id not in active_threads:
1254
+ if any(dep in completed_tasks and completed_tasks[dep].status == SubagentStatus.FAILED for dep in t.dependencies):
1255
+ t.status = SubagentStatus.CANCELLED
1256
+ t.status_message = "Cancelled due to upstream failure"
1257
+ completed_tasks[t.task_id] = t
1258
+
1259
+ # Launch ready tasks up to max_workers
1260
+ for t in ready_tasks:
1261
+ if len(active_threads) >= self.max_workers:
1262
+ break
1263
+ t.status = SubagentStatus.RUNNING
1264
+ deps_for_worker = {dep: completed_tasks[dep] for dep in t.dependencies if dep in completed_tasks}
1265
+ th = threading.Thread(
1266
+ target=_worker_wrapper,
1267
+ args=(t, deps_for_worker),
1268
+ daemon=True,
1269
+ )
1270
+ active_threads[t.task_id] = th
1271
+ th.start()
1272
+
1273
+ # Clean up finished threads
1274
+ with lock:
1275
+ finished_ids = [tid for tid, th in active_threads.items() if not th.is_alive() and tid in completed_tasks]
1276
+ for fid in finished_ids:
1277
+ del active_threads[fid]
1278
+
1279
+ time.sleep(0.02)
1280
+
1281
+ # Wait for all active threads to finish
1282
+ for th in list(active_threads.values()):
1283
+ th.join(timeout=1.0)
1284
+
1285
+ stop_listener.set()
1286
+ listener_thread.join(timeout=1.0)
1287
+
1288
+ total_duration = time.time() - start_time
1289
+ return self.aggregator.aggregate(tasks=tasks, total_duration=total_duration)
1290
+
1291
+ def run_prompt(
1292
+ self,
1293
+ prompt: str,
1294
+ context_files: Optional[List[str]] = None,
1295
+ target_roles: Optional[List[SubagentRole]] = None,
1296
+ event_callback: Optional[Callable[[SubagentMessage], None]] = None,
1297
+ ) -> SubagentRunResult:
1298
+ """Convenience method to decompose and execute a prompt."""
1299
+ dedup_warning = None
1300
+ dedup_dict = None
1301
+ if self.dedup_engine is not None or DedupEngine is not None:
1302
+ try:
1303
+ engine = self.dedup_engine or DedupEngine(repo_path=str(self.workspace_dir))
1304
+ d_match = engine.scan_for_duplicate(prompt)
1305
+ if d_match and d_match.is_duplicate:
1306
+ dedup_warning = f"Duplicate task detected ({d_match.confidence:.1%}): {d_match.explanation}"
1307
+ dedup_dict = d_match.to_dict()
1308
+ self.msg_queue.put(
1309
+ SubagentMessage(
1310
+ sender_id="dedup_engine",
1311
+ recipient_id="orchestrator",
1312
+ msg_type=SubagentMessageType.DEDUP_WARNING,
1313
+ payload={"warning": dedup_warning, "match": dedup_dict},
1314
+ )
1315
+ )
1316
+ except Exception:
1317
+ pass
1318
+
1319
+ tasks = self.decomposer.decompose(
1320
+ prompt=prompt,
1321
+ context_files=context_files,
1322
+ target_roles=target_roles,
1323
+ )
1324
+ res = self.dispatch(tasks=tasks, event_callback=event_callback)
1325
+ res.dedup_warning = dedup_warning
1326
+ res.dedup_match = dedup_dict
1327
+ return res
1328
+
1329
+
1330
+ # ==============================================================================
1331
+ # 6. CLI Visualization (Tree & Live Progress Dashboard)
1332
+ # ==============================================================================
1333
+
1334
+ class SubagentVisualizer:
1335
+ """
1336
+ Renders subagent task trees and live progress dashboards using Rich.
1337
+ """
1338
+
1339
+ ROLE_COLORS = {
1340
+ SubagentRole.EXPLORER: "cyan",
1341
+ SubagentRole.RESEARCHER: "blue",
1342
+ SubagentRole.REFACTORER: "magenta",
1343
+ SubagentRole.CODER: "green",
1344
+ SubagentRole.TESTER: "yellow",
1345
+ SubagentRole.CRITIC: "bright_yellow",
1346
+ SubagentRole.ARCHITECT: "bright_magenta",
1347
+ SubagentRole.CONFLICT_RESOLVER: "red",
1348
+ SubagentRole.PR_REVIEWER: "bright_cyan",
1349
+ SubagentRole.MCP_OPERATOR: "bright_blue",
1350
+ }
1351
+
1352
+ STATUS_GLYPHS = {
1353
+ SubagentStatus.PENDING: ("[dim]⏳ PENDING[/dim]", "dim"),
1354
+ SubagentStatus.RUNNING: ("[bold yellow]⚡ RUNNING[/bold yellow]", "yellow"),
1355
+ SubagentStatus.COMPLETED: ("[bold green]✔ COMPLETED[/bold green]", "green"),
1356
+ SubagentStatus.FAILED: ("[bold red]✘ FAILED[/bold red]", "red"),
1357
+ SubagentStatus.CANCELLED: ("[dim red]🚫 CANCELLED[/dim red]", "dim red"),
1358
+ }
1359
+
1360
+ @classmethod
1361
+ def render_tree(cls, tasks: List[SubagentTask], title: str = "Subagent Execution Graph") -> Tree:
1362
+ """Builds a rich hierarchical tree representing subagents and their dependencies."""
1363
+ root_tree = Tree(f"[bold cyan]📦 {title}[/bold cyan]")
1364
+ task_map = {t.task_id: t for t in tasks}
1365
+
1366
+ for task in tasks:
1367
+ role_color = cls.ROLE_COLORS.get(task.role, "white")
1368
+ status_badge, _ = cls.STATUS_GLYPHS.get(task.status, ("[dim]UNKNOWN[/dim]", "dim"))
1369
+ dur_str = f" ({task.duration_seconds:.2f}s)" if task.duration_seconds > 0 else ""
1370
+ deps_str = f" [dim]<- {', '.join(task.dependencies)}[/dim]" if task.dependencies else ""
1371
+
1372
+ node_label = (
1373
+ f"[{role_color}][bold]{task.role.value}[/bold][/{role_color}] "
1374
+ f"([cyan]{task.task_id}[/cyan]) - {task.name} {status_badge}{dur_str}{deps_str}"
1375
+ )
1376
+ node = root_tree.add(node_label)
1377
+ if task.status_message:
1378
+ node.add(f"[dim]{task.status_message}[/dim]")
1379
+
1380
+ return root_tree
1381
+
1382
+ @classmethod
1383
+ def render_dashboard(cls, tasks: List[SubagentTask], current_ram_mb: float = 0.0) -> Panel:
1384
+ """Constructs a live status dashboard table with progress bars."""
1385
+ table = Table(box=None, expand=True)
1386
+ table.add_column("Subagent Role", style="bold", width=14)
1387
+ table.add_column("Task Name", style="white", width=28)
1388
+ table.add_column("Status", width=14)
1389
+ table.add_column("Progress", width=20)
1390
+ table.add_column("Activity / Logs", style="dim", ratio=1)
1391
+
1392
+ for task in tasks:
1393
+ role_color = cls.ROLE_COLORS.get(task.role, "white")
1394
+ role_label = f"[{role_color}]{task.role.value}[/{role_color}]"
1395
+ status_badge, _ = cls.STATUS_GLYPHS.get(task.status, ("UNKNOWN", "dim"))
1396
+
1397
+ # Format mini progress bar
1398
+ pct = int(task.progress * 100)
1399
+ bar_len = 10
1400
+ filled = int(task.progress * bar_len)
1401
+ bar_str = f"[{role_color}]{'=' * filled}{'-' * (bar_len - filled)}[/{role_color}] {pct:3d}%"
1402
+
1403
+ table.add_row(
1404
+ role_label,
1405
+ task.name[:26],
1406
+ status_badge,
1407
+ bar_str,
1408
+ task.status_message[:60],
1409
+ )
1410
+
1411
+ title = f"[bold cyan]K-CLI Subagent Dispatcher[/bold cyan] | RSS RAM: [magenta]{current_ram_mb:.2f} MB[/magenta] / 1024 MB"
1412
+ return Panel(table, title=title, border_style="cyan")
1413
+
1414
+ @classmethod
1415
+ def execute_with_live_cli(
1416
+ cls,
1417
+ dispatcher: SubagentDispatcher,
1418
+ tasks: List[SubagentTask],
1419
+ console: Optional[Console] = None,
1420
+ ) -> SubagentRunResult:
1421
+ """
1422
+ Runs dispatcher with a live, animated Rich CLI interface.
1423
+ """
1424
+ c = console or Console()
1425
+ result_holder: List[SubagentRunResult] = []
1426
+
1427
+ def _make_panel():
1428
+ ram = dispatcher.check_ram_budget()
1429
+ return cls.render_dashboard(tasks, current_ram_mb=ram)
1430
+
1431
+ with Live(_make_panel(), console=c, refresh_per_second=10) as live:
1432
+ def _event_cb(msg: SubagentMessage):
1433
+ live.update(_make_panel())
1434
+
1435
+ res = dispatcher.dispatch(tasks=tasks, event_callback=_event_cb)
1436
+ result_holder.append(res)
1437
+ live.update(_make_panel())
1438
+
1439
+ return result_holder[0]
1440
+
1441
+
1442
+ # ==============================================================================
1443
+ # 7. High-Level Entrypoints & Shortcuts
1444
+ # ==============================================================================
1445
+
1446
+ def execute_subagents(
1447
+ prompt: str,
1448
+ context_files: Optional[List[str]] = None,
1449
+ target_roles: Optional[List[SubagentRole]] = None,
1450
+ driver: Optional[LLMDriver] = None,
1451
+ verifier: Optional[Verifier] = None,
1452
+ workspace_dir: Optional[Union[str, Path]] = None,
1453
+ max_workers: int = 4,
1454
+ show_ui: bool = True,
1455
+ console: Optional[Console] = None,
1456
+ ) -> SubagentRunResult:
1457
+ """
1458
+ Top-level helper function to decompose and execute a prompt using parallel subagents.
1459
+ """
1460
+ dispatcher = SubagentDispatcher(
1461
+ driver=driver,
1462
+ verifier=verifier,
1463
+ workspace_dir=workspace_dir,
1464
+ max_workers=max_workers,
1465
+ )
1466
+
1467
+ tasks = dispatcher.decomposer.decompose(
1468
+ prompt=prompt,
1469
+ context_files=context_files,
1470
+ target_roles=target_roles,
1471
+ )
1472
+
1473
+ if show_ui:
1474
+ return SubagentVisualizer.execute_with_live_cli(
1475
+ dispatcher=dispatcher,
1476
+ tasks=tasks,
1477
+ console=console,
1478
+ )
1479
+ else:
1480
+ return dispatcher.dispatch(tasks=tasks)
1481
+
1482
+
1483
+ __all__ = [
1484
+ "SubagentRole",
1485
+ "SubagentStatus",
1486
+ "SubagentMessageType",
1487
+ "SubagentMessage",
1488
+ "SubagentTask",
1489
+ "SubagentRunResult",
1490
+ "TaskDecomposer",
1491
+ "SubagentWorker",
1492
+ "PatchAggregator",
1493
+ "SubagentDispatcher",
1494
+ "SubagentVisualizer",
1495
+ "execute_subagents",
1496
+ ]