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,426 @@
1
+ """Skill configuration and installer for AI coding agents.
2
+
3
+ Integrates SKILL.md into Codex, Antigravity, Claude, and workspace skill directories.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ _EMBEDDED_SKILL_MD = """---
14
+ name: local-coding-agent
15
+ description: Offload and delegate atomic coding tasks, bug fixes, refactoring, and unit test implementations to local Ollama LLMs via MCP (delegate_code, apply_proposal) or direct CLI.
16
+ ---
17
+
18
+ # Local Coding Agent — AI Agent Delegation Skill
19
+
20
+ ## Overview
21
+
22
+ The `local-coding-agent` is a bounded, proposal-only controller designed to offload small, deterministic coding tasks to local LLMs (such as Qwen 2.5 Coder, Qwen3, Devstral, and Gemma) running via Ollama or OpenAI-compatible local endpoints.
23
+
24
+ ### Core Security & Architecture Invariants
25
+ - **Proposal-Only by Default**: The local model never directly writes to disk or executes arbitrary shell commands. It only generates structured diffs (`patch`) or search/replace blocks (`edits`).
26
+ - **External Test Verification**: Test results are never self-reported by the model; they are executed and verified strictly by the external runner.
27
+ - **Bounded Scope**: Context and files are strictly allowlisted. Tasks exceeding budget limits must be decomposed into atomic child subtasks.
28
+ - **Mediated Apply with Auto-Rollback**: Applying patches (`apply_proposal` or `--apply`) verifies the diff, applies it, re-runs all targeted test checks, and automatically rolls back if any test fails.
29
+ - **100% CLI Parity**: Every feature available via MCP is equally accessible via CLI subcommands for non-MCP agents and terminal workflows.
30
+
31
+ ---
32
+
33
+ ## Delegation Decision Matrix
34
+
35
+ Use this matrix to determine whether to delegate a task to the local model:
36
+
37
+ | Task Type | Delegate? | Reason / Action |
38
+ |---|---|---|
39
+ | Single-function bug fix | **YES** | Perfect fit. Provide targeted test check and allowlist 1 file. |
40
+ | Writing unit tests | **YES** | Provide the target implementation file + test file in `files`. |
41
+ | Localized refactor | **YES** | Clear acceptance criteria and targeted test runner command. |
42
+ | Edge-case handling | **YES** | Explicit constraints and expected inputs/outputs in `context`. |
43
+ | Cross-repo architecture | **NO** | Solve yourself. Too broad for bounded local model context. |
44
+ | Unknown / exploratory debugging | **NO** | First inspect and isolate the issue, then delegate the isolated fix. |
45
+ | Task with no test / verification check | **NO** | Provide a check command or solve directly. |
46
+
47
+ ---
48
+
49
+ ## MCP Tools Reference
50
+
51
+ When the `local-coding-agent` MCP server is active in your environment, two primary tools are available:
52
+
53
+ ### 1. `delegate_code`
54
+ Delegates one atomic, proposal-only coding task to a local Ollama model.
55
+
56
+ #### Parameters:
57
+ - `request_id` *(string, required)*: A unique caller-generated idempotency key (e.g. `task-fix-auth-01`).
58
+ - `workspace_ref` *(string, required)*: The registered workspace identifier (usually `"workspace"`).
59
+ - `model_profile` *(string, required)*: Named profile (e.g. `"qwen2.5-coder"`, `"qwen3-8b-q6k"`, `"devstral-small-2-24b"`).
60
+ - `task` *(object, required)*: The structured task envelope:
61
+ - `id` *(string)*: Short descriptive identifier (e.g. `"fix-null-dereference"`).
62
+ - `goal` *(string)*: 1 concise sentence describing the goal.
63
+ - `files` *(array of strings)*: List of allowlisted relative file paths (1 to 5 files, ideally 1-2).
64
+ - `context` *(string)*: Compact relevant context (types, function signatures, error traces). Do **not** dump entire irrelevant files.
65
+ - `constraints` *(array of strings)*: Guardrails (e.g. `["do not add external dependencies", "preserve public signature"]`).
66
+ - `checks` *(array of strings)*: Targeted test commands (e.g. `["pytest tests/test_auth.py -k test_null"]`). Non-empty patches require at least 1 check.
67
+ - `acceptance` *(array of strings)*: Bullet points describing acceptance criteria.
68
+
69
+ #### Example MCP Invocation:
70
+ ```json
71
+ {
72
+ "request_id": "req-20260818-unique-01",
73
+ "workspace_ref": "workspace",
74
+ "model_profile": "qwen2.5-coder",
75
+ "task": {
76
+ "id": "unique-preserve-order",
77
+ "goal": "Remove sorting from unique function and preserve first-seen insertion order",
78
+ "files": ["src/unique.py"],
79
+ "context": "def unique(items: list[Any]) -> list[Any]:\\n # currently does sorted(set(items))",
80
+ "constraints": [
81
+ "preserve public signature of unique()",
82
+ "do not add external dependencies"
83
+ ],
84
+ "checks": [
85
+ "pytest tests/test_unique.py"
86
+ ],
87
+ "acceptance": [
88
+ "diff modifies only src/unique.py",
89
+ "sorting logic is replaced with order-preserving deduplication",
90
+ "targeted test passes"
91
+ ]
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### 2. `apply_proposal`
97
+ Safely applies a previously accepted proposal to the workspace after verifying confirmation.
98
+
99
+ #### Parameters:
100
+ - `request_id` *(string, required)*: The request ID of the accepted proposal.
101
+ - `workspace_ref` *(string, required)*: The target workspace.
102
+ - `confirmation` *(object)*: Standard MCP elicitation confirmation containing `confirm: true`, `proposal_id`, `workspace_ref`, and `proposal_digest`.
103
+
104
+ ---
105
+
106
+ ## Interpreting Result Envelopes
107
+
108
+ The return value is a controller-owned JSON object:
109
+
110
+ ```json
111
+ {
112
+ "status": "accepted",
113
+ "summary": "Replaced sorted set with dict.fromkeys to preserve insertion order",
114
+ "patch": "--- a/src/unique.py\\n+++ b/src/unique.py\\n@@ -5,2 +5,2 @@\\n- return sorted(list(set(items)))\\n+ return list(dict.fromkeys(items))\\n",
115
+ "checks": [
116
+ {
117
+ "command": "pytest tests/test_unique.py",
118
+ "passed": true,
119
+ "evidence": "1 passed in 0.04s"
120
+ }
121
+ ],
122
+ "risks": [],
123
+ "validation": {
124
+ "syntax_valid": true,
125
+ "allowlist_clean": true
126
+ },
127
+ "applied": false
128
+ }
129
+ ```
130
+
131
+ ### Result Statuses:
132
+ - **`accepted`**: Valid diff generated, all targeted checks executed and passed.
133
+ - **`rejected`**: The proposal failed syntax validation, touched non-allowlisted files, or failed targeted test checks. Check `error` and `risks`.
134
+ - **`needs_context`**: The local model requested clarification. Inspect the question, refine `context`, and retry.
135
+ - **`failed`**: Controller policy violation, timeout, or queue overload.
136
+
137
+ ---
138
+
139
+ ## Model Profile Selection & Multi-Runtime Support
140
+
141
+ The controller is runtime-agnostic and natively supports two local inference backends:
142
+ 1. **Ollama Runtime** (`provider="ollama"`, default: `http://127.0.0.1:11434`): Standard native Ollama API with VRAM introspection and dynamic model loading/unloading.
143
+ 2. **llama.cpp / llama-server & OpenAI-Compatible** (`provider="openai"`, default: `http://127.0.0.1:8080`): Standalone `llama-server`, vLLM, or LM Studio endpoints providing `/v1/chat/completions` with exact timing measurements (`prompt_ms`, `predicted_ms`) and tool-calling support.
144
+
145
+ ### Model Tiers:
146
+
147
+ | Model Tier / Class | Memory / VRAM | Example Profiles / Runtimes | When to Choose |
148
+ |---|---|---|---|
149
+ | **Ultra-Lightweight (1B-3B)** | 2-4 GB VRAM / CPU | `qwen2.5-1.5b`, `gemma4-e2b-q4`, `ling-3.0-tiny-q6k` (llama-server) | Trivial fixes, single-line edits, docstrings, type annotations, ultra-fast response (<2s). |
150
+ | **Standard Workhorse (7B-14B)** | 6-12 GB VRAM | `qwen2.5-coder`, `qwen3-8b-q6k`, `qwen2.5-coder-7b-q4` | **Default choice.** Standard functions, bug fixes, localized refactoring, unit test generation. |
151
+ | **Advanced Reasoning (24B-32B)** | 16-24 GB VRAM | `devstral-small-2-24b`, `qwen3.8-27b-q4`, `qwen3-coder-30b` | Complex algorithms, subtle edge cases, multi-step logic, difficult refactors. |
152
+ | **llama-server / Custom OpenAI** | Any GGUF / GPU | `ling-3.0-tiny-q6k` (llama-server:8080), custom vLLM | Standalone `llama-server` instances or custom OpenAI-compatible proxies. |
153
+
154
+ ### Dynamic Discovery Rule
155
+ Never guess or hardcode installed models. Determine host capabilities dynamically:
156
+ - **Discover active & installed profiles**: Run `local-agent profiles list --check-ollama --json` or `local-agent doctor --json`.
157
+ - **Targeting llama-server**: Specify `--endpoint http://127.0.0.1:8080` or choose an OpenAI-provider profile.
158
+ - **Default Profile**: If unspecified, use the default configured profile (`qwen2.5-coder` or the MCP server default).
159
+
160
+
161
+
162
+
163
+ ---
164
+
165
+ ## Error Handling & Task Decomposition
166
+
167
+ ### 1. Preflight Budget Violations (`too_many_files`, `context_too_large`)
168
+ If a task touches more than 5 files or context exceeds 32KB, decompose the task into child subtasks using the decomposition tool:
169
+ ```bash
170
+ local-agent decompose --task task.json --json
171
+ ```
172
+ Or split the task into independent child envelopes per file.
173
+
174
+ ### 2. Pinpointed Prescriptions
175
+ When a small local model produces a syntax or diff formatting error, the Pinpointed Prescriptions Engine returns a laser-focused deterministic hint (e.g. exact SEARCH/REPLACE alignment rules). Pass this hint directly in `constraints` on retry.
176
+
177
+ ---
178
+
179
+ ## CLI Console Control (Fool-Proof Fallback for Any Agent / Shell)
180
+
181
+ For AI agents operating in shell environments without MCP tools, or for direct script automation, every capability is directly available via `local-agent` / `python -m local_coding_agent`:
182
+
183
+ ### Delegate a Task (Single or Speculative Multi-Draft Racing):
184
+ ```bash
185
+ local-agent delegate --task '{"id":"fix-1","goal":"Fix bug","files":["src/foo.py"],"checks":["pytest tests/test_foo.py"]}' --json
186
+ # Or run speculative racing between 2 drafts:
187
+ local-agent delegate --task task.json --speculative-drafts 2 --json
188
+ # Or target ANY installed model tag without registering a profile (--model override):
189
+ local-agent delegate --task task.json --model qwen2.5-coder:7b --json
190
+ ```
191
+ Accepted/candidate patches are also persisted to `.local-run/proposals/<task_id>.patch`
192
+ and referenced as `patch_file` in the JSON output. Environment defaults: `LCA_PROFILE`
193
+ (default `qwen2.5-coder`), `LCA_WORKSPACE`.
194
+
195
+ ### Apply Patch Directly:
196
+ ```bash
197
+ local-agent delegate --task task.json --apply --json
198
+ # Or apply a raw patch with verification (`--file` declares the scope and is
199
+ # repeatable; the patch must touch only those files, and at least one `--check`
200
+ # is mandatory — apply without a check is refused):
201
+ local-agent apply --patch-file changes.diff --workspace . --file src/foo.py --check "pytest tests/" --json
202
+ ```
203
+
204
+ ### Fast Semantic Linter Pre-Gate:
205
+ ```bash
206
+ local-agent lint-patch --patch-file changes.diff --workspace . --json
207
+ ```
208
+
209
+ ### AST-Guided Skeletonization:
210
+ ```bash
211
+ local-agent skeletonize src/large_module.py --symbol target_function --json
212
+ ```
213
+
214
+ ### Decompose a Broad Task:
215
+ ```bash
216
+ local-agent decompose --task-file wide_task.json --strategy by_files --json
217
+ ```
218
+
219
+ ### Manage Profiles:
220
+ ```bash
221
+ local-agent profiles list --json
222
+ local-agent profiles get qwen2.5-coder --json
223
+ ```
224
+
225
+ ### Manage Ollama Memory & VRAM:
226
+ ```bash
227
+ local-agent memory status --json
228
+ local-agent memory unload qwen2.5-coder:latest --json
229
+ local-agent memory enforce --limit 12884901888 --keep qwen3-8b-q6k:latest --json
230
+ ```
231
+
232
+ ### Check System & Remediate Environment:
233
+ ```bash
234
+ local-agent doctor --json
235
+ # Or auto-remediate missing MCP configs, Agent Skills, and setup:
236
+ local-agent doctor --fix
237
+ ```
238
+
239
+ ### Launch Desktop AI Coding Harness (R23 Cockpit):
240
+ ```bash
241
+ local-agent desktop --port 8767
242
+ # Or force open in system browser:
243
+ local-agent desktop --browser
244
+ ```
245
+
246
+ ### Tool Output Spill Store & Fast Ripgrep (R24):
247
+ ```bash
248
+ # Read or paginate oversized spill artifact:
249
+ local-agent spill-read locator:spill:session_123/456_output.txt --offset 0 --limit 100 --json
250
+
251
+ # Fast ripgrep search across workspace:
252
+ local-agent grep "def my_func" src/*.py --json
253
+ ```
254
+
255
+ ### LSP Code Intelligence & Navigation (R25):
256
+ ```bash
257
+ # Extract document symbols (classes, functions, methods):
258
+ local-agent lsp --operation symbols --file src/service.py --json
259
+
260
+ # Jump to symbol definition:
261
+ local-agent lsp --operation definition --file src/app.py --line 42 --char 15 --json
262
+ ```
263
+
264
+ ### Interactive Chat with Mode Routing (R31):
265
+ ```bash
266
+ # Single-shot classified chat. Mode defaults to hybrid (auto-router to chat/build/plan):
267
+ local-agent chat "fix off-by-one in sliding window" --json
268
+ # Force a specific mode:
269
+ local-agent chat "refactor calculate_total to integer cents" --mode build --json
270
+ local-agent chat "explain what main.py does" --mode chat --json
271
+ local-agent chat "plan the migration to the new API" --mode plan --json
272
+ ```
273
+ The hybrid mode routes each prompt to `chat` (plain completion), `build` (Controller
274
+ tool-loop → patch + external test evidence), or `plan` (read-only exploration →
275
+ `PlanArtifact` with goal/steps/risks/files_to_modify). A small local model
276
+ (`qwen2.5-1.5b` or `ling-3.0-tiny-q6k`) is consulted in an isolated context every
277
+ N requests to auto-select the mode; on failure it falls back to deterministic
278
+ heuristics. The desktop harness (`local-agent desktop`) exposes the same four
279
+ modes via the Chat/Build/Plan/Auto selector.
280
+
281
+ ### Multi-Turn Chat REPL & Persistent Sessions:
282
+ ```bash
283
+ # Interactive multi-turn REPL with persistent session history:
284
+ local-agent chat --repl
285
+ # Create/resume a named session:
286
+ local-agent chat --repl --session-id my-session
287
+ # List persisted sessions or show a full event transcript:
288
+ local-agent sessions list
289
+ local-agent sessions show my-session
290
+ ```
291
+
292
+ ### Install / Export Agent Skill:
293
+ ```bash
294
+ local-agent init-skill --write
295
+ ```
296
+
297
+ ### Wire Cloud Agents to Local Models (OpenAI-compatible /v1):
298
+ The Desktop Harness exposes an OpenAI-compatible router at `/v1` that proxies
299
+ `/v1/models` and `/v1/chat/completions` to whichever local backend serves the
300
+ requested model (resident llama-server first, then Ollama). Point any
301
+ OpenAI-compatible cloud agent at your local models:
302
+ ```bash
303
+ # Preview the Codex provider config (writes ~/.codex/config.toml with --write):
304
+ local-agent init-agent --agent codex --model qwen2.5-coder:latest
305
+ # Generic OpenAI-compatible clients: prints OPENAI_BASE_URL / OPENAI_API_KEY env:
306
+ local-agent init-agent --agent generic --json
307
+ # Launch Codex against local models afterwards:
308
+ codex -c model_provider=local_agent -m "qwen2.5-coder:latest"
309
+ ```
310
+ Prerequisite: `local-agent desktop` must be running and a model loaded in its
311
+ Local Inference Servers panel. Requests for models no backend serves receive a
312
+ prescriptive 404 listing available model ids.
313
+ """
314
+
315
+
316
+
317
+ def get_skill_content(workspace: str | Path = ".") -> str:
318
+ """Retrieve skill markdown content from file or embedded fallback."""
319
+ candidate_paths = [
320
+ Path(workspace) / "skills" / "local-coding-agent" / "SKILL.md",
321
+ Path(__file__).resolve().parent.parent / "skills" / "local-coding-agent" / "SKILL.md",
322
+ ]
323
+ for path in candidate_paths:
324
+ if path.is_file():
325
+ try:
326
+ return path.read_text(encoding="utf-8")
327
+ except OSError:
328
+ pass
329
+ return _EMBEDDED_SKILL_MD.strip() + "\n"
330
+
331
+
332
+ def _get_skill_target_path(client: str, workspace: str | Path = ".") -> Path:
333
+ home = Path.home()
334
+ c = client.lower().strip()
335
+ if c in ("codex", "chatgpt"):
336
+ return home / ".codex" / "skills" / "local-coding-agent" / "SKILL.md"
337
+ if c in ("antigravity", "agy", "gemini"):
338
+ return home / ".gemini" / "antigravity" / "skills" / "local-coding-agent" / "SKILL.md"
339
+ if c == "claude":
340
+ return home / ".claude" / "skills" / "local-coding-agent" / "SKILL.md"
341
+ if c == "workspace":
342
+ return Path(workspace).resolve() / "skills" / "local-coding-agent" / "SKILL.md"
343
+ return home / ".codex" / "skills" / "local-coding-agent" / "SKILL.md"
344
+
345
+
346
+ def _detect_installed_agent_dirs(workspace: str | Path = ".") -> list[tuple[str, Path]]:
347
+ home = Path.home()
348
+ detected = []
349
+
350
+ # Codex
351
+ codex_dir = home / ".codex"
352
+ if codex_dir.is_dir() or (home / ".codex" / "skills").is_dir():
353
+ detected.append(("codex", codex_dir / "skills" / "local-coding-agent" / "SKILL.md"))
354
+
355
+ # Antigravity
356
+ agy_dir = home / ".gemini" / "antigravity"
357
+ if agy_dir.is_dir() or (agy_dir / "skills").is_dir():
358
+ detected.append(("antigravity", agy_dir / "skills" / "local-coding-agent" / "SKILL.md"))
359
+
360
+ # Claude
361
+ claude_dir = home / ".claude"
362
+ if claude_dir.is_dir():
363
+ detected.append(("claude", claude_dir / "skills" / "local-coding-agent" / "SKILL.md"))
364
+
365
+ # Always include workspace target
366
+ ws_target = Path(workspace).resolve() / "skills" / "local-coding-agent" / "SKILL.md"
367
+ detected.append(("workspace", ws_target))
368
+
369
+ return detected
370
+
371
+
372
+ def integrate_skill_config(
373
+ client: str = "auto",
374
+ workspace: str | Path = ".",
375
+ target_path: str | Path | None = None,
376
+ dry_run: bool = False,
377
+ print_content: bool = False,
378
+ ) -> dict[str, Any]:
379
+ """Export or install the Agent Skill to agent skill directories."""
380
+ content = get_skill_content(workspace)
381
+
382
+ if print_content:
383
+ return {"action": "print", "content": content}
384
+
385
+ client_norm = client.lower().strip()
386
+
387
+ if target_path:
388
+ dest_path = Path(target_path).resolve()
389
+ if dest_path.is_dir():
390
+ dest_path = dest_path / "SKILL.md"
391
+ targets = [(client_norm or "custom", dest_path)]
392
+ elif client_norm in ("auto", "all"):
393
+ targets = _detect_installed_agent_dirs(workspace)
394
+ else:
395
+ targets = [(client_norm, _get_skill_target_path(client_norm, workspace))]
396
+
397
+ results = []
398
+ for client_name, path in targets:
399
+ written = False
400
+ error_message = None
401
+ if not dry_run:
402
+ try:
403
+ path.parent.mkdir(parents=True, exist_ok=True)
404
+ path.write_text(content, encoding="utf-8")
405
+ written = True
406
+ except Exception as error:
407
+ error_message = str(error)
408
+ result = {
409
+ "client": client_name,
410
+ "path": str(path),
411
+ "written": written,
412
+ "status": "installed" if written else ("dry_run_preview" if dry_run else "failed"),
413
+ }
414
+ if error_message is not None:
415
+ result["error"] = error_message
416
+ results.append(result)
417
+
418
+ if target_path or (len(results) == 1 and client_norm not in ("auto", "all")):
419
+ return results[0]
420
+
421
+ return {
422
+ "action": "install_skill",
423
+ "results": results,
424
+ "dry_run": dry_run,
425
+ }
426
+
@@ -0,0 +1,268 @@
1
+ """Interactive smoke test and end-to-end task runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .controller import Controller
14
+ from .ollama_adapter import build_client
15
+ from .profiles import ModelProfile, get_profile
16
+ from .task import TaskEnvelope
17
+
18
+
19
+ class MockSmokeOllamaClient:
20
+ """Mock client providing a scripted tool-loop response for isolated smoke tests."""
21
+
22
+ def __init__(self, profile: ModelProfile, test_command: str = "") -> None:
23
+ self.profile = profile
24
+ self.test_command = test_command
25
+ self.turn = 0
26
+ self.endpoint = profile.endpoint
27
+
28
+ def available_models(self) -> dict[str, Any]:
29
+ return {"models": [{"name": self.profile.model}]}
30
+
31
+ def chat(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]] | None = None, **kwargs: Any) -> dict[str, Any]:
32
+ self.turn += 1
33
+ if self.turn == 1:
34
+ return {
35
+ "message": {
36
+ "role": "assistant",
37
+ "content": "",
38
+ "tool_calls": [
39
+ {
40
+ "id": "call-read-1",
41
+ "function": {
42
+ "name": "read_file",
43
+ "arguments": {"path": "calculator.py"},
44
+ },
45
+ }
46
+ ],
47
+ },
48
+ "eval_count": 45,
49
+ "eval_duration": 450_000_000,
50
+ "prompt_eval_count": 120,
51
+ "prompt_eval_duration": 100_000_000,
52
+ }
53
+ elif self.turn == 2:
54
+ return {
55
+ "message": {
56
+ "role": "assistant",
57
+ "content": "",
58
+ "tool_calls": [
59
+ {
60
+ "id": "call-patch-1",
61
+ "function": {
62
+ "name": "propose_patch",
63
+ "arguments": {
64
+ "edits": [
65
+ {
66
+ "file": "calculator.py",
67
+ "search": " return a - b",
68
+ "replace": " return a + b",
69
+ }
70
+ ]
71
+ },
72
+ },
73
+ }
74
+ ],
75
+ },
76
+ "eval_count": 60,
77
+ "eval_duration": 600_000_000,
78
+ "prompt_eval_count": 200,
79
+ "prompt_eval_duration": 150_000_000,
80
+ }
81
+ else:
82
+ return {
83
+ "message": {
84
+ "role": "assistant",
85
+ "content": json.dumps({
86
+ "status": "candidate",
87
+ "summary": "Fixed subtraction to addition in calculator.py",
88
+ "edits": [
89
+ {
90
+ "file": "calculator.py",
91
+ "search": " return a - b",
92
+ "replace": " return a + b",
93
+ }
94
+ ],
95
+ "checks": [],
96
+ "risks": [],
97
+ }),
98
+ },
99
+ "eval_count": 40,
100
+ "eval_duration": 400_000_000,
101
+ "prompt_eval_count": 260,
102
+ "prompt_eval_duration": 180_000_000,
103
+ }
104
+
105
+
106
+
107
+ def run_smoke_test(
108
+ profile_name: str = "qwen2.5-coder",
109
+ workspace_dir: str | Path | None = None,
110
+ use_mock: bool = False,
111
+ fallback_to_mock: bool = False,
112
+ verbose: bool = True,
113
+ ) -> dict[str, Any]:
114
+ steps = []
115
+ start_total = time.perf_counter()
116
+
117
+ if verbose:
118
+ print("=" * 60)
119
+ print(" Local Coding Agent — End-to-End Smoke Test")
120
+ print("=" * 60)
121
+
122
+ # 1. Setup isolated workspace
123
+ is_temp = workspace_dir is None
124
+ temp_dir_obj = tempfile.TemporaryDirectory() if is_temp else None
125
+ ws_path = Path(temp_dir_obj.name if temp_dir_obj else workspace_dir).resolve()
126
+
127
+ try:
128
+ # Create calculator.py
129
+ target_file = ws_path / "calculator.py"
130
+ target_file.write_text("def add(a: int, b: int) -> int:\n return a - b\n", encoding="utf-8")
131
+
132
+ steps.append({"step": "workspace_prep", "status": "ok", "message": f"Workspace created at {ws_path}"})
133
+ if verbose:
134
+ print("[OK] Step 1: Isolated test workspace prepared.")
135
+
136
+ # 2. Build task envelope
137
+ task = TaskEnvelope(
138
+ id="smoke-fix-add",
139
+ goal="Fix addition bug in calculator.py so that add(a, b) returns a + b",
140
+ files=["calculator.py"],
141
+ context="The function add currently subtracts instead of adding.",
142
+ constraints=["Do not change the function signature"],
143
+ checks=[],
144
+ acceptance=["calculator.py fixed"],
145
+ )
146
+ steps.append({"step": "task_envelope", "status": "ok", "message": "TaskEnvelope ready"})
147
+ if verbose:
148
+ print("[OK] Step 2: Atomic TaskEnvelope formulated.")
149
+
150
+ # 3. Client selection
151
+ profile = get_profile(profile_name)
152
+ mock_fallback = False
153
+ backend_verified = False
154
+
155
+ if use_mock:
156
+ client: Any = MockSmokeOllamaClient(profile)
157
+ if verbose:
158
+ print("[INFO] Step 3: Using scripted mock model runner.")
159
+ else:
160
+ try:
161
+ real_client = build_client(profile)
162
+ real_client.available_models()
163
+ client = real_client
164
+ backend_verified = True
165
+ if verbose:
166
+ print(f"[OK] Step 3: Connected to live Ollama ({profile.model}).")
167
+ except Exception as exc:
168
+ if fallback_to_mock:
169
+ mock_fallback = True
170
+ client = MockSmokeOllamaClient(profile)
171
+ if verbose:
172
+ print(f"[WARN] Step 3: Live Ollama unavailable ({exc}). Using mock fallback.")
173
+ else:
174
+ steps.append({
175
+ "step": "backend_readiness",
176
+ "status": "fail",
177
+ "message": f"Live backend unavailable: {exc}",
178
+ })
179
+ return {
180
+ "success": False,
181
+ "status": "backend_unavailable",
182
+ "backend_verified": False,
183
+ "synthetic": False,
184
+ "tps": None,
185
+ "duration_seconds": round(time.perf_counter() - start_total, 2),
186
+ "mock_fallback": False,
187
+ "error": str(exc),
188
+ "steps": steps,
189
+ }
190
+
191
+
192
+ # 4. Controller execution
193
+ start_exec = time.perf_counter()
194
+ controller = Controller(client, ws_path, max_turns=5)
195
+ run_res = controller.run(task, apply=False)
196
+ exec_duration = round(time.perf_counter() - start_exec, 2)
197
+
198
+ status = run_res.get("status")
199
+ success = status == "accepted"
200
+ steps.append({
201
+ "step": "execution",
202
+ "status": "ok" if success else "fail",
203
+ "message": f"Controller status: {status} in {exec_duration}s",
204
+ })
205
+
206
+ if verbose:
207
+ tag = "[OK]" if success else "[FAIL]"
208
+ print(f"{tag} Step 4: Delegation completed. Status: {status} ({exec_duration}s)")
209
+
210
+ # 5. TPS Calculation & validation summary
211
+ total_eval_tokens = 0
212
+ total_eval_duration_ns = 0
213
+ for ev in run_res.get("audit", []):
214
+ if ev.get("event") == "model_response":
215
+ dur = ev.get("eval_duration_ns") or 0
216
+ cnt = ev.get("eval_tokens") or 0
217
+ total_eval_tokens += cnt
218
+ total_eval_duration_ns += dur
219
+
220
+ tps: float | None = (
221
+ round(total_eval_tokens / (total_eval_duration_ns / 1e9), 1)
222
+ if total_eval_duration_ns > 0
223
+ else None
224
+ )
225
+ tps_label = f" ({tps} tokens/sec)" if tps is not None else ""
226
+
227
+ if success:
228
+ steps.append({
229
+ "step": "validation",
230
+ "status": "ok",
231
+ "message": f"Patch candidate validated{tps_label}",
232
+ })
233
+ if verbose:
234
+ print(f"[OK] Step 5: Patch candidate validated. Generation speed:{tps_label}")
235
+ else:
236
+ steps.append({
237
+ "step": "validation",
238
+ "status": "fail",
239
+ "message": f"Patch validation skipped or failed (Status: {status})",
240
+ })
241
+ if verbose:
242
+ print(f"[FAIL] Step 5: Patch validation skipped or failed (Status: {status}).")
243
+ print("")
244
+ print("-" * 60)
245
+ print(" Smoke Test Summary")
246
+ print("-" * 60)
247
+ print(f" Result Status : {status.upper() if status else 'UNKNOWN'}")
248
+ print(f" Total Time : {round(time.perf_counter() - start_total, 2)}s")
249
+ if tps is not None:
250
+ print(f" Eval TPS : {tps} tok/s")
251
+ print(f" Monitor UI : http://127.0.0.1:8765/dashboard")
252
+ print("=" * 60)
253
+
254
+
255
+ return {
256
+ "success": success,
257
+ "status": status,
258
+ "backend_verified": backend_verified,
259
+ "synthetic": use_mock or mock_fallback,
260
+ "tps": tps,
261
+ "duration_seconds": round(time.perf_counter() - start_total, 2),
262
+ "mock_fallback": mock_fallback,
263
+ "result": run_res,
264
+ "steps": steps,
265
+ }
266
+ finally:
267
+ if temp_dir_obj:
268
+ temp_dir_obj.cleanup()