etch-loop 0.1.0__tar.gz → 0.2.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,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: etch-loop
3
+ Version: 0.2.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.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)
@@ -0,0 +1,105 @@
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "etch-loop"
3
- version = "0.1.0"
3
+ version = "0.2.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.2.0"
@@ -0,0 +1,306 @@
1
+ """Codebase analysis for generating tailored prompt files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from collections import Counter
7
+ from pathlib import Path
8
+
9
+
10
+ _LANG_MAP: dict[str, str] = {
11
+ ".py": "Python",
12
+ ".ts": "TypeScript",
13
+ ".tsx": "TypeScript",
14
+ ".js": "JavaScript",
15
+ ".jsx": "JavaScript",
16
+ ".go": "Go",
17
+ ".rs": "Rust",
18
+ ".rb": "Ruby",
19
+ ".java": "Java",
20
+ ".kt": "Kotlin",
21
+ ".swift": "Swift",
22
+ ".cpp": "C++",
23
+ ".c": "C",
24
+ ".cs": "C#",
25
+ ".php": "PHP",
26
+ ".ex": "Elixir",
27
+ ".exs": "Elixir",
28
+ ".hs": "Haskell",
29
+ ".scala": "Scala",
30
+ ".clj": "Clojure",
31
+ ".lua": "Lua",
32
+ ".sh": "Shell",
33
+ ".bash": "Shell",
34
+ }
35
+
36
+ _FRAMEWORK_HINTS: dict[str, str] = {
37
+ "pyproject.toml": "Python project",
38
+ "setup.py": "Python project",
39
+ "package.json": "Node.js project",
40
+ "go.mod": "Go module",
41
+ "Cargo.toml": "Rust crate",
42
+ "Gemfile": "Ruby project",
43
+ "pom.xml": "Java/Maven project",
44
+ "build.gradle": "Java/Gradle project",
45
+ "mix.exs": "Elixir/Mix project",
46
+ "composer.json": "PHP project",
47
+ }
48
+
49
+ _SKIP_DIRS = {
50
+ ".git", "node_modules", "__pycache__", ".venv", "venv", "env",
51
+ "dist", "build", ".next", "target", "vendor", ".cache",
52
+ }
53
+
54
+
55
+ def analyze(root: Path | None = None) -> dict:
56
+ """Analyze a codebase and return structured info.
57
+
58
+ Returns a dict with keys:
59
+ languages: list of (language, file_count) sorted by count
60
+ source_dirs: list of top-level source directories
61
+ entry_points: list of likely entry point files
62
+ framework: detected framework/project type string or None
63
+ total_files: int
64
+ is_git: bool
65
+ """
66
+ root = root or Path.cwd()
67
+
68
+ files = _list_files(root)
69
+ total = len(files)
70
+
71
+ # Language detection
72
+ ext_counts: Counter[str] = Counter()
73
+ for f in files:
74
+ ext = Path(f).suffix.lower()
75
+ if ext in _LANG_MAP:
76
+ ext_counts[ext] += 1
77
+
78
+ lang_counts: Counter[str] = Counter()
79
+ for ext, count in ext_counts.items():
80
+ lang_counts[_LANG_MAP[ext]] += count
81
+
82
+ languages = lang_counts.most_common(3)
83
+
84
+ # Source directories (top-level dirs that contain source files)
85
+ top_dirs: Counter[str] = Counter()
86
+ for f in files:
87
+ parts = Path(f).parts
88
+ if len(parts) > 1 and parts[0] not in _SKIP_DIRS:
89
+ top_dirs[parts[0]] += 1
90
+
91
+ source_dirs = [d for d, _ in top_dirs.most_common(5) if not d.startswith(".")]
92
+
93
+ # Entry points
94
+ entry_points = _find_entry_points(root, files)
95
+
96
+ # Framework detection
97
+ framework = None
98
+ for marker, label in _FRAMEWORK_HINTS.items():
99
+ if (root / marker).exists():
100
+ framework = label
101
+ break
102
+
103
+ return {
104
+ "languages": languages,
105
+ "source_dirs": source_dirs,
106
+ "entry_points": entry_points,
107
+ "framework": framework,
108
+ "total_files": total,
109
+ "is_git": (root / ".git").exists(),
110
+ }
111
+
112
+
113
+ def build_init_prompt(info: dict) -> str:
114
+ """Build the Claude prompt used during etch init to analyze the codebase."""
115
+ file_tree = "\n".join(f" {f}" for f in _list_files(Path.cwd())[:60])
116
+ if not file_tree:
117
+ file_tree = " (no tracked files)"
118
+
119
+ lang_summary = ", ".join(f"{lang} ({n})" for lang, n in info["languages"]) or "unknown"
120
+ framework = info["framework"] or "unknown"
121
+
122
+ return f"""You are analyzing a codebase to configure an automated edge-case hunting tool.
123
+
124
+ The tool will run Claude Code in a fix-break loop to find and patch edge cases.
125
+ Your job is to write a focused scope description that tells the fixer exactly where to look.
126
+
127
+ ## Codebase stats
128
+ - Framework: {framework}
129
+ - Languages: {lang_summary}
130
+ - Total files: {info["total_files"]}
131
+
132
+ ## File tree
133
+ {file_tree}
134
+
135
+ ## Instructions
136
+
137
+ Read the key source files in this codebase. Then write a concise scope description covering:
138
+ - The highest-risk areas for edge cases in THIS specific codebase
139
+ - Specific files or modules worth focusing on
140
+ - Any patterns you spotted that suggest missing error handling
141
+
142
+ Rules:
143
+ - Output ONLY the scope description as plain prose or bullet points
144
+ - No markdown headers, no preamble, no "here is the scope" intro
145
+ - Be specific to this codebase — not generic advice
146
+ - Keep it under 150 words
147
+ """
148
+
149
+
150
+ def build_scan_md(info: dict, agent_scope: str | None = None) -> str:
151
+ """Generate a tailored SCAN.md from analysis results."""
152
+ scope = agent_scope.strip() if agent_scope else _format_scope(info)
153
+
154
+ return f"""# SCAN — scanner prompt
155
+
156
+ You are a code analyst. Your job is to find edge cases before the fixer runs.
157
+
158
+ ## Your mission
159
+
160
+ Read the codebase and produce a precise, actionable list of issues:
161
+ - Unhandled edge cases and boundary conditions
162
+ - Missing null/None/empty checks
163
+ - Unhandled exceptions and error paths
164
+ - Off-by-one errors
165
+ - Race conditions or unsafe concurrent access
166
+ - Missing input validation at system boundaries
167
+
168
+ For each issue, include the file path, line number (if known), and a one-line description.
169
+
170
+ ## Rules
171
+
172
+ 1. DO NOT edit any files — read only
173
+ 2. List each issue on its own line, e.g.:
174
+ - src/auth.py:42 — no check for empty token string
175
+ - src/api.js:108 — unhandled promise rejection in fetchUser()
176
+ 3. Be specific — vague findings are not useful
177
+ 4. End your output with EXACTLY one of these tokens on its own line:
178
+ - `ETCH_ISSUES_FOUND` — if you found issues worth fixing
179
+ - `ETCH_ALL_CLEAR` — if the code looks solid
180
+
181
+ ## Scope
182
+
183
+ {scope}
184
+ """
185
+
186
+
187
+ def build_etch_md(info: dict, agent_scope: str | None = None) -> str:
188
+ """Generate a tailored ETCH.md from analysis results."""
189
+ scope = agent_scope.strip() if agent_scope else _format_scope(info)
190
+
191
+ return f"""# ETCH — fixer prompt
192
+
193
+ You are a surgical code reviewer focused on edge cases and robustness.
194
+
195
+ ## Your mission
196
+
197
+ Scan the codebase for:
198
+ - Unhandled edge cases and boundary conditions
199
+ - Missing null/None/empty checks
200
+ - Unhandled exceptions and error paths
201
+ - Off-by-one errors
202
+ - Race conditions or unsafe concurrent access
203
+ - Missing input validation at system boundaries
204
+
205
+ ## Rules
206
+
207
+ 1. Fix only what you find — do not refactor, rename, or reorganize
208
+ 2. One logical fix per commit (the harness will commit for you)
209
+ 3. Do not add comments explaining what you fixed
210
+ 4. If you find nothing, make no changes
211
+
212
+ ## Scope
213
+
214
+ {scope}
215
+
216
+ ## Commit format
217
+
218
+ The harness commits automatically. Each commit will be:
219
+ fix(edge): <short description of what was fixed>
220
+ """
221
+
222
+
223
+ def build_break_md(info: dict, agent_scope: str | None = None) -> str:
224
+ """Generate a tailored BREAK.md from analysis results."""
225
+ scope = agent_scope.strip() if agent_scope else _format_scope(info)
226
+
227
+ return f"""# BREAK — breaker prompt
228
+
229
+ You are an adversarial code reviewer. Your job is to find anything that could go wrong.
230
+
231
+ ## Your mission
232
+
233
+ Scan the entire codebase with fresh eyes. Do not limit yourself to recent changes.
234
+
235
+ Look for:
236
+ - Edge cases and boundary conditions that are unhandled anywhere in the code
237
+ - Functions that assume valid input without checking
238
+ - Error paths that are silently swallowed or ignored
239
+ - Race conditions, off-by-one errors, null/empty/zero not guarded
240
+ - Anything that would cause unexpected behavior in production
241
+
242
+ Be adversarial — think like someone actively trying to make this code fail.
243
+
244
+ ## Rules
245
+
246
+ 1. DO NOT edit any files — read only
247
+ 2. Report your findings clearly, one per line
248
+ 3. End your output with EXACTLY one of these tokens on its own line:
249
+ - `ETCH_ISSUES_FOUND` — if you found anything worth fixing
250
+ - `ETCH_ALL_CLEAR` — if the code looks solid
251
+
252
+ ## Scope
253
+
254
+ {scope}
255
+ """
256
+
257
+
258
+ def _format_scope(info: dict) -> str:
259
+ lines = []
260
+ if info["source_dirs"]:
261
+ dirs = " ".join(f"{d}/" for d in info["source_dirs"])
262
+ lines.append(f"Directories: {dirs}")
263
+ if info["entry_points"]:
264
+ eps = " ".join(info["entry_points"][:3])
265
+ lines.append(f"Entry points: {eps}")
266
+ if info["framework"]:
267
+ lines.append(f"Project type: {info['framework']}")
268
+ lines.append(f"Total tracked files: {info['total_files']}")
269
+ return "\n".join(lines) if lines else "Entire repository"
270
+
271
+
272
+ def _list_files(root: Path) -> list[str]:
273
+ """Return tracked files via git ls-files, falling back to filesystem walk."""
274
+ try:
275
+ result = subprocess.run(
276
+ ["git", "ls-files"],
277
+ cwd=root,
278
+ capture_output=True,
279
+ text=True,
280
+ timeout=10,
281
+ )
282
+ if result.returncode == 0 and result.stdout.strip():
283
+ return result.stdout.strip().splitlines()
284
+ except (subprocess.TimeoutExpired, FileNotFoundError):
285
+ pass
286
+
287
+ # Fallback: walk filesystem
288
+ files = []
289
+ for p in root.rglob("*"):
290
+ if p.is_file() and not any(part in _SKIP_DIRS for part in p.parts):
291
+ try:
292
+ files.append(str(p.relative_to(root)))
293
+ except ValueError:
294
+ pass
295
+ return files
296
+
297
+
298
+ def _find_entry_points(root: Path, files: list[str]) -> list[str]:
299
+ """Heuristically identify entry point files."""
300
+ candidates = []
301
+ names = {"main.py", "app.py", "index.ts", "index.js", "main.go",
302
+ "main.rs", "server.py", "server.ts", "cli.py", "manage.py"}
303
+ for f in files:
304
+ if Path(f).name in names:
305
+ candidates.append(f)
306
+ return candidates[:4]
@@ -0,0 +1,107 @@
1
+ """CLI entry points for etch-loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from etch import agent, analyze, display, loop
10
+ from etch.agent import AgentError
11
+
12
+ app = typer.Typer(
13
+ name="etch",
14
+ help="Run Claude Code in a fix-break loop, hunting for edge cases.",
15
+ add_completion=False,
16
+ pretty_exceptions_show_locals=False,
17
+ )
18
+
19
+
20
+ @app.command()
21
+ def init() -> None:
22
+ """Analyze the codebase with Claude and write tailored SCAN.md, ETCH.md, BREAK.md."""
23
+ root = Path.cwd()
24
+ info = analyze.analyze(root)
25
+ init_prompt = analyze.build_init_prompt(info)
26
+
27
+ agent_scope: str | None = None
28
+ with display.InitDisplay() as disp:
29
+ disp.start_scan()
30
+ try:
31
+ agent_scope = agent.run(init_prompt)
32
+ disp.finish_scan(success=True)
33
+ except AgentError as exc:
34
+ disp.finish_scan(success=False)
35
+ disp.add_line(display.SYM_NEUTRAL, display.DIM, f"falling back to static analysis ({exc})")
36
+
37
+ for dest, content, label in [
38
+ (root / "SCAN.md", analyze.build_scan_md(info, agent_scope), "SCAN.md"),
39
+ (root / "ETCH.md", analyze.build_etch_md(info, agent_scope), "ETCH.md"),
40
+ (root / "BREAK.md", analyze.build_break_md(info, agent_scope), "BREAK.md"),
41
+ ]:
42
+ if dest.exists():
43
+ disp.add_line(display.SYM_NEUTRAL, display.DIM, f"{label} already exists, skipping")
44
+ else:
45
+ dest.write_text(content, encoding="utf-8")
46
+ disp.add_line(display.SYM_OK, display.GREEN, label)
47
+
48
+
49
+ @app.command()
50
+ def run(
51
+ focus: str = typer.Argument(
52
+ default=None,
53
+ help="Optional focus description, e.g. 'the auth module' or 'error handling in payments'.",
54
+ ),
55
+ prompt: str = typer.Option(
56
+ "./ETCH.md",
57
+ "--prompt",
58
+ help="Path to the fixer prompt file (ETCH.md).",
59
+ show_default=True,
60
+ ),
61
+ max_iterations: int = typer.Option(
62
+ 20,
63
+ "--max-iterations",
64
+ "-n",
65
+ help="Maximum number of fix-break cycles.",
66
+ show_default=True,
67
+ min=1,
68
+ ),
69
+ no_commit: bool = typer.Option(
70
+ False,
71
+ "--no-commit",
72
+ help="Skip git commits after fixer runs.",
73
+ is_flag=True,
74
+ ),
75
+ dry_run: bool = typer.Option(
76
+ False,
77
+ "--dry-run",
78
+ help="Print the fixer prompt and exit without running.",
79
+ is_flag=True,
80
+ ),
81
+ verbose: bool = typer.Option(
82
+ False,
83
+ "--verbose",
84
+ help="Stream agent output to the terminal.",
85
+ is_flag=True,
86
+ ),
87
+ ) -> None:
88
+ """Run the fix-break loop against the current repository.
89
+
90
+ Optionally pass a focus description to narrow the scan:
91
+
92
+ etch run "the authentication module"
93
+
94
+ etch run "error handling in the payments flow"
95
+ """
96
+ try:
97
+ loop.run(
98
+ prompt_path=prompt,
99
+ max_iterations=max_iterations,
100
+ no_commit=no_commit,
101
+ dry_run=dry_run,
102
+ verbose=verbose,
103
+ focus=focus,
104
+ )
105
+ except KeyboardInterrupt:
106
+ display.print_interrupted()
107
+ raise typer.Exit(code=130)