alpiecode 5.1.1__tar.gz → 6.0.0__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.1.1 → alpiecode-6.0.0}/PKG-INFO +1 -1
  2. {alpiecode-5.1.1 → alpiecode-6.0.0}/pyproject.toml +1 -1
  3. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/agent.py +15 -7
  5. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/cli.py +2 -0
  6. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/compaction.py +1 -1
  7. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/discovery.py +18 -15
  8. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/guardian.py +9 -4
  9. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/orchestrator.py +23 -6
  10. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/prompt.py +29 -6
  11. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/tools.py +39 -9
  12. {alpiecode-5.1.1 → alpiecode-6.0.0}/README.md +0 -0
  13. {alpiecode-5.1.1 → alpiecode-6.0.0}/setup.cfg +0 -0
  14. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/alpiecode/__init__.py +0 -0
  15. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/alpiecode.egg-info/SOURCES.txt +0 -0
  16. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  17. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
  18. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/alpiecode.egg-info/requires.txt +0 -0
  19. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/alpiecode.egg-info/top_level.txt +0 -0
  20. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/__init__.py +0 -0
  21. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/backends/__init__.py +0 -0
  22. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/backends/base.py +0 -0
  23. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/backends/local_backend.py +0 -0
  24. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/backends/openai_backend.py +0 -0
  25. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/cache.py +0 -0
  26. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/client.py +0 -0
  27. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/config.py +0 -0
  28. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/context.py +0 -0
  29. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/doctor.py +0 -0
  30. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/executor.py +0 -0
  31. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/extension/alpiecode.vsix +0 -0
  32. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/github.py +0 -0
  33. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/ipython_ext.py +0 -0
  34. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/local_model.py +0 -0
  35. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/media.py +0 -0
  36. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/memory.py +0 -0
  37. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/server.py +0 -0
  38. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/session.py +0 -0
  39. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/updater.py +0 -0
  40. {alpiecode-5.1.1 → alpiecode-6.0.0}/src/codeagent/vscode_installer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 5.1.1
3
+ Version: 6.0.0
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.1.1"
7
+ version = "6.0.0"
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.1.1
3
+ Version: 6.0.0
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
@@ -37,6 +37,7 @@ _build_system_prompt = lambda workdir, is_offline=False: PromptBuilder().build_s
37
37
  try:
38
38
  from rich.console import Console
39
39
  from rich.markdown import Markdown
40
+ from rich.markup import escape
40
41
  from rich.panel import Panel
41
42
  from rich.rule import Rule
42
43
  from rich.text import Text
@@ -46,6 +47,9 @@ try:
46
47
  except ImportError:
47
48
  HAS_RICH = False
48
49
 
50
+ def escape(text: str) -> str:
51
+ return text
52
+
49
53
  class _FallbackConsole:
50
54
  def print(self, *args, **kwargs):
51
55
  kwargs.pop("style", None)
@@ -78,7 +82,7 @@ def _print_tool_call(turn: int, name: str, args: dict):
78
82
  if HAS_RICH:
79
83
  args_str = json.dumps(display_args, indent=2)
80
84
  console.print(f"\n🔧 [bold cyan]Tool:[/bold cyan] [bold]{name}[/bold]", highlight=False)
81
- console.print(f" {args_str}", style="cyan", highlight=False)
85
+ console.print(Text(f" {args_str}", style="cyan"))
82
86
  else:
83
87
  console.print(f"\n🔧 Tool: {name}({display_args})")
84
88
 
@@ -86,7 +90,7 @@ def _print_tool_call(turn: int, name: str, args: dict):
86
90
  def _print_tool_result(result: str):
87
91
  truncated = result[:1500] + ("..." if len(result) > 1500 else "")
88
92
  if HAS_RICH:
89
- console.print(f" → {truncated}", style="green", highlight=False)
93
+ console.print(Text(f" → {truncated}", style="green"))
90
94
  else:
91
95
  console.print(f" → {truncated}")
92
96
 
@@ -97,7 +101,7 @@ def _print_assistant_message(content: str):
97
101
  md = Markdown(content)
98
102
  console.print(Panel(md, title="🤖 Assistant", border_style="green", padding=(0, 1)))
99
103
  except Exception:
