alpiecode 5.0.0__tar.gz → 5.1.1__tar.gz

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 (40) hide show
  1. {alpiecode-5.0.0 → alpiecode-5.1.1}/PKG-INFO +1 -1
  2. {alpiecode-5.0.0 → alpiecode-5.1.1}/pyproject.toml +1 -1
  3. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/SOURCES.txt +1 -0
  5. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/agent.py +25 -1
  6. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/cli.py +4 -0
  7. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/config.py +1 -1
  8. alpiecode-5.1.1/src/codeagent/discovery.py +572 -0
  9. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/orchestrator.py +20 -8
  10. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/prompt.py +126 -0
  11. {alpiecode-5.0.0 → alpiecode-5.1.1}/README.md +0 -0
  12. {alpiecode-5.0.0 → alpiecode-5.1.1}/setup.cfg +0 -0
  13. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/alpiecode/__init__.py +0 -0
  14. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  15. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
  16. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/requires.txt +0 -0
  17. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/top_level.txt +0 -0
  18. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/__init__.py +0 -0
  19. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/backends/__init__.py +0 -0
  20. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/backends/base.py +0 -0
  21. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/backends/local_backend.py +0 -0
  22. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/backends/openai_backend.py +0 -0
  23. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/cache.py +0 -0
  24. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/client.py +0 -0
  25. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/compaction.py +0 -0
  26. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/context.py +0 -0
  27. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/doctor.py +0 -0
  28. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/executor.py +0 -0
  29. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/extension/alpiecode.vsix +0 -0
  30. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/github.py +0 -0
  31. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/guardian.py +0 -0
  32. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/ipython_ext.py +0 -0
  33. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/local_model.py +0 -0
  34. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/media.py +0 -0
  35. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/memory.py +0 -0
  36. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/server.py +0 -0
  37. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/session.py +0 -0
  38. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/tools.py +0 -0
  39. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/updater.py +0 -0
  40. {alpiecode-5.0.0 → alpiecode-5.1.1}/src/codeagent/vscode_installer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 5.0.0
3
+ Version: 5.1.1
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "5.0.0"
7
+ version = "5.1.1"
8
8
  description = "AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 5.0.0
3
+ Version: 5.1.1
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -15,6 +15,7 @@ src/codeagent/client.py
15
15
  src/codeagent/compaction.py
16
16
  src/codeagent/config.py
17
17
  src/codeagent/context.py
18
+ src/codeagent/discovery.py
18
19
  src/codeagent/doctor.py
19
20
  src/codeagent/executor.py
20
21
  src/codeagent/github.py
@@ -156,6 +156,7 @@ def run_agent(
156
156
  github_repo: str = None,
157
157
  server_url: str = None,
158
158
  complexity: str = None,
159
+ debug: bool = False,
159
160
  ) -> list:
160
161
  """Run non-interactive agent task with Rich presentation."""
161
162
  workdir = workdir.resolve()
@@ -200,8 +201,27 @@ def run_agent(
200
201
  _checkpoint(workdir, "checkpoint: start")
201
202
 
202
203
  current_turn = 0
204
+ last_discovery = {}
205
+
203
206
  for event in event_stream:
204
- if event.type == "start" and verbose:
207
+ if event.type == "discovery" and verbose:
208
+ last_discovery = event.data
209
+ if debug and HAS_RICH:
210
+ console.print(Panel(
211
+ f"[bold cyan]🔍 Autonomous Discovery Engine[/bold cyan]\n"
212
+ f"• Intent: [bold]{event.data.get('intent')}[/bold]\n"
213
+ f"• Complexity: [bold]{event.data.get('complexity')}[/bold]\n"
214
+ f"• OS: {event.data.get('os')} | Shell: {event.data.get('shell')}\n"
215
+ f"• Project: {event.data.get('project_type')} ({event.data.get('file_count', 0)} files)\n"
216
+ f"• Frameworks: {', '.join(event.data.get('frameworks', [])) or 'None'}",
217
+ title="Pre-Execution Intelligence",
218
+ border_style="dim cyan",
219
+ padding=(0, 1)
220
+ ))
221
+ elif debug:
222
+ print(f"[Discovery] intent={event.data.get('intent')}, complexity={event.data.get('complexity')}, shell={event.data.get('shell')}, project={event.data.get('project_type')}")
223
+
224
+ elif event.type == "start" and verbose:
205
225
  data = event.data
206
226
  if HAS_RICH:
207
227
  console.rule("[bold blue]Agent Started[/bold blue]")
@@ -223,6 +243,10 @@ def run_agent(
223
243
  comp_label = {"qa": "Q&A (instant)", "low": "Low (fast)", "medium": "Medium (balanced)", "high": "High (thorough)"}.get(comp, comp)
224
244
  comp_color = {"qa": "cyan", "low": "green", "medium": "yellow", "high": "red"}.get(comp, "white")
225
245
  console.print(f"⚡ Complexity: [bold {comp_color}]{comp_label}[/bold {comp_color}]", style="dim")
246
+ if last_discovery:
247
+ intent_str = last_discovery.get('intent', 'create').title()
248
+ proj_str = last_discovery.get('project_type', 'empty')
249
+ console.print(f"🔍 Discovery: {intent_str} on {proj_str} project ({last_discovery.get('shell', 'bash')})", style="dim")
226
250
  if cfg.enable_thinking:
227
251
  console.print(f"🧠 Reasoning: [bold green]ON[/bold green]", style="dim")
228
252
  else:
@@ -81,6 +81,7 @@ def main():
81
81
  common.add_argument("--no-thinking", "--non-thinking", dest="no_thinking", action="store_true", help="Disable VLM reasoning mode")
82
82
  common.add_argument("--no-update", action="store_true", help="Skip automatic update check")
83
83
  common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
84
+ common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
84
85
 
85
86
  parser = argparse.ArgumentParser(
86
87
  prog="alpiecode",
@@ -170,6 +171,7 @@ def main():
170
171
  video_path=getattr(args, "video", None),
171
172
  url=getattr(args, "url", None),
172
173
  github_repo=getattr(args, "github", None),
174
+ debug=getattr(args, "debug", False),
173
175
  )
174
176
 
175
177
  elif args.command == "chat":
@@ -196,6 +198,7 @@ def main():
196
198
  video_path=getattr(args, "video", None),
197
199
  url=getattr(args, "url", None),
198
200
  github_repo=getattr(args, "github", None),
201
+ debug=getattr(args, "debug", False),
199
202
  )
200
203
 
201
204
  elif args.command == "doctor":
@@ -231,6 +234,7 @@ def main():
231
234
  video_path=getattr(args, "video", None),
232
235
  url=getattr(args, "url", None),
233
236
  github_repo=getattr(args, "github", None),
237
+ debug=getattr(args, "debug", False),
234
238
  )
