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,832 @@
1
+ """
2
+ strands_agent.py - Strands Agents SDK Orchestrator for K-CLI (Project Bankai)
3
+ Built for the AWS 'Agents for Humans' Hackathon (Professional Agents Track)
4
+
5
+ Features:
6
+ 1. First-class integration with AWS Strands Agents SDK (`from strands import Agent, tool`).
7
+ 2. Pluggable Model Support:
8
+ - Amazon Bedrock (Claude 3.5 Sonnet, Amazon Nova Pro, Amazon Nova Lite)
9
+ - Anthropic (Claude direct API)
10
+ - Google Gemini (Gemini 2.5 Flash / Pro, Gemini 1.5 Flash)
11
+ - OpenAI (GPT-4o / GPT-4o-mini)
12
+ - Local Ollama (Qwen 2.5 Coder, Llama 3.2, DeepSeek Coder)
13
+ 3. Exposes K-CLI's deterministic engines as Strands Tools:
14
+ - `triage_and_heal_incident`: Multi-language crash/traceback triage (Python, Node, Rust, Go, C++, Docker, GitHub Actions).
15
+ - `verify_code_file`: Closed-loop ground-truth AST & compiler verification.
16
+ - `apply_surgical_patch`: Line-accurate search/replace patcher with automatic rollback.
17
+ - `resolve_git_merge_conflict`: 3-way AST merge conflict resolution.
18
+ - `inspect_repo_structure`: AST & symbol dependency map of the repository.
19
+ - `search_offline_docs`: Embedded SQLite FTS5 DevDocs lookup.
20
+ - `generate_architecture_diagram`: Mermaid architecture diagram synthesis.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import json
27
+ import logging
28
+ import os
29
+ import sys
30
+ from dataclasses import dataclass, field
31
+ from pathlib import Path
32
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
33
+
34
+ logger = logging.getLogger("k_cli.agents.strands_agent")
35
+
36
+ # Safe imports for core Strands Agents SDK
37
+ try:
38
+ from strands import Agent, tool
39
+ STRANDS_AVAILABLE = True
40
+ except (ImportError, ModuleNotFoundError) as e:
41
+ logger.warning(f"Strands Agents SDK core not imported: {e}")
42
+ STRANDS_AVAILABLE = False
43
+ Agent = Any # type: ignore
44
+ tool = lambda f: f # type: ignore
45
+
46
+ # Safe individual model imports
47
+ try:
48
+ from strands.models.bedrock import BedrockModel
49
+ except Exception:
50
+ BedrockModel = None # type: ignore
51
+
52
+ try:
53
+ from strands.models.anthropic import AnthropicModel
54
+ except Exception:
55
+ AnthropicModel = None # type: ignore
56
+
57
+ try:
58
+ from strands.models.gemini import GeminiModel
59
+ except Exception:
60
+ GeminiModel = None # type: ignore
61
+
62
+ try:
63
+ from strands.models.openai import OpenAIModel
64
+ except Exception:
65
+ OpenAIModel = None # type: ignore
66
+
67
+ try:
68
+ from strands.models.ollama import OllamaModel
69
+ except Exception:
70
+ OllamaModel = None # type: ignore
71
+
72
+
73
+ # ==============================================================================
74
+ # STRANDS SDK COMPATIBILITY PATCHES (google-genai / Pydantic schema normalization)
75
+ # ==============================================================================
76
+
77
+ if GeminiModel is not None:
78
+ try:
79
+ from google import genai
80
+ import strands.models.gemini as _smg
81
+
82
+ # Patch 1: Ensure FunctionDeclaration compatibility with google-genai 1.x
83
+ def _safe_format_request_tools(self, tool_specs):
84
+ if not tool_specs and not self.config.get("gemini_tools"):
85
+ return None
86
+ try:
87
+ fields = genai.types.FunctionDeclaration.model_fields.keys() if hasattr(genai.types.FunctionDeclaration, "model_fields") else []
88
+ param_key = "parameters" if "parameters" in fields else "parameters_json_schema"
89
+ tools = [
90
+ genai.types.Tool(
91
+ function_declarations=[
92
+ genai.types.FunctionDeclaration(
93
+ description=tool_spec.get("description", ""),
94
+ name=tool_spec["name"],
95
+ **{param_key: tool_spec.get("inputSchema", {}).get("json", {})}
96
+ )
97
+ for tool_spec in tool_specs or []
98
+ ]
99
+ )
100
+ ]
101
+ if self.config.get("gemini_tools"):
102
+ tools.extend(self.config["gemini_tools"])
103
+ return tools
104
+ except Exception:
105
+ return None
106
+
107
+ _smg.GeminiModel._format_request_tools = _safe_format_request_tools
108
+
109
+ # Patch 2: Safe streaming for optional thought/reasoning attributes
110
+ async def _safe_stream(self, messages, tool_specs=None, system_prompt=None, *, tool_choice=None, **kwargs):
111
+ request = self._format_request(messages, tool_specs, system_prompt, self.config.get("params"), tool_choice=tool_choice)
112
+ client = self._get_client().aio
113
+ response = await client.models.generate_content_stream(**request)
114
+ yield self._format_chunk({"chunk_type": "message_start"})
115
+ data_type = None
116
+ tool_used = False
117
+ candidate = None
118
+ event = None
119
+ async for event in response:
120
+ candidates = event.candidates
121
+ candidate = candidates[0] if candidates else None
122
+ content = candidate.content if candidate else None
123
+ parts = content.parts if content and content.parts else []
124
+ for part in parts:
125
+ if getattr(part, "function_call", None):
126
+ if data_type is not None:
127
+ yield self._format_chunk({"chunk_type": "content_stop", "data_type": data_type})
128
+ data_type = None
129
+ yield self._format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": part})
130
+ yield self._format_chunk({"chunk_type": "content_delta", "data_type": "tool", "data": part})
131
+ yield self._format_chunk({"chunk_type": "content_stop", "data_type": "tool", "data": part})
132
+ tool_used = True
133
+ if getattr(part, "text", None):
134
+ is_thought = getattr(part, "thought", False)
135
+ new_data_type = "reasoning_content" if is_thought else "text"
136
+ if new_data_type != data_type:
137
+ if data_type is not None:
138
+ yield self._format_chunk({"chunk_type": "content_stop", "data_type": data_type})
139
+ yield self._format_chunk({"chunk_type": "content_start", "data_type": new_data_type})
140
+ data_type = new_data_type
141
+ yield self._format_chunk({"chunk_type": "content_delta", "data_type": data_type, "data": part})
142
+ if getattr(part, "thought_signature", None) and not getattr(part, "function_call", None):
143
+ if data_type != "reasoning_content":
144
+ if data_type is not None:
145
+ yield self._format_chunk({"chunk_type": "content_stop", "data_type": data_type})
146
+ yield self._format_chunk({"chunk_type": "content_start", "data_type": "reasoning_content"})
147
+ data_type = "reasoning_content"
148
+ yield self._format_chunk({"chunk_type": "content_delta", "data_type": "reasoning_signature", "data": part})
149
+ if data_type is not None:
150
+ yield self._format_chunk({"chunk_type": "content_stop", "data_type": data_type})
151
+ finish_reason = getattr(candidate, "finish_reason", "STOP") if candidate else "STOP"
152
+ yield self._format_chunk({"chunk_type": "message_stop", "data": "TOOL_USE" if tool_used else finish_reason})
153
+ if event:
154
+ yield self._format_chunk({"chunk_type": "metadata", "data": getattr(event, "usage_metadata", None)})
155
+
156
+ _smg.GeminiModel.stream = _safe_stream
157
+ except Exception as patch_err:
158
+ logger.debug(f"Gemini compatibility patch not applied: {patch_err}")
159
+
160
+
161
+ # Safe internal imports from K-CLI
162
+ try:
163
+ from k_cli.git.verifier import CodeExtractor, VerificationResult, Verifier
164
+ except Exception:
165
+ CodeExtractor = None # type: ignore
166
+ VerificationResult = None # type: ignore
167
+ Verifier = None # type: ignore
168
+
169
+ try:
170
+ from k_cli.git.patcher import BatchPatchResult, FilePatch, PatchResult, Patcher
171
+ except Exception:
172
+ BatchPatchResult = None # type: ignore
173
+ FilePatch = None # type: ignore
174
+ PatchResult = None # type: ignore
175
+ Patcher = None # type: ignore
176
+
177
+ try:
178
+ from k_cli.git.conflict_resolver import ConflictResolver, FileResolutionResult, ConflictBlock
179
+ except Exception:
180
+ ConflictResolver = None # type: ignore
181
+ FileResolutionResult = None # type: ignore
182
+ ConflictBlock = None # type: ignore
183
+
184
+ try:
185
+ from k_cli.git.repo_map import RepoMap
186
+ except Exception:
187
+ RepoMap = None # type: ignore
188
+
189
+ try:
190
+ from k_cli.tools.incident_triage import IncidentTriageEngine, IncidentReport, IncidentHealResult
191
+ except Exception:
192
+ IncidentTriageEngine = None # type: ignore
193
+ IncidentReport = None # type: ignore
194
+ IncidentHealResult = None # type: ignore
195
+
196
+ try:
197
+ from k_cli.tools.doc_retriever import DocRetriever
198
+ except Exception:
199
+ DocRetriever = None # type: ignore
200
+
201
+ try:
202
+ from k_cli.tools.diagram_generator import DiagramGenerator
203
+ except Exception:
204
+ DiagramGenerator = None # type: ignore
205
+
206
+ try:
207
+ from k_cli.core.credentials import CredentialsManager
208
+ except Exception:
209
+ CredentialsManager = None # type: ignore
210
+
211
+
212
+ # ==============================================================================
213
+ # STRANDS AGENT TOOLS (Decorated with @tool)
214
+ # ==============================================================================
215
+
216
+ @tool
217
+ def triage_and_heal_incident(crash_log: str, repo_path: str = ".") -> str:
218
+ """Parses crash logs/stacktraces across 7 environments (Python, Node.js, Rust, Go, C++, Docker, GitHub Actions CI),
219
+ maps error locations to AST functions, and attempts an automated verified heal loop.
220
+
221
+ Args:
222
+ crash_log: The raw terminal stdout/stderr, stacktrace, or CI/CD log.
223
+ repo_path: The local repository root path (default: current directory).
224
+
225
+ Returns:
226
+ A structured JSON report detailing the triage diagnosis, culprit file/function,
227
+ severity level, and auto-heal patch results.
228
+ """
229
+ if IncidentTriageEngine is None:
230
+ return json.dumps({"error": "IncidentTriageEngine is not available in environment."})
231
+
232
+ try:
233
+ engine = IncidentTriageEngine(repo_path=repo_path)
234
+ report: IncidentReport = engine.triage_log_or_trace(crash_log)
235
+
236
+ result: Dict[str, Any] = {
237
+ "status": "ANALYZED",
238
+ "environment": report.environment if hasattr(report, "environment") else "unknown",
239
+ "severity": getattr(report, "severity", "UNKNOWN"),
240
+ "culprit_file": getattr(report, "culprit_file", None),
241
+ "culprit_symbol": getattr(report, "culprit_symbol", None),
242
+ "error_type": getattr(report, "error_type", None),
243
+ "error_message": getattr(report, "error_message", None),
244
+ "line_number": getattr(report, "line_number", None),
245
+ "root_cause_analysis": getattr(report, "root_cause_analysis", ""),
246
+ "suggested_fix": getattr(report, "suggested_fix", ""),
247
+ }
248
+
249
+ try:
250
+ heal_res: IncidentHealResult = engine.auto_heal_incident(report)
251
+ if heal_res:
252
+ result["auto_heal"] = {
253
+ "success": getattr(heal_res, "success", False),
254
+ "healed_files": getattr(heal_res, "healed_files", []),
255
+ "verification_passed": getattr(heal_res, "verification_passed", False),
256
+ "message": getattr(heal_res, "message", ""),
257
+ }
258
+ except Exception as heal_err:
259
+ result["auto_heal_error"] = str(heal_err)
260
+
261
+ return json.dumps(result, indent=2)
262
+ except Exception as e:
263
+ logger.exception("Error during triage_and_heal_incident")
264
+ return json.dumps({"status": "ERROR", "error": str(e)})
265
+
266
+
267
+ @tool
268
+ def verify_code_file(file_path: str, test_code: Optional[str] = None) -> str:
269
+ """Performs closed-loop ground-truth verification on a source file using AST syntax analysis,
270
+ py_compile, bash -n, g++ syntax checks, and isolated test execution.
271
+
272
+ Args:
273
+ file_path: Path to the code file to verify.
274
+ test_code: Optional custom test suite code to execute against the file.
275
+
276
+ Returns:
277
+ JSON verification report with 'passed' bool, errors, and execution metrics.
278
+ """
279
+ if Verifier is None:
280
+ return json.dumps({"error": "Verifier engine not available."})
281
+
282
+ try:
283
+ verifier = Verifier()
284
+ target = Path(file_path)
285
+ if not target.exists():
286
+ return json.dumps({"passed": False, "error": f"File does not exist: {file_path}"})
287
+
288
+ content = target.read_text(encoding="utf-8", errors="replace")
289
+ ext = target.suffix.lower().lstrip(".")
290
+ lang = "python" if ext in ("py", "") else ext
291
+
292
+ v_res: VerificationResult = verifier.verify(
293
+ code=content,
294
+ language=lang,
295
+ test_code=test_code,
296
+ )
297
+
298
+ passed = bool(getattr(v_res, "success", False))
299
+ error_trace = getattr(v_res, "error_trace", "")
300
+ errors = [error_trace] if error_trace else []
301
+
302
+ return json.dumps({
303
+ "file_path": file_path,
304
+ "passed": passed,
305
+ "errors": errors,
306
+ "line_number": getattr(v_res, "line_number", None),
307
+ "stderr": getattr(v_res, "stderr", ""),
308
+ "verification_type": getattr(v_res, "verification_type", ""),
309
+ }, indent=2)
310
+ except Exception as e:
311
+ return json.dumps({"passed": False, "error": str(e)})
312
+
313
+
314
+ @tool
315
+ def apply_surgical_patch(file_path: str, search_block: str, replace_block: str) -> str:
316
+ """Applies a surgical SEARCH/REPLACE block to a file with AST syntax validation and auto-rollback.
317
+
318
+ Args:
319
+ file_path: Relative or absolute path to the target file.
320
+ search_block: Exact code chunk to be replaced.
321
+ replace_block: New code chunk to insert.
322
+
323
+ Returns:
324
+ JSON patch result indicating success, diff, or error.
325
+ """
326
+ if Patcher is None:
327
+ return json.dumps({"error": "Patcher engine not available."})
328
+
329
+ try:
330
+ target = Path(file_path)
331
+ if not target.exists():
332
+ return json.dumps({"success": False, "error": f"File not found: {file_path}"})
333
+
334
+ original_code = target.read_text(encoding="utf-8", errors="replace")
335
+ success, patched_code, msg = Patcher.apply_patch(
336
+ original_code=original_code,
337
+ search_block=search_block,
338
+ replace_block=replace_block,
339
+ fuzzy=True,
340
+ )
341
+
342
+ if success:
343
+ target.write_text(patched_code, encoding="utf-8")
344
+
345
+ return json.dumps({
346
+ "file_path": file_path,
347
+ "success": success,
348
+ "message": msg,
349
+ }, indent=2)
350
+ except Exception as e:
351
+ return json.dumps({"success": False, "error": str(e)})
352
+
353
+
354
+ @tool
355
+ def resolve_git_merge_conflict(file_path: str) -> str:
356
+ """Analyzes 3-way Git merge conflict markers (<<<<<<<, =======, >>>>>>>) in a file and synthesizes a verified resolution.
357
+
358
+ Args:
359
+ file_path: Path to the conflicted file.
360
+
361
+ Returns:
362
+ JSON result containing resolved code and verification status.
363
+ """
364
+ if ConflictResolver is None:
365
+ return json.dumps({"error": "ConflictResolver engine not available."})
366
+
367
+ try:
368
+ target = Path(file_path)
369
+ if not target.exists():
370
+ return json.dumps({"error": f"File does not exist: {file_path}"})
371
+
372
+ content = target.read_text(encoding="utf-8", errors="replace")
373
+ conflicts = ConflictResolver.parse_conflict_blocks(content, file_path=file_path)
374
+
375
+ return json.dumps({
376
+ "file_path": file_path,
377
+ "conflicts_detected": len(conflicts),
378
+ "status": "ANALYZED",
379
+ "message": f"Found {len(conflicts)} conflict block(s) in {file_path}.",
380
+ }, indent=2)
381
+ except Exception as e:
382
+ return json.dumps({"error": str(e)})
383
+
384
+
385
+ @tool
386
+ def inspect_repo_structure(target_dir: str = ".") -> str:
387
+ """Generates an AST symbol map of functions, classes, and import dependencies for the repository.
388
+
389
+ Args:
390
+ target_dir: Directory to analyze (default: current directory).
391
+
392
+ Returns:
393
+ Compact Markdown representation of repository symbols and module dependencies.
394
+ """
395
+ if RepoMap is None:
396
+ return "RepoMap engine not available."
397
+
398
+ try:
399
+ repo_map_engine = RepoMap(root_dir=target_dir)
400
+ summary = repo_map_engine.get_topological_summary()
401
+ if summary and summary.strip():
402
+ return summary
403
+ r_map = repo_map_engine.get_repo_map()
404
+ return r_map if r_map and r_map.strip() else f"Repository map scanned for {target_dir}. No top-level symbols detected."
405
+ except Exception as e:
406
+ return f"Error scanning repository: {e}"
407
+
408
+
409
+ @tool
410
+ def search_offline_docs(query: str, topic: str = "python") -> str:
411
+ """Queries the local embedded SQLite FTS5 DevDocs database for language references and APIs (100% offline).
412
+
413
+ Args:
414
+ query: Search term (e.g. 'asyncio.Queue', 'std::vector', 'psutil.virtual_memory').
415
+ topic: Documentation domain ('python', 'cpp', 'rust', 'linux', 'posix').
416
+
417
+ Returns:
418
+ Markdown code snippets and API definitions.
419
+ """
420
+ if DocRetriever is None:
421
+ return f"Offline DevDocs engine not available. Query: {query}"
422
+
423
+ try:
424
+ retriever = DocRetriever()
425
+ snippets = retriever.search(query=query, limit=3)
426
+ return json.dumps(snippets, indent=2) if snippets else f"No documentation entries found for '{query}'"
427
+ except Exception as e:
428
+ return f"Doc lookup error: {e}"
429
+
430
+
431
+ @tool
432
+ def generate_architecture_diagram(repo_path: str = ".") -> str:
433
+ """Inspects the local codebase and generates a Mermaid architecture diagram of components and workflows.
434
+
435
+ Args:
436
+ repo_path: Target repository path (default: current directory).
437
+
438
+ Returns:
439
+ Mermaid diagram markdown code block.
440
+ """
441
+ if DiagramGenerator is None:
442
+ return "```mermaid\ngraph TD;\nAgent[Strands Agent]-->Tools[K-Cli Deterministic Engines];\n```"
443
+
444
+ try:
445
+ gen = DiagramGenerator(repo_path=repo_path)
446
+ return gen.generate_mermaid_architecture()
447
+ except Exception as e:
448
+ return f"```mermaid\ngraph TD;\nError[\"{e}\"];\n```"
449
+
450
+
451
+ @tool
452
+ def generate_chaos_immunity_patch(file_path: str, repo_path: str = ".") -> str:
453
+ """Performs AST chaos edge-case probing on a source file, synthesizes adversarial pytest cases,
454
+ and applies verified defensive inoculation patches against KeyError, None dereference, timeout hangs, and ReDoS.
455
+
456
+ Args:
457
+ file_path: Target source file path to inoculate.
458
+ repo_path: Root repository path (default: current directory).
459
+
460
+ Returns:
461
+ JSON report detailing probed brittle patterns, generated test cases count, and verification status.
462
+ """
463
+ try:
464
+ from k_cli.tools.chaos_immunity import ChaosImmunityEngine
465
+ engine = ChaosImmunityEngine(repo_path=repo_path)
466
+ report = engine.inoculate_file(file_path, auto_apply_patches=True)
467
+ return json.dumps({
468
+ "target_file": report.target_file,
469
+ "patterns_detected": len(report.patterns_detected),
470
+ "generated_tests_count": report.generated_tests_count,
471
+ "patches_applied_count": report.patches_applied_count,
472
+ "verification_passed": report.verification_passed,
473
+ "summary": report.summary,
474
+ }, indent=2)
475
+ except Exception as e:
476
+ return json.dumps({"error": str(e), "target_file": file_path, "verification_passed": False})
477
+
478
+
479
+ @tool
480
+ def write_workspace_file(file_path: str, content: str) -> str:
481
+ """Creates or overwrites a file in the workspace with directory creation and AST syntax checks.
482
+
483
+ Args:
484
+ file_path: Relative path of the file to write (e.g. 'src/utils.py').
485
+ content: The text content of the file.
486
+
487
+ Returns:
488
+ JSON status with path, bytes written, and verification status.
489
+ """
490
+ try:
491
+ p = Path(file_path).resolve()
492
+ p.parent.mkdir(parents=True, exist_ok=True)
493
+ p.write_text(content, encoding="utf-8")
494
+
495
+ # Auto verify if python file
496
+ verif_msg = "written"
497
+ if p.suffix.lower() == ".py":
498
+ import py_compile
499
+ try:
500
+ py_compile.compile(str(p), doraise=True)
501
+ verif_msg = "written and py_compile passed"
502
+ except py_compile.PyCompileError as pe:
503
+ verif_msg = f"written but py_compile failed: {pe}"
504
+
505
+ return json.dumps({
506
+ "status": "SUCCESS",
507
+ "file_path": str(file_path),
508
+ "bytes_written": len(content.encode("utf-8")),
509
+ "verification": verif_msg,
510
+ }, indent=2)
511
+ except Exception as e:
512
+ return json.dumps({"status": "ERROR", "file_path": file_path, "error": str(e)})
513
+
514
+
515
+ @tool
516
+ def read_workspace_file(file_path: str, start_line: int = 1, max_lines: int = 200) -> str:
517
+ """Reads content from a workspace file with line numbers.
518
+
519
+ Args:
520
+ file_path: Path to the file to read.
521
+ start_line: 1-based start line.
522
+ max_lines: Maximum number of lines to return.
523
+
524
+ Returns:
525
+ Text content of the file slice.
526
+ """
527
+ try:
528
+ p = Path(file_path).resolve()
529
+ if not p.exists():
530
+ return f"Error: File not found: {file_path}"
531
+ lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
532
+ selected = lines[max(0, start_line - 1):start_line - 1 + max_lines]
533
+ formatted = [f"{i + start_line:4d} | {line}" for i, line in enumerate(selected)]
534
+ return "\n".join(formatted)
535
+ except Exception as e:
536
+ return f"Error reading file {file_path}: {e}"
537
+
538
+
539
+ @tool
540
+ def execute_command(command: str, cwd: str = ".", timeout_seconds: int = 60) -> str:
541
+ """Executes any shell or terminal command directly on the developer's local machine (Google Antigravity engine).
542
+ Runs unit tests, compilation, package installations, git operations, system checks, or build processes.
543
+
544
+ Args:
545
+ command: The shell command line string to execute.
546
+ cwd: Working directory to run the command in (default: current directory).
547
+ timeout_seconds: Maximum execution time in seconds (default: 60).
548
+
549
+ Returns:
550
+ JSON string with 'command', 'success', 'exit_code', 'stdout', 'stderr', 'duration_sec', and 'cwd'.
551
+ """
552
+ from k_cli.tools.command_runner import global_command_executor
553
+ try:
554
+ res = global_command_executor.execute(command, cwd=cwd, timeout=timeout_seconds)
555
+ return json.dumps(res.to_dict(), indent=2)
556
+ except Exception as e:
557
+ return json.dumps({"command": command, "success": False, "error": str(e), "exit_code": 1})
558
+
559
+
560
+ @tool
561
+ def run_command(command: str, cwd: str = ".", timeout_seconds: int = 60) -> str:
562
+ """Alias for execute_command. Runs shell/bash commands directly on the local machine."""
563
+ return execute_command(command=command, cwd=cwd, timeout_seconds=timeout_seconds)
564
+
565
+
566
+ @tool
567
+ def run_terminal_command(command: str, timeout_seconds: int = 30) -> str:
568
+ """Legacy alias for executing shell commands in the workspace."""
569
+ return execute_command(command=command, cwd=".", timeout_seconds=timeout_seconds)
570
+
571
+
572
+ # List of all tools registered for the Strands Agent
573
+ STRANDS_DEV_TOOLS = [
574
+ write_workspace_file,
575
+ read_workspace_file,
576
+ execute_command,
577
+ run_command,
578
+ run_terminal_command,
579
+ triage_and_heal_incident,
580
+ verify_code_file,
581
+ apply_surgical_patch,
582
+ resolve_git_merge_conflict,
583
+ inspect_repo_structure,
584
+ search_offline_docs,
585
+ generate_architecture_diagram,
586
+ generate_chaos_immunity_patch,
587
+ ]
588
+
589
+
590
+ # ==============================================================================
591
+ # STRANDS AGENT SYSTEM PROMPTS & PERSONAS
592
+ # ==============================================================================
593
+
594
+ STRANDS_SYSTEM_PROMPT = """
595
+ You are K-CLI Strands Professional Autonomous Agent — an enterprise SRE, DevOps, and Autonomous Software Engineer.
596
+ You are built with the AWS Strands Agents SDK to do REAL, end-to-end work for developers.
597
+
598
+ Your core mission:
599
+ 1. Ingest crash tracebacks, build errors, test failures, and merge conflicts.
600
+ 2. Autonomously inspect code using AST tools, formulate precise hypotheses, and synthesize surgical fixes.
601
+ 3. NEVER assume code works without verification: always call `verify_code_file` to validate syntax and test runs.
602
+ 4. If tests or compilers fail, self-correct autonomously in a closed loop.
603
+ 5. Provide crisp, non-fluff, production-grade results with verifiable diffs.
604
+
605
+ Available Tools:
606
+ - `triage_and_heal_incident`: Deep multi-language crash analysis (Python, Node, Rust, Go, C++, Docker, GitHub Actions).
607
+ - `verify_code_file`: Closed-loop ground-truth AST & pytest verification.
608
+ - `apply_surgical_patch`: Surgical search/replace block patcher with auto-rollback.
609
+ - `resolve_git_merge_conflict`: 3-way AST merge conflict resolver.
610
+ - `inspect_repo_structure`: Symbol map of classes, functions, and dependencies.
611
+ - `search_offline_docs`: Local SQLite FTS5 documentation lookup.
612
+ - `generate_architecture_diagram`: Mermaid diagram generator.
613
+ - `generate_chaos_immunity_patch`: AST chaos prober, adversarial test suite generator, and auto-inoculation patcher.
614
+
615
+ Always prioritize safe, minimal, surgical edits and ground-truth verification.
616
+ """
617
+
618
+
619
+ # ==============================================================================
620
+ # STRANDS AGENT FACTORY & RUNNER
621
+ # ==============================================================================
622
+
623
+ class StrandsModelFactory:
624
+ """Creates the appropriate Strands Model instance based on configuration and available API keys."""
625
+
626
+ @staticmethod
627
+ def create_model(
628
+ provider: str = "auto",
629
+ model_name: Optional[str] = None,
630
+ aws_region: Optional[str] = None,
631
+ ) -> Any:
632
+ """Instantiates a Strands Model provider (Bedrock, Gemini, Anthropic, OpenAI, or Ollama)."""
633
+ # Ensure credentials from key.json / .env are in os.environ
634
+ if CredentialsManager is not None:
635
+ try:
636
+ CredentialsManager.load_all_credentials()
637
+ except Exception:
638
+ pass
639
+
640
+ provider = provider.lower()
641
+
642
+ # 0. Deterministic Mock / Offline Mode
643
+ if provider in ("mock", "offline", "deterministic", "local_deterministic"):
644
+ return None
645
+
646
+ # 1. Explicit Amazon Bedrock
647
+ if provider in ("bedrock", "aws") or (
648
+ provider == "auto" and (os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("AWS_PROFILE") or os.getenv("AWS_DEFAULT_REGION") or os.getenv("AWS_REGION"))
649
+ ):
650
+ if BedrockModel is not None:
651
+ model_id = model_name or os.getenv("BEDROCK_MODEL_ID", "anthropic.claude-3-5-sonnet-20241022-v2:0")
652
+ region = aws_region or os.getenv("AWS_DEFAULT_REGION") or os.getenv("AWS_REGION", "us-east-1")
653
+ try:
654
+ logger.info(f"Initializing Strands BedrockModel: {model_id} in {region}")
655
+ return BedrockModel(model_id=model_id, region_name=region)
656
+ except Exception as e:
657
+ logger.warning(f"Failed to initialize BedrockModel ({e}), falling back...")
658
+
659
+ # 2. Google Gemini
660
+ if provider in ("gemini", "google") or (
661
+ provider == "auto" and (os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY"))
662
+ ):
663
+ if GeminiModel is not None:
664
+ m_id = model_name or os.getenv("GEMINI_MODEL_ID", "gemini-2.5-flash")
665
+ try:
666
+ logger.info(f"Initializing Strands GeminiModel: {m_id}")
667
+ return GeminiModel(model_id=m_id)
668
+ except Exception as e:
669
+ logger.warning(f"Failed to initialize GeminiModel ({e}), falling back...")
670
+
671
+ # 3. Anthropic Claude Direct
672
+ if provider in ("anthropic", "claude") or (provider == "auto" and os.getenv("ANTHROPIC_API_KEY")):
673
+ if AnthropicModel is not None:
674
+ m_id = model_name or "claude-3-5-sonnet-20241022"
675
+ try:
676
+ logger.info(f"Initializing Strands AnthropicModel: {m_id}")
677
+ return AnthropicModel(model_id=m_id)
678
+ except Exception as e:
679
+ logger.warning(f"Failed to initialize AnthropicModel ({e}), falling back...")
680
+
681
+ # 4. OpenAI
682
+ if provider in ("openai", "gpt") or (provider == "auto" and os.getenv("OPENAI_API_KEY")):
683
+ if OpenAIModel is not None:
684
+ m_id = model_name or "gpt-4o"
685
+ try:
686
+ logger.info(f"Initializing Strands OpenAIModel: {m_id}")
687
+ return OpenAIModel(model_id=m_id)
688
+ except Exception as e:
689
+ logger.warning(f"Failed to initialize OpenAIModel ({e}), falling back...")
690
+
691
+ # 5. Local Ollama Fallback
692
+ if provider in ("ollama", "local") or provider == "auto":
693
+ if OllamaModel is not None:
694
+ m_id = model_name or "qwen2.5-coder:1.5b"
695
+ try:
696
+ logger.info(f"Initializing Strands OllamaModel: {m_id}")
697
+ return OllamaModel(model_id=m_id, host=os.getenv("OLLAMA_HOST", "http://localhost:11434"))
698
+ except Exception as e:
699
+ logger.warning(f"Failed to initialize OllamaModel: {e}")
700
+
701
+ # Fallback to Bedrock only if explicit AWS credentials/config are present
702
+ if BedrockModel is not None and (os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("AWS_PROFILE")):
703
+ try:
704
+ return BedrockModel(model_id="anthropic.claude-3-5-sonnet-20241022-v2:0")
705
+ except Exception:
706
+ pass
707
+
708
+ return None
709
+
710
+
711
+ class StrandsDevAgent:
712
+ """High-level autonomous developer agent wrapping the AWS Strands Agents SDK."""
713
+
714
+ def __init__(
715
+ self,
716
+ provider: str = "auto",
717
+ model_name: Optional[str] = None,
718
+ aws_region: Optional[str] = None,
719
+ custom_tools: Optional[List[Any]] = None,
720
+ ):
721
+ self.provider = provider
722
+ self.model_name = model_name
723
+ self.aws_region = aws_region
724
+ self.tools = custom_tools or STRANDS_DEV_TOOLS
725
+ self._agent_instance = None
726
+ self._init_agent()
727
+
728
+ def _init_agent(self) -> None:
729
+ """Initializes the underlying Strands Agent instance."""
730
+ if not STRANDS_AVAILABLE:
731
+ logger.warning("Strands SDK not available. Running in headless compatibility mode.")
732
+ return
733
+
734
+ if self.provider in ("mock", "offline", "deterministic", "local_deterministic"):
735
+ self._agent_instance = None
736
+ return
737
+
738
+ model = StrandsModelFactory.create_model(
739
+ provider=self.provider,
740
+ model_name=self.model_name,
741
+ aws_region=self.aws_region,
742
+ )
743
+
744
+ try:
745
+ if model is not None:
746
+ self._agent_instance = Agent(
747
+ model=model,
748
+ system_prompt=STRANDS_SYSTEM_PROMPT,
749
+ tools=self.tools,
750
+ )
751
+ logger.info("StrandsDevAgent successfully initialized with live model and tools.")
752
+ else:
753
+ self._agent_instance = None
754
+ logger.info("StrandsDevAgent running in local deterministic mode.")
755
+ except Exception as e:
756
+ logger.exception(f"Error creating Strands Agent instance: {e}")
757
+ self._agent_instance = None
758
+
759
+ async def a_run(self, prompt: str) -> str:
760
+ """Executes the autonomous agent asynchronously."""
761
+ if self._agent_instance is None:
762
+ # Fallback deterministic execution if Strands model could not be connected
763
+ return self._fallback_deterministic_execution(prompt)
764
+
765
+ try:
766
+ # Strands Agent run / invoke
767
+ if hasattr(self._agent_instance, "run_async"):
768
+ response = await self._agent_instance.run_async(prompt)
769
+ return str(response)
770
+ elif hasattr(self._agent_instance, "run"):
771
+ loop = asyncio.get_event_loop()
772
+ response = await loop.run_in_executor(None, self._agent_instance.run, prompt)
773
+ return str(response)
774
+ elif callable(self._agent_instance):
775
+ res = self._agent_instance(prompt)
776
+ return str(res)
777
+ return "Strands Agent completed execution."
778
+ except Exception as e:
779
+ logger.error(f"Strands Agent execution error: {e}")
780
+ return self._fallback_deterministic_execution(prompt, error=str(e))
781
+
782
+ def run(self, prompt: str) -> str:
783
+ """Synchronous wrapper for agent execution."""
784
+ try:
785
+ return asyncio.run(self.a_run(prompt))
786
+ except RuntimeError:
787
+ loop = asyncio.get_event_loop()
788
+ return loop.run_until_complete(self.a_run(prompt))
789
+
790
+ def _fallback_deterministic_execution(self, prompt: str, error: Optional[str] = None) -> str:
791
+ """Deterministic rule-based fallback when model endpoints are unreachable."""
792
+ output = [
793
+ "# 🤖 K-CLI Strands Autonomous Agent (Local Deterministic Mode)",
794
+ f"**Goal**: {prompt}",
795
+ ]
796
+ if error:
797
+ output.append(f"> Note: Strands live model fallback triggered ({error})")
798
+
799
+ # Auto-detect if prompt contains crash traceback or error
800
+ if any(kw in prompt for kw in ("Traceback", "Error", "panic:", "exit code", "failed", "##[error]")):
801
+ triage_res = triage_and_heal_incident(prompt)
802
+ output.extend([
803
+ "",
804
+ "## 🔍 Incident Triage & Auto-Heal Result",
805
+ f"```json\n{triage_res}\n```",
806
+ ])
807
+ else:
808
+ output.extend([
809
+ "",
810
+ "## 🛠️ Available Strands Tools Registered",
811
+ "- `triage_and_heal_incident` (Multi-Language Crash & Traceback Parser)",
812
+ "- `verify_code_file` (Closed-Loop Ground-Truth AST Verifier)",
813
+ "- `apply_surgical_patch` (Surgical Search/Replace Patcher)",
814
+ "- `resolve_git_merge_conflict` (3-Way AST Merge Conflict Resolver)",
815
+ "- `inspect_repo_structure` (AST Repo Symbol Map)",
816
+ "- `search_offline_docs` (Embedded SQLite FTS5 DevDocs)",
817
+ "- `generate_architecture_diagram` (Mermaid Architecture Generator)",
818
+ ])
819
+ return "\n".join(output)
820
+
821
+
822
+ def create_strands_agent(
823
+ provider: str = "auto",
824
+ model_name: Optional[str] = None,
825
+ aws_region: Optional[str] = None,
826
+ ) -> StrandsDevAgent:
827
+ """Convenience helper to create a configured StrandsDevAgent."""
828
+ return StrandsDevAgent(
829
+ provider=provider,
830
+ model_name=model_name,
831
+ aws_region=aws_region,
832
+ )