local-coding-agent 1.1.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 (101) hide show
  1. local_coding_agent/__init__.py +314 -0
  2. local_coding_agent/__main__.py +5 -0
  3. local_coding_agent/acp_server/__init__.py +38 -0
  4. local_coding_agent/acp_server/_codec.py +95 -0
  5. local_coding_agent/acp_server/_protocol.py +13 -0
  6. local_coding_agent/acp_server/_server.py +663 -0
  7. local_coding_agent/acp_server/_session.py +24 -0
  8. local_coding_agent/acp_server/_tools.py +126 -0
  9. local_coding_agent/ast_compactor.py +130 -0
  10. local_coding_agent/atomizer.py +77 -0
  11. local_coding_agent/benchmark/__init__.py +57 -0
  12. local_coding_agent/benchmark/_cases.py +307 -0
  13. local_coding_agent/benchmark/_judge.py +175 -0
  14. local_coding_agent/benchmark/_oracles.py +157 -0
  15. local_coding_agent/benchmark/_runner.py +259 -0
  16. local_coding_agent/benchmark/_stats.py +22 -0
  17. local_coding_agent/benchmark/_summary.py +143 -0
  18. local_coding_agent/benchmark_oracle_worker.py +169 -0
  19. local_coding_agent/calibration.py +154 -0
  20. local_coding_agent/capability.py +450 -0
  21. local_coding_agent/chat_compaction.py +276 -0
  22. local_coding_agent/cli/__init__.py +12 -0
  23. local_coding_agent/cli/_dispatch.py +377 -0
  24. local_coding_agent/cli/_handlers.py +320 -0
  25. local_coding_agent/cli/_handlers2.py +725 -0
  26. local_coding_agent/cli/_input.py +61 -0
  27. local_coding_agent/cli/_main.py +159 -0
  28. local_coding_agent/cli/_parser.py +327 -0
  29. local_coding_agent/context_manager.py +155 -0
  30. local_coding_agent/controller/__init__.py +18 -0
  31. local_coding_agent/controller/_constants.py +108 -0
  32. local_coding_agent/controller/_controller.py +1008 -0
  33. local_coding_agent/controller/_post_apply.py +216 -0
  34. local_coding_agent/delegator.py +180 -0
  35. local_coding_agent/desktop/__init__.py +8 -0
  36. local_coding_agent/desktop/app.py +100 -0
  37. local_coding_agent/desktop/assets/lucide.min.js +12 -0
  38. local_coding_agent/desktop/assets/tailwind.css +1 -0
  39. local_coding_agent/desktop/client_js.py +1271 -0
  40. local_coding_agent/desktop/components.py +555 -0
  41. local_coding_agent/desktop/server/__init__.py +37 -0
  42. local_coding_agent/desktop/server/_handlers.py +2178 -0
  43. local_coding_agent/desktop/server/_models.py +204 -0
  44. local_coding_agent/desktop/server/_server.py +347 -0
  45. local_coding_agent/desktop/server/_telemetry.py +87 -0
  46. local_coding_agent/desktop/styles.py +76 -0
  47. local_coding_agent/desktop/ui.py +53 -0
  48. local_coding_agent/doctor.py +413 -0
  49. local_coding_agent/hooks/__init__.py +19 -0
  50. local_coding_agent/hooks/_adapters.py +458 -0
  51. local_coding_agent/hooks/_bridge.py +346 -0
  52. local_coding_agent/hooks/_decision.py +26 -0
  53. local_coding_agent/lsp/__init__.py +60 -0
  54. local_coding_agent/lsp/_client.py +322 -0
  55. local_coding_agent/lsp/_fallback.py +297 -0
  56. local_coding_agent/lsp/_manager.py +173 -0
  57. local_coding_agent/lsp/_mock.py +200 -0
  58. local_coding_agent/lsp/_types.py +398 -0
  59. local_coding_agent/mcp_config.py +398 -0
  60. local_coding_agent/mcp_server.py +377 -0
  61. local_coding_agent/memory.py +140 -0
  62. local_coding_agent/mode_router.py +200 -0
  63. local_coding_agent/model_scanner.py +525 -0
  64. local_coding_agent/monitor.py +635 -0
  65. local_coding_agent/observation_policy.py +184 -0
  66. local_coding_agent/ollama_adapter.py +736 -0
  67. local_coding_agent/plan_mode.py +653 -0
  68. local_coding_agent/prescriptions.py +278 -0
  69. local_coding_agent/profiles.py +239 -0
  70. local_coding_agent/repository_tools.py +782 -0
  71. local_coding_agent/ripgrep.py +326 -0
  72. local_coding_agent/semantic_linter.py +122 -0
  73. local_coding_agent/service.py +405 -0
  74. local_coding_agent/session_events.py +684 -0
  75. local_coding_agent/session_query.py +578 -0
  76. local_coding_agent/skill_config.py +426 -0
  77. local_coding_agent/smoke.py +268 -0
  78. local_coding_agent/speculative_racing.py +85 -0
  79. local_coding_agent/spill.py +258 -0
  80. local_coding_agent/stats.py +255 -0
  81. local_coding_agent/stdio.py +132 -0
  82. local_coding_agent/subagent.py +516 -0
  83. local_coding_agent/task.py +90 -0
  84. local_coding_agent/task_store.py +142 -0
  85. local_coding_agent/tasks.py +240 -0
  86. local_coding_agent/terminal/__init__.py +35 -0
  87. local_coding_agent/terminal/_manager.py +156 -0
  88. local_coding_agent/terminal/_process.py +148 -0
  89. local_coding_agent/terminal/_session.py +452 -0
  90. local_coding_agent/terminal/_tools.py +233 -0
  91. local_coding_agent/terminal/_types.py +60 -0
  92. local_coding_agent/tool_call_parser.py +178 -0
  93. local_coding_agent/validators.py +563 -0
  94. local_coding_agent/vram_fit.py +240 -0
  95. local_coding_agent/worker_pool.py +516 -0
  96. local_coding_agent-1.1.0.dist-info/METADATA +202 -0
  97. local_coding_agent-1.1.0.dist-info/RECORD +101 -0
  98. local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
  99. local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
  100. local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
  101. local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,76 @@
