adelie-ai 0.3.7 → 0.3.9
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/monitor_ai.py +25 -2
- package/adelie/agents/reviewer_ai.py +105 -3
- package/adelie/agents/runner_ai.py +23 -2
- package/adelie/agents/tester_ai.py +123 -6
- package/adelie/env_strategy.py +19 -3
- package/adelie/harness_manager.py +11 -4
- package/adelie/orchestrator.py +1 -0
- package/package.json +1 -1
package/adelie/__init__.py
CHANGED
|
@@ -176,17 +176,35 @@ def run_health_check(
|
|
|
176
176
|
f"- Active: {len(active_services)}/{len(http_results)}\n"
|
|
177
177
|
)
|
|
178
178
|
|
|
179
|
-
# 2. Process Checks — clean up dead processes
|
|
179
|
+
# 2. Process Checks — clean up dead processes AND verify HTTP health
|
|
180
180
|
processes = _load_tracked_processes()
|
|
181
181
|
alive = 0
|
|
182
182
|
dead = 0
|
|
183
183
|
alive_procs = []
|
|
184
|
+
process_http_issues = []
|
|
184
185
|
for proc in processes:
|
|
185
186
|
pid = proc.get("pid")
|
|
186
187
|
if pid and _check_process(pid):
|
|
187
188
|
alive += 1
|
|
188
189
|
alive_procs.append(proc)
|
|
189
190
|
console.print(f" [green]✅ PID {pid}[/green] — {proc.get('description', '?')}")
|
|
191
|
+
|
|
192
|
+
# If the process has a port, verify HTTP health too
|
|
193
|
+
port = proc.get("port")
|
|
194
|
+
if port:
|
|
195
|
+
http_check = _check_http(f"http://localhost:{port}", timeout=3)
|
|
196
|
+
if http_check["status"] == "healthy":
|
|
197
|
+
console.print(f" [green]✅ HTTP :{port}[/green] — {http_check['response_ms']}ms")
|
|
198
|
+
elif http_check["status"] == "down":
|
|
199
|
+
console.print(f" [yellow]⚠️ HTTP :{port} not responding (process may still be starting)[/yellow]")
|
|
200
|
+
process_http_issues.append(
|
|
201
|
+
f"PID {pid} alive but HTTP :{port} not responding — {proc.get('description', '?')}"
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
console.print(f" [red]❌ HTTP :{port} — {http_check['status']}[/red]")
|
|
205
|
+
process_http_issues.append(
|
|
206
|
+
f"PID {pid} HTTP :{port} {http_check['status']} — {proc.get('description', '?')}"
|
|
207
|
+
)
|
|
190
208
|
else:
|
|
191
209
|
dead += 1
|
|
192
210
|
|
|
@@ -196,9 +214,14 @@ def run_health_check(
|
|
|
196
214
|
process_file = RUNNER_ROOT / "processes.json"
|
|
197
215
|
process_file.write_text(json.dumps(alive_procs, indent=2), encoding="utf-8")
|
|
198
216
|
|
|
217
|
+
# Add HTTP issues to alerts
|
|
218
|
+
alerts.extend(process_http_issues)
|
|
219
|
+
|
|
199
220
|
report_parts.append(
|
|
200
221
|
f"### Processes\n"
|
|
201
|
-
f"- Alive: {alive} | Cleaned: {dead}
|
|
222
|
+
f"- Alive: {alive} | Cleaned: {dead}"
|
|
223
|
+
+ (f" | HTTP issues: {len(process_http_issues)}" if process_http_issues else "")
|
|
224
|
+
+ "\n"
|
|
202
225
|
)
|
|
203
226
|
|
|
204
227
|
# 3. Log Error Scan
|
|
@@ -31,11 +31,101 @@ console = Console()
|
|
|
31
31
|
REVIEW_ROOT = WORKSPACE_PATH.parent / "reviews"
|
|
32
32
|
|
|
33
33
|
_FALLBACK_PROMPT = """You are Reviewer AI — a senior code reviewer in an autonomous AI loop.
|
|
34
|
-
Output a single valid JSON object with overall_score, issues, summary, and approved fields.
|
|
34
|
+
Output a single valid JSON object with overall_score, issues, summary, and approved fields.
|
|
35
|
+
|
|
36
|
+
CROSS-FILE VALIDATION (CRITICAL):
|
|
37
|
+
- Check that ALL import/require references match the actual API signatures
|
|
38
|
+
of the imported files (provided as "Related Files" context below).
|
|
39
|
+
- Mismatched function signatures, missing exports, wrong parameter counts,
|
|
40
|
+
or incompatible types are CRITICAL severity issues.
|
|
41
|
+
- If a function is called with arguments that don't match its definition,
|
|
42
|
+
that is a CRITICAL bug."""
|
|
35
43
|
|
|
36
44
|
SYSTEM_PROMPT = load_prompt("reviewer", _FALLBACK_PROMPT)
|
|
37
45
|
|
|
38
46
|
|
|
47
|
+
def _read_imported_files(
|
|
48
|
+
written_files: list[dict],
|
|
49
|
+
workspace_root: Path,
|
|
50
|
+
) -> list[str]:
|
|
51
|
+
"""
|
|
52
|
+
Find and read files that are imported/required by the review targets.
|
|
53
|
+
This enables cross-file interface validation.
|
|
54
|
+
|
|
55
|
+
Returns list of "--- filepath ---\ncontent" strings for related files.
|
|
56
|
+
"""
|
|
57
|
+
import re as _re
|
|
58
|
+
|
|
59
|
+
# Collect all import targets from the written files
|
|
60
|
+
import_patterns = [
|
|
61
|
+
# ES6: import ... from './path' or "./path"
|
|
62
|
+
_re.compile(r"from\s+['\"](\.{1,2}/[^'\"]+)['\"]"),
|
|
63
|
+
# require: require('./path')
|
|
64
|
+
_re.compile(r"require\s*\(\s*['\"](\.{1,2}/[^'\"]+)['\"]\s*\)"),
|
|
65
|
+
# Python: from .module import ...
|
|
66
|
+
_re.compile(r"from\s+\.(\w+)\s+import"),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Track which files we've already included to avoid duplicates
|
|
70
|
+
written_paths = {f.get("filepath", "") for f in written_files}
|
|
71
|
+
related_contents: list[str] = []
|
|
72
|
+
seen_paths: set[str] = set()
|
|
73
|
+
|
|
74
|
+
for finfo in written_files:
|
|
75
|
+
fp = finfo.get("filepath", "")
|
|
76
|
+
full_path = workspace_root / fp
|
|
77
|
+
if not full_path.exists():
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
source = full_path.read_text(encoding="utf-8")
|
|
82
|
+
except Exception:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
file_dir = full_path.parent
|
|
86
|
+
|
|
87
|
+
for pattern in import_patterns:
|
|
88
|
+
for match in pattern.finditer(source):
|
|
89
|
+
import_path = match.group(1)
|
|
90
|
+
|
|
91
|
+
# Resolve the import to an actual file
|
|
92
|
+
candidate_base = file_dir / import_path
|
|
93
|
+
candidates = [
|
|
94
|
+
candidate_base,
|
|
95
|
+
candidate_base.with_suffix(".ts"),
|
|
96
|
+
candidate_base.with_suffix(".tsx"),
|
|
97
|
+
candidate_base.with_suffix(".js"),
|
|
98
|
+
candidate_base.with_suffix(".jsx"),
|
|
99
|
+
candidate_base.with_suffix(".py"),
|
|
100
|
+
candidate_base / "index.ts",
|
|
101
|
+
candidate_base / "index.js",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
for candidate in candidates:
|
|
105
|
+
if candidate.exists() and candidate.is_file():
|
|
106
|
+
try:
|
|
107
|
+
rel = candidate.relative_to(workspace_root).as_posix()
|
|
108
|
+
except ValueError:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
if rel in written_paths or rel in seen_paths:
|
|
112
|
+
break # Already in review context
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
content = candidate.read_text(encoding="utf-8")
|
|
116
|
+
# Only include first 2000 chars to avoid token overflow
|
|
117
|
+
related_contents.append(
|
|
118
|
+
f"--- {rel} (RELATED — imported by {fp}) ---\n"
|
|
119
|
+
f"{content[:2000]}"
|
|
120
|
+
)
|
|
121
|
+
seen_paths.add(rel)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
return related_contents[:10] # Cap at 10 related files
|
|
127
|
+
|
|
128
|
+
|
|
39
129
|
def run_review(
|
|
40
130
|
coder_name: str,
|
|
41
131
|
written_files: list[dict],
|
|
@@ -90,9 +180,21 @@ def run_review(
|
|
|
90
180
|
f"## Coder: {coder_name}\n"
|
|
91
181
|
f"## Files to Review\n\n"
|
|
92
182
|
+ "\n\n".join(file_contents)
|
|
93
|
-
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Add cross-file context: files imported by the review targets
|
|
186
|
+
related_files = _read_imported_files(written_files, workspace_root)
|
|
187
|
+
if related_files:
|
|
188
|
+
user_prompt += (
|
|
189
|
+
f"\n\n## Related Files (imported by review targets — check interface compatibility)\n\n"
|
|
190
|
+
+ "\n\n".join(related_files)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
user_prompt += (
|
|
194
|
+
f"\n\n{get_context_prompt_section()}{get_rules_prompt_section()}{get_skills_prompt_section('reviewer')}"
|
|
94
195
|
+ policy_section
|
|
95
|
-
+ "\n\nReview these files
|
|
196
|
+
+ "\n\nReview these files. Check that function calls match their definitions "
|
|
197
|
+
"in the Related Files. Output a JSON object."
|
|
96
198
|
)
|
|
97
199
|
|
|
98
200
|
try:
|
|
@@ -59,9 +59,11 @@ DEPLOY_COMMANDS = RUN_COMMANDS + [
|
|
|
59
59
|
BLOCKED_FLAGS = {"-c", "--eval", "eval", "exec", "--exec", "-e"}
|
|
60
60
|
|
|
61
61
|
# Dangerous shell metacharacters
|
|
62
|
-
|
|
62
|
+
# NOTE: With shell=False (subprocess.run with list args), these are harmless
|
|
63
|
+
# string literals. We only block truly dangerous injection patterns.
|
|
64
|
+
BLOCKED_CHARS = {";", "`", "$("}
|
|
63
65
|
|
|
64
|
-
EXEC_TIMEOUT_BUILD =
|
|
66
|
+
EXEC_TIMEOUT_BUILD = 180 # Increased for large monorepo installs
|
|
65
67
|
EXEC_TIMEOUT_RUN = 10 # Short timeout — we just check if it starts
|
|
66
68
|
EXEC_TIMEOUT_DEPLOY = 180
|
|
67
69
|
|
|
@@ -547,6 +549,25 @@ def run_pipeline(
|
|
|
547
549
|
if result["pid"]:
|
|
548
550
|
_save_process(result["pid"], cmd, desc)
|
|
549
551
|
else:
|
|
552
|
+
# ── Auto-recovery: clean retry for npm install failures ──
|
|
553
|
+
is_npm_install = cmd.strip().startswith("npm") and "install" in cmd
|
|
554
|
+
if is_npm_install and tier == "build":
|
|
555
|
+
console.print(f" [yellow]🔄 npm install failed — cleaning node_modules and retrying…[/yellow]")
|
|
556
|
+
nm_path = cwd / "node_modules"
|
|
557
|
+
if nm_path.exists():
|
|
558
|
+
shutil.rmtree(nm_path, ignore_errors=True)
|
|
559
|
+
# Retry with clean state
|
|
560
|
+
retry_result = _execute(cmd, cwd, timeout, background)
|
|
561
|
+
if retry_result["returncode"] == 0:
|
|
562
|
+
succeeded += 1
|
|
563
|
+
console.print(f" [green]✅ OK (clean retry)[/green]")
|
|
564
|
+
log_entries.append({
|
|
565
|
+
"tier": tier, "command": cmd,
|
|
566
|
+
"description": desc + " (clean retry)",
|
|
567
|
+
"result": retry_result,
|
|
568
|
+
})
|
|
569
|
+
continue # Skip the error recording below
|
|
570
|
+
|
|
550
571
|
failed += 1
|
|
551
572
|
console.print(f" [red]❌ Failed (rc={result['returncode']})[/red]")
|
|
552
573
|
if result["stderr"]:
|
|
@@ -41,7 +41,8 @@ ALLOWED_COMMANDS = [
|
|
|
41
41
|
BLOCKED_FLAGS = {"-c", "--eval", "eval", "exec", "--exec", "-e"}
|
|
42
42
|
|
|
43
43
|
# Dangerous shell metacharacters
|
|
44
|
-
|
|
44
|
+
# NOTE: With shell=False (subprocess.run with list args), most are harmless.
|
|
45
|
+
BLOCKED_CHARS = {";", "`", "$("}
|
|
45
46
|
|
|
46
47
|
EXEC_TIMEOUT = 60 # seconds
|
|
47
48
|
|
|
@@ -73,9 +74,96 @@ RULES:
|
|
|
73
74
|
- Use relative imports when possible
|
|
74
75
|
- Tests should assert expected behavior, not just run without checking
|
|
75
76
|
- Keep tests focused and fast
|
|
77
|
+
|
|
78
|
+
CRITICAL DEPENDENCY RULES:
|
|
79
|
+
- You will be told which test runner and devDependencies are available.
|
|
80
|
+
- ONLY import packages that are listed as available. Do NOT assume packages exist.
|
|
81
|
+
- If NO test framework (vitest/jest/mocha) is installed, write tests using ONLY:
|
|
82
|
+
* Node.js built-in `assert` module (const assert = require('assert');)
|
|
83
|
+
* Node.js built-in `test` module if Node >= 18 (const { test } = require('node:test');)
|
|
84
|
+
* Direct require() of the source file being tested
|
|
85
|
+
- Do NOT import @testing-library/react, enzyme, or any DOM testing library unless
|
|
86
|
+
explicitly listed as available.
|
|
87
|
+
- For TypeScript source files, write tests in plain JavaScript (.js) using require()
|
|
88
|
+
UNLESS vitest or jest with ts-jest is available.
|
|
89
|
+
- Test files MUST have .js extension unless a test runner supporting .ts is available.
|
|
90
|
+
- Do NOT use `npx tsx` or `ts-node` to run tests — use the detected test runner.
|
|
76
91
|
"""
|
|
77
92
|
|
|
78
93
|
|
|
94
|
+
def _detect_test_runner(workspace_root: Path) -> dict:
|
|
95
|
+
"""
|
|
96
|
+
Detect available test runners in the project.
|
|
97
|
+
|
|
98
|
+
Returns dict with:
|
|
99
|
+
- runner: 'vitest' | 'jest' | 'pytest' | 'none'
|
|
100
|
+
- bin_path: absolute path to the runner binary (or empty)
|
|
101
|
+
"""
|
|
102
|
+
import shutil
|
|
103
|
+
|
|
104
|
+
# Check node_modules/.bin for JS/TS test runners
|
|
105
|
+
bin_dir = workspace_root / "node_modules" / ".bin"
|
|
106
|
+
|
|
107
|
+
# Check all workspace subdirs too (monorepo support)
|
|
108
|
+
bin_dirs = [bin_dir]
|
|
109
|
+
pkg_path = workspace_root / "package.json"
|
|
110
|
+
if pkg_path.exists():
|
|
111
|
+
try:
|
|
112
|
+
pkg = json.loads(pkg_path.read_text(encoding="utf-8"))
|
|
113
|
+
for ws in pkg.get("workspaces", []):
|
|
114
|
+
if "*" not in ws:
|
|
115
|
+
ws_bin = workspace_root / ws / "node_modules" / ".bin"
|
|
116
|
+
if ws_bin.exists():
|
|
117
|
+
bin_dirs.append(ws_bin)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
for bd in bin_dirs:
|
|
122
|
+
vitest_bin = bd / "vitest"
|
|
123
|
+
if vitest_bin.exists():
|
|
124
|
+
return {"runner": "vitest", "bin_path": str(vitest_bin)}
|
|
125
|
+
jest_bin = bd / "jest"
|
|
126
|
+
if jest_bin.exists():
|
|
127
|
+
return {"runner": "jest", "bin_path": str(jest_bin)}
|
|
128
|
+
|
|
129
|
+
# Check for pytest
|
|
130
|
+
if shutil.which("pytest"):
|
|
131
|
+
return {"runner": "pytest", "bin_path": shutil.which("pytest") or "pytest"}
|
|
132
|
+
|
|
133
|
+
return {"runner": "none", "bin_path": ""}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_available_devdeps(workspace_root: Path) -> list[str]:
|
|
137
|
+
"""
|
|
138
|
+
Read package.json devDependencies and dependencies to tell the LLM
|
|
139
|
+
which packages are actually available for import.
|
|
140
|
+
"""
|
|
141
|
+
all_deps: set[str] = set()
|
|
142
|
+
|
|
143
|
+
# Read root package.json
|
|
144
|
+
for pkg_file in [workspace_root / "package.json"]:
|
|
145
|
+
if pkg_file.exists():
|
|
146
|
+
try:
|
|
147
|
+
pkg = json.loads(pkg_file.read_text(encoding="utf-8"))
|
|
148
|
+
all_deps.update(pkg.get("dependencies", {}).keys())
|
|
149
|
+
all_deps.update(pkg.get("devDependencies", {}).keys())
|
|
150
|
+
|
|
151
|
+
# Also check workspace package.json files
|
|
152
|
+
for ws in pkg.get("workspaces", []):
|
|
153
|
+
if "*" not in ws:
|
|
154
|
+
ws_pkg = workspace_root / ws / "package.json"
|
|
155
|
+
if ws_pkg.exists():
|
|
156
|
+
ws_data = json.loads(ws_pkg.read_text(encoding="utf-8"))
|
|
157
|
+
all_deps.update(ws_data.get("dependencies", {}).keys())
|
|
158
|
+
all_deps.update(ws_data.get("devDependencies", {}).keys())
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
# Filter out build tools that aren't importable in tests
|
|
163
|
+
non_importable = {"typescript", "vite", "@vitejs/plugin-react", "concurrently"}
|
|
164
|
+
return sorted(all_deps - non_importable)
|
|
165
|
+
|
|
166
|
+
|
|
79
167
|
def _is_command_allowed(cmd: str) -> bool:
|
|
80
168
|
"""Check if a command is safe to execute."""
|
|
81
169
|
# Block shell metacharacters in raw command string
|
|
@@ -161,12 +249,16 @@ def run_tests(
|
|
|
161
249
|
f"generating tests for {len(source_files)} file(s)"
|
|
162
250
|
)
|
|
163
251
|
|
|
164
|
-
# ── Environment Strategy
|
|
252
|
+
# ── Environment Strategy ────────────────────────────────────────────────────────
|
|
165
253
|
from adelie.env_strategy import detect_env, select_strategy, wrap_command, get_current_phase, ensure_env
|
|
166
254
|
env_profile = detect_env(workspace_root)
|
|
167
255
|
env_profile = ensure_env(env_profile, workspace_root)
|
|
168
256
|
env_strategy = select_strategy(env_profile, phase=get_current_phase())
|
|
169
257
|
|
|
258
|
+
# ── Detect test runner and available devDependencies ─────────────────
|
|
259
|
+
test_runner_info = _detect_test_runner(workspace_root)
|
|
260
|
+
available_devdeps = _get_available_devdeps(workspace_root)
|
|
261
|
+
|
|
170
262
|
# Read source files for context
|
|
171
263
|
file_contents = []
|
|
172
264
|
for finfo in source_files:
|
|
@@ -179,7 +271,20 @@ def run_tests(
|
|
|
179
271
|
except Exception:
|
|
180
272
|
pass
|
|
181
273
|
|
|
274
|
+
# Build runner info section for prompt
|
|
275
|
+
runner_section = "## Available Test Environment\n"
|
|
276
|
+
runner_section += f"Test runner: {test_runner_info['runner']}\n"
|
|
277
|
+
runner_section += f"Available devDependencies: {', '.join(available_devdeps) if available_devdeps else 'NONE'}\n"
|
|
278
|
+
runner_section += f"IMPORTANT: You can ONLY import packages listed above. Do NOT import anything else.\n"
|
|
279
|
+
if test_runner_info['runner'] == 'none':
|
|
280
|
+
runner_section += (
|
|
281
|
+
"No test framework is installed. Write tests using ONLY Node.js built-in 'assert' module.\n"
|
|
282
|
+
"Use require() to import source files with relative paths from the project root.\n"
|
|
283
|
+
"Test files MUST have .js extension.\n"
|
|
284
|
+
)
|
|
285
|
+
|
|
182
286
|
user_prompt = (
|
|
287
|
+
f"{runner_section}\n"
|
|
183
288
|
f"## Source Files to Test\n\n"
|
|
184
289
|
+ "\n\n".join(file_contents)
|
|
185
290
|
+ f"\n\n## Max Test Layer: {max_test_layer}\n"
|
|
@@ -272,11 +377,23 @@ def run_tests(
|
|
|
272
377
|
run_cmd = f'{python} -m pytest "{script_path}" -v --tb=short'
|
|
273
378
|
elif lang in ("javascript", "js", "typescript", "ts"):
|
|
274
379
|
ext = script_path.suffix.lower()
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
380
|
+
runner = test_runner_info.get("runner", "none")
|
|
381
|
+
runner_bin = test_runner_info.get("bin_path", "")
|
|
382
|
+
|
|
383
|
+
if runner == "vitest" and runner_bin:
|
|
384
|
+
run_cmd = f'{runner_bin} run "{script_path}"'
|
|
385
|
+
elif runner == "jest" and runner_bin:
|
|
386
|
+
run_cmd = f'{runner_bin} --testMatch "{script_path}"'
|
|
387
|
+
elif ext in (".ts", ".tsx", ".jsx"):
|
|
388
|
+
# No test runner for TS/JSX — cannot execute directly.
|
|
389
|
+
# Skip this test and log a warning.
|
|
390
|
+
console.print(
|
|
391
|
+
f" [yellow]⚠️ Skipping {name}: .ts/.tsx/.jsx test requires "
|
|
392
|
+
f"vitest or jest (not installed)[/yellow]"
|
|
393
|
+
)
|
|
394
|
+
continue
|
|
279
395
|
else:
|
|
396
|
+
# Plain .js — run with node directly
|
|
280
397
|
run_cmd = f'node "{script_path}"'
|
|
281
398
|
else:
|
|
282
399
|
# Fall back to LLM-provided command if language unknown
|
package/adelie/env_strategy.py
CHANGED
|
@@ -245,6 +245,10 @@ def _bootstrap_npm(project_root: Path) -> bool:
|
|
|
245
245
|
2. npm install --legacy-peer-deps (dependency conflicts)
|
|
246
246
|
3. npm install --force (last resort)
|
|
247
247
|
|
|
248
|
+
Between each attempt, corrupt node_modules are removed so the next
|
|
249
|
+
install starts from a clean state (prevents half-installed packages
|
|
250
|
+
like date-fns with only .d.ts stubs).
|
|
251
|
+
|
|
248
252
|
Returns True if any attempt succeeded.
|
|
249
253
|
"""
|
|
250
254
|
import sys
|
|
@@ -255,7 +259,14 @@ def _bootstrap_npm(project_root: Path) -> bool:
|
|
|
255
259
|
("npm install --force", "npm install --force"),
|
|
256
260
|
]
|
|
257
261
|
|
|
258
|
-
for label, cmd in strategies:
|
|
262
|
+
for idx, (label, cmd) in enumerate(strategies):
|
|
263
|
+
# Clean corrupt node_modules before retry attempts
|
|
264
|
+
if idx > 0:
|
|
265
|
+
node_modules = project_root / "node_modules"
|
|
266
|
+
if node_modules.exists():
|
|
267
|
+
console.print(" [dim] 🗑️ Removing corrupt node_modules before retry…[/dim]")
|
|
268
|
+
shutil.rmtree(node_modules, ignore_errors=True)
|
|
269
|
+
|
|
259
270
|
console.print(f" [dim] ▶ {label}…[/dim]")
|
|
260
271
|
try:
|
|
261
272
|
if _win:
|
|
@@ -276,8 +287,13 @@ def _bootstrap_npm(project_root: Path) -> bool:
|
|
|
276
287
|
timeout=180,
|
|
277
288
|
)
|
|
278
289
|
if result.returncode == 0:
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
# Verify the install actually produced valid modules
|
|
291
|
+
node_modules = project_root / "node_modules"
|
|
292
|
+
if node_modules.is_dir():
|
|
293
|
+
console.print(f" [green] ✅ {label} succeeded[/green]")
|
|
294
|
+
return True
|
|
295
|
+
else:
|
|
296
|
+
console.print(f" [dim] ⚠️ {label} exited 0 but node_modules missing[/dim]")
|
|
281
297
|
else:
|
|
282
298
|
stderr_short = result.stderr.strip()[-200:] if result.stderr else ""
|
|
283
299
|
console.print(f" [dim] ⚠️ {label} failed — trying fallback…[/dim]")
|
|
@@ -129,13 +129,14 @@ CODER TASK RULES FOR MID PHASE:
|
|
|
129
129
|
- Each task should be self-contained — the coder creates files from scratch.
|
|
130
130
|
- Be SPECIFIC: include exact filenames, tech stack, data models in task descriptions.""",
|
|
131
131
|
"transition_criteria": {
|
|
132
|
-
"description": "Transition to MID_1 when: core features are implemented, basic tests pass, implementation_plan tasks are mostly complete.",
|
|
132
|
+
"description": "Transition to MID_1 when: core features are implemented, basic tests pass (>=30%), build succeeds, implementation_plan tasks are mostly complete.",
|
|
133
133
|
"conditions": {
|
|
134
134
|
"min_loops": 15,
|
|
135
135
|
"min_kb_files": 8,
|
|
136
136
|
"required_files": ["implementation", "test"],
|
|
137
|
-
"min_test_pass_rate": 0.
|
|
137
|
+
"min_test_pass_rate": 0.3,
|
|
138
138
|
"min_review_score": 4,
|
|
139
|
+
"require_build_success": True,
|
|
139
140
|
},
|
|
140
141
|
},
|
|
141
142
|
"next_phase": "mid_1",
|
|
@@ -170,13 +171,14 @@ Your decision criteria in this phase:
|
|
|
170
171
|
- Use EXPORT for test results and roadmap updates
|
|
171
172
|
- If roadmap has gaps, request updates via kb_updates_needed""",
|
|
172
173
|
"transition_criteria": {
|
|
173
|
-
"description": "Transition to MID_2 when: tests pass, roadmap is updated, no critical duplicates, operational guide exists.",
|
|
174
|
+
"description": "Transition to MID_2 when: tests pass (>=50%), build succeeds, roadmap is updated, no critical duplicates, operational guide exists.",
|
|
174
175
|
"conditions": {
|
|
175
176
|
"min_loops": 20,
|
|
176
177
|
"min_kb_files": 10,
|
|
177
178
|
"required_files": ["operations", "test_result"],
|
|
178
|
-
"min_test_pass_rate": 0.
|
|
179
|
+
"min_test_pass_rate": 0.5,
|
|
179
180
|
"min_review_score": 5,
|
|
181
|
+
"require_build_success": True,
|
|
180
182
|
},
|
|
181
183
|
},
|
|
182
184
|
"next_phase": "mid_2",
|
|
@@ -579,6 +581,7 @@ class HarnessManager:
|
|
|
579
581
|
test_pass_rate: float = 0.0,
|
|
580
582
|
avg_review_score: float = 0.0,
|
|
581
583
|
loop_multiplier: float = 1.0,
|
|
584
|
+
build_success: bool = False,
|
|
582
585
|
) -> str | None:
|
|
583
586
|
"""
|
|
584
587
|
Check if conditions for the next phase are met.
|
|
@@ -622,6 +625,10 @@ class HarnessManager:
|
|
|
622
625
|
if avg_review_score < conditions.get("min_review_score", 0):
|
|
623
626
|
return None
|
|
624
627
|
|
|
628
|
+
# Check require_build_success
|
|
629
|
+
if conditions.get("require_build_success", False) and not build_success:
|
|
630
|
+
return None
|
|
631
|
+
|
|
625
632
|
return next_phase
|
|
626
633
|
|
|
627
634
|
def get_phase_order(self) -> list[str]:
|
package/adelie/orchestrator.py
CHANGED