etch-loop 0.3.1__tar.gz → 0.4.1__tar.gz
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.
- {etch_loop-0.3.1 → etch_loop-0.4.1}/PKG-INFO +1 -1
- {etch_loop-0.3.1 → etch_loop-0.4.1}/pyproject.toml +1 -1
- etch_loop-0.4.1/src/etch/__init__.py +1 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/agent.py +32 -9
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/analyze.py +85 -7
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/cli.py +7 -4
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/git.py +2 -2
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/loop.py +98 -9
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/prompt.py +29 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/report.py +8 -0
- etch_loop-0.4.1/src/etch/templates/RUN.md +20 -0
- etch_loop-0.3.1/src/etch/__init__.py +0 -1
- {etch_loop-0.3.1 → etch_loop-0.4.1}/.github/workflows/workflow.yml +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/README.md +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/display.py +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/signals.py +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/templates/BREAK.md +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/templates/ETCH.md +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/src/etch/templates/SCAN.md +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/tests/__init__.py +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/tests/test_git.py +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/tests/test_loop.py +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/tests/test_prompt.py +0 -0
- {etch_loop-0.3.1 → etch_loop-0.4.1}/tests/test_signals.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.1"
|
|
@@ -51,18 +51,34 @@ def run(
|
|
|
51
51
|
except OSError as exc:
|
|
52
52
|
raise AgentError(f"Failed to launch claude: {exc}") from exc
|
|
53
53
|
|
|
54
|
-
# Write prompt to stdin and close it
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
# Write prompt to stdin and close it — run in thread to avoid blocking on full pipe buffer
|
|
55
|
+
stdin_exc: list[Exception] = []
|
|
56
|
+
|
|
57
|
+
def write_stdin() -> None:
|
|
58
|
+
try:
|
|
59
|
+
process.stdin.write(prompt)
|
|
60
|
+
process.stdin.close()
|
|
61
|
+
except BrokenPipeError as exc:
|
|
62
|
+
stdin_exc.append(exc)
|
|
63
|
+
|
|
64
|
+
stdin_writer = threading.Thread(target=write_stdin, daemon=True)
|
|
65
|
+
stdin_writer.start()
|
|
66
|
+
stdin_writer.join(timeout=30)
|
|
67
|
+
if stdin_writer.is_alive():
|
|
68
|
+
process.kill()
|
|
69
|
+
raise AgentError("Timed out writing prompt to claude stdin")
|
|
70
|
+
if stdin_exc:
|
|
71
|
+
process.kill()
|
|
72
|
+
raise AgentError(f"Failed to write prompt to claude stdin: {stdin_exc[0]}") from stdin_exc[0]
|
|
73
|
+
|
|
74
|
+
if process.stdout is None:
|
|
75
|
+
process.kill()
|
|
76
|
+
raise AgentError("claude subprocess has no stdout")
|
|
60
77
|
|
|
61
78
|
output_lines: list[str] = []
|
|
62
79
|
lock = threading.Lock()
|
|
63
80
|
|
|
64
81
|
def read_stdout() -> None:
|
|
65
|
-
assert process.stdout is not None
|
|
66
82
|
for line in process.stdout:
|
|
67
83
|
with lock:
|
|
68
84
|
output_lines.append(line)
|
|
@@ -73,9 +89,16 @@ def run(
|
|
|
73
89
|
|
|
74
90
|
reader = threading.Thread(target=read_stdout, daemon=True)
|
|
75
91
|
reader.start()
|
|
76
|
-
reader.join()
|
|
92
|
+
reader.join(timeout=300)
|
|
93
|
+
if reader.is_alive():
|
|
94
|
+
process.kill()
|
|
95
|
+
raise AgentError("claude subprocess timed out (output reader still running)")
|
|
77
96
|
|
|
78
|
-
|
|
97
|
+
try:
|
|
98
|
+
process.wait(timeout=10)
|
|
99
|
+
except subprocess.TimeoutExpired:
|
|
100
|
+
process.kill()
|
|
101
|
+
raise AgentError("claude subprocess timed out waiting for exit")
|
|
79
102
|
|
|
80
103
|
# Capture stderr for error reporting
|
|
81
104
|
stderr_output = ""
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import subprocess
|
|
6
7
|
from collections import Counter
|
|
7
8
|
from pathlib import Path
|
|
@@ -107,12 +108,14 @@ def analyze(root: Path | None = None) -> dict:
|
|
|
107
108
|
"framework": framework,
|
|
108
109
|
"total_files": total,
|
|
109
110
|
"is_git": (root / ".git").exists(),
|
|
111
|
+
"root": root,
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
|
|
113
115
|
def build_init_prompt(info: dict) -> str:
|
|
114
116
|
"""Build the Claude prompt used during etch init to analyze the codebase."""
|
|
115
|
-
|
|
117
|
+
root = info.get("root", Path.cwd())
|
|
118
|
+
file_tree = "\n".join(f" {f}" for f in _list_files(root)[:60])
|
|
116
119
|
if not file_tree:
|
|
117
120
|
file_tree = " (no tracked files)"
|
|
118
121
|
|
|
@@ -257,6 +260,78 @@ Be adversarial — think like someone actively trying to make this code fail.
|
|
|
257
260
|
"""
|
|
258
261
|
|
|
259
262
|
|
|
263
|
+
def build_run_md(info: dict) -> str:
|
|
264
|
+
"""Generate a tailored RUN.md based on detected build system."""
|
|
265
|
+
root = info.get("root", Path.cwd())
|
|
266
|
+
commands = _detect_run_commands(root)
|
|
267
|
+
|
|
268
|
+
if commands:
|
|
269
|
+
cmd_list = "\n".join(f"- `{cmd}`" for cmd in commands)
|
|
270
|
+
else:
|
|
271
|
+
cmd_list = "- (detect and run the appropriate build/test command for this project)"
|
|
272
|
+
|
|
273
|
+
return f"""# RUN — build and test validation
|
|
274
|
+
|
|
275
|
+
You are a build validator. The fixer has made changes. Your job is to run the project's build and test suite to confirm everything still works.
|
|
276
|
+
|
|
277
|
+
## Commands to run
|
|
278
|
+
|
|
279
|
+
{cmd_list}
|
|
280
|
+
|
|
281
|
+
## Rules
|
|
282
|
+
|
|
283
|
+
1. Run each command and observe the output
|
|
284
|
+
2. If ALL commands pass:
|
|
285
|
+
- Write `ETCH_SUMMARY: <e.g. "all 47 tests passed">`
|
|
286
|
+
- Write `ETCH_ALL_CLEAR`
|
|
287
|
+
3. If ANY command fails:
|
|
288
|
+
- Write `ETCH_SUMMARY: <what failed, e.g. "3 tests failed in test_auth.py — TypeError on line 42">`
|
|
289
|
+
- Include the relevant error output so the fixer can diagnose it
|
|
290
|
+
- Write `ETCH_ISSUES_FOUND`
|
|
291
|
+
|
|
292
|
+
Do not fix anything — only run and report.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _detect_run_commands(root: Path) -> list[str]:
|
|
297
|
+
"""Detect build/test commands from project files."""
|
|
298
|
+
commands: list[str] = []
|
|
299
|
+
|
|
300
|
+
if (root / "pyproject.toml").exists() or (root / "setup.py").exists():
|
|
301
|
+
commands.append("python -m pytest")
|
|
302
|
+
|
|
303
|
+
if (root / "package.json").exists():
|
|
304
|
+
try:
|
|
305
|
+
pkg = json.loads((root / "package.json").read_text(encoding="utf-8"))
|
|
306
|
+
scripts = pkg.get("scripts", {})
|
|
307
|
+
if "build" in scripts:
|
|
308
|
+
commands.append("npm run build")
|
|
309
|
+
if "test" in scripts:
|
|
310
|
+
commands.append("npm test")
|
|
311
|
+
except (OSError, json.JSONDecodeError):
|
|
312
|
+
commands.append("npm test")
|
|
313
|
+
|
|
314
|
+
if (root / "Cargo.toml").exists():
|
|
315
|
+
commands.append("cargo test")
|
|
316
|
+
|
|
317
|
+
if (root / "go.mod").exists():
|
|
318
|
+
commands.append("go test ./...")
|
|
319
|
+
|
|
320
|
+
if (root / "Gemfile").exists():
|
|
321
|
+
commands.append("bundle exec rspec")
|
|
322
|
+
|
|
323
|
+
if (root / "mix.exs").exists():
|
|
324
|
+
commands.append("mix test")
|
|
325
|
+
|
|
326
|
+
if (root / "pom.xml").exists():
|
|
327
|
+
commands.append("mvn test -q")
|
|
328
|
+
|
|
329
|
+
if not commands and (root / "Makefile").exists():
|
|
330
|
+
commands.append("make test")
|
|
331
|
+
|
|
332
|
+
return commands
|
|
333
|
+
|
|
334
|
+
|
|
260
335
|
def _format_scope(info: dict) -> str:
|
|
261
336
|
lines = []
|
|
262
337
|
if info["source_dirs"]:
|
|
@@ -288,12 +363,15 @@ def _list_files(root: Path) -> list[str]:
|
|
|
288
363
|
|
|
289
364
|
# Fallback: walk filesystem
|
|
290
365
|
files = []
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
366
|
+
try:
|
|
367
|
+
for p in root.rglob("*"):
|
|
368
|
+
if p.is_file() and not any(part in _SKIP_DIRS for part in p.parts):
|
|
369
|
+
try:
|
|
370
|
+
files.append(str(p.relative_to(root)))
|
|
371
|
+
except ValueError:
|
|
372
|
+
pass
|
|
373
|
+
except OSError:
|
|
374
|
+
pass
|
|
297
375
|
return files
|
|
298
376
|
|
|
299
377
|
|
|
@@ -21,6 +21,8 @@ app = typer.Typer(
|
|
|
21
21
|
def init() -> None:
|
|
22
22
|
"""Analyze the codebase with Claude and write tailored SCAN.md, ETCH.md, BREAK.md."""
|
|
23
23
|
root = Path.cwd()
|
|
24
|
+
etch_dir = root / "etch-loop"
|
|
25
|
+
etch_dir.mkdir(exist_ok=True)
|
|
24
26
|
info = analyze.analyze(root)
|
|
25
27
|
init_prompt = analyze.build_init_prompt(info)
|
|
26
28
|
|
|
@@ -35,9 +37,10 @@ def init() -> None:
|
|
|
35
37
|
disp.add_line(display.SYM_NEUTRAL, display.DIM, f"falling back to static analysis ({exc})")
|
|
36
38
|
|
|
37
39
|
for dest, content, label in [
|
|
38
|
-
(
|
|
39
|
-
(
|
|
40
|
-
(
|
|
40
|
+
(etch_dir / "SCAN.md", analyze.build_scan_md(info, agent_scope), "etch-loop/SCAN.md"),
|
|
41
|
+
(etch_dir / "ETCH.md", analyze.build_etch_md(info, agent_scope), "etch-loop/ETCH.md"),
|
|
42
|
+
(etch_dir / "BREAK.md", analyze.build_break_md(info, agent_scope), "etch-loop/BREAK.md"),
|
|
43
|
+
(etch_dir / "RUN.md", analyze.build_run_md(info), "etch-loop/RUN.md"),
|
|
41
44
|
]:
|
|
42
45
|
if dest.exists():
|
|
43
46
|
disp.add_line(display.SYM_NEUTRAL, display.DIM, f"{label} already exists, skipping")
|
|
@@ -53,7 +56,7 @@ def run(
|
|
|
53
56
|
help="Optional focus description, e.g. 'the auth module' or 'error handling in payments'.",
|
|
54
57
|
),
|
|
55
58
|
prompt: str = typer.Option(
|
|
56
|
-
"./ETCH.md",
|
|
59
|
+
"./etch-loop/ETCH.md",
|
|
57
60
|
"--prompt",
|
|
58
61
|
help="Path to the fixer prompt file (ETCH.md).",
|
|
59
62
|
show_default=True,
|
|
@@ -48,8 +48,8 @@ def has_changes() -> bool:
|
|
|
48
48
|
raise GitError(f"Failed to run git status: {exc}") from exc
|
|
49
49
|
|
|
50
50
|
if status.returncode != 0:
|
|
51
|
-
stderr =
|
|
52
|
-
raise GitError(f"git
|
|
51
|
+
stderr = status.stderr.strip()
|
|
52
|
+
raise GitError(f"git status failed (exit {status.returncode}): {stderr}")
|
|
53
53
|
|
|
54
54
|
return bool(status.stdout.strip())
|
|
55
55
|
|
|
@@ -49,6 +49,9 @@ def run(
|
|
|
49
49
|
display.print_error(str(exc))
|
|
50
50
|
return
|
|
51
51
|
|
|
52
|
+
# Runner is optional — None means the phase is skipped
|
|
53
|
+
run_text = prompt.load_run(prompt_path)
|
|
54
|
+
|
|
52
55
|
if focus:
|
|
53
56
|
scan_text += f"\n\n## User focus\n\nConcentrate on: {focus}\n"
|
|
54
57
|
break_text += f"\n\n## User focus\n\nConcentrate your adversarial review on: {focus}\n"
|
|
@@ -63,9 +66,59 @@ def run(
|
|
|
63
66
|
}
|
|
64
67
|
last_breaker_signal: str | None = None
|
|
65
68
|
last_breaker_output: str | None = None
|
|
69
|
+
last_runner_output: str | None = None
|
|
66
70
|
iteration_log: list[dict] = []
|
|
67
71
|
|
|
68
72
|
with display.EtchDisplay(target=str(prompt_path.parent)) as disp:
|
|
73
|
+
|
|
74
|
+
# ── Runner helper — called at every clean exit point ──────────────────
|
|
75
|
+
def try_runner(iter_entry: dict) -> str:
|
|
76
|
+
"""Run the runner phase if configured.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
"skip" — no RUN.md, proceed with clean exit
|
|
80
|
+
"clear" — runner passed, proceed with clean exit
|
|
81
|
+
"issues" — runner failed, continue the loop
|
|
82
|
+
"error" — agent error, break the loop
|
|
83
|
+
"""
|
|
84
|
+
nonlocal last_runner_output
|
|
85
|
+
if not run_text:
|
|
86
|
+
return "skip"
|
|
87
|
+
|
|
88
|
+
disp.start_phase("runner")
|
|
89
|
+
runner_start = time.monotonic()
|
|
90
|
+
try:
|
|
91
|
+
runner_output = agent.run(run_text, verbose=verbose)
|
|
92
|
+
except AgentError as exc:
|
|
93
|
+
disp.finish_phase("runner", status="error", detail=str(exc),
|
|
94
|
+
duration=time.monotonic() - runner_start, success=False)
|
|
95
|
+
return "error"
|
|
96
|
+
|
|
97
|
+
runner_duration = time.monotonic() - runner_start
|
|
98
|
+
runner_signal = signals.parse(runner_output)
|
|
99
|
+
runner_detail = (
|
|
100
|
+
signals.extract_summary(runner_output)
|
|
101
|
+
or signals.extract_finding(runner_output)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if runner_signal == "clear":
|
|
105
|
+
disp.finish_phase("runner", status="all clear",
|
|
106
|
+
detail=runner_detail or "build passed",
|
|
107
|
+
duration=runner_duration, success=True)
|
|
108
|
+
iter_entry["runner"] = {"status": "all clear", "detail": runner_detail}
|
|
109
|
+
last_runner_output = None
|
|
110
|
+
return "clear"
|
|
111
|
+
else:
|
|
112
|
+
disp.record_issue()
|
|
113
|
+
stats["issues"] += 1
|
|
114
|
+
disp.finish_phase("runner", status="build failed",
|
|
115
|
+
detail=runner_detail or "build failed",
|
|
116
|
+
duration=runner_duration, success=False)
|
|
117
|
+
iter_entry["runner"] = {"status": "build failed", "detail": runner_detail}
|
|
118
|
+
last_runner_output = runner_output
|
|
119
|
+
return "issues"
|
|
120
|
+
|
|
121
|
+
# ── Main loop ─────────────────────────────────────────────────────────
|
|
69
122
|
for iteration in range(1, max_iterations + 1):
|
|
70
123
|
stats["iterations"] = iteration
|
|
71
124
|
disp.start_iteration(iteration)
|
|
@@ -95,9 +148,19 @@ def run(
|
|
|
95
148
|
detail=scanner_detail or "nothing to fix",
|
|
96
149
|
duration=scanner_duration, success=True)
|
|
97
150
|
iter_entry["scanner"] = {"status": "all clear", "detail": scanner_detail}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
151
|
+
runner_result = try_runner(iter_entry)
|
|
152
|
+
if runner_result == "error":
|
|
153
|
+
stats["reason"] = "agent_error"
|
|
154
|
+
iteration_log.append(iter_entry)
|
|
155
|
+
break
|
|
156
|
+
elif runner_result == "issues":
|
|
157
|
+
stats["reason"] = "issues"
|
|
158
|
+
iteration_log.append(iter_entry)
|
|
159
|
+
continue
|
|
160
|
+
else: # "clear" or "skip"
|
|
161
|
+
stats["reason"] = "no_changes"
|
|
162
|
+
iteration_log.append(iter_entry)
|
|
163
|
+
break
|
|
101
164
|
|
|
102
165
|
disp.finish_phase("scanner", status="issues found",
|
|
103
166
|
detail=scanner_detail or "issues found",
|
|
@@ -116,6 +179,12 @@ def run(
|
|
|
116
179
|
f"{last_breaker_output.strip()}\n\n"
|
|
117
180
|
f"Also address these if not already covered above.\n"
|
|
118
181
|
)
|
|
182
|
+
if last_runner_output:
|
|
183
|
+
fixer_prompt += (
|
|
184
|
+
f"\n\n## Build/test failures from previous iteration\n\n"
|
|
185
|
+
f"{last_runner_output.strip()}\n\n"
|
|
186
|
+
f"Fix the underlying code issues causing these failures.\n"
|
|
187
|
+
)
|
|
119
188
|
|
|
120
189
|
# ── Fixer phase ───────────────────────────────────────────────────
|
|
121
190
|
disp.start_phase("fixer")
|
|
@@ -147,9 +216,19 @@ def run(
|
|
|
147
216
|
duration=fixer_duration, success=True)
|
|
148
217
|
iter_entry["fixer"] = {"status": "no changes", "detail": "nothing to fix"}
|
|
149
218
|
if last_breaker_signal != "issues":
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
219
|
+
runner_result = try_runner(iter_entry)
|
|
220
|
+
if runner_result == "error":
|
|
221
|
+
stats["reason"] = "agent_error"
|
|
222
|
+
iteration_log.append(iter_entry)
|
|
223
|
+
break
|
|
224
|
+
elif runner_result == "issues":
|
|
225
|
+
stats["reason"] = "issues"
|
|
226
|
+
iteration_log.append(iter_entry)
|
|
227
|
+
continue
|
|
228
|
+
else: # "clear" or "skip"
|
|
229
|
+
stats["reason"] = "no_changes"
|
|
230
|
+
iteration_log.append(iter_entry)
|
|
231
|
+
break
|
|
153
232
|
iteration_log.append(iter_entry)
|
|
154
233
|
# Fall through to breaker
|
|
155
234
|
|
|
@@ -205,9 +284,19 @@ def run(
|
|
|
205
284
|
detail=breaker_detail or "no issues found",
|
|
206
285
|
duration=breaker_duration, success=True)
|
|
207
286
|
iter_entry["breaker"] = {"status": "all clear", "detail": breaker_detail}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
287
|
+
runner_result = try_runner(iter_entry)
|
|
288
|
+
if runner_result == "error":
|
|
289
|
+
stats["reason"] = "agent_error"
|
|
290
|
+
iteration_log.append(iter_entry)
|
|
291
|
+
break
|
|
292
|
+
elif runner_result == "issues":
|
|
293
|
+
stats["reason"] = "issues"
|
|
294
|
+
iteration_log.append(iter_entry)
|
|
295
|
+
continue
|
|
296
|
+
else: # "clear" or "skip"
|
|
297
|
+
stats["reason"] = "clear"
|
|
298
|
+
iteration_log.append(iter_entry)
|
|
299
|
+
break
|
|
211
300
|
else:
|
|
212
301
|
disp.record_issue()
|
|
213
302
|
stats["issues"] += 1
|
|
@@ -75,6 +75,35 @@ def load_break(path: str | Path | None = None) -> str:
|
|
|
75
75
|
raise PromptError(f"BREAK.md not found. Searched: {searched}")
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def load_run(path: str | Path | None = None) -> str | None:
|
|
79
|
+
"""Load RUN.md if it exists. Returns None if not found — runner phase is optional.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
path: Optional path. If this is ETCH.md, looks for RUN.md alongside it.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
File contents as a string, or None if RUN.md is not present.
|
|
86
|
+
"""
|
|
87
|
+
candidates: list[Path] = []
|
|
88
|
+
|
|
89
|
+
if path is not None:
|
|
90
|
+
p = Path(path)
|
|
91
|
+
if p.name.upper() == "RUN.MD":
|
|
92
|
+
candidates.append(p)
|
|
93
|
+
else:
|
|
94
|
+
candidates.append(p.parent / "RUN.md")
|
|
95
|
+
|
|
96
|
+
candidates.append(Path.cwd() / "RUN.md")
|
|
97
|
+
|
|
98
|
+
for candidate in candidates:
|
|
99
|
+
if candidate.exists() and candidate.is_file():
|
|
100
|
+
content = candidate.read_text(encoding="utf-8")
|
|
101
|
+
if content.strip():
|
|
102
|
+
return content
|
|
103
|
+
|
|
104
|
+
return None # Optional phase — no error if absent
|
|
105
|
+
|
|
106
|
+
|
|
78
107
|
def load_scan(path: str | Path | None = None) -> str:
|
|
79
108
|
"""Load and return the content of SCAN.md.
|
|
80
109
|
|
|
@@ -67,6 +67,14 @@ def write(
|
|
|
67
67
|
if detail:
|
|
68
68
|
lines.append(f"\n> {detail}\n")
|
|
69
69
|
|
|
70
|
+
runner = entry.get("runner")
|
|
71
|
+
if runner:
|
|
72
|
+
status = runner.get("status", "")
|
|
73
|
+
detail = runner.get("detail", "")
|
|
74
|
+
lines.append(f"**runner** — {status}")
|
|
75
|
+
if detail:
|
|
76
|
+
lines.append(f"\n> {detail}\n")
|
|
77
|
+
|
|
70
78
|
path.write_text("\n".join(lines), encoding="utf-8")
|
|
71
79
|
return path
|
|
72
80
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# RUN — build and test validation
|
|
2
|
+
|
|
3
|
+
You are a build validator. The fixer has made changes. Your job is to run the project's build and test suite to confirm everything still works.
|
|
4
|
+
|
|
5
|
+
## Commands to run
|
|
6
|
+
|
|
7
|
+
[configured by etch init]
|
|
8
|
+
|
|
9
|
+
## Rules
|
|
10
|
+
|
|
11
|
+
1. Run each command and observe the output
|
|
12
|
+
2. If ALL commands pass:
|
|
13
|
+
- Write `ETCH_SUMMARY: <e.g. "all 47 tests passed">`
|
|
14
|
+
- Write `ETCH_ALL_CLEAR`
|
|
15
|
+
3. If ANY command fails:
|
|
16
|
+
- Write `ETCH_SUMMARY: <what failed, e.g. "3 tests failed in test_auth.py — TypeError on line 42">`
|
|
17
|
+
- Include the relevant error output so the fixer can diagnose it
|
|
18
|
+
- Write `ETCH_ISSUES_FOUND`
|
|
19
|
+
|
|
20
|
+
Do not fix anything — only run and report.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|