adelie-ai 0.2.11 → 0.2.13

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/README.md CHANGED
@@ -6,14 +6,14 @@
6
6
 
7
7
  <p align="center">
8
8
  <strong>Autonomous AI Orchestration System</strong><br/>
9
- <sub>10 specialized agents · 6-phase lifecycle · zero human intervention</sub>
9
+ <sub>13 specialized agents · 6-phase lifecycle · zero human intervention</sub>
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
13
  <a href="https://www.npmjs.com/package/adelie-ai"><img src="https://img.shields.io/npm/v/adelie-ai?style=flat-square&logo=npm&color=CB3837" alt="npm version" /></a>
14
14
  <img src="https://img.shields.io/badge/python-3.10+-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python" />
15
15
  <img src="https://img.shields.io/badge/LLM-Gemini%20│%20Ollama-FF6F00?style=flat-square" alt="LLM" />
16
- <img src="https://img.shields.io/badge/tests-197%20passing-2EA043?style=flat-square" alt="Tests" />
16
+ <img src="https://img.shields.io/badge/tests-636%20passing-2EA043?style=flat-square" alt="Tests" />
17
17
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License" /></a>
18
18
  </p>
19
19
 
@@ -23,7 +23,8 @@
23
23
  <a href="#architecture">Architecture</a>&ensp;·&ensp;
24
24
  <a href="#cli">CLI</a>&ensp;·&ensp;
25
25
  <a href="#dashboard">Dashboard</a>&ensp;·&ensp;
26
- <a href="#configuration">Configuration</a>
26
+ <a href="https://ade1ie.github.io/adelie/">Docs</a>&ensp;·&ensp;
27
+ <a href="https://github.com/Ade1ie/adelie/blob/main/CHANGELOG.md">Changelog</a>
27
28
  </p>
28
29
 
29
30
  ---
@@ -33,8 +34,8 @@
33
34
  Adelie is an autonomous AI orchestrator that plans, codes, reviews, tests, deploys, and evolves software projects through a coordinated multi-agent loop. It ships as a single CLI (`npm install -g adelie-ai`) and requires only an LLM provider — no cloud backend, no account.
34
35
 
35
36
  ```
36
- (o_ Adelie v0.2.1
37
- //\ ollama · deepseek-v3.1:671b-cloud
37
+ (o_ Adelie v0.2.12
38
+ //\ gemini · gemini-2.0-flash
38
39
  V_/_ Phase: mid_2
39
40
  ```
40
41
 
@@ -49,9 +50,25 @@ Adelie is an autonomous AI orchestrator that plans, codes, reviews, tests, deplo
49
50
  7. **Tester** runs tests and reports failures
50
51
  8. **Runner** builds, installs, deploys
51
52
  9. **Monitor** watches system health
52
- 10. **Phase gates** decide when to advance the project lifecycle
53
+ 10. **Analyst** evaluates trends and growth opportunities
54
+ 11. **Inform** generates human-readable project status reports
55
+ 12. **Phase gates** decide when to advance the project lifecycle
53
56
 
