etch-loop 0.5.4__tar.gz → 0.6.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.
@@ -0,0 +1 @@
1
+ etch-loop/
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: etch-loop
3
+ Version: 0.6.0
4
+ Summary: Run Claude Code in a fix-break loop until your codebase is clean
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: rich
8
+ Requires-Dist: typer
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ ```
14
+ ███████╗████████╗ ██████╗██╗ ██╗
15
+ ██╔════╝╚══██╔══╝██╔════╝██║ ██║
16
+ █████╗ ██║ ██║ ███████║
17
+ ██╔══╝ ██║ ██║ ██╔══██║
18
+ ███████╗ ██║ ╚██████╗██║ ██║
19
+ ╚══════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ loop
20
+ ```
21
+
22
+ > Run Claude Code in a scan-fix-break loop until your codebase is clean.
23
+
24
+ ---
25
+
26
+ ```
27
+ ╭───────────────────────── etch loop v0.5.5 my-project ─────────────────────────╮
28
+ │ - iteration 1 │
29
+ │ x scanner issues found 3 bugs — null deref auth.py:42, off-by-one... │
30
+ │ + fixer committed fixed 3 issues — null guard in auth.py, bounds... │
31
+ │ x breaker issues unguarded access still reachable in session.py │
32
+ │ - iteration 2 │
33
+ │ + scanner all clear no confirmed bugs found │
34
+ │ + runner all clear wrote 4 tests, all 31 passed │
35
+ ╰───────────────── iterations 2 fixes 1 breaker issues 1 3m 12s elapsed ───╯
36
+ ```
37
+
38
+ ---
39
+
40
+ ## install
41
+
42
+ ```bash
43
+ uv tool install etch-loop
44
+ ```
45
+
46
+ Or with pip:
47
+
48
+ ```bash
49
+ pip install etch-loop
50
+ ```
51
+
52
+ ## usage
53
+
54
+ ```bash
55
+ etch init # analyze codebase, write prompt files to etch-loop/
56
+ etch run # start the loop
57
+ etch run "the auth module" # focus on a specific area
58
+ etch run -n 5 # max 5 iterations
59
+ etch run --no-commit # fix without committing
60
+ etch run --no-git # disable all git operations
61
+ etch run --dry-run # preview prompt, don't run
62
+ etch run --verbose # stream full Claude output
63
+ ```
64
+
65
+ ---
66
+
67
+ ## how it works
68
+
69
+ Each iteration runs four phases: **scan → fix → break**, then once everything is clean: **run**.
70
+
71
+ ### 1. Scanner
72
+ Reads the codebase and produces a precise list of confirmed bugs — file paths, line numbers, one-line descriptions. Only genuine issues, no style notes.
73
+
74
+ ### 2. Fixer
75
+ Receives the scanner's list and fixes exactly those issues. Commits each fix with a summary message. Does not refactor or touch code unrelated to the reported bugs.
76
+
77
+ ### 3. Breaker
78
+ Adversarially reviews only the files the fixer just changed, looking for anything introduced or missed. If it finds nothing, the loop stops clean. If it finds issues, the next iteration's scanner re-checks those specific spots to confirm what's actually still broken.
79
+
80
+ ### 4. Runner *(final step)*
81
+ Runs only when the loop exits cleanly. Writes targeted tests for what was changed, runs the full test suite, then deletes the test files it created. Reports pass/fail.
82
+
83
+ ```
84
+ loop exits clean
85
+
86
+
87
+ [ runner ] → writes tests → runs suite → cleans up → ETCH_ALL_CLEAR
88
+ ```
89
+
90
+ Each agent writes a short `<etch_summary>` that appears directly in the terminal dashboard. The fixer's summary doubles as the git commit message.
91
+
92
+ ---
93
+
94
+ ## etch init
95
+
96
+ `etch init` runs Claude against your codebase, detects languages and structure, then writes four prompt files tailored to your project into an `etch-loop/` subfolder. No placeholders to fill in.
97
+
98
+ ```
99
+ ╭─ etch init v0.5.5 ────────────────────────────────╮
100
+ │ > analyzing ░░░░░▓▒ ░░░░░░░░░░░░░░░░░░░░░░░░ │
101
+ │ + analyzed codebase │
102
+ │ + etch-loop/SCAN.md │
103
+ │ + etch-loop/ETCH.md │
104
+ │ + etch-loop/BREAK.md │
105
+ │ + etch-loop/RUN.md │
106
+ ╰────────────────────────────────────────────────────╯
107
+ ```
108
+
109
+ | File | Purpose |
110
+ |---|---|
111
+ | `etch-loop/SCAN.md` | Scanner prompt — what to look for and how to report findings |
112
+ | `etch-loop/ETCH.md` | Fixer prompt — surgical fixes only, no refactoring |
113
+ | `etch-loop/BREAK.md` | Breaker prompt — adversarial review of changed files |
114
+ | `etch-loop/RUN.md` | Runner prompt — write tests, run suite, clean up |
115
+
116
+ All four files are editable. The `etch-loop/` directory is excluded from analysis so etch never reads its own files as part of your codebase.
117
+
118
+ Run reports are saved to `etch-loop/etch-reports/` after each run.
119
+
120
+ ---
121
+
122
+ ## reports
123
+
124
+ After every run, a markdown report is saved to `etch-loop/etch-reports/`:
125
+
126
+ ```
127
+ etch-loop/etch-reports/etch-report-2026-03-10-15-31.md
128
+ ```
129
+
130
+ It contains the full iteration log — what each phase found, what was fixed, and runner results.
131
+
132
+ ---
133
+
134
+ ## requirements
135
+
136
+ - Python 3.11+
137
+ - [`claude`](https://claude.ai/code) CLI installed and authenticated
138
+ - A git repository (optional — use `--no-git` to skip all git operations)
@@ -0,0 +1,126 @@
1
+ ```
2
+ ███████╗████████╗ ██████╗██╗ ██╗
3
+ ██╔════╝╚══██╔══╝██╔════╝██║ ██║
4
+ █████╗ ██║ ██║ ███████║
5
+ ██╔══╝ ██║ ██║ ██╔══██║
6
+ ███████╗ ██║ ╚██████╗██║ ██║
7
+ ╚══════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ loop
8
+ ```
9
+
10
+ > Run Claude Code in a scan-fix-break loop until your codebase is clean.
11
+
12
+ ---
13
+
14
+ ```
15
+ ╭───────────────────────── etch loop v0.5.5 my-project ─────────────────────────╮
16
+ │ - iteration 1 │
17
+ │ x scanner issues found 3 bugs — null deref auth.py:42, off-by-one... │
18
+ │ + fixer committed fixed 3 issues — null guard in auth.py, bounds... │
19
+ │ x breaker issues unguarded access still reachable in session.py │
20
+ │ - iteration 2 │
21
+ │ + scanner all clear no confirmed bugs found │
22
+ │ + runner all clear wrote 4 tests, all 31 passed │
23
+ ╰───────────────── iterations 2 fixes 1 breaker issues 1 3m 12s elapsed ───╯
24
+ ```
25
+
26
+ ---
27
+
28
+ ## install
29
+
30
+ ```bash
31
+ uv tool install etch-loop
32
+ ```
33
+
34
+ Or with pip:
35
+
36
+ ```bash
37
+ pip install etch-loop
38
+ ```
39
+
40
+ ## usage
41
+
42
+ ```bash
43
+ etch init # analyze codebase, write prompt files to etch-loop/
44
+ etch run # start the loop
45
+ etch run "the auth module" # focus on a specific area
46
+ etch run -n 5 # max 5 iterations
47
+ etch run --no-commit # fix without committing
48
+ etch run --no-git # disable all git operations
49
+ etch run --dry-run # preview prompt, don't run
50
+ etch run --verbose # stream full Claude output
51
+ ```
52
+
53
+ ---
54
+
55
+ ## how it works
56
+
57
+ Each iteration runs four phases: **scan → fix → break**, then once everything is clean: **run**.
58
+
59
+ ### 1. Scanner
60
+ Reads the codebase and produces a precise list of confirmed bugs — file paths, line numbers, one-line descriptions. Only genuine issues, no style notes.
61
+
62
+ ### 2. Fixer
63
+ Receives the scanner's list and fixes exactly those issues. Commits each fix with a summary message. Does not refactor or touch code unrelated to the reported bugs.
64
+
65
+ ### 3. Breaker
66
+ Adversarially reviews only the files the fixer just changed, looking for anything introduced or missed. If it finds nothing, the loop stops clean. If it finds issues, the next iteration's scanner re-checks those specific spots to confirm what's actually still broken.
67
+
68
+ ### 4. Runner *(final step)*
69
+ Runs only when the loop exits cleanly. Writes targeted tests for what was changed, runs the full test suite, then deletes the test files it created. Reports pass/fail.
70
+
71
+ ```
72
+ loop exits clean
73
+
74
+
75
+ [ runner ] → writes tests → runs suite → cleans up → ETCH_ALL_CLEAR
76
+ ```
77
+
78
+ Each agent writes a short `<etch_summary>` that appears directly in the terminal dashboard. The fixer's summary doubles as the git commit message.
79
+
80
+ ---
81
+
82
+ ## etch init
83
+
84
+ `etch init` runs Claude against your codebase, detects languages and structure, then writes four prompt files tailored to your project into an `etch-loop/` subfolder. No placeholders to fill in.
85
+
86
+ ```
87
+ ╭─ etch init v0.5.5 ────────────────────────────────╮
88
+ │ > analyzing ░░░░░▓▒ ░░░░░░░░░░░░░░░░░░░░░░░░ │
89
+ │ + analyzed codebase │
90
+ │ + etch-loop/SCAN.md │
91
+ │ + etch-loop/ETCH.md │
92
+ │ + etch-loop/BREAK.md │
93
+ │ + etch-loop/RUN.md │
94
+ ╰────────────────────────────────────────────────────╯
95
+ ```
96
+
97
+ | File | Purpose |
98
+ |---|---|
99
+ | `etch-loop/SCAN.md` | Scanner prompt — what to look for and how to report findings |
100
+ | `etch-loop/ETCH.md` | Fixer prompt — surgical fixes only, no refactoring |
101
+ | `etch-loop/BREAK.md` | Breaker prompt — adversarial review of changed files |
102
+ | `etch-loop/RUN.md` | Runner prompt — write tests, run suite, clean up |
103
+
104
+ All four files are editable. The `etch-loop/` directory is excluded from analysis so etch never reads its own files as part of your codebase.
105
+
106
+ Run reports are saved to `etch-loop/etch-reports/` after each run.
107
+
108
+ ---
109
+
110
+ ## reports
111
+
112
+ After every run, a markdown report is saved to `etch-loop/etch-reports/`:
113
+
114
+ ```
115
+ etch-loop/etch-reports/etch-report-2026-03-10-15-31.md
116
+ ```
117
+
118
+ It contains the full iteration log — what each phase found, what was fixed, and runner results.
119
+
120
+ ---
121
+
122
+ ## requirements
123
+
124
+ - Python 3.11+
125
+ - [`claude`](https://claude.ai/code) CLI installed and authenticated
126
+ - A git repository (optional — use `--no-git` to skip all git operations)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "etch-loop"
3
- version = "0.5.4"
3
+ version = "0.6.0"
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.6.0"
@@ -59,7 +59,7 @@ def run(
59
59
  try:
60
60
  process.stdin.write(prompt)
61
61
  process.stdin.close()
62
- except OSError as exc:
62
+ except (OSError, ValueError) as exc:
63
63
  stdin_exc.append(exc)
64
64
 
65
65
  stdin_writer = threading.Thread(target=write_stdin, daemon=True)
@@ -67,18 +67,24 @@ def run(
67
67
  stdin_writer.join(timeout=30)
68
68
  if stdin_writer.is_alive():
69
69
  process.kill()
70
- process.wait()
70
+ try:
71
+ process.wait(timeout=10)
72
+ except subprocess.TimeoutExpired:
73
+ pass
71
74
  try:
72
75
  process.stdin.close()
73
- except OSError:
76
+ except (OSError, ValueError):
74
77
  pass
75
78
  raise AgentError("Timed out writing prompt to claude stdin")
76
79
  if stdin_exc:
77
80
  process.kill()
78
- process.wait()
81
+ try:
82
+ process.wait(timeout=10)
83
+ except subprocess.TimeoutExpired:
84
+ pass
79
85
  try:
80
86
  process.stdin.close()
81
- except OSError:
87
+ except (OSError, ValueError):
82
88
  pass
83
89
  raise AgentError(f"Failed to write prompt to claude stdin: {stdin_exc[0]}") from stdin_exc[0]
84
90
 
@@ -107,7 +113,10 @@ def run(
107
113
  reader.join(timeout=timeout)
108
114
  if reader.is_alive():
109
115
  process.kill()
110
- process.wait()
116
+ try:
117
+ process.wait(timeout=5)
118
+ except subprocess.TimeoutExpired:
119
+ pass
111
120
  stderr_reader.join(timeout=10)
112
121
  if stderr_reader.is_alive():
113
122
  with lock:
@@ -118,7 +127,10 @@ def run(
118
127
  process.wait(timeout=10)
119
128
  except subprocess.TimeoutExpired:
120
129
  process.kill()
121
- process.wait()
130
+ try:
131
+ process.wait(timeout=5)
132
+ except subprocess.TimeoutExpired:
133
+ pass
122
134
  raise AgentError("claude subprocess timed out waiting for exit")
123
135
 
124
136
  stderr_reader.join(timeout=10)
@@ -50,6 +50,7 @@ _FRAMEWORK_HINTS: dict[str, str] = {
50
50
  _SKIP_DIRS = {
51
51
  ".git", "node_modules", "__pycache__", ".venv", "venv", "env",
52
52
  "dist", "build", ".next", "target", "vendor", ".cache",
53
+ "etch-loop", # etch metadata — never part of the codebase being analyzed
53
54
  }
54
55
 
55
56
 
@@ -173,7 +174,7 @@ For each issue, include the file path, line number (if known), and a one-line de
173
174
  ## Rules
174
175
 
175
176
  1. DO NOT edit any files — read only
176
- **IGNORE the `etch-loop/` directory entirely** it contains etch tool metadata, not production code
177
+ **DO NOT read any file inside `etch-loop/`**those are etch tool config files, not your codebase
177
178
  2. Only report issues you are confident are genuine bugs — not observations, not style, not "could be cleaner"
178
179
  3. If something is already handled correctly, do NOT report it — even if the handling is unusual
179
180
  4. If you are unsure whether something is a bug, leave it out
@@ -214,7 +215,7 @@ Scan the codebase for:
214
215
 
215
216
  ## Rules
216
217
 
217
- 0. **IGNORE the `etch-loop/` directory entirely** it contains etch tool metadata, not production code
218
+ 0. **DO NOT read or edit any file inside `etch-loop/`**those are etch tool config files. Editing them corrupts the tool and is never a valid fix.
218
219
  1. Fix only what you find — do not refactor, rename, or reorganize
219
220
  2. Do not add comments explaining what you fixed
220
221
  3. If you find nothing, make no changes
@@ -257,7 +258,7 @@ Be adversarial — think like someone actively trying to make this code fail.
257
258
  ## Rules
258
259
 
259
260
  1. DO NOT edit any files — read only
260
- **IGNORE the `etch-loop/` directory entirely** it contains etch tool metadata, not production code
261
+ **DO NOT read any file inside `etch-loop/`**those are etch tool config files, not your codebase
261
262
  2. Report your findings clearly, one per line
262
263
  3. Before the signal token, write your summary in this exact format — it appears directly in the terminal:
263
264
  `<etch_summary>2 issues — unguarded empty list in sorter.py:14, exception swallowed in loader.py:67</etch_summary>`
@@ -93,6 +93,12 @@ def run(
93
93
  help="Stream agent output to the terminal.",
94
94
  is_flag=True,
95
95
  ),
96
+ user: bool = typer.Option(
97
+ False,
98
+ "--user",
99
+ help="Add a user-perspective lens: scanner and breaker also look for realistic user inputs and sequences that the code doesn't handle.",
100
+ is_flag=True,
101
+ ),
96
102
  ) -> None:
97
103
  """Run the fix-break loop against the current repository.
98
104
 
@@ -111,6 +117,7 @@ def run(
111
117
  dry_run=dry_run,
112
118
  verbose=verbose,
113
119
  focus=focus,
120
+ user=user,
114
121
  )
115
122
  except KeyboardInterrupt:
116
123
  display.print_interrupted()
@@ -11,6 +11,25 @@ from etch.git import GitError
11
11
  from etch.prompt import PromptError, load_scan
12
12
 
13
13
 
14
+ _USER_PERSPECTIVE = """
15
+ ## User perspective (additional lens)
16
+
17
+ Also think about this code from the perspective of a real end user interacting with it.
18
+ What inputs, sequences, or behaviors would a realistic user trigger that the code doesn't handle?
19
+
20
+ Look for:
21
+ - Empty, whitespace-only, or missing inputs where the code assumes content
22
+ - Inputs at the edges of what the interface allows (very long strings, zero, negative numbers)
23
+ - Unexpected but valid orderings of operations (calling things out of sequence)
24
+ - Malformed or non-UTF-8 data coming from files, network, or user input
25
+ - Concurrent use (two users/processes doing the same thing simultaneously)
26
+ - Users who skip optional steps, then trigger code that assumed they ran
27
+ - Realistic typos or near-valid inputs that bypass validation
28
+
29
+ Report only cases where the code would actually fail or behave incorrectly — not hypothetical misuse.
30
+ """
31
+
32
+
14
33
  def run(
15
34
  prompt_path: str | Path,
16
35
  max_iterations: int = 20,
@@ -19,6 +38,7 @@ def run(
19
38
  dry_run: bool = False,
20
39
  verbose: bool = False,
21
40
  focus: str | None = None,
41
+ user: bool = False,
22
42
  ) -> None:
23
43
  """Run the scan-fix-break loop."""
24
44
  prompt_path = Path(prompt_path)
@@ -60,6 +80,10 @@ def run(
60
80
  scan_text += f"\n\n## User focus\n\nConcentrate on: {focus}\n"
61
81
  break_text += f"\n\n## User focus\n\nConcentrate your adversarial review on: {focus}\n"
62
82
 
83
+ if user:
84
+ scan_text += _USER_PERSPECTIVE
85
+ break_text += _USER_PERSPECTIVE
86
+
63
87
  start_time = time.monotonic()
64
88
  stats: dict = {
65
89
  "iterations": 0,
@@ -178,6 +202,17 @@ def run(
178
202
 
179
203
  # ── Commit ────────────────────────────────────────────────────────
180
204
  if no_git or changed:
205
+ _raw_summary = (
206
+ signals.extract_summary(_fixer_output)
207
+ or signals.extract_commit_message(_fixer_output, fallback="")
208
+ )
209
+ if no_git and (not _raw_summary or _raw_summary.lower().startswith("nothing")):
210
+ disp.finish_phase("fixer", status="no changes", detail="nothing to fix",
211
+ duration=fixer_duration, success=True)
212
+ iter_entry["fixer"] = {"status": "no changes", "detail": "nothing to fix"}
213
+ stats["reason"] = "stalled" if last_breaker_signal == "issues" else "no_changes"
214
+ iteration_log.append(iter_entry)
215
+ break
181
216
  fixer_summary = (
182
217
  signals.extract_summary(_fixer_output)
183
218
  or signals.extract_commit_message(_fixer_output, fallback="")
@@ -27,8 +27,8 @@ def load(path: str | Path) -> str:
27
27
 
28
28
  try:
29
29
  content = p.read_text(encoding="utf-8")
30
- except FileNotFoundError:
31
- raise PromptError(f"Prompt file not found: {p}")
30
+ except OSError as e:
31
+ raise PromptError(f"Cannot read prompt file: {p}: {e}") from e
32
32
  if not content.strip():
33
33
  raise PromptError(f"Prompt file is empty: {p}")
34
34
 
@@ -71,7 +71,7 @@ def load_break(path: str | Path | None = None) -> str:
71
71
  if candidate.exists() and candidate.is_file():
72
72
  try:
73
73
  content = candidate.read_text(encoding="utf-8")
74
- except FileNotFoundError:
74
+ except OSError:
75
75
  continue
76
76
  if not content.strip():
77
77
  raise PromptError(f"BREAK.md is empty: {candidate}")
@@ -105,7 +105,7 @@ def load_run(path: str | Path | None = None) -> str | None:
105
105
  if candidate.exists() and candidate.is_file():
106
106
  try:
107
107
  content = candidate.read_text(encoding="utf-8")
108
- except FileNotFoundError:
108
+ except OSError:
109
109
  continue
110
110
  if not content.strip():
111
111
  raise PromptError(f"RUN.md is empty: {candidate}")
@@ -137,7 +137,7 @@ def load_scan(path: str | Path | None = None) -> str:
137
137
  if candidate.exists() and candidate.is_file():
138
138
  try:
139
139
  content = candidate.read_text(encoding="utf-8")
140
- except FileNotFoundError:
140
+ except OSError:
141
141
  continue
142
142
  if not content.strip():
143
143
  raise PromptError(f"SCAN.md is empty: {candidate}")
@@ -18,7 +18,7 @@ Be adversarial — think like someone actively trying to make this code fail.
18
18
  ## Rules
19
19
 
20
20
  1. DO NOT edit any files — read only
21
- **IGNORE the `etch-loop/` directory entirely** it contains etch tool metadata, not production code
21
+ **DO NOT read any file inside `etch-loop/`**those are etch tool config files, not your codebase
22
22
  2. Report your findings clearly, one per line
23
23
  3. Before the signal token, write your summary in this exact format — it appears directly in the terminal:
24
24
  `<etch_summary>2 issues — unguarded empty list in sorter.py:14, exception swallowed in loader.py:67</etch_summary>`
@@ -14,7 +14,7 @@ Scan the codebase for:
14
14
 
15
15
  ## Rules
16
16
 
17
- 0. **IGNORE the `etch-loop/` directory entirely** it contains etch tool metadata, not production code
17
+ 0. **DO NOT read or edit any file inside `etch-loop/`**those are etch tool config files. Editing them corrupts the tool and is never a valid fix.
18
18
  1. Fix only what you find — do not refactor, rename, or reorganize
19
19
  2. Do not add comments explaining what you fixed
20
20
  3. If you find nothing, make no changes
@@ -17,7 +17,7 @@ For each issue, include the file path, line number (if known), and a one-line de
17
17
  ## Rules
18
18
 
19
19
  1. DO NOT edit any files — read only
20
- **IGNORE the `etch-loop/` directory entirely** it contains etch tool metadata, not production code
20
+ **DO NOT read any file inside `etch-loop/`**those are etch tool config files, not your codebase
21
21
  2. Only report issues you are confident are genuine bugs — not observations, not style, not "could be cleaner"
22
22
  3. If something is already handled correctly, do NOT report it — even if the handling is unusual
23
23
  4. If you are unsure whether something is a bug, leave it out
@@ -34,7 +34,7 @@ wheels = [
34
34
 
35
35
  [[package]]
36
36
  name = "etch-loop"
37
- version = "0.5.2"
37
+ version = "0.5.5"
38
38
  source = { editable = "." }
39
39
  dependencies = [
40
40
  { name = "rich" },
etch_loop-0.5.4/PKG-INFO DELETED
@@ -1,117 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: etch-loop
3
- Version: 0.5.4
4
- Summary: Run Claude Code in a fix-break loop until your codebase is clean
5
- License: MIT
6
- Requires-Python: >=3.11
7
- Requires-Dist: rich
8
- Requires-Dist: typer
9
- Provides-Extra: dev
10
- Requires-Dist: pytest; extra == 'dev'
11
- Description-Content-Type: text/markdown
12
-
13
- ```
14
- ███████╗████████╗ ██████╗██╗ ██╗
15
- ██╔════╝╚══██╔══╝██╔════╝██║ ██║
16
- █████╗ ██║ ██║ ███████║
17
- ██╔══╝ ██║ ██║ ██╔══██║
18
- ███████╗ ██║ ╚██████╗██║ ██║
19
- ╚══════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ loop
20
- ```
21
-
22
- > Run Claude Code in a scan-fix-break loop until your codebase is clean.
23
-
24
- ---
25
-
26
- ```
27
- ┌─ etch loop v0.2.0 . ───────────────────────────────────────────────┐
28
- │ │
29
- │ - iteration 1 │
30
- │ + scanner issues found src/auth.py:42 — no empty token check │
31
- │ + fixer committed fix(edge): guard empty token in auth │
32
- │ x breaker issues unguarded access still reachable │
33
- │ │
34
- │ - iteration 2 │
35
- │ + scanner issues found src/auth.py:61 — missing None check │
36
- │ + fixer committed fix(edge): null guard on session obj │
37
- │ > breaker running ░░░░░░▓▒ ░░░░░░░░░░░░░░░░░░░░░░░░░░ │
38
- │ │
39
- ├──────────────────────────────────────────────────────────────────────┤
40
- │ iterations 2 fixes 2 breaker issues 1 1m 48s elapsed │
41
- └──────────────────────────────────────────────────────────────────────┘
42
- ```
43
-
44
- ---
45
-
46
- ## install
47
-
48
- ```bash
49
- uv tool install etch-loop
50
- ```
51
-
52
- ## usage
53
-
54
- ```bash
55
- etch init # analyze codebase with Claude, write prompt files
56
- etch run # start the loop
57
- etch run "the auth module" # focus on a specific area
58
- etch run -n 5 # max 5 iterations
59
- etch run --dry-run # preview prompt, don't run
60
- etch run --verbose # show full Claude output
61
- ```
62
-
63
- ---
64
-
65
- ## how it works
66
-
67
- Each iteration has three phases: **scan → fix → break**.
68
-
69
- 1. **Scanner** reads the codebase and outputs a specific list of issues — file paths, line numbers, descriptions
70
- 2. If the scanner finds nothing, the loop stops
71
- 3. **Fixer** receives the scanner's list and fixes those exact issues, then commits
72
- 4. **Breaker** adversarially reviews the full codebase, looking for anything missed or newly introduced
73
- 5. If the breaker finds nothing, the loop stops — clean pass
74
- 6. If the breaker finds something, it's fed back to the next iteration's fixer
75
-
76
- ```
77
- ┌─ done ───────────────────────────────────────────────────┐
78
- │ │
79
- │ iterations 3 │
80
- │ fixes 3 │
81
- │ breaker issues 1 │
82
- │ elapsed 2m 44s │
83
- │ │
84
- └──────────────────────────────────────────────────────────┘
85
- ```
86
-
87
- ---
88
-
89
- ## etch init
90
-
91
- `etch init` runs Claude against your codebase before writing any files. It reads your source, detects the languages and structure, and generates three prompt files tailored to your project — no placeholders to edit.
92
-
93
- ```
94
- ┌─ etch init v0.2.0 ───────────────────────────────────────┐
95
- │ > analyzing ░░░░░▓▒ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
96
- │ + analyzed codebase │
97
- │ + SCAN.md │
98
- │ + ETCH.md │
99
- │ + BREAK.md │
100
- └──────────────────────────────────────────────────────────┘
101
- ```
102
-
103
- **`SCAN.md`** — tells the scanner what to look for and how to report findings.
104
-
105
- **`ETCH.md`** — tells the fixer how to fix things: surgical, no refactoring, one fix per commit.
106
-
107
- **`BREAK.md`** — tells the breaker to scan the full codebase adversarially and report anything that could go wrong.
108
-
109
- All three files are editable. Use `etch run "focus description"` to narrow the scope without editing files.
110
-
111
- ---
112
-
113
- ## requirements
114
-
115
- - Python 3.11+
116
- - [`claude`](https://claude.ai/code) CLI installed and authenticated
117
- - A git repository (etch-loop commits each fix automatically)
etch_loop-0.5.4/README.md DELETED
@@ -1,105 +0,0 @@
1
- ```
2
- ███████╗████████╗ ██████╗██╗ ██╗
3
- ██╔════╝╚══██╔══╝██╔════╝██║ ██║
4
- █████╗ ██║ ██║ ███████║
5
- ██╔══╝ ██║ ██║ ██╔══██║
6
- ███████╗ ██║ ╚██████╗██║ ██║
7
- ╚══════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ loop
8
- ```
9
-
10
- > Run Claude Code in a scan-fix-break loop until your codebase is clean.
11
-
12
- ---
13
-
14
- ```
15
- ┌─ etch loop v0.2.0 . ───────────────────────────────────────────────┐
16
- │ │
17
- │ - iteration 1 │
18
- │ + scanner issues found src/auth.py:42 — no empty token check │
19
- │ + fixer committed fix(edge): guard empty token in auth │
20
- │ x breaker issues unguarded access still reachable │
21
- │ │
22
- │ - iteration 2 │
23
- │ + scanner issues found src/auth.py:61 — missing None check │
24
- │ + fixer committed fix(edge): null guard on session obj │
25
- │ > breaker running ░░░░░░▓▒ ░░░░░░░░░░░░░░░░░░░░░░░░░░ │
26
- │ │
27
- ├──────────────────────────────────────────────────────────────────────┤
28
- │ iterations 2 fixes 2 breaker issues 1 1m 48s elapsed │
29
- └──────────────────────────────────────────────────────────────────────┘
30
- ```
31
-
32
- ---
33
-
34
- ## install
35
-
36
- ```bash
37
- uv tool install etch-loop
38
- ```
39
-
40
- ## usage
41
-
42
- ```bash
43
- etch init # analyze codebase with Claude, write prompt files
44
- etch run # start the loop
45
- etch run "the auth module" # focus on a specific area
46
- etch run -n 5 # max 5 iterations
47
- etch run --dry-run # preview prompt, don't run
48
- etch run --verbose # show full Claude output
49
- ```
50
-
51
- ---
52
-
53
- ## how it works
54
-
55
- Each iteration has three phases: **scan → fix → break**.
56
-
57
- 1. **Scanner** reads the codebase and outputs a specific list of issues — file paths, line numbers, descriptions
58
- 2. If the scanner finds nothing, the loop stops
59
- 3. **Fixer** receives the scanner's list and fixes those exact issues, then commits
60
- 4. **Breaker** adversarially reviews the full codebase, looking for anything missed or newly introduced
61
- 5. If the breaker finds nothing, the loop stops — clean pass
62
- 6. If the breaker finds something, it's fed back to the next iteration's fixer
63
-
64
- ```
65
- ┌─ done ───────────────────────────────────────────────────┐
66
- │ │
67
- │ iterations 3 │
68
- │ fixes 3 │
69
- │ breaker issues 1 │
70
- │ elapsed 2m 44s │
71
- │ │
72
- └──────────────────────────────────────────────────────────┘
73
- ```
74
-
75
- ---
76
-
77
- ## etch init
78
-
79
- `etch init` runs Claude against your codebase before writing any files. It reads your source, detects the languages and structure, and generates three prompt files tailored to your project — no placeholders to edit.
80
-
81
- ```
82
- ┌─ etch init v0.2.0 ───────────────────────────────────────┐
83
- │ > analyzing ░░░░░▓▒ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
84
- │ + analyzed codebase │
85
- │ + SCAN.md │
86
- │ + ETCH.md │
87
- │ + BREAK.md │
88
- └──────────────────────────────────────────────────────────┘
89
- ```
90
-
91
- **`SCAN.md`** — tells the scanner what to look for and how to report findings.
92
-
93
- **`ETCH.md`** — tells the fixer how to fix things: surgical, no refactoring, one fix per commit.
94
-
95
- **`BREAK.md`** — tells the breaker to scan the full codebase adversarially and report anything that could go wrong.
96
-
97
- All three files are editable. Use `etch run "focus description"` to narrow the scope without editing files.
98
-
99
- ---
100
-
101
- ## requirements
102
-
103
- - Python 3.11+
104
- - [`claude`](https://claude.ai/code) CLI installed and authenticated
105
- - A git repository (etch-loop commits each fix automatically)
@@ -1,41 +0,0 @@
1
- # BREAK — breaker prompt
2
-
3
- You are an adversarial code reviewer. Your job is to find anything that could go wrong.
4
-
5
- ## Your mission
6
-
7
- Scan the entire codebase with fresh eyes. Do not limit yourself to recent changes.
8
-
9
- Look for:
10
- - Edge cases and boundary conditions that are unhandled anywhere in the code
11
- - Functions that assume valid input without checking
12
- - Error paths that are silently swallowed or ignored
13
- - Race conditions, off-by-one errors, null/empty/zero not guarded
14
- - Anything that would cause unexpected behavior in production
15
-
16
- Be adversarial — think like someone actively trying to make this code fail.
17
-
18
- ## Rules
19
-
20
- 1. DO NOT edit any files — read only
21
- **IGNORE the `etch-loop/` directory entirely** — it contains etch tool metadata, not production code
22
- 2. Report your findings clearly, one per line
23
- 3. Before the signal token, write your summary in this exact format — it appears directly in the terminal:
24
- `<etch_summary>2 issues — unguarded empty list in sorter.py:14, exception swallowed in loader.py:67</etch_summary>`
25
- `<etch_summary>no issues found — code looks solid</etch_summary>`
26
- **IMPORTANT: write `<etch_summary>` ONLY in your text response — never inside any file you read or edit.**
27
- 4. End with EXACTLY one of these on its own line:
28
- `ETCH_ISSUES_FOUND`
29
- `ETCH_ALL_CLEAR`
30
-
31
- ## Scope
32
-
33
- - `loop.py`: The no_git/no_commit branch logic at line 181 (`if no_git or changed`) can reach the commit block even when `changed` is False if `no_git=True`, silently skipping the `git.has_changes()` call entirely. The `last_breaker_signal` fall-through at line 174 (fixer sees "no changes" but breaker had prior issues) proceeds to breaker without committing — the iteration state machine has several interacting flags that can produce silent no-ops.
34
-
35
- - `agent.py`: The `stderr_reader` thread is joined with a 10-second timeout but its aliveness is never checked; a hung stderr drain can silently drop error output. If `process.kill()` is called after the stdout reader times out, `process.wait()` is called but `stderr_reader` may still be running, causing a race on `stderr_lines`.
36
-
37
- - `signals.py` / `extract_commit_message`: The heuristic line-picker can return a token string (e.g. `ETCH_ISSUES_FOUND`) as a commit message if the token appears after other text rather than on its own line, since `parse()` requires exact-line match but `extract_commit_message` does not exclude token lines.
38
-
39
- - `git.py` / `has_changes`: Exit code 128 (no commits yet) falls through to `git status --porcelain`, which correctly handles new repos, but `git diff --quiet HEAD` on an empty repo also silently ignores any staged changes — newly staged files in a repo with no HEAD commit would still register as changes, so the two-step detection logic is correct but fragile.
40
-
41
- - `prompt.py`: `load_break` and `load_scan` deduplicate the cwd candidate only when `path` is already cwd — if `path.parent == cwd`, both the sibling candidate and the cwd fallback point to the same file, causing it to be read twice in the loop (harmless but surprising). More critically, `load_run` returns `None` for an empty `RUN.md` rather than raising, silently skipping the runner phase with no feedback.
@@ -1,40 +0,0 @@
1
- # ETCH — fixer prompt
2
-
3
- You are a surgical code reviewer focused on edge cases and robustness.
4
-
5
- ## Your mission
6
-
7
- Scan the codebase for:
8
- - Unhandled edge cases and boundary conditions
9
- - Missing null/None/empty checks
10
- - Unhandled exceptions and error paths
11
- - Off-by-one errors
12
- - Race conditions or unsafe concurrent access
13
- - Missing input validation at system boundaries
14
-
15
- ## Rules
16
-
17
- 0. **IGNORE the `etch-loop/` directory entirely** — it contains etch tool metadata, not production code
18
- 1. Fix only what you find — do not refactor, rename, or reorganize
19
- 2. Do not add comments explaining what you fixed
20
- 3. If you find nothing, make no changes
21
-
22
- ## Scope
23
-
24
- - `loop.py`: The no_git/no_commit branch logic at line 181 (`if no_git or changed`) can reach the commit block even when `changed` is False if `no_git=True`, silently skipping the `git.has_changes()` call entirely. The `last_breaker_signal` fall-through at line 174 (fixer sees "no changes" but breaker had prior issues) proceeds to breaker without committing — the iteration state machine has several interacting flags that can produce silent no-ops.
25
-
26
- - `agent.py`: The `stderr_reader` thread is joined with a 10-second timeout but its aliveness is never checked; a hung stderr drain can silently drop error output. If `process.kill()` is called after the stdout reader times out, `process.wait()` is called but `stderr_reader` may still be running, causing a race on `stderr_lines`.
27
-
28
- - `signals.py` / `extract_commit_message`: The heuristic line-picker can return a token string (e.g. `ETCH_ISSUES_FOUND`) as a commit message if the token appears after other text rather than on its own line, since `parse()` requires exact-line match but `extract_commit_message` does not exclude token lines.
29
-
30
- - `git.py` / `has_changes`: Exit code 128 (no commits yet) falls through to `git status --porcelain`, which correctly handles new repos, but `git diff --quiet HEAD` on an empty repo also silently ignores any staged changes — newly staged files in a repo with no HEAD commit would still register as changes, so the two-step detection logic is correct but fragile.
31
-
32
- - `prompt.py`: `load_break` and `load_scan` deduplicate the cwd candidate only when `path` is already cwd — if `path.parent == cwd`, both the sibling candidate and the cwd fallback point to the same file, causing it to be read twice in the loop (harmless but surprising). More critically, `load_run` returns `None` for an empty `RUN.md` rather than raising, silently skipping the runner phase with no feedback.
33
-
34
- ## Terminal output (required)
35
-
36
- After making changes (or deciding there is nothing to fix), write your summary in this exact format — it appears in the terminal and is used as the commit message:
37
- `<etch_summary>fixed 3 issues — null guard in auth.py, bounds check in parser.py, timeout in agent.py</etch_summary>`
38
- `<etch_summary>nothing to fix — all reported issues were already handled</etch_summary>`
39
-
40
- **IMPORTANT: write `<etch_summary>` ONLY in your text response — never inside any file you edit or create.**
@@ -1,36 +0,0 @@
1
- # RUN — test writer and build validator
2
-
3
- You are a test engineer. The fixer has just made changes. Your job is to write tests for what was changed, then run the full suite to confirm everything works.
4
-
5
- ## Your mission
6
-
7
- 1. **Write or update tests** — look at what the fixer changed and write targeted tests covering:
8
- - The specific edge cases that were fixed
9
- - Boundary conditions around the changed code
10
- - Any regression paths that could break silently
11
- Write tests in the project's existing test style and location.
12
-
13
- 2. **Run the test suite** — run the full build and test commands to confirm everything passes.
14
-
15
- ## Build and test commands
16
-
17
- - `python -m pytest`
18
-
19
- ## Rules
20
-
21
- 0. **IGNORE the `etch-loop/` directory entirely** — it contains etch tool metadata, not production code
22
- 1. You MAY edit test files — that is your job
23
- 2. Do NOT touch production code — only tests
24
- 3. If tests fail because of flawed test logic, fix the test and re-run before reporting
25
- 4. **After tests pass, clean up everything you created during this session:**
26
- - Delete every test file you wrote
27
- - Delete any `__pycache__` directories inside the test directory
28
- - If you created the test directory itself, remove it entirely (e.g. `rm -rf tests/`)
29
- - Leave no temporary files or empty directories behind
30
- 5. When done, write your summary in this exact format — it appears directly in the terminal:
31
- `<etch_summary>wrote 4 tests, all 51 passed</etch_summary>`
32
- `<etch_summary>2 tests failed — TypeError in test_auth.py:38, production bug in token.py:12</etch_summary>`
33
- **IMPORTANT: write `<etch_summary>` ONLY in your text response — never inside any file you edit or create.**
34
- 6. End with EXACTLY one of these on its own line:
35
- `ETCH_ALL_CLEAR` — if all tests pass
36
- `ETCH_ISSUES_FOUND` — if tests reveal a bug in production code
@@ -1,45 +0,0 @@
1
- # SCAN — scanner prompt
2
-
3
- You are a code analyst. Your job is to find genuine bugs before the fixer runs.
4
-
5
- ## Your mission
6
-
7
- Read the codebase and produce a precise, actionable list of real issues:
8
- - Unhandled edge cases and boundary conditions
9
- - Missing null/None/empty checks that will cause crashes or wrong results
10
- - Unhandled exceptions and error paths
11
- - Off-by-one errors
12
- - Race conditions or unsafe concurrent access
13
- - Missing input validation at system boundaries
14
-
15
- For each issue, include the file path, line number (if known), and a one-line description of what will go wrong.
16
-
17
- ## Rules
18
-
19
- 1. DO NOT edit any files — read only
20
- **IGNORE the `etch-loop/` directory entirely** — it contains etch tool metadata, not production code
21
- 2. Only report issues you are confident are genuine bugs — not observations, not style, not "could be cleaner"
22
- 3. If something is already handled correctly, do NOT report it — even if the handling is unusual
23
- 4. If you are unsure whether something is a bug, leave it out
24
- 5. List each confirmed issue on its own line, e.g.:
25
- - src/auth.py:42 — crashes with empty token string (no guard)
26
- - src/api.js:108 — unhandled promise rejection will silently fail
27
- 6. Before the signal token, write your summary in this exact format — it appears directly in the terminal:
28
- `<etch_summary>3 bugs found — null deref in auth.py:42, off-by-one in parser.py:88</etch_summary>`
29
- `<etch_summary>no confirmed bugs found</etch_summary>`
30
- **IMPORTANT: write `<etch_summary>` ONLY in your text response — never inside any file you read or edit.**
31
- 7. End with EXACTLY one of these on its own line:
32
- `ETCH_ISSUES_FOUND`
33
- `ETCH_ALL_CLEAR`
34
-
35
- ## Scope
36
-
37
- - `loop.py`: The no_git/no_commit branch logic at line 181 (`if no_git or changed`) can reach the commit block even when `changed` is False if `no_git=True`, silently skipping the `git.has_changes()` call entirely. The `last_breaker_signal` fall-through at line 174 (fixer sees "no changes" but breaker had prior issues) proceeds to breaker without committing — the iteration state machine has several interacting flags that can produce silent no-ops.
38
-
39
- - `agent.py`: The `stderr_reader` thread is joined with a 10-second timeout but its aliveness is never checked; a hung stderr drain can silently drop error output. If `process.kill()` is called after the stdout reader times out, `process.wait()` is called but `stderr_reader` may still be running, causing a race on `stderr_lines`.
40
-
41
- - `signals.py` / `extract_commit_message`: The heuristic line-picker can return a token string (e.g. `ETCH_ISSUES_FOUND`) as a commit message if the token appears after other text rather than on its own line, since `parse()` requires exact-line match but `extract_commit_message` does not exclude token lines.
42
-
43
- - `git.py` / `has_changes`: Exit code 128 (no commits yet) falls through to `git status --porcelain`, which correctly handles new repos, but `git diff --quiet HEAD` on an empty repo also silently ignores any staged changes — newly staged files in a repo with no HEAD commit would still register as changes, so the two-step detection logic is correct but fragile.
44
-
45
- - `prompt.py`: `load_break` and `load_scan` deduplicate the cwd candidate only when `path` is already cwd — if `path.parent == cwd`, both the sibling candidate and the cwd fallback point to the same file, causing it to be read twice in the loop (harmless but surprising). More critically, `load_run` returns `None` for an empty `RUN.md` rather than raising, silently skipping the runner phase with no feedback.
@@ -1 +0,0 @@
1
- __version__ = "0.5.4"
File without changes
File without changes
File without changes
File without changes