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.
- package/adelie/__init__.py +1 -1
- package/adelie/agents/coder_manager.py +17 -5
- package/adelie/orchestrator.py +104 -6
- package/adelie/utils/dep_sync.py +18 -0
- package/package.json +1 -1
package/adelie/__init__.py
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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]"
|
package/adelie/orchestrator.py
CHANGED
|
@@ -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
|
-
# ──
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
package/adelie/utils/dep_sync.py
CHANGED
|
@@ -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"]):
|