235
239
 
236
240
  elif args.command == "diff":
@@ -18,7 +18,7 @@ CONFIG_DIR = Path.home() / ".alpiecode"
18
18
  CONFIG_PATH = CONFIG_DIR / "config.json"
19
19
 
20
20
  # Config version — bump this when defaults change to trigger auto-migration
21
- CONFIG_VERSION = 6 # v6: smart flow, max_turns 20, complexity-aware
21
+ CONFIG_VERSION = 7 # v7: autonomous discovery engine (zero-flag flow)
22
22
 
23
23
  DEFAULTS = {
24
24
  "base_url": "http://20.245.200.125:8000/v1", # Primary endpoint
@@ -0,0 +1,572 @@
1
+ """
2
+ Discovery Engine for AlpieCode -- Zero-LLM Pre-Execution Intelligence.
3
+
4
+ Runs in under 50ms before the agent turn loop starts. Detects:
5
+ - Task intent (qa, create, modify, debug, explain)
6
+ - Environment (OS, shell, Python version, venv, git)
7
+ - Repository state (project type, frameworks, file count, entry points)
8
+ - Smart complexity (from intent + repo state combined)
9
+
10
+ All detection is heuristic/filesystem-based -- no LLM calls, no network.
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import platform
16
+ import re
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import List, Optional, Tuple
23
+
24
+
25
+ # -- Task Context ----------------------------------------------------------
26
+
27
+ @dataclass
28
+ class TaskContext:
29
+ """Pre-computed intelligence about the task and environment."""
30
+
31
+ # Intent
32
+ intent: str = "create" # qa, create, modify, debug, explain
33
+ complexity: str = "low" # qa, low, medium, high
34
+
35
+ # Environment
36
+ os_name: str = "linux" # linux, darwin, windows
37
+ shell: str = "bash" # bash, powershell, cmd, sh, wsl
38
+ python_cmd: str = "python3" # python3 or python (platform-dependent)
39
+ python_version: str = "" # e.g. "3.11.2"
40
+ has_venv: bool = False
41
+ has_git: bool = False
42
+
43
+ # Repository
44
+ project_type: str = "empty" # python, node, rust, go, java, empty, unknown
45
+ frameworks: List[str] = field(default_factory=list)
46
+ file_count: int = 0
47
+ has_tests: bool = False
48
+ entry_points: List[str] = field(default_factory=list)
49
+ dependencies: List[str] = field(default_factory=list)
50
+
51
+ # Budget (computed from complexity)
52
+ max_turns: int = 10
53
+ max_tokens: int = 8192
54
+ tool_set: str = "core" # none, core, full
55
+ enable_thinking: bool = False
56
+
57
+
58
+ # -- Intent Detection ------------------------------------------------------
59
+
60
+ # Question patterns -- user wants an answer, not code
61
+ _QA_STARTERS = [
62
+ "what is", "what are", "what\'s", "what does", "what do",
63
+ "explain", "how does", "how do", "why does", "why do", "why is",
64
+ "describe", "define", "compare", "difference between",
65
+ "tell me about", "who invented", "who created", "when was",
66
+ "can you explain", "could you explain", "please explain",
67
+ "what\'s the difference", "is it possible", "is there a way",
68
+ ]
69
+
70
+ # Action verbs that indicate code generation
71
+ _ACTION_VERBS = [
72
+ "build", "create", "write", "make", "implement", "develop",
73
+ "generate", "code", "script", "program", "design", "set up",
74
+ "setup", "scaffold", "bootstrap", "initialize", "init",
75
+ ]
76
+
77
+ # Modification verbs
78
+ _MODIFY_VERBS = [
79
+ "fix", "add", "modify", "change", "update", "remove", "delete",
80
+ "refactor", "migrate", "upgrade", "downgrade", "rename",
81
+ "move", "restructure", "reorganize", "optimize", "improve",
82
+ "convert", "transform", "replace", "swap",
83
+ ]
84
+
85
+ # Debug verbs
86
+ _DEBUG_VERBS = [
87
+ "debug", "investigate", "diagnose", "troubleshoot", "trace",
88
+ "find the bug", "find the error", "find the issue",
89
+ "why is this", "what\'s wrong", "what is wrong",
90
+ ]
91
+
92
+ # Explain verbs (when target is a file or code)
93
+ _EXPLAIN_VERBS = [
94
+ "explain this", "explain the", "walk me through",
95
+ "how does this work", "what does this do",
96
+ "analyze this", "review this", "read this",
97
+ ]
98
+
99
+
100
+ def detect_intent(task: str) -> str:
101
+ """Classify task intent: qa, create, modify, debug, or explain.
102
+
103
+ Returns one of: 'qa', 'create', 'modify', 'debug', 'explain'
104
+ """
105
+ t = task.lower().strip()
106
+
107
+ # Q&A: starts with a question pattern AND has no action verbs
108
+ all_action = _ACTION_VERBS + _MODIFY_VERBS
109
+ if any(t.startswith(pat) for pat in _QA_STARTERS):
110
+ if not any(re.search(r"\b" + re.escape(av) + r"\b", t) for av in all_action):
111
+ return "qa"
112
+
113
+ if t.endswith("?") and not any(re.search(r"\b" + re.escape(av) + r"\b", t) for av in all_action):
114
+ return "qa"
115
+
116
+ # Check if task starts with a create verb ("build a...", "create a...", "write a...")
117
+ if any(t.startswith(cv) for cv in ["build", "create", "write", "make", "implement", "develop", "generate", "code", "design"]):
118
+ return "create"
119
+
120
+ # Debug: explicitly asking to debug/investigate
121
+ if any(re.search(r"\b" + re.escape(dv) + r"\b", t) for dv in _DEBUG_VERBS):
122
+ return "debug"
123
+
124
+ # Explain: asking to explain existing code
125
+ if any(re.search(r"\b" + re.escape(ev) + r"\b", t) for ev in _EXPLAIN_VERBS):
126
+ return "explain"
127
+
128
+ # Modify: acting on existing code (e.g. "refactor", "fix", "update")
129
+ if any(re.search(r"\b" + re.escape(mv) + r"\b", t) for mv in ["refactor", "migrate", "fix", "update", "modify", "change", "delete", "remove", "rename"]):
130
+ return "modify"
131
+
132
+ # Create: building something new
133
+ if any(re.search(r"\b" + re.escape(av) + r"\b", t) for av in _ACTION_VERBS):
134
+ return "create"
135
+
136
+ if any(re.search(r"\b" + re.escape(mv) + r"\b", t) for mv in _MODIFY_VERBS):
137
+ return "modify"
138
+
139
+ if t.endswith("?"):
140
+ return "qa"
141
+
142
+ return "create"
143
+
144
+
145
+ # -- Environment Detection -------------------------------------------------
146
+
147
+ def detect_environment() -> dict:
148
+ """Detect OS, shell, Python version, and runtime environment.
149
+
150
+ Returns dict with: os_name, shell, python_cmd, python_version
151
+ """
152
+ os_name = platform.system().lower()
153
+ if os_name == "linux":
154
+ # Check if we are inside WSL
155
+ try:
156
+ with open("/proc/version", "r") as f:
157
+ version_info = f.read().lower()
158
+ if "microsoft" in version_info or "wsl" in version_info:
159
+ os_name = "wsl"
160
+ except (FileNotFoundError, PermissionError):
161
+ pass
162
+
163
+ # Shell detection
164
+ shell = _detect_shell(os_name)
165
+
166
+ # Python command
167
+ python_cmd = "python" if os_name == "windows" else "python3"
168
+
169
+ # Python version
170
+ python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
171
+
172
+ return {
173
+ "os_name": os_name if os_name != "wsl" else "linux",
174
+ "shell": "wsl" if os_name == "wsl" else shell,
175
+ "python_cmd": python_cmd,
176
+ "python_version": python_version,
177
+ }
178
+
179
+
180
+ def _detect_shell(os_name: str) -> str:
181
+ """Detect available shell, preferring WSL on Windows like Claude/Codex agents."""
182
+
183
+ if os_name == "windows":
184
+ # On Windows: prefer WSL (like Claude, Codex, Gemini agents do)
185
+ if _is_wsl_available():
186
+ return "wsl"
187
+ # Fallback to PowerShell
188
+ if shutil.which("powershell") or shutil.which("pwsh"):
189
+ return "powershell"
190
+ return "cmd"
191
+
192
+ if os_name in ("linux", "wsl"):
193
+ return "bash" # Standard on Linux/WSL
194
+
195
+ if os_name == "darwin":
196
+ return "bash" # Our commands work on both bash and zsh
197
+
198
+ return "sh"
199
+
200
+
201
+ def _is_wsl_available() -> bool:
202
+ """Check if WSL is installed and has at least one working distro."""
203
+ try:
204
+ result = subprocess.run(
205
+ ["wsl", "--list", "--quiet"],
206
+ capture_output=True, text=True, timeout=3,
207
+ )
208
+ return result.returncode == 0 and bool(result.stdout.strip())
209
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
210
+ return False
211
+
212
+
213
+ # -- Repository Scanner ----------------------------------------------------
214
+
215
+ def scan_repository(workdir: Path) -> dict:
216
+ """Quick scan of repository to detect project type, frameworks, etc.
217
+
218
+ Designed to run in under 30ms by checking only key indicator files.
219
+ """
220
+ workdir = workdir.resolve()
221
+
222
+ has_git = (workdir / ".git").is_dir()
223
+ has_venv = (workdir / ".venv").is_dir() or (workdir / "venv").is_dir()
224
+
225
+ # Quick file count (capped scan)
226
+ file_count = _quick_file_count(workdir, max_files=500)
227
+
228
+ # Detect project type and frameworks by checking indicator files
229
+ project_type, frameworks = _detect_project(workdir)
230
+
231
+ # Find entry points
232
+ entry_points = _find_entry_points(workdir, project_type)
233
+
234
+ # Check for tests
235
+ has_tests = _has_test_files(workdir)
236
+
237
+ # Read key dependencies (fast -- just reads manifest files)
238
+ dependencies = _read_dependencies(workdir, project_type)
239
+
240
+ return {
241
+ "has_git": has_git,
242
+ "has_venv": has_venv,
243
+ "file_count": file_count,
244
+ "project_type": project_type,
245
+ "frameworks": frameworks,
246
+ "entry_points": entry_points,
247
+ "has_tests": has_tests,
248
+ "dependencies": dependencies,
249
+ }
250
+
251
+
252
+ def _quick_file_count(workdir: Path, max_files: int = 500) -> int:
253
+ """Count tracked files quickly, respecting .gitignore if possible."""
254
+ try:
255
+ result = subprocess.run(
256
+ ["git", "ls-files", "--cached", "--others", "--exclude-standard"],
257
+ cwd=workdir, capture_output=True, text=True, timeout=3,
258
+ )
259
+ if result.returncode == 0 and result.stdout.strip():
260
+ return len(result.stdout.strip().splitlines())
261
+ except (FileNotFoundError, subprocess.TimeoutExpired):
262
+ pass
263
+
264
+ # Fallback: walk directory
265
+ count = 0
266
+ skip_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv",
267
+ ".tox", "dist", "build", ".eggs", ".mypy_cache"}
268
+ for root, dirs, files in os.walk(workdir):
269
+ dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith(".")]
270
+ count += len(files)
271
+ if count >= max_files:
272
+ return count
273
+ return count
274
+
275
+
276
+ def _detect_project(workdir: Path) -> Tuple[str, List[str]]:
277
+ """Detect project type and frameworks from indicator files."""
278
+ frameworks = []
279
+
280
+ # Python indicators
281
+ has_pyproject = (workdir / "pyproject.toml").exists()
282
+ has_setup_py = (workdir / "setup.py").exists()
283
+ has_requirements = (workdir / "requirements.txt").exists()
284
+ has_pipfile = (workdir / "Pipfile").exists()
285
+
286
+ if has_pyproject or has_setup_py or has_requirements or has_pipfile:
287
+ frameworks = _detect_python_frameworks(workdir)
288
+ return "python", frameworks
289
+
290
+ # Node.js indicators
291
+ if (workdir / "package.json").exists():
292
+ frameworks = _detect_node_frameworks(workdir)
293
+ return "node", frameworks
294
+
295
+ # Rust
296
+ if (workdir / "Cargo.toml").exists():
297
+ return "rust", []
298
+
299
+ # Go
300
+ if (workdir / "go.mod").exists():
301
+ return "go", []
302
+
303
+ # Java
304
+ if (workdir / "pom.xml").exists() or (workdir / "build.gradle").exists():
305
+ return "java", []
306
+
307
+ # Check if there are any source files at all
308
+ for ext in (".py", ".js", ".ts", ".rs", ".go", ".java", ".c", ".cpp"):
309
+ for _ in workdir.glob(f"*{ext}"):
310
+ return "unknown", []
311
+
312
+ return "empty", []
313
+
314
+
315
+ def _detect_python_frameworks(workdir: Path) -> List[str]:
316
+ """Detect Python frameworks from requirements/pyproject."""
317
+ frameworks = []
318
+ content = ""
319
+
320
+ for manifest in ["requirements.txt", "pyproject.toml", "setup.py", "Pipfile"]:
321
+ path = workdir / manifest
322
+ if path.exists():
323
+ try:
324
+ content += path.read_text(errors="replace").lower()
325
+ except Exception:
326
+ pass
327
+
328
+ framework_map = {
329
+ "fastapi": "fastapi",
330
+ "flask": "flask",
331
+ "django": "django",
332
+ "pytest": "pytest",
333
+ "sqlalchemy": "sqlalchemy",
334
+ "pydantic": "pydantic",
335
+ "celery": "celery",
336
+ "redis": "redis",
337
+ "pymongo": "mongodb",
338
+ "torch": "pytorch",
339
+ "tensorflow": "tensorflow",
340
+ "numpy": "numpy",
341
+ "pandas": "pandas",
342
+ "streamlit": "streamlit",
343
+ "gradio": "gradio",
344
+ }
345
+
346
+ for pkg, name in framework_map.items():
347
+ if pkg in content:
348
+ frameworks.append(name)
349
+
350
+ return frameworks
351
+
352
+
353
+ def _detect_node_frameworks(workdir: Path) -> List[str]:
354
+ """Detect Node.js frameworks from package.json."""
355
+ frameworks = []
356
+ pkg_path = workdir / "package.json"
357
+ try:
358
+ data = json.loads(pkg_path.read_text(errors="replace"))
359
+ all_deps = {}
360
+ all_deps.update(data.get("dependencies", {}))
361
+ all_deps.update(data.get("devDependencies", {}))
362
+
363
+ framework_map = {
364
+ "react": "react", "next": "nextjs", "vue": "vue",
365
+ "angular": "angular", "express": "express",
366
+ "fastify": "fastify", "nest": "nestjs",
367
+ "jest": "jest", "mocha": "mocha",
368
+ "typescript": "typescript",
369
+ }
370
+ for pkg, name in framework_map.items():
371
+ if any(pkg in dep for dep in all_deps):
372
+ frameworks.append(name)
373
+ except Exception:
374
+ pass
375
+ return frameworks
376
+
377
+
378
+ def _find_entry_points(workdir: Path, project_type: str) -> List[str]:
379
+ """Find likely entry point files."""
380
+ entry_names = [
381
+ "main.py", "app.py", "server.py", "manage.py", "run.py",
382
+ "index.py", "cli.py", "__main__.py",
383
+ "index.js", "index.ts", "app.js", "app.ts", "server.js", "server.ts",
384
+ "main.go", "main.rs", "Main.java",
385
+ ]
386
+ found = []
387
+ for name in entry_names:
388
+ if (workdir / name).exists():
389
+ found.append(name)
390
+ # Also check src/ directory
391
+ if (workdir / "src" / name).exists():
392
+ found.append(f"src/{name}")
393
+ return found[:5]
394
+
395
+
396
+ def _has_test_files(workdir: Path) -> bool:
397
+ """Check if the project has test files."""
398
+ test_indicators = ["tests", "test", "__tests__", "spec"]
399
+ for d in test_indicators:
400
+ if (workdir / d).is_dir():
401
+ return True
402
+
403
+ # Check for test_*.py or *_test.py files in root
404
+ try:
405
+ for f in workdir.iterdir():
406
+ if f.is_file() and (f.name.startswith("test_") or f.name.endswith("_test.py")):
407
+ return True
408
+ except PermissionError:
409
+ pass
410
+ return False
411
+
412
+
413
+ def _read_dependencies(workdir: Path, project_type: str) -> List[str]:
414
+ """Read top-level dependency names (fast, no resolution)."""
415
+ deps = []
416
+ if project_type == "python":
417
+ req_path = workdir / "requirements.txt"
418
+ if req_path.exists():
419
+ try:
420
+ for line in req_path.read_text(errors="replace").splitlines():
421
+ line = line.strip()
422
+ if line and not line.startswith("#") and not line.startswith("-"):
423
+ name = re.split(r"[=<>!~;\[]", line)[0].strip()
424
+ if name:
425
+ deps.append(name)
426
+ except Exception:
427
+ pass
428
+ return deps[:20]
429
+
430
+
431
+ # -- Complexity Engine ------------------------------------------------------
432
+
433
+ def compute_complexity(intent: str, repo_info: dict, task: str) -> str:
434
+ """Compute task complexity from intent + repo state + task text.
435
+
436
+ This is smarter than pure keyword matching because it considers
437
+ the actual project context.
438
+
439
+ Returns: 'qa', 'low', 'medium', or 'high'
440
+ """
441
+ if intent == "qa":
442
+ return "qa"
443
+
444
+ task_lower = task.lower()
445
+ file_count = repo_info.get("file_count", 0)
446
+ frameworks = repo_info.get("frameworks", [])
447
+ project_type = repo_info.get("project_type", "empty")
448
+
449
+ # -- HIGH: explicitly complex multi-component tasks --
450
+ high_keywords = [
451
+ "full stack", "fullstack", "full-stack",
452
+ "microservice", "e-commerce", "ecommerce",
453
+ "docker", "kubernetes", "ci/cd", "pipeline",
454
+ "machine learning", "deep learning", "neural network",
455
+ "train a model", "training pipeline",
456
+ "authentication", "oauth", "jwt auth",
457
+ "database migration", "schema migration",
458
+ ]
459
+ if any(kw in task_lower for kw in high_keywords):
460
+ return "high"
461
+
462
+ # Large codebase + modify/debug -> high
463
+ if intent in ("modify", "debug") and file_count > 50:
464
+ if any(kw in task_lower for kw in ["refactor", "restructure", "redesign", "rewrite", "migrate"]):
465
+ return "high"
466
+
467
+ # -- MEDIUM: multi-step tasks --
468
+ medium_keywords = [
469
+ "api", "rest api", "graphql", "server",
470
+ "game", "snake", "tetris", "chess", "pong", "sudoku",
471
+ "website", "web page", "web app", "webapp",
472
+ "test suite", "unit tests", "integration test",
473
+ "dashboard", "portfolio",
474
+ "react", "vue", "angular", "next.js",
475
+ "django", "flask app", "fastapi app",
476
+ ]
477
+ if any(kw in task_lower for kw in medium_keywords):
478
+ return "medium"
479
+
480
+ # Modify/debug on existing project with frameworks -> medium
481
+ if intent in ("modify", "debug") and frameworks:
482
+ return "medium"
483
+
484
+ # Modify/debug on project with 10+ files -> medium
485
+ if intent in ("modify", "debug") and file_count > 10:
486
+ return "medium"
487
+
488
+ # Create on existing project (adding to it) -> medium
489
+ if intent == "create" and project_type not in ("empty", "unknown") and file_count > 5:
490
+ return "medium"
491
+
492
+ # -- LOW: everything else --
493
+ return "low"
494
+
495
+
496
+ # -- Complexity Configuration -----------------------------------------------
497
+
498
+ COMPLEXITY_CONFIG = {
499
+ "qa": {
500
+ "max_turns": 3,
501
+ "max_tokens": 4096,
502
+ "tool_set": "none",
503
+ "enable_thinking": False,
504
+ },
505
+ "low": {
506
+ "max_turns": 10,
507
+ "max_tokens": 8192,
508
+ "tool_set": "core",
509
+ "enable_thinking": False,
510
+ },
511
+ "medium": {
512
+ "max_turns": 20,
513
+ "max_tokens": 8192,
514
+ "tool_set": "full",
515
+ "enable_thinking": False,
516
+ },
517
+ "high": {
518
+ "max_turns": 40,
519
+ "max_tokens": 16384,
520
+ "tool_set": "full",
521
+ "enable_thinking": True,
522
+ },
523
+ }
524
+
525
+
526
+ # -- Main Entry Point -------------------------------------------------------
527
+
528
+ def build_task_context(task: str, workdir: Path) -> TaskContext:
529
+ """Build complete TaskContext by running all discovery phases.
530
+
531
+ This is the single entry point -- call this from the orchestrator.
532
+ Runs in under 50ms total, no LLM calls, no network.
533
+ """
534
+ # Phase 1: Intent
535
+ intent = detect_intent(task)
536
+
537
+ # Phase 2: Environment
538
+ env = detect_environment()
539
+
540
+ # Phase 3: Repository scan
541
+ repo = scan_repository(workdir)
542
+
543
+ # Phase 4: Complexity (uses intent + repo)
544
+ complexity = compute_complexity(intent, repo, task)
545
+
546
+ # Phase 5: Budget from complexity
547
+ budget = COMPLEXITY_CONFIG[complexity]
548
+
549
+ return TaskContext(
550
+ # Intent
551
+ intent=intent,
552
+ complexity=complexity,
553
+ # Environment
554
+ os_name=env["os_name"],
555
+ shell=env["shell"],
556
+ python_cmd=env["python_cmd"],
557
+ python_version=env["python_version"],
558
+ has_venv=repo["has_venv"],
559
+ has_git=repo["has_git"],
560
+ # Repository
561
+ project_type=repo["project_type"],
562
+ frameworks=repo["frameworks"],
563
+ file_count=repo["file_count"],
564
+ has_tests=repo["has_tests"],
565
+ entry_points=repo["entry_points"],
566
+ dependencies=repo["dependencies"],
567
+ # Budget
568
+ max_turns=budget["max_turns"],
569
+ max_tokens=budget["max_tokens"],
570
+ tool_set=budget["tool_set"],
571
+ enable_thinking=budget["enable_thinking"],
572
+ )
@@ -14,7 +14,8 @@ from .backends.openai_backend import OpenAIBackend
14
14
  from .cache import get_cache
15
15
  from .config import Config, is_server_reachable
16
16
  from .memory import extract_and_save_memories
17
- from .prompt import PromptBuilder, classify_task, COMPLEXITY_CONFIG
17
+ from .discovery import build_task_context, COMPLEXITY_CONFIG
18
+ from .prompt import PromptBuilder, classify_task
18
19
  from .session import Session, SessionManager
19
20
 
20
21
 
@@ -55,15 +56,25 @@ class AgentOrchestrator:
55
56
  ) -> Iterator[AgentEvent]:
56
57
  """Run full agent task loop. Yields AgentEvents."""
57
58
 
58
- # ── Auto-classify complexity if not provided ──
59
- if complexity is None:
60
- complexity = classify_task(task)
59
+ # ── Phase 0: Discovery pre-compute task intelligence ──
60
+ task_context = build_task_context(task, session.workdir)
61
+ complexity = task_context.complexity
62
+
63
+ yield AgentEvent("discovery", {
64
+ "intent": task_context.intent,
65
+ "complexity": task_context.complexity,
66
+ "os": task_context.os_name,
67
+ "shell": task_context.shell,
68
+ "project_type": task_context.project_type,
69
+ "frameworks": task_context.frameworks,
70
+ "file_count": task_context.file_count,
71
+ })
61
72
 
62
73
  comp_cfg = COMPLEXITY_CONFIG.get(complexity, COMPLEXITY_CONFIG["low"])
63
74
 
64
75
  # ── Determine effective max_turns and max_tokens ──
65
- effective_max_turns = min(cfg.max_turns, comp_cfg["max_turns"])
66
- effective_max_tokens = comp_cfg["max_tokens"]
76
+ effective_max_turns = task_context.max_turns
77
+ effective_max_tokens = task_context.max_tokens
67
78
 
68
79
  # User override: if they set --max-turns explicitly, respect it
69
80
  if cfg.max_turns != 20: # 20 is new default, so non-default = explicit
@@ -102,7 +113,8 @@ class AgentOrchestrator:
102
113
  # ── Configure tools & system prompt based on complexity ──
103
114
  active_tools = self.prompt_builder.get_tools(is_offline=is_offline, complexity=complexity)
104
115
  system_prompt = self.prompt_builder.build_system_prompt(
105
- session.workdir, is_offline=is_offline, complexity=complexity
116
+ session.workdir, is_offline=is_offline, complexity=complexity,
117
+ task_context=task_context,
106
118
  )
107
119
  session.context.set_system_prompt(system_prompt)
108
120
 
@@ -126,7 +138,7 @@ class AgentOrchestrator:
126
138
  })
127
139
 
128
140
  # ── Adaptive thinking ──
129
- enable_thinking = cfg.enable_thinking
141
+ enable_thinking = cfg.enable_thinking or task_context.enable_thinking
130
142
  if enable_thinking and complexity in ("qa", "low"):
131
143
  enable_thinking = False
132
144
  yield AgentEvent("adaptive_mode", {"message": "Simple task detected, skipping deep reasoning."})
@@ -359,6 +359,89 @@ COMPLEXITY_CONFIG = {
359
359
  }
360
360
 
361
361
 
362
+
363
+
364
+ # -- Shell-Aware Prompt Fragments ------------------------------------------
365
+
366
+ BASH_RULES = """\
367
+ ## Shell Environment: Bash (Linux/macOS)
368
+ - You are ALREADY in the project root directory. NEVER run `cd /home/...` or `cd /testbed`
369
+ - Use `&&` to chain commands
370
+ - Use `export VAR=value` for environment variables
371
+ - Use `python3` (not `python` which may be Python 2)
372
+ - For testing python code without extra packages, use `python3 -m unittest`
373
+ - Background processes: `command &` (but AVOID for servers -- use TestClient)
374
+ - File paths use `/` forward slashes
375
+ """
376
+
377
+ WSL_RULES = """\
378
+ ## Shell Environment: WSL Bash (Windows Subsystem for Linux)
379
+ - You are ALREADY in the project root directory. NEVER run `cd /home/...` or `cd /testbed`
380
+ - Use bash syntax: `&&` to chain, `export VAR=value` for env vars
381
+ - Use `python3` (not `python`)
382
+ - For testing python code, use `python3 -m unittest`
383
+ - The working directory is a Linux path (e.g. /home/user/project)
384
+ - Do NOT use PowerShell or Windows commands (no `dir`, `type`, `$env:`)
385
+ - File paths use `/` forward slashes
386
+ - Network: `localhost` in WSL may differ from Windows -- use `127.0.0.1`
387
+ """
388
+
389
+ POWERSHELL_RULES = """\
390
+ ## Shell Environment: PowerShell (Windows)
391
+ - Use `;` to chain commands, NOT `&&` (PowerShell does not support `&&`)
392
+ - Use `$env:VAR = "value"` for environment variables, NOT `export VAR=value`
393
+ - Use `python` (not `python3`)
394
+ - Do NOT use `echo -e` or ANSI escapes -- use `Write-Output`
395
+ - Do NOT use `grep` -- use `Select-String` or `findstr`
396
+ - Do NOT use `&` for background processes -- they will fail
397
+ - File paths use `\\` but `/` also works in most cases
398
+ - Do NOT use `curl` -- use `Invoke-WebRequest` or Python's requests/httpx
399
+ """
400
+
401
+ REPO_CONTEXT_TEMPLATE = """\
402
+ ## Current Project Context
403
+ - Project type: {project_type}
404
+ - Frameworks: {frameworks}
405
+ - Tracked files: {file_count}
406
+ - Entry points: {entry_points}
407
+ - Has tests: {has_tests}
408
+ {extra}"""
409
+
410
+ INTENT_CREATE = """\
411
+ ## Task Intent: Create New Code
412
+ - Write the COMPLETE, WORKING code on Turn 1 using write_file
413
+ - Do NOT explore the filesystem first -- start coding immediately
414
+ - Include ALL imports, ALL functions, ALL logic -- no stubs, no TODOs
415
+ - After writing, run with bash to verify
416
+ - Fix any errors with edit_file, then re-run
417
+ - When everything works: DONE: <summary>
418
+ """
419
+
420
+ INTENT_MODIFY = """\
421
+ ## Task Intent: Modify Existing Code
422
+ - Turn 1: Use read_file to read the relevant file(s)
423
+ - Turn 2: Use edit_file to make targeted changes
424
+ - Turn 3: Use bash to test the changes
425
+ - Fix any regressions, then: DONE: <summary>
426
+ """
427
+
428
+ INTENT_DEBUG = """\
429
+ ## Task Intent: Debug / Investigate
430
+ - Turn 1: Reproduce the issue -- run the failing command/test
431
+ - Turn 2: Read error output carefully, use read_file to examine source
432
+ - Turn 3: Fix the root cause with edit_file (not the symptom)
433
+ - Turn 4: Re-run to verify the fix
434
+ - DONE: <summary of what was wrong and how it was fixed>
435
+ """
436
+
437
+ INTENT_EXPLAIN = """\
438
+ ## Task Intent: Explain / Analyze
439
+ - Read the relevant file(s) with read_file
440
+ - Provide a clear, structured explanation
441
+ - Do NOT modify any files unless explicitly asked
442
+ - DONE: <explanation>
443
+ """
444
+
362
445
  class PromptBuilder:
363
446
  """Constructs system prompts and user content."""
364
447
 
@@ -367,7 +450,9 @@ class PromptBuilder:
367
450
  workdir: Path,
368
451
  is_offline: bool = False,
369
452
  complexity: str = "low",
453
+ task_context=None,
370
454
  ) -> str:
455
+ """Build system prompt, optionally enhanced with TaskContext discovery."""
371
456
  if is_offline:
372
457
  prompt = OFFLINE_SYSTEM_PROMPT
373
458
  elif complexity == "high":
@@ -375,6 +460,47 @@ class PromptBuilder:
375
460
  else:
376
461
  prompt = SYSTEM_PROMPT
377
462
 
463
+ # -- Inject shell-aware rules from TaskContext --
464
+ if task_context is not None:
465
+ shell = getattr(task_context, "shell", "bash")
466
+ if shell == "powershell":
467
+ prompt += "\n\n" + POWERSHELL_RULES
468
+ elif shell == "wsl":
469
+ prompt += "\n\n" + WSL_RULES
470
+ elif shell == "bash":
471
+ prompt += "\n\n" + BASH_RULES
472
+
473
+ # Inject repo context (if not an empty dir)
474
+ if getattr(task_context, "project_type", "empty") != "empty":
475
+ frameworks_str = ", ".join(task_context.frameworks) if task_context.frameworks else "none detected"
476
+ entry_str = ", ".join(task_context.entry_points) if task_context.entry_points else "none found"
477
+ extra_lines = []
478
+ if task_context.has_venv:
479
+ extra_lines.append("- Virtual environment: detected (.venv)")
480
+ if task_context.dependencies:
481
+ extra_lines.append(f"- Key dependencies: {', '.join(task_context.dependencies[:8])}")
482
+ extra = "\n".join(extra_lines)
483
+ prompt += "\n\n" + REPO_CONTEXT_TEMPLATE.format(
484
+ project_type=task_context.project_type,
485
+ frameworks=frameworks_str,
486
+ file_count=task_context.file_count,
487
+ entry_points=entry_str,
488
+ has_tests="yes" if task_context.has_tests else "no",
489
+ extra=extra,
490
+ )
491
+
492
+ # Inject intent-specific workflow
493
+ intent = getattr(task_context, "intent", "create")
494
+ if intent == "create":
495
+ prompt += "\n\n" + INTENT_CREATE
496
+ elif intent == "modify":
497
+ prompt += "\n\n" + INTENT_MODIFY
498
+ elif intent == "debug":
499
+ prompt += "\n\n" + INTENT_DEBUG
500
+ elif intent == "explain":
501
+ prompt += "\n\n" + INTENT_EXPLAIN
502
+ # qa intent: no extra prompt needed (model answers directly)
503
+
378
504
  memories = format_memories_for_prompt(workdir)
379
505
  if memories:
380
506
  prompt += f"\n\n{memories}"
File without changes
File without changes