etch-loop 0.4.8__tar.gz → 0.5.0__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.4.8 → etch_loop-0.5.0}/PKG-INFO +1 -1
- {etch_loop-0.4.8 → etch_loop-0.5.0}/pyproject.toml +1 -1
- etch_loop-0.5.0/src/etch/__init__.py +1 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/agent.py +13 -10
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/analyze.py +3 -2
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/display.py +2 -2
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/loop.py +47 -26
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/templates/RUN.md +3 -2
- etch_loop-0.4.8/src/etch/__init__.py +0 -1
- {etch_loop-0.4.8 → etch_loop-0.5.0}/.github/workflows/workflow.yml +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/README.md +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/cli.py +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/git.py +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/prompt.py +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/report.py +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/signals.py +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/templates/BREAK.md +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/templates/ETCH.md +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/src/etch/templates/SCAN.md +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/tests/__init__.py +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/tests/test_git.py +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/tests/test_loop.py +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/tests/test_prompt.py +0 -0
- {etch_loop-0.4.8 → etch_loop-0.5.0}/tests/test_signals.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -13,6 +13,7 @@ def run(
|
|
|
13
13
|
prompt: str,
|
|
14
14
|
verbose: bool = False,
|
|
15
15
|
tick_callback: Callable[[str], None] | None = None,
|
|
16
|
+
timeout: int = 300,
|
|
16
17
|
) -> str:
|
|
17
18
|
"""Run the Claude agent with the given prompt piped to stdin.
|
|
18
19
|
|
|
@@ -58,7 +59,7 @@ def run(
|
|
|
58
59
|
try:
|
|
59
60
|
process.stdin.write(prompt)
|
|
60
61
|
process.stdin.close()
|
|
61
|
-
except
|
|
62
|
+
except OSError as exc:
|
|
62
63
|
stdin_exc.append(exc)
|
|
63
64
|
|
|
64
65
|
stdin_writer = threading.Thread(target=write_stdin, daemon=True)
|
|
@@ -71,11 +72,8 @@ def run(
|
|
|
71
72
|
process.kill()
|
|
72
73
|
raise AgentError(f"Failed to write prompt to claude stdin: {stdin_exc[0]}") from stdin_exc[0]
|
|
73
74
|
|
|
74
|
-
if process.stdout is None:
|
|
75
|
-
process.kill()
|
|
76
|
-
raise AgentError("claude subprocess has no stdout")
|
|
77
|
-
|
|
78
75
|
output_lines: list[str] = []
|
|
76
|
+
stderr_lines: list[str] = []
|
|
79
77
|
lock = threading.Lock()
|
|
80
78
|
|
|
81
79
|
def read_stdout() -> None:
|
|
@@ -87,23 +85,28 @@ def run(
|
|
|
87
85
|
if tick_callback is not None:
|
|
88
86
|
tick_callback(line)
|
|
89
87
|
|
|
88
|
+
def read_stderr() -> None:
|
|
89
|
+
for line in process.stderr:
|
|
90
|
+
stderr_lines.append(line)
|
|
91
|
+
|
|
90
92
|
reader = threading.Thread(target=read_stdout, daemon=True)
|
|
93
|
+
stderr_reader = threading.Thread(target=read_stderr, daemon=True)
|
|
91
94
|
reader.start()
|
|
92
|
-
|
|
95
|
+
stderr_reader.start()
|
|
96
|
+
reader.join(timeout=timeout)
|
|
93
97
|
if reader.is_alive():
|
|
94
98
|
process.kill()
|
|
95
99
|
raise AgentError("claude subprocess timed out (output reader still running)")
|
|
96
100
|
|
|
101
|
+
stderr_reader.join(timeout=10)
|
|
102
|
+
|
|
97
103
|
try:
|
|
98
104
|
process.wait(timeout=10)
|
|
99
105
|
except subprocess.TimeoutExpired:
|
|
100
106
|
process.kill()
|
|
101
107
|
raise AgentError("claude subprocess timed out waiting for exit")
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
stderr_output = ""
|
|
105
|
-
if process.stderr:
|
|
106
|
-
stderr_output = process.stderr.read().strip()
|
|
109
|
+
stderr_output = "".join(stderr_lines).strip()
|
|
107
110
|
|
|
108
111
|
if process.returncode != 0:
|
|
109
112
|
detail = stderr_output or "(no stderr)"
|
|
@@ -299,10 +299,11 @@ You are a test engineer. The fixer has just made changes. Your job is to write t
|
|
|
299
299
|
1. You MAY edit test files — that is your job
|
|
300
300
|
2. Do NOT touch production code — only tests
|
|
301
301
|
3. If tests fail because of flawed test logic, fix the test and re-run before reporting
|
|
302
|
-
4.
|
|
302
|
+
4. **After tests pass, delete every test file you created during this session** — leave no temporary test files behind
|
|
303
|
+
5. When done, write your summary in this exact format — it appears directly in the terminal:
|
|
303
304
|
`<etch_summary>wrote 4 tests, all 51 passed</etch_summary>`
|
|
304
305
|
`<etch_summary>2 tests failed — TypeError in test_auth.py:38, production bug in token.py:12</etch_summary>`
|
|
305
|
-
|
|
306
|
+
6. End with EXACTLY one of these on its own line:
|
|
306
307
|
`ETCH_ALL_CLEAR` — if all tests pass
|
|
307
308
|
`ETCH_ISSUES_FOUND` — if tests reveal a bug in production code
|
|
308
309
|
"""
|
|
@@ -376,8 +376,8 @@ class InitDisplay:
|
|
|
376
376
|
|
|
377
377
|
table = Table.grid(padding=(0, 1))
|
|
378
378
|
table.add_column(width=2) # symbol
|
|
379
|
-
table.add_column(width=
|
|
380
|
-
table.add_column() #
|
|
379
|
+
table.add_column(width=20) # label
|
|
380
|
+
table.add_column() # scanbar (only used while scanning)
|
|
381
381
|
|
|
382
382
|
for sym, color, text in lines:
|
|
383
383
|
table.add_row(
|
|
@@ -50,7 +50,11 @@ def run(
|
|
|
50
50
|
return
|
|
51
51
|
|
|
52
52
|
# Runner is optional — None means the phase is skipped
|
|
53
|
-
|
|
53
|
+
try:
|
|
54
|
+
run_text = prompt.load_run(prompt_path)
|
|
55
|
+
except PromptError as exc:
|
|
56
|
+
display.print_error(str(exc))
|
|
57
|
+
return
|
|
54
58
|
|
|
55
59
|
if focus:
|
|
56
60
|
scan_text += f"\n\n## User focus\n\nConcentrate on: {focus}\n"
|
|
@@ -105,6 +109,15 @@ def run(
|
|
|
105
109
|
iteration_log.append(iter_entry)
|
|
106
110
|
break
|
|
107
111
|
|
|
112
|
+
if scanner_signal == "empty":
|
|
113
|
+
disp.finish_phase("scanner", status="no signal",
|
|
114
|
+
detail="agent produced no output token",
|
|
115
|
+
duration=scanner_duration, success=False)
|
|
116
|
+
iter_entry["scanner"] = {"status": "no signal", "detail": "agent produced no output token"}
|
|
117
|
+
stats["reason"] = "agent_error"
|
|
118
|
+
iteration_log.append(iter_entry)
|
|
119
|
+
break
|
|
120
|
+
|
|
108
121
|
disp.finish_phase("scanner", status="issues found",
|
|
109
122
|
detail=scanner_detail or "issues found",
|
|
110
123
|
duration=scanner_duration, success=False)
|
|
@@ -156,34 +169,34 @@ def run(
|
|
|
156
169
|
stats["reason"] = "no_changes"
|
|
157
170
|
iteration_log.append(iter_entry)
|
|
158
171
|
break
|
|
159
|
-
iteration_log.append(iter_entry)
|
|
160
172
|
# Fall through to breaker
|
|
161
173
|
|
|
162
174
|
# ── Commit ────────────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
if no_git or changed:
|
|
176
|
+
fixer_summary = (
|
|
177
|
+
signals.extract_summary(_fixer_output)
|
|
178
|
+
or signals.extract_commit_message(_fixer_output, fallback="")
|
|
179
|
+
)
|
|
180
|
+
commit_msg = signals.extract_commit_message(
|
|
181
|
+
_fixer_output, fallback=f"fix(edge): iteration {iteration}"
|
|
182
|
+
)
|
|
183
|
+
if not no_git and not no_commit:
|
|
184
|
+
try:
|
|
185
|
+
git.commit(commit_msg)
|
|
186
|
+
except GitError as exc:
|
|
187
|
+
disp.finish_phase("fixer", status="commit error", detail=str(exc),
|
|
188
|
+
duration=fixer_duration, success=False)
|
|
189
|
+
stats["reason"] = "git_error"
|
|
190
|
+
iteration_log.append(iter_entry)
|
|
191
|
+
break
|
|
179
192
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
disp.record_fix()
|
|
194
|
+
stats["fixes"] += 1
|
|
195
|
+
status_label = "changed" if (no_git or no_commit) else "committed"
|
|
196
|
+
fixer_detail = fixer_summary or commit_msg
|
|
197
|
+
disp.finish_phase("fixer", status=status_label, detail=fixer_detail,
|
|
198
|
+
duration=fixer_duration, success=True)
|
|
199
|
+
iter_entry["fixer"] = {"status": status_label, "detail": fixer_detail}
|
|
187
200
|
|
|
188
201
|
# ── Breaker phase ─────────────────────────────────────────────────
|
|
189
202
|
disp.start_phase("breaker")
|
|
@@ -214,6 +227,14 @@ def run(
|
|
|
214
227
|
stats["reason"] = "clear"
|
|
215
228
|
iteration_log.append(iter_entry)
|
|
216
229
|
break
|
|
230
|
+
elif signal == "empty":
|
|
231
|
+
disp.finish_phase("breaker", status="no signal",
|
|
232
|
+
detail="agent produced no output token",
|
|
233
|
+
duration=breaker_duration, success=False)
|
|
234
|
+
iter_entry["breaker"] = {"status": "no signal", "detail": "agent produced no output token"}
|
|
235
|
+
stats["reason"] = "agent_error"
|
|
236
|
+
iteration_log.append(iter_entry)
|
|
237
|
+
break
|
|
217
238
|
else:
|
|
218
239
|
disp.record_issue()
|
|
219
240
|
stats["issues"] += 1
|
|
@@ -232,7 +253,7 @@ def run(
|
|
|
232
253
|
disp.start_phase("runner")
|
|
233
254
|
runner_start = time.monotonic()
|
|
234
255
|
try:
|
|
235
|
-
runner_output = agent.run(run_text, verbose=verbose)
|
|
256
|
+
runner_output = agent.run(run_text, verbose=verbose, timeout=600)
|
|
236
257
|
runner_duration = time.monotonic() - runner_start
|
|
237
258
|
runner_signal = signals.parse(runner_output)
|
|
238
259
|
runner_detail = (
|
|
@@ -21,9 +21,10 @@ You are a test engineer. The fixer has just made changes. Your job is to write t
|
|
|
21
21
|
1. You MAY edit test files — that is your job
|
|
22
22
|
2. Do NOT touch production code — only tests
|
|
23
23
|
3. If tests fail because of flawed test logic, fix the test and re-run before reporting
|
|
24
|
-
4.
|
|
24
|
+
4. **After tests pass, delete every test file you created during this session** — leave no temporary test files behind
|
|
25
|
+
5. When done, write your summary in this exact format — it appears directly in the terminal:
|
|
25
26
|
`<etch_summary>wrote 4 tests, all 51 passed</etch_summary>`
|
|
26
27
|
`<etch_summary>2 tests failed — TypeError in test_auth.py:38, production bug in token.py:12</etch_summary>`
|
|
27
|
-
|
|
28
|
+
6. End with EXACTLY one of these on its own line:
|
|
28
29
|
`ETCH_ALL_CLEAR` — if all tests pass
|
|
29
30
|
`ETCH_ISSUES_FOUND` — if tests reveal a bug in production code
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.8"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|