etch-loop 0.4.1__tar.gz → 0.4.3__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.
Files changed (24) hide show
  1. {etch_loop-0.4.1 → etch_loop-0.4.3}/PKG-INFO +1 -1
  2. {etch_loop-0.4.1 → etch_loop-0.4.3}/pyproject.toml +1 -1
  3. etch_loop-0.4.3/src/etch/__init__.py +1 -0
  4. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/display.py +2 -0
  5. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/loop.py +44 -93
  6. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/report.py +1 -0
  7. etch_loop-0.4.1/src/etch/__init__.py +0 -1
  8. {etch_loop-0.4.1 → etch_loop-0.4.3}/.github/workflows/workflow.yml +0 -0
  9. {etch_loop-0.4.1 → etch_loop-0.4.3}/README.md +0 -0
  10. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/agent.py +0 -0
  11. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/analyze.py +0 -0
  12. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/cli.py +0 -0
  13. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/git.py +0 -0
  14. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/prompt.py +0 -0
  15. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/signals.py +0 -0
  16. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/templates/BREAK.md +0 -0
  17. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/templates/ETCH.md +0 -0
  18. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/templates/RUN.md +0 -0
  19. {etch_loop-0.4.1 → etch_loop-0.4.3}/src/etch/templates/SCAN.md +0 -0
  20. {etch_loop-0.4.1 → etch_loop-0.4.3}/tests/__init__.py +0 -0
  21. {etch_loop-0.4.1 → etch_loop-0.4.3}/tests/test_git.py +0 -0
  22. {etch_loop-0.4.1 → etch_loop-0.4.3}/tests/test_loop.py +0 -0
  23. {etch_loop-0.4.1 → etch_loop-0.4.3}/tests/test_prompt.py +0 -0
  24. {etch_loop-0.4.1 → etch_loop-0.4.3}/tests/test_signals.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etch-loop
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: Run Claude Code in a fix-break loop until your codebase is clean
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "etch-loop"
3
- version = "0.4.1"
3
+ version = "0.4.3"
4
4
  requires-python = ">=3.11"
5
5
  description = "Run Claude Code in a fix-break loop until your codebase is clean"
6
6
  readme = "README.md"
@@ -0,0 +1 @@
1
+ __version__ = "0.4.3"
@@ -216,6 +216,8 @@ class EtchDisplay:
216
216
  title = f"[{AMBER}]- stopped (max iterations)[/{AMBER}]"
217
217
  elif reason == "no_changes":
218
218
  title = f"[{GREEN}]+ clean — fixer found nothing[/{GREEN}]"
219
+ elif reason == "build_failed":
220
+ title = f"[{RED}]x build failed[/{RED}]"
219
221
  else:
220
222
  title = f"[{FG}]done[/{FG}]"
221
223
 
@@ -66,58 +66,11 @@ def run(
66
66
  }
67
67
  last_breaker_signal: str | None = None
68
68
  last_breaker_output: str | None = None
69
- last_runner_output: str | None = None
70
69
  iteration_log: list[dict] = []
70
+ final_runner_entry: dict = {}
71
71
 
72
72
  with display.EtchDisplay(target=str(prompt_path.parent)) as disp:
73
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
74
  # ── Main loop ─────────────────────────────────────────────────────────
122
75
  for iteration in range(1, max_iterations + 1):
123
76
  stats["iterations"] = iteration
@@ -148,19 +101,9 @@ def run(
148
101
  detail=scanner_detail or "nothing to fix",
149
102
  duration=scanner_duration, success=True)
150
103
  iter_entry["scanner"] = {"status": "all clear", "detail": scanner_detail}
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
104
+ stats["reason"] = "no_changes"
105
+ iteration_log.append(iter_entry)
106
+ break
164
107
 
