adelie-ai 0.3.2 → 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.2"
15
+ __version__ = "0.3.5"
@@ -44,6 +44,41 @@ Output a single valid JSON array of files to create/update."""
44
44
  SYSTEM_PROMPT = load_prompt("coder", _FALLBACK_PROMPT)
45
45
 
46
46
 
47
+ def _get_framework_guidelines(framework: str) -> str:
48
+ """Return framework-specific coding rules to inject into coder prompt."""
49
+ guidelines = {
50
+ "nextjs": (
51
+ "\n## ⚠️ Next.js App Router — MANDATORY Rules\n"
52
+ "- Any component using useState, useEffect, onClick, or other client hooks "
53
+ "MUST start with `'use client';` as the FIRST line.\n"
54
+ "- `layout.tsx` is a SERVER component — NEVER use client-side hooks, "
55
+ "context providers, or event handlers in it without `'use client'`.\n"
56
+ "- Dynamic routes use `[param]`, `[...catch]`, `[[...optional]]` syntax — "
57
+ "these are valid directory names.\n"
58
+ "- API routes go in `src/app/api/` as `route.ts` files.\n"
59
+ "- Use `next/navigation` (not `next/router`) for App Router.\n"
60
+ "- The package name for lucide icons is `lucide-react` (NOT `@lucide/react`).\n\n"
61
+ ),
62
+ "nuxt": (
63
+ "\n## ⚠️ Nuxt 3 — MANDATORY Rules\n"
64
+ "- Use `<script setup lang='ts'>` for composition API.\n"
65
+ "- Auto-imports: `ref`, `computed`, `useRoute` etc. are auto-imported.\n"
66
+ "- Pages go in `pages/`, layouts in `layouts/`.\n\n"
67
+ ),
68
+ "sveltekit": (
69
+ "\n## ⚠️ SvelteKit — MANDATORY Rules\n"
70
+ "- Pages go in `src/routes/` as `+page.svelte`.\n"
71
+ "- Server-only code uses `+page.server.ts` or `+server.ts`.\n"
72
+ "- Layouts use `+layout.svelte`.\n\n"
73
+ ),
74
+ "vite": (
75
+ "\n## ⚠️ Vite + React — MANDATORY Rules\n"
76
+ "- Entry point is `index.html` referencing `src/main.tsx`.\n"
77
+ "- Use `vite.config.ts` with `@vitejs/plugin-react`.\n\n"
78
+ ),
79
+ }
80
+ return guidelines.get(framework, "")
81
+
47
82
  def _get_coder_log_dir(layer: int, coder_name: str) -> Path:
48
83
  """Get or create the log directory for a coder."""
49
84
  log_dir = CODER_ROOT / "layer" / str(layer) / coder_name
@@ -174,6 +209,16 @@ def run_coder(
174
209
  f"## Lower Layer Coder Logs\n{lower_logs}\n\n"
175
210
  )
176
211
 
212
+ # Bug #7: Inject framework-specific coding rules to prevent common mistakes
213
+ try:
214
+ from adelie.agents.expert_ai import _detect_framework
215
+ fw = _detect_framework(workspace_root)
216
+ fw_guidelines = _get_framework_guidelines(fw)
217
+ if fw_guidelines:
218
+ user_prompt += fw_guidelines
219
+ except Exception:
220
+ pass
221
+
177
222
  if feedback:
178
223
  user_prompt += (
179
224
  f"## ⚠️ REVIEWER FEEDBACK (FIX THESE ISSUES)\n{feedback}\n\n"
@@ -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]"
@@ -444,8 +444,9 @@ def _get_scaffolding_need() -> str:
444
444
  return ""
445
445
 
446
446
  lines = [
447
- "⚠️ CRITICAL: The following entry files are MISSING. Without them, the build WILL FAIL.",
448
- "Create a 'project_scaffolding' coder task (layer 0) to generate these BEFORE any feature tasks:",
447
+ "ℹ️ SCAFFOLDING NOTE: The following entry files are missing.",
448
+ "If no scaffolding coder has been created yet, create ONE scaffolding task (layer 0).",
449
+ "If scaffolding was ALREADY attempted in a previous cycle, SKIP this and focus on feature tasks:",
449
450
  ]
450
451
  for c in checks:
451
452
  lines.append(f" - {c['file']}: {c['desc']}")
@@ -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
@@ -112,6 +113,12 @@ class Orchestrator:
112
113
  self._last_assembled_contexts: list | None = None
113
114
  self._last_build_errors: list[dict] = []
114
115
 
116
+ # Track coder/reviewer results for next cycle's Expert AI context (Bug #4)
117
+ self._last_coder_result: dict | None = None
118
+
119
+ # Track consecutive zero-file coder cycles for emergency reset (Bug #10)
120
+ self._zero_file_streak: int = 0
121
+
115
122
  # Auto-scan: if KB is empty and project has existing code, scan first
116
123
  self._auto_scan_done = False
117
124
 
@@ -181,6 +188,10 @@ class Orchestrator:
181
188
  # Include recent build errors for Expert AI context
182
189
  if self._last_build_errors:
183
190
  state["build_errors"] = self._last_build_errors[:3] # 최대 3개
191
+ # Include last cycle's coder/reviewer result so Expert AI can avoid
192
+ # re-issuing tasks that previously failed (Bug #4)
193
+ if self._last_coder_result:
194
+ state["last_coder_result"] = self._last_coder_result
184
195
  return state
185
196
 
186
197
  def get_agent_context(self, agent_type: AgentType | str) -> dict:
@@ -656,6 +667,7 @@ class Orchestrator:
656
667
  # Force state transition on critical loops
657
668
  if self.state.value in ("new_logic", "error"):
658
669
  console.print("[bold yellow]🔧 Forcing transition to NORMAL state[/bold yellow]")
670
+ self._archive_errors() # Clear outstanding errors to prevent stale context loop
659
671
  self.state = LoopState.NORMAL
660
672
  system_state["situation"] = "normal"
661
673
  else:
@@ -976,7 +988,10 @@ class Orchestrator:
976
988
  )] or all_written_files
977
989
 
978
990
  for retry in range(MAX_REVIEW_RETRIES + 1):
979
- review = run_review(coder_name=name, written_files=task_files)
991
+ # Bug #6: pass STAGING_ROOT so reviewer reads files from
992
+ # staging (where Coder wrote them), not PROJECT_ROOT.
993
+ from adelie.agents.coder_ai import STAGING_ROOT as _REVIEW_STAGING
994
+ review = run_review(coder_name=name, written_files=task_files, workspace_root=_REVIEW_STAGING)
980
995
  score = review.get("overall_score", 5)
981
996
  self._review_score_history.append(score)
982
997
 
@@ -1005,6 +1020,44 @@ class Orchestrator:
1005
1020
  self._emit_agent_end("Reviewer", f"error: {e}")
1006
1021
  else:
1007
1022
  self._emit_agent_end("Reviewer", "approved" if reviewer_approved else "rejected")
1023
+
1024
+ # Bug #3: Log review failures to KB so Expert AI avoids re-issuing
1025
+ # the exact same task that was rejected.
1026
+ if not reviewer_approved and all_written_files:
1027
+ failed_files = [f.get("filepath", "") for f in all_written_files]
1028
+ review_summary = review.get("summary", "N/A") if review else "N/A"
1029
+ review_issues = ""
1030
+ if review:
1031
+ for issue in review.get("issues", [])[:5]:
1032
+ review_issues += f"- [{issue.get('severity')}] {issue.get('title')}: {issue.get('suggestion', '')}\n"
1033
+ failure_note = (
1034
+ f"# Review Failure Log (Cycle #{self.loop_iteration})\n\n"
1035
+ f"The following files were rejected by Reviewer AI after "
1036
+ f"{MAX_REVIEW_RETRIES + 1} attempts:\n"
1037
+ + "\n".join(f"- `{f}`" for f in failed_files) + "\n\n"
1038
+ f"**Review Summary**: {review_summary}\n\n"
1039
+ f"**Issues**:\n{review_issues}\n"
1040
+ f"Expert AI should NOT re-assign the same task until "
1041
+ f"the underlying issue is resolved.\n"
1042
+ )
1043
+ failure_path = WORKSPACE_PATH / "errors" / f"review_failure_{self.loop_iteration}.md"
1044
+ failure_path.parent.mkdir(parents=True, exist_ok=True)
1045
+ failure_path.write_text(failure_note, encoding="utf-8")
1046
+ retriever.update_index(
1047
+ f"errors/{failure_path.name}",
1048
+ tags=["error", "review-failure"],
1049
+ summary=f"Review rejected cycle #{self.loop_iteration}: {', '.join(failed_files[:3])}",
1050
+ )
1051
+ console.print(f"[yellow]📝 Review failure logged to KB for Expert AI awareness[/yellow]")
1052
+
1053
+ # Bug #4: Record coder/reviewer results for next cycle context
1054
+ self._last_coder_result = {
1055
+ "cycle": self.loop_iteration,
1056
+ "files_written": len(all_written_files),
1057
+ "reviewer_approved": reviewer_approved,
1058
+ "review_score": review.get("overall_score", 0) if review else 0,
1059
+ "review_summary": review.get("summary", "")[:300] if review else "",
1060
+ }
1008
1061
  elif all_written_files and self.phase != "initial":
1009
1062
  # Reviewer not scheduled this cycle — auto-approve staged files
1010
1063
  reviewer_approved = True
@@ -1099,10 +1152,56 @@ class Orchestrator:
1099
1152
  except Exception as e:
1100
1153
  console.print(f"[dim]⚠️ PolicyGate error: {e}[/dim]")
1101
1154
 
1102
- # ── Promote staged files to project (after review + policy) ───────
1155
+ # ── Syntax validation with Coder Retry Loop (before promotion) ───
1156
+ syntax_passed = True
1103
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:
1104
1203
  with self._staging_lock:
1105
- self._promote_staged_files(all_written_files)
1204
+ promoted_count = self._promote_staged_files(all_written_files)
1106
1205
  self._cleanup_staging()
1107
1206
 
1108
1207
  # ── Git auto-commit (MID_1+) ──────────────────────────────────────
@@ -1117,6 +1216,36 @@ class Orchestrator:
1117
1216
  except Exception as e:
1118
1217
  console.print(f"[dim]⚠️ Git commit error: {e}[/dim]")
1119
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
+
1120
1249
  # ══════════════════════════════════════════════════════════════════════
1121
1250
  # PHASE 3: Tester AI + Runner AI (PARALLEL)
1122
1251
  # After code is promoted, testing and building are independent.
@@ -1143,13 +1272,14 @@ class Orchestrator:
1143
1272
  parallel_names.append("Runner")
1144
1273
  console.print(f"[dim] ⚡ Phase 3: parallel execution [{', '.join(parallel_names)}][/dim]")
1145
1274
 
1275
+ import copy
1146
1276
  with ThreadPoolExecutor(max_workers=len(parallel_names), thread_name_prefix="adelie-p3") as pool:
1147
1277
  futures = {}
1148
1278
 
1149
1279
  if run_tester:
1150
1280
  _test_pass_results = []
1151
1281
  _test_metrics = {}
1152
- _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
1153
1283
  _p3_all_files = list(all_written_files)
1154
1284
 
1155
1285
  def _run_tester():
@@ -1193,7 +1323,11 @@ class Orchestrator:
1193
1323
  if coder_result.get("total_files", 0) > 0:
1194
1324
  new_files = self._collect_staged_files(fix_start_time)
1195
1325
  if new_files:
1196
- _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)
1197
1331
  with self._staging_lock:
1198
1332
  self._promote_staged_files(_files)
1199
1333
  self._cleanup_staging()
@@ -1479,6 +1613,19 @@ class Orchestrator:
1479
1613
  if self._recover_count >= self.MAX_RECOVER_RETRIES:
1480
1614
  console.print(f"[yellow]⚠️ Max recovery retries ({self.MAX_RECOVER_RETRIES}) reached — entering maintenance.[/yellow]")
1481
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
+
1482
1629
  self.state = LoopState.MAINTENANCE
1483
1630
  else:
1484
1631
  console.print(f"[yellow]🔄 Recovery attempt {self._recover_count}/{self.MAX_RECOVER_RETRIES} — clearing errors and returning to normal.[/yellow]")
@@ -1501,9 +1648,8 @@ class Orchestrator:
1501
1648
  f"[yellow]⚠️ Max new_logic cycles ({self.MAX_NEW_LOGIC_CYCLES}) reached — "
1502
1649
  f"transitioning to normal.[/yellow]"
1503
1650
  )
1504
- self.state = LoopState.NORMAL
1651
+ next_situation = "normal"
1505
1652
  self._new_logic_count = 0
1506
- return
1507
1653
  else:
1508
1654
  self._new_logic_count = 0
1509
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.2",
3
+ "version": "0.3.5",
4
4
  "description": "Adelie — Self-Communicating Autonomous AI Loop CLI",
5
5
  "bin": {
6
6
  "adelie": "bin/adelie.js"