54
- The loop runs continuously at a configurable interval (default 30 s), or once with `adelie run once`.
57
+ The loop runs continuously at a configurable interval (default 30 s), or once with `adelie run --once`.
58
+
59
+ ---
60
+
61
+ ## What's New in v0.2.12
62
+
63
+ 🔒 **Security Hardening** — Shell injection prevention (`&`, `>` blocked), path traversal protection via `Path.resolve()`, staging race condition locks.
64
+
65
+ 🧵 **Thread Safety** — `_usage_lock` protects global token counters during parallel agent execution.
66
+
67
+ 🪟 **Windows Stability** — Fixed `python3` Microsoft Store stub, venv `activate.bat` wrapper, `cmd /c` resolver, cross-platform path handling.
68
+
69
+ 📋 **Changelog** — [Full changelog](https://ade1ie.github.io/adelie/#changelog) now on the docs site with interactive expand/collapse.
70
+
71
+ > See [CHANGELOG.md](./CHANGELOG.md) for complete release history.
55
72
 
56
73
  ---
57
74
 
@@ -98,16 +115,17 @@ brew install adelie
98
115
  git clone https://github.com/Ade1ie/adelie.git
99
116
  cd adelie
100
117
  pip install -r requirements.txt
101
- python adelie/cli.py --version
118
+ python -c "from adelie.cli import main; main()"
102
119
  ```
103
120
 
104
121
  ### Update
105
122
 
106
123
  ```bash
107
- # npm
108
- npm install -g adelie-ai@latest
124
+ # Built-in update checker
125
+ adelie --update
109
126
 
110
- # curl / PowerShell — re-run the install command above
127
+ # or manual
128
+ npm install -g adelie-ai@latest
111
129
 
112
130
  # Homebrew
113
131
  brew upgrade adelie
@@ -136,7 +154,10 @@ adelie config --provider ollama --model gemma3:12b
136
154
  adelie run --goal "Build a REST API for task management"
137
155
 
138
156
  # Single cycle
139
- adelie run once --goal "Analyze and document the codebase"
157
+ adelie run --once --goal "Analyze and document the codebase"
158
+
159
+ # Resume a saved workspace
160
+ adelie run ws 1
140
161
  ```
141
162
 
142
163
  The real-time **dashboard** opens automatically at **http://localhost:5042**.
@@ -157,8 +178,9 @@ The real-time **dashboard** opens automatically at **http://localhost:5042**.
157
178
  | **Tester** | Executes tests, collects failures, feeds back to coder | After review |
158
179
  | **Runner** | Installs deps, builds, deploys (whitelisted commands) | Mid-phase + |
159
180
  | **Monitor** | System health, resource checks, service restarts | Periodic |
160
- | **Analyst** | Trend analysis, insights, KB synthesis | Periodic |
181
+ | **Analyst** | Trend analysis, market insights, KB synthesis | Periodic |
161
182
  | **Research** | Web search → KB for external knowledge | On demand |
183
+ | **Inform** | Human-readable project reports and status summaries | On demand |
162
184
 
163
185
  ### 6-Phase Lifecycle
164
186
 
@@ -178,6 +200,16 @@ The Coder Manager dispatches tasks across three dependency layers:
178
200
 
179
201
  Failed layers trigger targeted retries with reviewer feedback.
180
202
 
203
+ ### Security
204
+
205
+ Adelie enforces multiple security layers:
206
+
207
+ - **Shell injection prevention** — `BLOCKED_CHARS` filter blocks `&`, `>`, `|`, `;`, backticks, and other shell metacharacters in all subprocess commands
208
+ - **Path traversal protection** — `Path.resolve()` verification ensures generated files stay within the staging directory
209
+ - **Staging isolation** — Code is written to `.adelie/staging/` first, verified with `py_compile` / `node --check`, then promoted to project root
210
+ - **Thread-safe operations** — `_usage_lock` and `_staging_lock` prevent race conditions during parallel agent execution
211
+ - **Whitelisted commands** — Runner and Tester only execute pre-approved command patterns
212
+
181
213
  ---
182
214
 
183
215
  ## Architecture
@@ -192,7 +224,7 @@ Failed layers trigger targeted retries with reviewer feedback.
192
224
 
193
225
  Adelie serves a real-time monitoring UI at **`http://localhost:5042`** (auto-starts with `adelie run`).
194
226
 
195
- - **Agent grid** — live status of all 10 agents (idle / running / done / error)
227
+ - **Agent grid** — live status of all 13 agents (idle / running / done / error)
196
228
  - **Log stream** — real-time SSE-powered log feed with category filtering
197
229
  - **Cycle metrics** — tokens, LLM calls, files written, test results, review scores
198
230
  - **Phase timeline** — visual progress through the 6-phase lifecycle
@@ -211,6 +243,7 @@ Built with zero external dependencies — Python `http.server` + SSE + embedded
211
243
 
212
244
  ```bash
213
245
  adelie --version # Show version
246
+ adelie --update # Check for updates
214
247
  adelie help # Full command reference
215
248
  ```
216
249
 
@@ -220,13 +253,14 @@ adelie help # Full command reference
220
253
  adelie init [dir] # Initialize .adelie workspace
221
254
  adelie ws # List all workspaces
222
255
  adelie ws remove <N> # Remove workspace
256
+ adelie scan # Scan existing codebase → KB
223
257
  ```
224
258
 
225
259
  ### Execution
226
260
 
227
261
  ```bash
228
262
  adelie run --goal "…" # Start continuous loop
229
- adelie run once --goal "…" # Single cycle
263
+ adelie run --once --goal "…" # Single cycle
230
264
  adelie run ws <N> # Resume workspace #N
231
265
  ```
232
266
 
@@ -235,9 +269,9 @@ adelie run ws <N> # Resume workspace #N
235
269
  ```bash
236
270
  adelie config # Show current config
237
271
  adelie config --provider ollama # Switch LLM provider
238
- adelie config --model gpt-4o # Set model
272
+ adelie config --model gemma3:12b # Set model
239
273
  adelie config --api-key KEY # Set Gemini API key
240
- adelie config --ollama-url URL # Set Ollama server URL
274
+ adelie config --plan-mode true # Enable Plan Mode (human approval)
241
275
  ```
242
276
 
243
277
  ### Settings
@@ -260,6 +294,7 @@ adelie inform # AI-generated project report
260
294
  adelie phase # Show current phase
261
295
  adelie phase set <phase> # Set phase manually
262
296
  adelie metrics # Cycle metrics & history
297
+ adelie metrics --agents # Per-agent token usage
263
298
  ```
264
299
 
265
300
  ### Knowledge Base & Project
@@ -276,6 +311,15 @@ adelie spec load <file> # Load spec (MD/PDF/DOCX) into KB
276
311
  adelie git # Git status & recent commits
277
312
  ```
278
313
 
314
+ ### Customization
315
+
316
+ ```bash
317
+ adelie prompts # List agent system prompts
318
+ adelie prompts export # Export prompts for editing
319
+ adelie tools # List registered tools
320
+ adelie commands # List custom commands
321
+ ```
322
+
279
323
  ### Integrations
280
324
 
281
325
  ```bash
@@ -288,6 +332,24 @@ adelie ollama run [model] # Interactive chat
288
332
 
289
333
  ---
290
334
 
335
+ ## Plan Mode
336
+
337
+ Enable Plan Mode for human-in-the-loop control:
338
+
339
+ ```bash
340
+ adelie config --plan-mode true
341
+ ```
342
+
343
+ When enabled, the Expert AI generates a **plan** before executing code changes. You can review, approve, or reject from the interactive REPL:
344
+
345
+ | Command | Action |
346
+ |:--|:--|
347
+ | `/plan` | View pending plan |
348
+ | `/approve` | Execute the plan |
349
+ | `/reject [reason]` | Reject and provide feedback |
350
+
351
+ ---
352
+
291
353
  ## Configuration
292
354
 
293
355
  ### Environment (`.adelie/.env`)
@@ -352,13 +414,15 @@ Use functional components with TypeScript props…
352
414
  | 📊 **Dashboard** | Real-time web UI with SSE streaming on port 5042 |
353
415
  | 🔄 **Loop Detector** | 5 stuck-pattern types with escalating interventions |
354
416
  | ⚡ **Scheduler** | Per-agent frequency control with cooldown/priority |
417
+ | 🔒 **Security** | Shell injection prevention, path traversal protection, staging isolation |
418
+ | 📋 **Plan Mode** | Human-in-the-loop approval for code changes |
355
419
 
356
420
  ---
357
421
 
358
422
  ## Testing
359
423
 
360
424
  ```bash
361
- python -m pytest tests/ -v # 197 tests
425
+ python -m pytest tests/ -v # 636 tests
362
426
  ```
363
427
 
364
428
  ---
@@ -368,13 +432,20 @@ python -m pytest tests/ -v # 197 tests
368
432
  ```
369
433
  adelie/
370
434
  ├── orchestrator.py # Main loop — state machine + phase gates
371
- ├── cli.py # All CLI commands
435
+ ├── commands/ # CLI command modules
436
+ │ ├── workspace.py # init, ws
437
+ │ ├── run.py # run, run once, run ws
438
+ │ ├── config.py # config, settings
439
+ │ ├── monitoring.py # status, phase, metrics, inform
440
+ │ ├── knowledge.py # kb, feedback, goal, research, spec, scan
441
+ │ └── integrations.py # ollama, telegram, git, tools, prompts
442
+ ├── cli.py # CLI entry point + argparse routing
372
443
  ├── config.py # Configuration & env loading
373
444
  ├── llm_client.py # LLM abstraction (Gemini + Ollama + fallback)
374
445
  ├── interactive.py # REPL + dashboard integration
375
446
  ├── dashboard.py # Real-time web server (HTTP + SSE)
376
447
  ├── dashboard_html.py # Embedded dashboard UI template
377
- ├── agents/ # 10 specialized AI agents
448
+ ├── agents/ # 12 specialized AI agents
378
449
  │ ├── writer_ai.py # Knowledge Base curator
379
450
  │ ├── expert_ai.py # Strategic decision maker
380
451
  │ ├── coder_ai.py # Code generator
@@ -385,6 +456,7 @@ adelie/
385
456
  │ ├── monitor_ai.py # Health monitor
386
457
  │ ├── analyst_ai.py # Trend analyzer
387
458
  │ ├── research_ai.py # Web researcher
459
+ │ ├── inform_ai.py # Status report generator
388
460
  │ └── scanner_ai.py # Initial codebase scanner
389
461
  ├── kb/ # Knowledge Base (retriever + embeddings)
390
462
  ├── channels/ # Multichannel providers (Discord, Slack)
@@ -393,12 +465,13 @@ adelie/
393
465
  ├── sandbox.py # Docker/Seatbelt isolation
394
466
  ├── gateway.py # REST API gateway
395
467
  ├── skill_manager.py # Skill registry
468
+ ├── env_strategy.py # Runtime environment detection
469
+ ├── plan_mode.py # Plan Mode (human approval)
396
470
  ├── loop_detector.py # Stuck-pattern detection
397
471
  ├── scheduler.py # Per-agent scheduling
398
472
  ├── phases.py # Lifecycle phase definitions
399
473
  ├── hooks.py # Event-driven plugin system
400
- ├── process_supervisor.py # Subprocess management
401
- └── env_strategy.py # Runtime environment detection
474
+ └── process_supervisor.py # Subprocess management
402
475
  ```
403
476
 
404
477
  ---
@@ -418,6 +491,15 @@ python -m pytest tests/ -v # Ensure all tests pass
418
491
 
419
492
  ---
420
493
 
494
+ ## Links
495
+
496
+ - 📖 [Documentation](https://ade1ie.github.io/adelie/) — Full command reference & guides
497
+ - 📋 [Changelog](https://github.com/Ade1ie/adelie/blob/main/CHANGELOG.md) — Release history
498
+ - 📦 [npm](https://www.npmjs.com/package/adelie-ai) — `npm install -g adelie-ai`
499
+ - 🐛 [Issues](https://github.com/Ade1ie/adelie/issues) — Bug reports
500
+
501
+ ---
502
+
421
503
  ## License
422
504
 
423
505
  [MIT](./LICENSE)
@@ -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 = (
@@ -171,7 +171,8 @@ def _detect_os() -> dict:
171
171
  osrel = Path("/etc/os-release").read_text()
172
172
  for line in osrel.splitlines():
173
173
  if line.startswith("PRETTY_NAME="):
174
- os_name = f"Linux ({line.split('=', 1)[1].strip('\"')})"
174
+ pretty = line.split("=", 1)[1].strip('"')
175
+ os_name = f"Linux ({pretty})"
175
176
  break
176
177
  except Exception:
177
178
  pass
@@ -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.13",
4
4
  "description": "Adelie — Self-Communicating Autonomous AI Loop CLI",
5
5
  "bin": {
6
6
  "adelie": "bin/adelie.js"