165
108
  disp.finish_phase("scanner", status="issues found",
166
109
  detail=scanner_detail or "issues found",
@@ -179,12 +122,6 @@ def run(
179
122
  f"{last_breaker_output.strip()}\n\n"
180
123
  f"Also address these if not already covered above.\n"
181
124
  )
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
- )
188
125
 
189
126
  # ── Fixer phase ───────────────────────────────────────────────────
190
127
  disp.start_phase("fixer")
@@ -216,19 +153,9 @@ def run(
216
153
  duration=fixer_duration, success=True)
217
154
  iter_entry["fixer"] = {"status": "no changes", "detail": "nothing to fix"}
218
155
  if last_breaker_signal != "issues":
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
156
+ stats["reason"] = "no_changes"
157
+ iteration_log.append(iter_entry)
158
+ break
232
159
  iteration_log.append(iter_entry)
233
160
  # Fall through to breaker
234
161
 
@@ -284,19 +211,9 @@ def run(
284
211
  detail=breaker_detail or "no issues found",
285
212
  duration=breaker_duration, success=True)
286
213
  iter_entry["breaker"] = {"status": "all clear", "detail": breaker_detail}
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
214
+ stats["reason"] = "clear"
215
+ iteration_log.append(iter_entry)
216
+ break
300
217
  else:
301
218
  disp.record_issue()
302
219
  stats["issues"] += 1
@@ -310,6 +227,38 @@ def run(
310
227
  else:
311
228
  stats["reason"] = "max_iterations"
312
229
 
230
+ # ── Runner — final step, only when loop ended cleanly ─────────────────
231
+ if run_text and stats["reason"] in ("clear", "no_changes"):
232
+ disp.start_phase("runner")
233
+ runner_start = time.monotonic()
234
+ try:
235
+ runner_output = agent.run(run_text, verbose=verbose)
236
+ runner_duration = time.monotonic() - runner_start
237
+ runner_signal = signals.parse(runner_output)
238
+ runner_detail = (
239
+ signals.extract_summary(runner_output)
240
+ or signals.extract_finding(runner_output)
241
+ )
242
+ if runner_signal == "clear":
243
+ disp.finish_phase("runner", status="all clear",
244
+ detail=runner_detail or "build passed",
245
+ duration=runner_duration, success=True)
246
+ final_runner_entry = {"status": "all clear", "detail": runner_detail}
247
+ else:
248
+ disp.record_issue()
249
+ stats["issues"] += 1
250
+ disp.finish_phase("runner", status="build failed",
251
+ detail=runner_detail or "build failed",
252
+ duration=runner_duration, success=False)
253
+ final_runner_entry = {"status": "build failed", "detail": runner_detail}
254
+ stats["reason"] = "build_failed"
255
+ except AgentError as exc:
256
+ disp.finish_phase("runner", status="error",
257
+ detail=str(exc),
258
+ duration=time.monotonic() - runner_start, success=False)
259
+ final_runner_entry = {"status": "error", "detail": str(exc)}
260
+ stats["reason"] = "agent_error"
261
+
313
262
  stats["elapsed"] = time.monotonic() - start_time
314
263
 
315
264
  # Live panel is fully closed before printing anything below
@@ -317,6 +266,8 @@ def run(
317
266
 
318
267
  # ── Write report ──────────────────────────────────────────────────────────
319
268
  try:
269
+ if final_runner_entry:
270
+ iteration_log.append({"n": "runner", "runner": final_runner_entry})
320
271
  report_path = report.write(stats, iteration_log, output_dir=prompt_path.parent)
321
272
  if not no_git and not no_commit and stats["fixes"] > 0:
322
273
  try:
@@ -87,6 +87,7 @@ def _reason_label(reason: str) -> str:
87
87
  "interrupted": "interrupted",
88
88
  "agent_error": "stopped — agent error",
89
89
  "git_error": "stopped — git error",
90
+ "build_failed": "stopped — build/tests failed",
90
91
  }.get(reason, reason)
91
92
 
92
93
 
@@ -1 +0,0 @@
1
- __version__ = "0.4.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