100
- console.print(Panel(content, title="🤖 Assistant", border_style="green", padding=(0, 1)))
104
+ console.print(Panel(Text(content), title="🤖 Assistant", border_style="green", padding=(0, 1)))
101
105
  else:
102
106
  console.print(f"\n🤖 Assistant: {content}")
103
107
 
@@ -225,7 +229,7 @@ def run_agent(
225
229
  data = event.data
226
230
  if HAS_RICH:
227
231
  console.rule("[bold blue]Agent Started[/bold blue]")
228
- console.print(f"📋 Task: {task.splitlines()[0]}", style="bold")
232
+ console.print(Text(f"📋 Task: {task.splitlines()[0]}", style="bold"))
229
233
  if github_repo:
230
234
  console.print(f"🐙 GitHub Repo: {github_repo}", style="cyan")
231
235
  if image_path:
@@ -288,8 +292,9 @@ def run_agent(
288
292
  _checkpoint(workdir, "checkpoint: response")
289
293
 
290
294
  elif event.type == "fallback" and verbose:
295
+ err_str = escape(str(event.data.get('error', '')))
291
296
  if HAS_RICH:
292
- console.print(f"\n⚠️ [bold yellow]Online Server Error / Timeout[/bold yellow] ({event.data['error']})", style="yellow")
297
+ console.print(f"\n⚠️ [bold yellow]Online Server Error / Timeout[/bold yellow] ({err_str})", style="yellow")
293
298
  console.print("🔄 [bold cyan]Auto-falling back to local GGUF engine...[/bold cyan]", style="cyan")
294
299
  else:
295
300
  print(f"\n⚠️ Online Server Error: {event.data['error']}")
@@ -297,7 +302,7 @@ def run_agent(
297
302
 
298
303
  elif event.type == "error" and verbose:
299
304
  if HAS_RICH:
300
- console.print(f"\n❌ [bold red]Model Error[/bold red]\n Error: {event.data['error']}\n")
305
+ console.print(Text(f"\n❌ Model Error\n Error: {event.data['error']}\n", style="bold red"))
301
306
  else:
302
307
  print(f"\n❌ Model Error: {event.data['error']}")
303
308
 
@@ -385,7 +390,10 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
385
390
  _checkpoint(workdir, "checkpoint: done")
386
391
 
387
392
  elif event.type == "error":
388
- console.print(f"❌ Model error: {event.data['error']}", style="bold red" if HAS_RICH else None)
393
+ if HAS_RICH:
394
+ console.print(Text(f"❌ Model error: {event.data['error']}", style="bold red"))
395
+ else:
396
+ print(f"❌ Model error: {event.data['error']}")
389
397
 
390
398
  elif event.type == "done":
391
399
  break
@@ -162,6 +162,7 @@ def main():
162
162
  if args.command == "run":
163
163
  if args.max_turns:
164
164
  cfg.max_turns = args.max_turns
165
+ cfg._explicit_max_turns = True
165
166
  _show_banner()
166
167
  from .agent import run_agent
167
168
  run_agent(
@@ -177,6 +178,7 @@ def main():
177
178
  elif args.command == "chat":
178
179
  if args.max_turns:
179
180
  cfg.max_turns = args.max_turns
181
+ cfg._explicit_max_turns = True
180
182
  _show_banner()
181
183
  from .agent import run_chat
182
184
  run_chat(Path(args.workdir), cfg, verbose=not args.quiet)
@@ -20,7 +20,7 @@ MAX_CONTEXT_TOKENS = 262_144
20
20
  # Start compacting when we hit this percentage of the context window
21
21
  COMPACT_THRESHOLD = 0.70
22
22
  # Number of recent turns to always keep intact
23
- KEEP_RECENT_TURNS = 20
23
+ KEEP_RECENT_TURNS = 12
24
24
  # Approximate chars per token (rough heuristic)
25
25
  CHARS_PER_TOKEN = 4
26
26
 
@@ -178,16 +178,18 @@ def detect_environment() -> dict:
178
178
 
179
179
 
180
180
  def _detect_shell(os_name: str) -> str:
181
- """Detect available shell, preferring WSL on Windows like Claude/Codex agents."""
181
+ """Detect available shell, aligned with tools.py execution path."""
182
182
 
183
183
  if os_name == "windows":
184
- # On Windows: prefer WSL (like Claude, Codex, Gemini agents do)
184
+ # Check if bash binary exists (Git Bash / MSYS)
185
+ if shutil.which("bash"):
186
+ return "bash"
185
187
  if _is_wsl_available():
186
188
  return "wsl"
187
189
  # Fallback to PowerShell
188
190
  if shutil.which("powershell") or shutil.which("pwsh"):
189
191
  return "powershell"
190
- return "cmd"
192
+ return "cmd"
191
193
 
192
194
  if os_name in ("linux", "wsl"):
193
195
  return "bash" # Standard on Linux/WSL
@@ -466,13 +468,14 @@ def compute_complexity(intent: str, repo_info: dict, task: str) -> str:
466
468
 
467
469
  # -- MEDIUM: multi-step tasks --
468
470
  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",
471
+ "api", "rest api", "graphql", "server", "backend", "frontend",
472
+ "game", "snake", "tetris", "chess", "pong", "sudoku", "flappy", "arcade",
473
+ "website", "web page", "web app", "webapp", "html", "css", "javascript",
474
+ "test suite", "unit tests", "integration test", "e2e test",
475
+ "dashboard", "portfolio", "application", "app with", "notes app", "todo app",
476
+ "react", "vue", "angular", "next.js", "tailwind",
477
+ "django", "flask app", "fastapi app", "express",
478
+ "refactor", "migrate", "redesign", "restructure", "investigate",
476
479
  ]
477
480
  if any(kw in task_lower for kw in medium_keywords):
478
481
  return "medium"
@@ -503,19 +506,19 @@ COMPLEXITY_CONFIG = {
503
506
  "enable_thinking": False,
504
507
  },
505
508
  "low": {
506
- "max_turns": 10,
509
+ "max_turns": 15,
507
510
  "max_tokens": 8192,
508
511
  "tool_set": "core",
509
512
  "enable_thinking": False,
510
513
  },
511
514
  "medium": {
512
- "max_turns": 20,
513
- "max_tokens": 8192,
515
+ "max_turns": 40,
516
+ "max_tokens": 16384,
514
517
  "tool_set": "full",
515
- "enable_thinking": False,
518
+ "enable_thinking": True,
516
519
  },
517
520
  "high": {
518
- "max_turns": 40,
521
+ "max_turns": 60,
519
522
  "max_tokens": 16384,
520
523
  "tool_set": "full",
521
524
  "enable_thinking": True,
@@ -16,12 +16,17 @@ from typing import Tuple
16
16
  try:
17
17
  from rich.console import Console
18
18
  from rich.panel import Panel
19
+ from rich.text import Text
20
+ from rich.markup import escape
19
21
  console = Console()
20
22
  HAS_RICH = True
21
23
  except ImportError:
22
24
  HAS_RICH = False
23
25
  console = None
24
26
 
27
+ def escape(text: str) -> str:
28
+ return text
29
+
25
30
 
26
31
  class RiskLevel(Enum):
27
32
  SAFE = "safe"
@@ -134,19 +139,19 @@ def gate_command(command: str, auto_approve: bool = False) -> bool:
134
139
  if risk == RiskLevel.WARNING:
135
140
  if auto_approve:
136
141
  if HAS_RICH:
137
- console.print(f" ⚠️ [yellow]{reason}[/yellow]", highlight=False)
142
+ console.print(Text(f" ⚠️ {reason}", style="yellow"))
138
143
  return True
139
144
  # In interactive mode, show warning but proceed
140
145
  if HAS_RICH:
141
- console.print(f" ⚠️ [yellow]{reason}[/yellow]", highlight=False)
146
+ console.print(Text(f" ⚠️ {reason}", style="yellow"))
142
147
  return True
143
148
 
144
149
  if risk == RiskLevel.DANGEROUS:
145
150
  if HAS_RICH:
146
151
  console.print(Panel(
147
152
  f"[bold red]🛑 BLOCKED — Dangerous Command[/bold red]\n\n"
148
- f"Command: [cyan]{command}[/cyan]\n"
149
- f"Reason: {reason}\n\n"
153
+ f"Command: [cyan]{escape(command)}[/cyan]\n"
154
+ f"Reason: {escape(reason)}\n\n"
150
155
  f"This command has been blocked for safety.\n"
151
156
  f"If you need to run it, do so manually in your terminal.",
152
157
  border_style="red",
@@ -77,7 +77,7 @@ class AgentOrchestrator:
77
77
  effective_max_tokens = task_context.max_tokens
78
78
 
79
79
  # User override: if they set --max-turns explicitly, respect it
80
- if cfg.max_turns != 20: # 20 is new default, so non-default = explicit
80
+ if getattr(cfg, "_explicit_max_turns", False):
81
81
  effective_max_turns = cfg.max_turns
82
82
 
83
83
  # ── Response cache check ──
@@ -137,6 +137,13 @@ class AgentOrchestrator:
137
137
  "complexity": complexity,
138
138
  })
139
139
 
140
+ if complexity in ("medium", "high"):
141
+ session.context.add_user_message(
142
+ f"[BUDGET & GOAL] Available turn budget: {effective_max_turns} turns. "
143
+ "Plan the needed components, create the complete files, verify with bash, "
144
+ "and finish with DONE: <summary> as soon as verification succeeds."
145
+ )
146
+
140
147
  # ── Adaptive thinking ──
141
148
  enable_thinking = cfg.enable_thinking or task_context.enable_thinking
142
149
  if enable_thinking and complexity in ("qa", "low"):
@@ -155,17 +162,27 @@ class AgentOrchestrator:
155
162
  if session.context.check_and_compact():
156
163
  yield AgentEvent("compaction", {"turn": turn + 1})
157
164
 
158
- # ── Wrap-up injection at 80% of turns ──
159
- if not wrap_up_injected and turn >= int(effective_max_turns * 0.8):
165
+ # ── Progressive wrap-up injection ──
166
+ # Step 1: Gentle verification reminder at 70%
167
+ if not wrap_up_injected and turn >= int(effective_max_turns * 0.70):
160
168
  wrap_up_injected = True
161
169
  remaining = effective_max_turns - turn
162
170
  session.context.add_user_message(
163
- f"[SYSTEM] You have {remaining} turns remaining. "
164
- "Finish your current work now. If code is written and tested, "
165
- "output DONE: <summary>. If code has errors, make one final fix attempt."
171
+ f"[SYSTEM] Turn budget update: {remaining} turns remaining. "
172
+ "Ensure all necessary files are created and run verification tests now. "
173
+ "As soon as verification succeeds, output DONE: <summary>."
166
174
  )
167
175
  yield AgentEvent("wrap_up", {"turn": turn + 1, "remaining": remaining})
168
176
 
177
+ # Step 2: Final wrap-up call at 90%
178
+ if not getattr(self, "_final_wrap_up_injected", False) and turn >= int(effective_max_turns * 0.90):
179
+ self._final_wrap_up_injected = True
180
+ remaining = effective_max_turns - turn
181
+ session.context.add_user_message(
182
+ f"[SYSTEM] FINAL TURNS: Only {remaining} turns remaining. "
183
+ "Do not start new exploration. Fix any remaining errors and output DONE: <summary>."
184
+ )
185
+
169
186
  yield AgentEvent("turn_start", {"turn": turn + 1})
170
187
 
171
188
  try:
@@ -339,19 +339,19 @@ COMPLEXITY_CONFIG = {
339
339
  "prompt": "default",
340
340
  },
341
341
  "low": {
342
- "max_turns": 10,
342
+ "max_turns": 15,
343
343
  "max_tokens": 8192,
344
344
  "tools": "core", # 5 core tools
345
345
  "prompt": "default",
346
346
  },
347
347
  "medium": {
348
- "max_turns": 20,
349
- "max_tokens": 8192,
348
+ "max_turns": 40,
349
+ "max_tokens": 16384,
350
350
  "tools": "full", # All 15 tools
351
351
  "prompt": "default",
352
352
  },
353
353
  "high": {
354
- "max_turns": 40,
354
+ "max_turns": 60,
355
355
  "max_tokens": 16384,
356
356
  "tools": "full", # All 15 tools
357
357
  "prompt": "high", # Detailed system prompt
@@ -408,7 +408,7 @@ REPO_CONTEXT_TEMPLATE = """\
408
408
  {extra}"""
409
409
 
410
410
  INTENT_CREATE = """\
411
- ## Task Intent: Create New Code
411
+ ## Task Intent: Create New Code (Single-file / Simple Tasks)
412
412
  - Write the COMPLETE, WORKING code on Turn 1 using write_file
413
413
  - Do NOT explore the filesystem first -- start coding immediately
414
414
  - Include ALL imports, ALL functions, ALL logic -- no stubs, no TODOs
@@ -417,6 +417,23 @@ INTENT_CREATE = """\
417
417
  - When everything works: DONE: <summary>
418
418
  """
419
419
 
420
+ INTENT_CREATE_COMPLEX = """\
421
+ ## Task Intent: Create Multi-Component Code (Medium/High Complexity)
422
+ - Turn 1: Think through and plan the architecture. Identify ALL files needed (e.g. HTML, CSS, JS, tests).
423
+ - Sequential Creation: Write each file completely using write_file in logical dependency order.
424
+ - No Thrashing: Do NOT delete files you just created with `rm`. If adjustments are needed, use edit_file or overwrite directly.
425
+ - Verification: After writing the files, run or verify them using bash (e.g. run test suite, build check, or verify syntax).
426
+ - Early Stopping: Once verified and working, output: DONE: <summary> immediately. Do not run redundant checks.
427
+ """
428
+
429
+ GOAL_DRIVEN_RULES = """\
430
+ ## Execution & Goal Convergence Rules
431
+ - Efficiency First: Plan your actions to minimize wasted turns. You can execute multiple tool calls in a single turn.
432
+ - No Thrashing: NEVER delete a file (e.g. `rm <file>`) immediately after creating it to start over. Use `edit_file` to modify what needs fixing.
433
+ - Immediate Completion: As soon as your code is written and verified, output `DONE: <summary>`. Do NOT linger or rerun commands that already passed.
434
+ - Targeted Editing: If `edit_file` fails, use `read_file` to inspect the exact lines and whitespace before attempting another edit.
435
+ """
436
+
420
437
  INTENT_MODIFY = """\
421
438
  ## Task Intent: Modify Existing Code
422
439
  - Turn 1: Use read_file to read the relevant file(s)
@@ -492,13 +509,19 @@ class PromptBuilder:
492
509
  # Inject intent-specific workflow
493
510
  intent = getattr(task_context, "intent", "create")
494
511
  if intent == "create":
495
- prompt += "\n\n" + INTENT_CREATE
512
+ if complexity in ("medium", "high"):
513
+ prompt += "\n\n" + INTENT_CREATE_COMPLEX
514
+ else:
515
+ prompt += "\n\n" + INTENT_CREATE
496
516
  elif intent == "modify":
497
517
  prompt += "\n\n" + INTENT_MODIFY
498
518
  elif intent == "debug":
499
519
  prompt += "\n\n" + INTENT_DEBUG
500
520
  elif intent == "explain":
501
521
  prompt += "\n\n" + INTENT_EXPLAIN
522
+
523
+ if complexity in ("medium", "high"):
524
+ prompt += "\n\n" + GOAL_DRIVEN_RULES
502
525
  # qa intent: no extra prompt needed (model answers directly)
503
526
 
504
527
  memories = format_memories_for_prompt(workdir)
@@ -1,3 +1,5 @@
1
+ _RECENTLY_WRITTEN_FILES = set()
2
+
1
3
  """
2
4
  Tool definitions and implementations for AlpieCode.
3
5
 
@@ -437,11 +439,11 @@ def _bash(workdir: Path, command: str) -> str:
437
439
  shell_cmd,
438
440
  cwd=workdir, capture_output=True, text=True,
439
441
  stdin=subprocess.DEVNULL,
440
- timeout=30, env=env,
442
+ timeout=120, env=env,
441
443
  )
442
444
 
443
- stdout = _smart_truncate(result.stdout, 6000)
444
- stderr = _smart_truncate(result.stderr, 4000)
445
+ stdout = _smart_truncate(result.stdout, 3000)
446
+ stderr = _smart_truncate(result.stderr, 2000)
445
447
 
446
448
  output = json.dumps({
447
449
  "stdout": stdout,
@@ -463,6 +465,20 @@ def _bash(workdir: Path, command: str) -> str:
463
465
  )
464
466
  output = hint + output
465
467
 
468
+ # ── Anti-thrashing guard: warned if deleting file created in this session ──
469
+ cmd_parts = command.strip().split()
470
+ if cmd_parts and cmd_parts[0] in ("rm", "del", "Remove-Item"):
471
+ for part in cmd_parts[1:]:
472
+ base_part = Path(part.strip('"\'')).name.lower()
473
+ if base_part in _RECENTLY_WRITTEN_FILES:
474
+ _RECENTLY_WRITTEN_FILES.discard(base_part)
475
+ output += (
476
+ f"\n\n💡 ADVICE: You deleted '{base_part}' which was created in a recent turn. "
477
+ "Avoid deleting and rewriting entire files from scratch. "
478
+ "For iterative improvements, use edit_file or overwrite directly with write_file."
479
+ )
480
+ break
481
+
466
482
  # ── Smart Guardrail 2: Silent success — script ran but no output ──
467
483
  elif result.returncode == 0 and not stdout.strip() and not stderr.strip():
468
484
  script_file = _extract_script_path(command)
@@ -505,7 +521,8 @@ def _read_file(workdir: Path, path: str, start_line: int = None, end_line: int =
505
521
  def _write_file(workdir: Path, path: str, content: str) -> str:
506
522
  p = workdir / path
507
523
  p.parent.mkdir(parents=True, exist_ok=True)
508
- p.write_text(content)
524
+ p.write_text(content, encoding="utf-8")
525
+ _RECENTLY_WRITTEN_FILES.add(Path(path).name.lower())
509
526
  return f"wrote {len(content)} bytes to {path}"
510
527
 
511
528
 
@@ -513,13 +530,25 @@ def _edit_file(workdir: Path, path: str, old_str: str, new_str: str) -> str:
513
530
  p = workdir / path
514
531
  if not p.exists():
515
532
  return f"error: file not found: {path}"
516
- text = p.read_text()
533
+ text = p.read_text(encoding="utf-8", errors="replace")
517
534
  count = text.count(old_str)
518
535
  if count == 0:
519
- return "error: old_str not found in file. Make sure it matches exactly (including whitespace)."
536
+ import difflib
537
+ lines = text.splitlines()
538
+ old_lines = [ol.strip() for ol in old_str.splitlines() if ol.strip()]
539
+ target_line = old_lines[0] if old_lines else old_str.strip()
540
+ close_matches = difflib.get_close_matches(target_line, [l.strip() for l in lines], n=3, cutoff=0.3)
541
+ nearby = [(i + 1, l) for i, l in enumerate(lines) if l.strip() in close_matches]
542
+ hint = ""
543
+ if nearby:
544
+ hint = "\nClosest matching lines in file:\n" + "\n".join(f" Line {n}: {l.strip()[:80]}" for n, l in nearby[:3])
545
+ return (
546
+ f"error: old_str not found in file '{path}'. It must match existing text EXACTLY (including whitespace/indentation).{hint}\n"
547
+ f"→ Action: Run read_file on '{path}' around these lines to see the exact whitespace, then retry edit_file."
548
+ )
520
549
  if count > 1:
521
- return f"error: old_str matched {count} times, need exactly 1 match. Use a more specific old_str."
522
- p.write_text(text.replace(old_str, new_str, 1))
550
+ return f"error: old_str matched {count} times in '{path}'. Must match exactly 1 occurrence. Include more surrounding lines in old_str to make it unique."
551
+ p.write_text(text.replace(old_str, new_str, 1), encoding="utf-8")
523
552
  return "edit applied"
524
553
 
525
554
 
@@ -689,9 +718,10 @@ def _request_user_input(question: str) -> str:
689
718
  try:
690
719
  from rich.console import Console
691
720
  from rich.panel import Panel
721
+ from rich.text import Text
692
722
  console = Console()
693
723
  console.print(Panel(
694
- f"[bold]{question}[/bold]",
724
+ Text(question, style="bold"),
695
725
  title="❓ Agent needs your input",
696
726
  border_style="yellow",
697
727
  ))
File without changes
File without changes