1
+ """CSS stylesheets, Tailwind configuration, and design tokens for Desktop AI Coding Harness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ DESKTOP_CSS = """
6
+ * { box-sizing: border-box; }
7
+
8
+ :root.dark {
9
+ --bg-app: #09090b;
10
+ --bg-header: #0e0e11;
11
+ --bg-sidebar: #0e0e11;
12
+ --bg-card: #131316;
13
+ --bg-card-subtle: #18181b;
14
+ --bg-input: #09090b;
15
+ --border-main: #27272a;
16
+ --border-subtle: #1f1f23;
17
+ --text-main: #f4f4f5;
18
+ --text-muted: #a1a1aa;
19
+ --text-subtle: #71717a;
20
+ --diff-add-bg: rgba(16, 185, 129, 0.12);
21
+ --diff-add-text: #86efac;
22
+ --diff-del-bg: rgba(239, 68, 68, 0.12);
23
+ --diff-del-text: #fca5a5;
24
+ }
25
+
26
+ :root:not(.dark) {
27
+ --bg-app: #fbfbfb;
28
+ --bg-header: #ffffff;
29
+ --bg-sidebar: #f4f4f5;
30
+ --bg-card: #ffffff;
31
+ --bg-card-subtle: #f4f4f5;
32
+ --bg-input: #ffffff;
33
+ --border-main: #e4e4e7;
34
+ --border-subtle: #ebebef;
35
+ --text-main: #09090b;
36
+ --text-muted: #52525b;
37
+ --text-subtle: #71717a;
38
+ --diff-add-bg: rgba(16, 185, 129, 0.10);
39
+ --diff-add-text: #059669;
40
+ --diff-del-bg: rgba(239, 68, 68, 0.10);
41
+ --diff-del-text: #dc2626;
42
+ }
43
+
44
+ body {
45
+ background-color: var(--bg-app);
46
+ color: var(--text-main);
47
+ font-feature-settings: "cv02", "cv03", "cv04", "cv11";
48
+ -webkit-font-smoothing: antialiased;
49
+ -moz-osx-font-smoothing: grayscale;
50
+ }
51
+
52
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
53
+ ::-webkit-scrollbar-track { background: transparent; }
54
+ ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 2px; }
55
+ :root:not(.dark) ::-webkit-scrollbar-thumb { background: #d4d4d8; }
56
+
57
+ .diff-del-bg { background-color: var(--diff-del-bg); color: var(--diff-del-text); }
58
+ .diff-add-bg { background-color: var(--diff-add-bg); color: var(--diff-add-text); }
59
+ .num-tabular { font-variant-numeric: tabular-nums; }
60
+
61
+ @keyframes spin {
62
+ from { transform: rotate(0deg); }
63
+ to { transform: rotate(360deg); }
64
+ }
65
+ .animate-spin-fast {
66
+ animation: spin 0.6s linear infinite;
67
+ }
68
+
69
+ @keyframes pulse-subtle {
70
+ 0%, 100% { opacity: 1; }
71
+ 50% { opacity: 0.5; }
72
+ }
73
+ .animate-pulse-subtle {
74
+ animation: pulse-subtle 1.8s ease-in-out infinite;
75
+ }
76
+ """
@@ -0,0 +1,53 @@
1
+ """Embedded production-grade HTML template and dynamic assembler for Desktop AI Coding Harness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from .client_js import DESKTOP_CLIENT_JS
8
+ from .components import (
9
+ render_chat_panel,
10
+ render_delegated_panel,
11
+ render_header,
12
+ render_modals,
13
+ render_sidebar,
14
+ render_toast,
15
+ )
16
+ from .styles import DESKTOP_CSS
17
+
18
+
19
+ def render_desktop_html(mutation_token: str = "") -> str:
20
+ """Assemble complete standalone single-page HTML application from modular components."""
21
+ parts = [
22
+ "<!DOCTYPE html>",
23
+ '<html lang="en" class="dark">',
24
+ "<head>",
25
+ ' <meta charset="UTF-8">',
26
+ ' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
27
+ " <title>Local AI Coding Harness</title>",
28
+ ' <link rel="stylesheet" href="/assets/tailwind.css">',
29
+ ' <script src="/assets/lucide.min.js"></script>',
30
+ " <style>",
31
+ DESKTOP_CSS,
32
+ " </style>",
33
+ "</head>",
34
+ '<body class="h-screen flex flex-col font-sans tracking-tight overflow-hidden">',
35
+ render_header(),
36
+ ' <main class="flex-1 flex overflow-hidden relative">',
37
+ render_sidebar(),
38
+ render_chat_panel(),
39
+ render_delegated_panel(),
40
+ " </main>",
41
+ render_modals(),
42
+ render_toast(),
43
+ f" <script>window.DESKTOP_MUTATION_TOKEN = {json.dumps(mutation_token)};</script>",
44
+ " <script>",
45
+ DESKTOP_CLIENT_JS,
46
+ " </script>",
47
+ "</body>",
48
+ "</html>",
49
+ ]
50
+ return "\n".join(parts)
51
+
52
+
53
+ DESKTOP_HTML_TEMPLATE = render_desktop_html()
@@ -0,0 +1,413 @@
1
+ """Diagnostic wizard and environment validator for local-coding-agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import os
7
+ import platform
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ from dataclasses import asdict, dataclass, field
13
+ from typing import Any, Sequence
14
+
15
+ from .ollama_adapter import OllamaClient
16
+ from .profiles import list_profiles
17
+
18
+ RECOMMENDED_PROFILES = [
19
+ {
20
+ "name": "qwen3-8b-q6k",
21
+ "model": "qwen3-8b-q6k:latest",
22
+ "description": "Recommended for 8-12 GB VRAM (Ultra-fast, high precision coding)",
23
+ "pull_cmd": "ollama run hf.co/unsloth/Qwen3-8B-GGUF:Q6_K",
24
+ },
25
+ {
26
+ "name": "qwen3.8-27b-q4",
27
+ "model": "qwen3.8-27b-q4:latest",
28
+ "description": "Recommended for 16-24 GB VRAM (Maximum capability & architecture reasoning)",
29
+ "pull_cmd": "ollama run hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M",
30
+ },
31
+ {
32
+ "name": "qwen2.5-coder",
33
+ "model": "qwen2.5-coder:latest",
34
+ "description": "Standard coding workhorse (7B / 14B)",
35
+ "pull_cmd": "ollama pull qwen2.5-coder",
36
+ },
37
+ ]
38
+
39
+
40
+ @dataclass
41
+ class CheckResult:
42
+ name: str
43
+ status: str # "ok" | "warn" | "fail"
44
+ message: str
45
+ details: dict[str, Any] = field(default_factory=dict)
46
+
47
+ @property
48
+ def is_ok(self) -> bool:
49
+ return self.status == "ok"
50
+
51
+ @property
52
+ def is_warn(self) -> bool:
53
+ return self.status == "warn"
54
+
55
+ @property
56
+ def is_fail(self) -> bool:
57
+ return self.status == "fail"
58
+
59
+
60
+ @dataclass
61
+ class DoctorReport:
62
+ healthy: bool
63
+ checks: list[CheckResult]
64
+ models: dict[str, Any]
65
+ system_info: dict[str, Any]
66
+
67
+ @property
68
+ def is_healthy(self) -> bool:
69
+ return self.healthy
70
+
71
+ def to_dict(self) -> dict[str, Any]:
72
+ return {
73
+ "healthy": self.healthy,
74
+ "checks": [asdict(c) for c in self.checks],
75
+ "models": self.models,
76
+ "system_info": self.system_info,
77
+ }
78
+
79
+ def render_text(self) -> str:
80
+ lines = []
81
+ lines.append("=" * 60)
82
+ lines.append(" Local Coding Agent — System Diagnostic Wizard")
83
+ lines.append("=" * 60)
84
+ lines.append("")
85
+
86
+ for check in self.checks:
87
+ tag = "[OK] " if check.status == "ok" else ("[WARN]" if check.status == "warn" else "[FAIL]")
88
+ lines.append(f"{tag} {check.name}: {check.message}")
89
+ if check.details:
90
+ for k, v in check.details.items():
91
+ lines.append(f" - {k}: {v}")
92
+
93
+ lines.append("")
94
+ lines.append("-" * 60)
95
+ lines.append(" Installed vs Recommended Models")
96
+ lines.append("-" * 60)
97
+
98
+ installed = self.models.get("installed", [])
99
+ if installed:
100
+ lines.append(f"Installed Ollama Models ({len(installed)}):")
101
+ for m in installed:
102
+ lines.append(f" • {m.get('name', 'unknown')}")
103
+ else:
104
+ lines.append("No models found in Ollama.")
105
+
106
+ missing = self.models.get("missing", [])
107
+ if missing:
108
+ lines.append("")
109
+ lines.append("Recommended Models To Install:")
110
+ for m in missing:
111
+ lines.append(f" • {m['name']} — {m['description']}")
112
+ lines.append(f" Command: {m['pull_cmd']}")
113
+
114
+ lines.append("")
115
+ lines.append("=" * 60)
116
+ if self.healthy:
117
+ lines.append(" Overall Status: READY (All critical checks passed)")
118
+ else:
119
+ lines.append(" Overall Status: ATTENTION REQUIRED (Check errors above)")
120
+ lines.append("=" * 60)
121
+ return "\n".join(lines)
122
+
123
+
124
+ def check_git_installed() -> CheckResult:
125
+ try:
126
+ proc = subprocess.run(
127
+ ["git", "--version"],
128
+ capture_output=True,
129
+ text=True,
130
+ check=False,
131
+ timeout=5,
132
+ )
133
+ if proc.returncode == 0:
134
+ version_str = proc.stdout.strip()
135
+ git_path = shutil.which("git") or "in PATH"
136
+ return CheckResult(
137
+ name="Git Executable",
138
+ status="ok",
139
+ message=f"{version_str} ({git_path})",
140
+ details={"version": version_str, "path": git_path},
141
+ )
142
+ return CheckResult(
143
+ name="Git Executable",
144
+ status="fail",
145
+ message=f"Git returned non-zero exit code: {proc.returncode}",
146
+ )
147
+ except Exception as exc:
148
+ return CheckResult(
149
+ name="Git Executable",
150
+ status="fail",
151
+ message=f"Git is not found or not accessible in PATH: {exc}",
152
+ )
153
+
154
+
155
+ def check_ollama_api(endpoint: str = "http://127.0.0.1:11434") -> tuple[CheckResult, list[str]]:
156
+ try:
157
+ from .profiles import ModelProfile
158
+ dummy_profile = ModelProfile(
159
+ name="check",
160
+ model="check",
161
+ endpoint=endpoint,
162
+ think=False,
163
+ temperature=0.0,
164
+ num_ctx=2048,
165
+ num_predict=128,
166
+ keep_alive="1m",
167
+ max_context_length=8192,
168
+ )
169
+ client = OllamaClient(dummy_profile)
170
+ start = time.perf_counter()
171
+ data = client.available_models()
172
+ latency_ms = round((time.perf_counter() - start) * 1000, 1)
173
+
174
+ raw_models = data.get("models", [])
175
+ names = [
176
+ m.get("name")
177
+ for m in raw_models
178
+ if isinstance(m, dict) and isinstance(m.get("name"), str)
179
+ ]
180
+ return CheckResult(
181
+ name="Ollama API",
182
+ status="ok",
183
+ message=f"Connected to {endpoint} (latency: {latency_ms}ms, {len(names)} models)",
184
+ details={"endpoint": endpoint, "latency_ms": latency_ms, "model_count": len(names)},
185
+ ), names
186
+ except Exception as exc:
187
+ return CheckResult(
188
+ name="Ollama API",
189
+ status="fail",
190
+ message=f"Failed to connect to Ollama at {endpoint}: {exc}",
191
+ details={"endpoint": endpoint, "error": str(exc)},
192
+ ), []
193
+
194
+
195
+ def check_host_memory() -> CheckResult:
196
+ total_ram_gb: float | None = None
197
+ free_ram_gb: float | None = None
198
+
199
+ try:
200
+ if sys.platform == "win32":
201
+ class MEMORYSTATUSEX(ctypes.Structure):
202
+ _fields_ = [
203
+ ("dwLength", ctypes.c_ulong),
204
+ ("dwMemoryLoad", ctypes.c_ulong),
205
+ ("ullTotalPhys", ctypes.c_ulonglong),
206
+ ("ullAvailPhys", ctypes.c_ulonglong),
207
+ ("ullTotalPageFile", ctypes.c_ulonglong),
208
+ ("ullAvailPageFile", ctypes.c_ulonglong),
209
+ ("ullTotalVirtual", ctypes.c_ulonglong),
210
+ ("ullAvailVirtual", ctypes.c_ulonglong),
211
+ ("ullAvailExtendedVirtual", ctypes.c_ulonglong),
212
+ ]
213
+ stat = MEMORYSTATUSEX()
214
+ stat.dwLength = ctypes.sizeof(stat)
215
+ ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
216
+ total_ram_gb = round(stat.ullTotalPhys / (1024 ** 3), 1)
217
+ free_ram_gb = round(stat.ullAvailPhys / (1024 ** 3), 1)
218
+ elif sys.platform == "linux" and os.path.exists("/proc/meminfo"):
219
+ mem_info: dict[str, int] = {}
220
+ with open("/proc/meminfo", "r", encoding="utf-8") as f:
221
+ for line in f:
222
+ parts = line.split(":")
223
+ if len(parts) == 2:
224
+ key = parts[0].strip()
225
+ val = parts[1].strip().split()[0]
226
+ if val.isdigit():
227
+ mem_info[key] = int(val)
228
+ if "MemTotal" in mem_info and "MemAvailable" in mem_info:
229
+ total_ram_gb = round(mem_info["MemTotal"] / (1024 ** 2), 1)
230
+ free_ram_gb = round(mem_info["MemAvailable"] / (1024 ** 2), 1)
231
+ except Exception:
232
+ pass
233
+
234
+ if total_ram_gb is not None:
235
+ status = "ok" if (free_ram_gb or 0) >= 4.0 else "warn"
236
+ msg = f"{total_ram_gb} GB total ({free_ram_gb} GB available)"
237
+ return CheckResult(
238
+ name="Host Memory",
239
+ status=status,
240
+ message=f"RAM: {msg}",
241
+ details={"total_ram_gb": total_ram_gb, "available_ram_gb": free_ram_gb},
242
+ )
243
+ return CheckResult(
244
+ name="Host Memory",
245
+ status="ok",
246
+ message="RAM metrics detected",
247
+ )
248
+
249
+
250
+ def recommend_models(installed_names: Sequence[str]) -> dict[str, Any]:
251
+ installed_list = []
252
+ missing_list = []
253
+
254
+ installed_lower = {name.lower() for name in installed_names}
255
+
256
+ for rec in RECOMMENDED_PROFILES:
257
+ target_name = rec["name"].lower()
258
+ target_model = rec["model"].lower()
259
+ # Recommendation status is based on exact served tags or an explicit
260
+ # untagged profile alias, never a broad substring (e.g. 0.6B must not
261
+ # satisfy an 8B recommendation).
262
+ is_present = target_name in installed_lower or target_model in installed_lower
263
+ if not is_present and ":" not in target_model:
264
+ aliases = {
265
+ installed.rsplit(":", 1)[0]
266
+ for installed in installed_lower
267
+ if installed.rsplit(":", 1)[0] == target_name
268
+ }
269
+ is_present = len(aliases) == 1
270
+ if is_present:
271
+ installed_list.append(rec)
272
+ else:
273
+ missing_list.append(rec)
274
+
275
+ return {
276
+ "installed": [{"name": n} for n in installed_names],
277
+ "recommended_installed": installed_list,
278
+ "missing": missing_list,
279
+ }
280
+
281
+
282
+ def diagnose_environment(endpoint: str = "http://127.0.0.1:11434") -> DoctorReport:
283
+ checks = []
284
+
285
+ # 1. Python version check
286
+ py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
287
+ py_ok = sys.version_info >= (3, 10)
288
+ checks.append(
289
+ CheckResult(
290
+ name="Python Runtime",
291
+ status="ok" if py_ok else "fail",
292
+ message=f"Python {py_ver} ({platform.platform()})",
293
+ details={"version": py_ver, "executable": sys.executable},
294
+ )
295
+ )
296
+
297
+ # 2. Git check
298
+ git_check = check_git_installed()
299
+ checks.append(git_check)
300
+
301
+ # 3. Host Memory check
302
+ mem_check = check_host_memory()
303
+ checks.append(mem_check)
304
+
305
+ # 4. Ollama API check
306
+ ollama_check, installed_models = check_ollama_api(endpoint)
307
+ checks.append(ollama_check)
308
+
309
+ # Model catalog & recommendations
310
+ models_rec = recommend_models(installed_models)
311
+
312
+ critical_failures = [c for c in checks if c.is_fail]
313
+ healthy = len(critical_failures) == 0
314
+
315
+ return DoctorReport(
316
+ healthy=healthy,
317
+ checks=checks,
318
+ models=models_rec,
319
+ system_info={
320
+ "platform": platform.platform(),
321
+ "python": py_ver,
322
+ "executable": sys.executable,
323
+ },
324
+ )
325
+
326
+
327
+ @dataclass
328
+ class DoctorFixReport:
329
+ success: bool
330
+ actions: list[str] = field(default_factory=list)
331
+ recommendations: list[str] = field(default_factory=list)
332
+ errors: list[str] = field(default_factory=list)
333
+
334
+ def to_dict(self) -> dict[str, Any]:
335
+ return {
336
+ "success": self.success,
337
+ "actions": self.actions,
338
+ "recommendations": self.recommendations,
339
+ "errors": self.errors,
340
+ }
341
+
342
+ def render_text(self) -> str:
343
+ lines = [
344
+ "=" * 60,
345
+ " Local Coding Agent — System Remediation Wizard (--fix)",
346
+ "=" * 60,
347
+ "",
348
+ "Actions Taken / Configured:",
349
+ ]
350
+ if self.actions:
351
+ for act in self.actions:
352
+ lines.append(f" [OK] {act}")
353
+ else:
354
+ lines.append(" (No actions needed)")
355
+
356
+ if self.errors:
357
+ lines.extend(["", "Failures:"])
358
+ lines.extend(f" [FAIL] {error}" for error in self.errors)
359
+
360
+ lines.extend([
361
+ "",
362
+ "-" * 60,
363
+ " Recommended Model Pulls (Run in Terminal):",
364
+ "-" * 60,
365
+ ])
366
+ for rec in self.recommendations:
367
+ lines.append(f" $ {rec}")
368
+ lines.append("")
369
+ return "\n".join(lines)
370
+
371
+
372
+ def remediate_environment(
373
+ endpoint: str = "http://127.0.0.1:11434",
374
+ write: bool = True,
375
+ ) -> DoctorFixReport:
376
+ """Remediate environment by configuring MCP, exporting skills, and recommending models."""
377
+ from .mcp_config import integrate_mcp_config
378
+ from .skill_config import integrate_skill_config
379
+
380
+ actions: list[str] = []
381
+ errors: list[str] = []
382
+
383
+ # 1. MCP Configuration for detected IDEs
384
+ mcp_res = integrate_mcp_config(client="all", dry_run=not write)
385
+ for sub in mcp_res.get("results", []):
386
+ if sub.get("error"):
387
+ errors.append(f"{sub.get('client', 'unknown')}: {sub['error']}")
388
+ continue
389
+ tag = "Applied MCP config" if write else "Previewed MCP config"
390
+ actions.append(f"{tag} for {sub.get('client')}: {sub.get('path')}")
391
+
392
+ # 2. Skill export for agents
393
+ skill_res = integrate_skill_config(client="auto", dry_run=not write)
394
+ for sub in skill_res.get("results", []):
395
+ if sub.get("error"):
396
+ errors.append(f"{sub.get('client', 'unknown')}: {sub['error']}")
397
+ continue
398
+ tag = "Installed Agent Skill" if write else "Previewed Agent Skill"
399
+ actions.append(f"{tag} for {sub.get('client')}: {sub.get('path')}")
400
+
401
+ # 3. Model pull prescriptions
402
+ recommendations: list[str] = [
403
+ "ollama run hf.co/unsloth/Qwen3-8B-GGUF:Q6_K",
404
+ "ollama run hf.co/unsloth/Qwen3.8-27B-GGUF:Q4_K_M",
405
+ "ollama pull qwen2.5-coder:latest",
406
+ ]
407
+
408
+ return DoctorFixReport(
409
+ success=not errors,
410
+ actions=actions,
411
+ recommendations=recommendations,
412
+ errors=errors,
413
+ )
@@ -0,0 +1,19 @@
1
+ """External agent hook bridges and protocol adapters (R30).
2
+
3
+ Provides lifecycle hook interception points (on_pre_tool_call, on_post_tool_call,
4
+ on_turn_start, on_turn_end, on_session_finish) and wire-protocol adapters compatible
5
+ with Codex and Claude Code hook standards.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ._adapters import ClaudeCodeHookAdapter, CodexHookAdapter
11
+ from ._bridge import HookBridge
12
+ from ._decision import HookDecision
13
+
14
+ __all__ = [
15
+ "ClaudeCodeHookAdapter",
16
+ "CodexHookAdapter",
17
+ "HookBridge",
18
+ "HookDecision",
19
+ ]