adelie-ai 0.2.11 → 0.2.12

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.
@@ -13,4 +13,4 @@ def _get_version() -> str:
13
13
  pass
14
14
  return "0.0.0"
15
15
 
16
- __version__ = "0.2.11"
16
+ __version__ = "0.2.12"
@@ -238,12 +238,20 @@ def run_coder(
238
238
  if not filepath or not content:
239
239
  continue
240
240
 
241
- # Sanitize — prevent writing outside workspace
241
+ # Sanitize — prevent writing outside staging area
242
+ # Check both Unix absolute paths (/...) and Windows (C:\...)
242
243
  if filepath.startswith("/") or ".." in filepath:
243
244
  console.print(
244
245
  f"[yellow]⚠️ Skipped unsafe path: {filepath}[/yellow]"
245
246
  )
246
247
  continue
248
+ # Resolve-based check: ensure the final path is under STAGING_ROOT
249
+ resolved_out = (STAGING_ROOT / filepath).resolve()
250
+ if not str(resolved_out).startswith(str(STAGING_ROOT.resolve())):
251
+ console.print(
252
+ f"[yellow]⚠️ Skipped path escaping staging: {filepath}[/yellow]"
253
+ )
254
+ continue
247
255
 
248
256
  out_path = STAGING_ROOT / filepath
249
257
  out_path.parent.mkdir(parents=True, exist_ok=True)
@@ -57,7 +57,7 @@ DEPLOY_COMMANDS = RUN_COMMANDS + [
57
57
  BLOCKED_FLAGS = {"-c", "--eval", "eval", "exec", "--exec", "-e"}
58
58
 
59
59
  # Dangerous shell metacharacters
60
- BLOCKED_CHARS = {";", "|", "&&", "||", "`", "$(", ">>", "<<"}
60
+ BLOCKED_CHARS = {";", "|", "&", "&&", "||", "`", "$(", ">", ">>", "<<"}
61
61
 
62
62
  EXEC_TIMEOUT_BUILD = 120
63
63
  EXEC_TIMEOUT_RUN = 10 # Short timeout — we just check if it starts
@@ -44,7 +44,7 @@ ALLOWED_COMMANDS = [
44
44
  BLOCKED_FLAGS = {"-c", "--eval", "eval", "exec", "--exec", "-e"}
45
45
 
46
46
  # Dangerous shell metacharacters
47
- BLOCKED_CHARS = {";", "|", "&&", "||", "`", "$(", ">>", "<<"}
47
+ BLOCKED_CHARS = {";", "|", "&", "&&", "||", "`", "$(", ">", ">>", "<<"}
48
48
 
49
49
  EXEC_TIMEOUT = 60 # seconds
50
50
 
@@ -301,13 +301,17 @@ Remember: output ONLY a valid JSON array.
301
301
  ratio = min(len_existing, len_new) / max(len_existing, len_new)
302
302
  if ratio > 0.90:
303
303
  # Check first 200 chars — if very similar, skip
304
- common_start = 0
305
- for a, b in zip(existing_body[:200], new_body[:200]):
306
- if a == b:
307
- common_start += 1
308
- if common_start / min(200, len(existing_body[:200])) > 0.7:
309
- console.print(f"[dim] ⏭ Skipped {cat}/{filename} (similar content)[/dim]")
310
- continue
304
+ compare_len = min(200, len(existing_body), len(new_body))
305
+ if compare_len < 20:
306
+ pass # Too short to compare meaningfully
307
+ else:
308
+ common_start = 0
309
+ for a, b in zip(existing_body[:compare_len], new_body[:compare_len]):
310
+ if a == b:
311
+ common_start += 1
312
+ if common_start / compare_len > 0.7:
313
+ console.print(f"[dim] ⏭ Skipped {cat}/{filename} (similar content)[/dim]")
314
+ continue
311
315
 
312
316
  # Prepend a frontmatter header for human readability
313
317
  header = (
@@ -20,8 +20,10 @@ Strategy selection follows the project phase:
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
+ import os
23
24
  import shutil
24
25
  import subprocess
26
+ import sys
25
27
  from dataclasses import dataclass, field
26
28
  from enum import Enum
27
29
  from pathlib import Path
@@ -124,6 +126,7 @@ def detect_env(project_root: Path) -> EnvProfile:
124
126
  # ── Python environments ───────────────────────────────────────────────
125
127
 
126
128
  # Standard venv
129
+ _is_win = sys.platform == "win32"
127
130
  for venv_dir in [".venv", "venv"]:
128
131
  venv_path = project_root / venv_dir
129
132
  if venv_path.is_dir():
@@ -133,7 +136,12 @@ def detect_env(project_root: Path) -> EnvProfile:
133
136
  if bin_dir.exists():
134
137
  profile.python_bin = str(bin_dir / "python")
135
138
  profile.pip_bin = str(bin_dir / "pip")
136
- profile.shell_wrapper = f"source {venv_path / 'bin' / 'activate'}"
139
+ # Windows: use activate.bat; Unix: source activate
140
+ if _is_win:
141
+ activate_script = bin_dir / "activate.bat"
142
+ profile.shell_wrapper = f"{activate_script} &&"
143
+ else:
144
+ profile.shell_wrapper = f"source {bin_dir / 'activate'}"
137
145
  profile.env_type = "venv"
138
146
  detected.append("venv")
139
147
  break
@@ -164,7 +172,7 @@ def detect_env(project_root: Path) -> EnvProfile:
164
172
  node_modules_bin = project_root / "node_modules" / ".bin"
165
173
  if node_modules_bin.is_dir():
166
174
  profile.node_bin = str(node_modules_bin / "node") if (node_modules_bin / "node").exists() else None
167
- profile.npm_prefix = str(node_modules_bin) + "/"
175
+ profile.npm_prefix = str(node_modules_bin) + os.sep
168
176
  if profile.env_type == "system":
169
177
  profile.env_type = "npm"
170
178
  detected.append("npm")
@@ -475,11 +483,16 @@ def _wrap_resolver(cmd: str, profile: EnvProfile) -> str:
475
483
  elif profile.env_type == "poetry":
476
484
  return f"poetry run {cmd}"
477
485
 
478
- # For standard venv: use bash -c with activation
479
- if profile.shell_wrapper and "source" in profile.shell_wrapper:
480
- # Escape single quotes in cmd
481
- escaped_cmd = cmd.replace("'", "'\\''")
482
- return f"bash -c '{profile.shell_wrapper} && {escaped_cmd}'"
486
+ # For standard venv: wrap with activation
487
+ if profile.shell_wrapper:
488
+ if sys.platform == "win32" and profile.shell_wrapper.endswith("&&"):
489
+ # Windows: cmd /c "activate.bat && command"
490
+ escaped_cmd = cmd.replace('"', '\\"')
491
+ return f'cmd /c "{profile.shell_wrapper} {escaped_cmd}"'
492
+ elif "source" in profile.shell_wrapper:
493
+ # Unix: bash -c "source activate && command"
494
+ escaped_cmd = cmd.replace("'", "'\\''")
495
+ return f"bash -c '{profile.shell_wrapper} && {escaped_cmd}'"
483
496
 
484
497
  # Fallback to direct if no resolver available
485
498
  return _wrap_direct(cmd, profile)
@@ -77,7 +77,7 @@ def list_categories() -> dict[str, int]:
77
77
  """Return a dict of {category_name: file_count} for all KB categories."""
78
78
  ensure_workspace()
79
79
  return {
80
- cat: len(list((WORKSPACE_PATH / cat).glob("*")))
80
+ cat: len(list((WORKSPACE_PATH / cat).glob("*.md")))
81
81
  for cat in KB_CATEGORIES
82
82
  }
83
83
 
@@ -32,6 +32,7 @@ console = Console()
32
32
 
33
33
  # ── Token usage tracking ─────────────────────────────────────────────────────
34
34
  _usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "calls": 0}
35
+ _usage_lock = threading.Lock()
35
36
 
36
37
  # Per-agent usage tracking (agent_name -> {prompt_tokens, completion_tokens, total_tokens, calls, time})
37
38
  _agent_usage: dict[str, dict] = {}
@@ -58,17 +59,19 @@ def _get_current_agent() -> str:
58
59
 
59
60
  def reset_usage() -> None:
60
61
  """Reset token counters (call at start of each loop)."""
61
- _usage["prompt_tokens"] = 0
62
- _usage["completion_tokens"] = 0
63
- _usage["total_tokens"] = 0
64
- _usage["calls"] = 0
62
+ with _usage_lock:
63
+ _usage["prompt_tokens"] = 0
64
+ _usage["completion_tokens"] = 0
65
+ _usage["total_tokens"] = 0
66
+ _usage["calls"] = 0
65
67
  with _agent_usage_lock:
66
68
  _agent_usage.clear()
67
69
 
68
70
 
69
71
  def get_usage() -> dict:
70
72
  """Return current accumulated token usage."""
71
- return dict(_usage)
73
+ with _usage_lock:
74
+ return dict(_usage)
72
75
 
73
76
 
74
77
  def get_agent_usage() -> dict[str, dict]:
@@ -78,10 +81,11 @@ def get_agent_usage() -> dict[str, dict]:
78
81
 
79
82
 
80
83
  def _record_usage(prompt: int, completion: int) -> None:
81
- _usage["prompt_tokens"] += prompt
82
- _usage["completion_tokens"] += completion
83
- _usage["total_tokens"] += prompt + completion
84
- _usage["calls"] += 1
84
+ with _usage_lock:
85
+ _usage["prompt_tokens"] += prompt
86
+ _usage["completion_tokens"] += completion
87
+ _usage["total_tokens"] += prompt + completion
88
+ _usage["calls"] += 1
85
89
 
86
90
  # Also record per-agent
87
91
  agent = _get_current_agent()
@@ -16,6 +16,7 @@ import json
16
16
  from typing import Callable, Optional
17
17
  import signal
18
18
  import sys
19
+ import threading
19
20
  import time
20
21
  from concurrent.futures import ThreadPoolExecutor, as_completed
21
22
  from datetime import datetime
@@ -94,6 +95,10 @@ class Orchestrator:
94
95
  # Process supervisor for spawned commands
95
96
  self.supervisor = ProcessSupervisor(max_concurrent=5)
96
97
 
98
+ # Lock to protect staging directory operations from race conditions
99
+ # between the tester thread (Phase 3) and the main orchestrator thread.
100
+ self._staging_lock = threading.Lock()
101
+
97
102
 
98
103
 
99
104
  # Graceful shutdown on Ctrl+C or SIGTERM
@@ -390,8 +395,15 @@ class Orchestrator:
390
395
  Verify staged files with lightweight syntax checks before promotion.
391
396
  Returns (passed, failed) file lists.
392
397
  """
398
+ import shutil
393
399
  import subprocess
394
400
  staging_root = ADELIE_ROOT / "staging"
401
+ # On Windows, 'python3' may resolve to the Microsoft Store stub
402
+ # (WindowsApps/python3.EXE) which doesn't work. Prefer sys.executable.
403
+ if sys.platform == "win32":
404
+ python_bin = sys.executable
405
+ else:
406
+ python_bin = shutil.which("python3") or shutil.which("python") or sys.executable
395
407
  passed: list[dict] = []
396
408
  failed: list[dict] = []
397
409
 
@@ -410,7 +422,7 @@ class Orchestrator:
410
422
  if ext == ".py":
411
423
  try:
412
424
  result = subprocess.run(
413
- ["python3", "-m", "py_compile", str(staged_path)],
425
+ [python_bin, "-m", "py_compile", str(staged_path)],
414
426
  capture_output=True, text=True, timeout=10,
415
427
  )
416
428
  if result.returncode != 0:
@@ -931,9 +943,13 @@ class Orchestrator:
931
943
  score = review.get("overall_score", 5)
932
944
  self._review_score_history.append(score)
933
945
 
934
- if review.get("approved", True) or retry >= MAX_REVIEW_RETRIES:
946
+ if review.get("approved", True):
935
947
  reviewer_approved = True
936
948
  break
949
+ if retry >= MAX_REVIEW_RETRIES:
950
+ # Retry limit reached — use actual review result (do NOT force approve)
951
+ reviewer_approved = False
952
+ break
937
953
 
938
954
  # Feed review back to coder for retry
939
955
  console.print(f" [yellow]🔄 Retry {retry+1}/{MAX_REVIEW_RETRIES} — sending feedback to coder[/yellow]")
@@ -988,8 +1004,9 @@ class Orchestrator:
988
1004
 
989
1005
  # ── Promote staged files to project (after review) ────────────────
990
1006
  if all_written_files and reviewer_approved:
991
- self._promote_staged_files(all_written_files)
992
- self._cleanup_staging()
1007
+ with self._staging_lock:
1008
+ self._promote_staged_files(all_written_files)
1009
+ self._cleanup_staging()
993
1010
 
994
1011
  # ── Git auto-commit (MID_1+) ──────────────────────────────────────
995
1012
  if self.phase in ("mid_1", "mid_2", "late", "evolve"):
@@ -1080,8 +1097,9 @@ class Orchestrator:
1080
1097
  new_files = self._collect_staged_files(fix_start_time)
1081
1098
  if new_files:
1082
1099
  _files = new_files
1083
- self._promote_staged_files(_files)
1084
- self._cleanup_staging()
1100
+ with self._staging_lock:
1101
+ self._promote_staged_files(_files)
1102
+ self._cleanup_staging()
1085
1103
  except Exception as ce:
1086
1104
  console.print(f"[red]❌ Coder fix error: {ce}[/red]")
1087
1105
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adelie-ai",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Adelie — Self-Communicating Autonomous AI Loop CLI",
5
5
  "bin": {
6
6
  "adelie": "bin/adelie.js"