adelie-ai 0.3.3 → 0.3.5

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.
@@ -12,4 +12,4 @@ def _get_version() -> str:
12
12
  except Exception:
13
13
  pass
14
14
  return "0.0.0"
15
- __version__ = "0.3.3"
15
+ __version__ = "0.3.5"
@@ -36,7 +36,13 @@ _STOP_WORDS = {
36
36
  }
37
37
 
38
38
  _DEDUP_THRESHOLD = 0.6 # Jaccard similarity >= 60% → duplicate
39
- MAX_CODERS_PER_FILE = 3
39
+ MAX_CODERS_PER_FILE = 5
40
+
41
+ # Keywords that indicate a "fix/patch" task (bypass per-file limit)
42
+ _FIX_KEYWORDS = {
43
+ "fix", "patch", "hotfix", "repair", "resolve", "debug",
44
+ "error", "bug", "broken", "typo", "incorrect", "wrong",
45
+ }
40
46
 
41
47
 
42
48
  def _tokenize(text: str) -> set[str]:
@@ -89,17 +95,20 @@ def _find_duplicate_coder(
89
95
  def _count_file_modifications(registry: dict, files: list[str]) -> int:
90
96
  """
91
97
  주어진 파일 목록과 겹치는 기존 코더 수를 반환.
92
- coder task description 내 파일 경로 매칭 기반.
98
+ coder task description 내 **전체 파일 경로** 매칭 기반.
99
+ (basename 매칭은 다른 경로의 동일 이름 파일을 오탐하므로 fullpath 사용)
93
100
  """
94
101
  if not files:
95
102
  return 0
96
103
 
97
- file_basenames = {f.rsplit("/", 1)[-1].lower() for f in files}
104
+ # Use full path for matching to avoid collisions
105
+ # e.g. "quests/create/page.tsx" and "login/page.tsx" are different files
106
+ file_fullpaths = {f.lower() for f in files}
98
107
  count = 0
99
108
 
100
109
  for coder in registry.get("coders", []):
101
110
  task_lower = coder.get("last_task", "").lower()
102
- if any(basename in task_lower for basename in file_basenames):
111
+ if any(fp in task_lower for fp in file_fullpaths):
103
112
  count += 1
104
113
 
105
114
  return count
@@ -270,7 +279,10 @@ def run_coders(
270
279
  name = existing_name
271
280
 
272
281
  # ── Per-file limit ─────────────────────────────────────
273
- if relevant and _count_file_modifications(registry, relevant) >= MAX_CODERS_PER_FILE:
282
+ # Fix/patch tasks bypass per-file limit — they MUST be able
283
+ # to modify files that need correction (Bug #8b)
284
+ is_fix_task = bool(_FIX_KEYWORDS & _tokenize(task_desc))
285
+ if relevant and not is_fix_task and _count_file_modifications(registry, relevant) >= MAX_CODERS_PER_FILE:
274
286
  console.print(
275
287
  f" [yellow]⏭ Skipped '{name}': target files modified "
276
288
  f"{MAX_CODERS_PER_FILE}+ times already[/yellow]"
@@ -12,6 +12,7 @@ State machine:
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import copy
15
16
  import json
16
17
  from typing import Callable, Optional
17
18
  import signal
@@ -115,6 +116,9 @@ class Orchestrator:
115
116
  # Track coder/reviewer results for next cycle's Expert AI context (Bug #4)
116
117
  self._last_coder_result: dict | None = None
117
118
 
119
+ # Track consecutive zero-file coder cycles for emergency reset (Bug #10)
120
+ self._zero_file_streak: int = 0
121
+
118
122
  # Auto-scan: if KB is empty and project has existing code, scan first
119
123
  self._auto_scan_done = False
120
124
 
@@ -663,6 +667,7 @@ class Orchestrator:
663
667
  # Force state transition on critical loops
664
668
  if self.state.value in ("new_logic", "error"):
665
669
  console.print("[bold yellow]🔧 Forcing transition to NORMAL state[/bold yellow]")
670
+ self._archive_errors() # Clear outstanding errors to prevent stale context loop
666
671
  self.state = LoopState.NORMAL
667
672
  system_state["situation"] = "normal"
668
673
  else:
@@ -1147,10 +1152,56 @@ class Orchestrator:
1147
1152
  except Exception as e:
1148
1153
  console.print(f"[dim]⚠️ PolicyGate error: {e}[/dim]")
1149
1154
 
1150
- # ── Promote staged files to project (after review + policy) ───────
1155
+ # ── Syntax validation with Coder Retry Loop (before promotion) ───
1156
+ syntax_passed = True
1151
1157
  if all_written_files and reviewer_approved and policy_passed:
1158
+ passed_files, failed_files = self._verify_staged_files(all_written_files)
1159
+ if failed_files:
1160
+ syntax_passed = False
1161
+ console.print(
1162
+ f"[yellow]⚠️ Syntax validation failed for {len(failed_files)} file(s) — "
1163
+ f"initiating retry[/yellow]"
1164
+ )
1165
+ if coder_tasks and self.phase != "initial":
1166
+ from adelie.agents.coder_manager import run_coders
1167
+ from adelie.phases import PHASE_INFO
1168
+ phase_info = PHASE_INFO.get(self.phase, {})
1169
+ max_layer = phase_info.get("max_coder_layer", 0)
1170
+
1171
+ # Build compiler failure feedback
1172
+ error_feedback = "## ⚠️ SYNTAX/COMPILATION FAILURE — FIX THESE ERRORS\n"
1173
+ for f in failed_files:
1174
+ error_feedback += f"### File: `{f.get('filepath')}`\n"
1175
+ error_feedback += f"Error: {f.get('error', 'Syntax error')}\n\n"
1176
+ error_feedback += "Fix the code to resolve all syntax, compile, and parse errors."
1177
+
1178
+ for task in coder_tasks:
1179
+ task["feedback"] = error_feedback
1180
+
1181
+ try:
1182
+ fix_start_time = time.time()
1183
+ run_coders(coder_tasks, max_active_layer=max_layer)
1184
+ new_files = self._collect_staged_files(fix_start_time)
1185
+ if new_files:
1186
+ all_written_files = new_files
1187
+ # Re-verify after retry
1188
+ passed_files, failed_files = self._verify_staged_files(all_written_files)
1189
+ if not failed_files:
1190
+ syntax_passed = True
1191
+ console.print("[bold green]✅ Syntax validation passed after retry[/bold green]")
1192
+ else:
1193
+ console.print(
1194
+ f"[red]❌ Syntax validation still failed for {len(failed_files)} file(s) "
1195
+ f"after retry[/red]"
1196
+ )
1197
+ except Exception as se:
1198
+ console.print(f"[dim]⚠️ Syntax retry exception: {se}[/dim]")
1199
+
1200
+ # ── Promote staged files to project (after review + policy + syntax check) ───────
1201
+ promoted_count = 0
1202
+ if all_written_files and reviewer_approved and policy_passed and syntax_passed:
1152
1203
  with self._staging_lock:
1153
- self._promote_staged_files(all_written_files)
1204
+ promoted_count = self._promote_staged_files(all_written_files)
1154
1205
  self._cleanup_staging()
1155
1206
 
1156
1207
  # ── Git auto-commit (MID_1+) ──────────────────────────────────────
@@ -1165,6 +1216,36 @@ class Orchestrator:
1165
1216
  except Exception as e:
1166
1217
  console.print(f"[dim]⚠️ Git commit error: {e}[/dim]")
1167
1218
 
1219
+ # ── Bug #10: Zero-file streak detection & emergency registry reset ────
1220
+ # Zero-file streak triggers when coder tasks exist but promoted_count is 0
1221
+ # (covers coder writing 0 files OR files being written but blocked/rejected before promotion)
1222
+ if promoted_count == 0 and coder_tasks:
1223
+ self._zero_file_streak += 1
1224
+ if self._zero_file_streak >= 3:
1225
+ console.print(
1226
+ f"[bold yellow]🔓 Emergency: {self._zero_file_streak} consecutive"
1227
+ f" zero-promoted cycles — resetting coder registry[/bold yellow]"
1228
+ )
1229
+ try:
1230
+ from adelie.agents.coder_manager import REGISTRY_PATH
1231
+ if REGISTRY_PATH.exists():
1232
+ import json as _json
1233
+ reg = _json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
1234
+ old_count = len(reg.get("coders", []))
1235
+ reg["coders"] = []
1236
+ REGISTRY_PATH.write_text(
1237
+ _json.dumps(reg, indent=2, ensure_ascii=False),
1238
+ encoding="utf-8",
1239
+ )
1240
+ console.print(
1241
+ f" [yellow]Cleared {old_count} coder(s) from registry[/yellow]"
1242
+ )
1243
+ except Exception as e:
1244
+ console.print(f" [dim]⚠️ Registry reset error: {e}[/dim]")
1245
+ self._zero_file_streak = 0
1246
+ else:
1247
+ self._zero_file_streak = 0
1248
+
1168
1249
  # ══════════════════════════════════════════════════════════════════════
1169
1250
  # PHASE 3: Tester AI + Runner AI (PARALLEL)
1170
1251
  # After code is promoted, testing and building are independent.
@@ -1191,13 +1272,14 @@ class Orchestrator:
1191
1272
  parallel_names.append("Runner")
1192
1273
  console.print(f"[dim] ⚡ Phase 3: parallel execution [{', '.join(parallel_names)}][/dim]")
1193
1274
 
1275
+ import copy
1194
1276
  with ThreadPoolExecutor(max_workers=len(parallel_names), thread_name_prefix="adelie-p3") as pool:
1195
1277
  futures = {}
1196
1278
 
1197
1279
  if run_tester:
1198
1280
  _test_pass_results = []
1199
1281
  _test_metrics = {}
1200
- _p3_coder_tasks = list(coder_tasks) # Copy for thread safety
1282
+ _p3_coder_tasks = copy.deepcopy(coder_tasks) # Thread-safe deep copy to prevent shared mutations
1201
1283
  _p3_all_files = list(all_written_files)
1202
1284
 
1203
1285
  def _run_tester():
@@ -1241,7 +1323,11 @@ class Orchestrator:
1241
1323
  if coder_result.get("total_files", 0) > 0:
1242
1324
  new_files = self._collect_staged_files(fix_start_time)
1243
1325
  if new_files:
1244
- _files = new_files
1326
+ # Merge new_files into _files by filepath to keep all original targets in validation
1327
+ existing_paths = {f["filepath"] for f in _files}
1328
+ for nf in new_files:
1329
+ if nf["filepath"] not in existing_paths:
1330
+ _files.append(nf)
1245
1331
  with self._staging_lock:
1246
1332
  self._promote_staged_files(_files)
1247
1333
  self._cleanup_staging()
@@ -1527,6 +1613,19 @@ class Orchestrator:
1527
1613
  if self._recover_count >= self.MAX_RECOVER_RETRIES:
1528
1614
  console.print(f"[yellow]⚠️ Max recovery retries ({self.MAX_RECOVER_RETRIES}) reached — entering maintenance.[/yellow]")
1529
1615
  self._archive_errors()
1616
+
1617
+ # Checkpoint Rollback: Revert to the latest safe state
1618
+ try:
1619
+ from adelie.checkpoint import CheckpointManager
1620
+ cp_mgr = CheckpointManager()
1621
+ cps = cp_mgr.list_checkpoints()
1622
+ if cps:
1623
+ latest_id = cps[0].checkpoint_id
1624
+ console.print(f"[bold yellow]🔄 Self-Healing: Rolling back project files to checkpoint {latest_id}...[/bold yellow]")
1625
+ cp_mgr.restore(latest_id)
1626
+ except Exception as cpe:
1627
+ console.print(f"[dim]⚠️ Rollback failed during recovery limit: {cpe}[/dim]")
1628
+
1530
1629
  self.state = LoopState.MAINTENANCE
1531
1630
  else:
1532
1631
  console.print(f"[yellow]🔄 Recovery attempt {self._recover_count}/{self.MAX_RECOVER_RETRIES} — clearing errors and returning to normal.[/yellow]")
@@ -1549,9 +1648,8 @@ class Orchestrator:
1549
1648
  f"[yellow]⚠️ Max new_logic cycles ({self.MAX_NEW_LOGIC_CYCLES}) reached — "
1550
1649
  f"transitioning to normal.[/yellow]"
1551
1650
  )
1552
- self.state = LoopState.NORMAL
1651
+ next_situation = "normal"
1553
1652
  self._new_logic_count = 0
1554
- return
1555
1653
  else:
1556
1654
  self._new_logic_count = 0
1557
1655
 
@@ -174,7 +174,25 @@ def sync_package_json(missing: list[str], project_root: Path) -> int:
174
174
  dev_deps = pkg.get("devDependencies", {})
175
175
  added = 0
176
176
 
177
+ # Known-invalid package name patterns — common LLM hallucinations
178
+ _INVALID_PKG_NAMES = {
179
+ "requested", "response", "result", "data", "config",
180
+ "utils", "helpers", "types", "models", "services",
181
+ "app", "main", "index", "test", "error", "handler",
182
+ "component", "module", "function", "class", "object",
183
+ "input", "output", "value", "item", "list", "array",
184
+ "string", "number", "boolean", "null", "undefined",
185
+ "client", "server", "api", "route", "page", "layout",
186
+ }
187
+
177
188
  for name in missing:
189
+ # Skip hallucinated/generic package names
190
+ name_lower = name.lower()
191
+ if name_lower in _INVALID_PKG_NAMES:
192
+ continue
193
+ # npm packages must be lowercase, start with letter/@, no spaces
194
+ if not re.match(r'^(@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9][a-z0-9._-]*$', name):
195
+ continue
178
196
  if name not in deps and name not in dev_deps:
179
197
  # Heuristic: testing/dev tools go to devDependencies
180
198
  if any(kw in name.lower() for kw in ["test", "jest", "vitest", "eslint", "prettier", "lint"]):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adelie-ai",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Adelie — Self-Communicating Autonomous AI Loop CLI",
5
5
  "bin": {
6
6
  "adelie": "bin/adelie.js"