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.
- package/adelie/__init__.py +1 -1
- package/adelie/agents/coder_ai.py +9 -1
- package/adelie/agents/runner_ai.py +1 -1
- package/adelie/agents/tester_ai.py +1 -1
- package/adelie/agents/writer_ai.py +11 -7
- package/adelie/env_strategy.py +20 -7
- package/adelie/kb/retriever.py +1 -1
- package/adelie/llm_client.py +13 -9
- package/adelie/orchestrator.py +24 -6
- package/package.json +1 -1
package/adelie/__init__.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 = (
|
package/adelie/env_strategy.py
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
479
|
-
if profile.shell_wrapper
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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)
|
package/adelie/kb/retriever.py
CHANGED
|
@@ -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
|
|
package/adelie/llm_client.py
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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()
|
package/adelie/orchestrator.py
CHANGED
|
@@ -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
|
-
[
|
|
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)
|
|
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.
|
|
992
|
-
|
|
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.
|
|
1084
|
-
|
|
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
|