emdash-cli 0.1.35__py3-none-any.whl → 0.1.46__py3-none-any.whl

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 (30) hide show
  1. emdash_cli/client.py +35 -0
  2. emdash_cli/clipboard.py +30 -61
  3. emdash_cli/commands/agent/__init__.py +14 -0
  4. emdash_cli/commands/agent/cli.py +100 -0
  5. emdash_cli/commands/agent/constants.py +53 -0
  6. emdash_cli/commands/agent/file_utils.py +178 -0
  7. emdash_cli/commands/agent/handlers/__init__.py +41 -0
  8. emdash_cli/commands/agent/handlers/agents.py +421 -0
  9. emdash_cli/commands/agent/handlers/auth.py +69 -0
  10. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  11. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  12. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  13. emdash_cli/commands/agent/handlers/misc.py +200 -0
  14. emdash_cli/commands/agent/handlers/rules.py +394 -0
  15. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  16. emdash_cli/commands/agent/handlers/setup.py +582 -0
  17. emdash_cli/commands/agent/handlers/skills.py +440 -0
  18. emdash_cli/commands/agent/handlers/todos.py +98 -0
  19. emdash_cli/commands/agent/handlers/verify.py +648 -0
  20. emdash_cli/commands/agent/interactive.py +657 -0
  21. emdash_cli/commands/agent/menus.py +728 -0
  22. emdash_cli/commands/agent.py +7 -1321
  23. emdash_cli/commands/server.py +99 -40
  24. emdash_cli/server_manager.py +70 -10
  25. emdash_cli/sse_renderer.py +36 -5
  26. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
  27. emdash_cli-0.1.46.dist-info/RECORD +49 -0
  28. emdash_cli-0.1.35.dist-info/RECORD +0 -30
  29. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
  30. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,648 @@
1
+ """Handler for /verify command - run verification checks."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from emdash_core.agent.verifier import VerifierManager, VerificationReport
11
+
12
+ console = Console()
13
+
14
+
15
+ def get_git_diff() -> str:
16
+ """Get git diff of staged and unstaged changes."""
17
+ try:
18
+ result = subprocess.run(
19
+ ["git", "diff", "HEAD"],
20
+ capture_output=True,
21
+ text=True,
22
+ cwd=Path.cwd(),
23
+ )
24
+ return result.stdout
25
+ except Exception:
26
+ return ""
27
+
28
+
29
+ def get_changed_files() -> list[str]:
30
+ """Get list of changed files."""
31
+ try:
32
+ result = subprocess.run(
33
+ ["git", "diff", "--name-only", "HEAD"],
34
+ capture_output=True,
35
+ text=True,
36
+ cwd=Path.cwd(),
37
+ )
38
+ return [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
39
+ except Exception:
40
+ return []
41
+
42
+
43
+ def display_report(report: VerificationReport) -> None:
44
+ """Display verification report."""
45
+ # Create results table
46
+ table = Table(show_header=True, header_style="bold")
47
+ table.add_column("Status", width=6)
48
+ table.add_column("Verifier", style="cyan")
49
+ table.add_column("Duration", justify="right")
50
+ table.add_column("Details")
51
+
52
+ for result in report.results:
53
+ status = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]"
54
+ duration = f"{result.duration:.1f}s"
55
+
56
+ # Build details
57
+ if result.passed:
58
+ details = "[dim]OK[/dim]"
59
+ elif result.issues:
60
+ details = result.issues[0][:50]
61
+ if len(result.issues) > 1:
62
+ details += f" (+{len(result.issues) - 1} more)"
63
+ else:
64
+ details = result.output[:50] if result.output else "Failed"
65
+
66
+ table.add_row(status, result.name, duration, details)
67
+
68
+ console.print(table)
69
+ console.print()
70
+
71
+ # Show summary
72
+ if report.all_passed:
73
+ console.print(f"[bold green]✓ {report.summary}[/bold green]")
74
+ else:
75
+ console.print(f"[bold red]✗ {report.summary}[/bold red]")
76
+
77
+ # Show detailed failures
78
+ for result in report.get_failures():
79
+ console.print()
80
+ console.print(Panel(
81
+ result.output[:1000] if result.output else "No output",
82
+ title=f"[red]{result.name}[/red]",
83
+ border_style="red",
84
+ ))
85
+
86
+
87
+ def build_retry_prompt(original_task: str, report: VerificationReport) -> str:
88
+ """Build a retry prompt that includes failure information."""
89
+ failures = report.get_failures()
90
+
91
+ failure_text = "\n".join([
92
+ f"- **{r.name}**: {', '.join(r.issues[:3]) if r.issues else r.output[:100]}"
93
+ for r in failures
94
+ ])
95
+
96
+ return f"""Continue working on this task. Previous attempt had verification failures:
97
+
98
+ {failure_text}
99
+
100
+ Original task: {original_task}
101
+
102
+ Fix the issues and complete the task."""
103
+
104
+
105
+ def prompt_retry_menu() -> str:
106
+ """Prompt user for action after failed verification.
107
+
108
+ Returns:
109
+ One of: 'retry', 'approve', 'stop'
110
+ """
111
+ from prompt_toolkit import PromptSession
112
+
113
+ console.print()
114
+ console.print("[bold]What would you like to do?[/bold]")
115
+ console.print(" [cyan]r[/cyan] Retry - feed failures back to agent")
116
+ console.print(" [green]a[/green] Approve - accept despite failures")
117
+ console.print(" [red]s[/red] Stop - end the loop")
118
+ console.print()
119
+
120
+ try:
121
+ ps = PromptSession()
122
+ choice = ps.prompt("Choice [r/a/s]: ").strip().lower()
123
+
124
+ if choice in ('r', 'retry'):
125
+ return 'retry'
126
+ elif choice in ('a', 'approve'):
127
+ return 'approve'
128
+ else:
129
+ return 'stop'
130
+ except (KeyboardInterrupt, EOFError):
131
+ return 'stop'
132
+
133
+
134
+ def show_verifiers_menu() -> tuple[str, str]:
135
+ """Show interactive verifiers menu.
136
+
137
+ Returns:
138
+ Tuple of (action, verifier_name) where action is one of:
139
+ - 'run': Run all verifiers
140
+ - 'create': Create new verifier
141
+ - 'delete': Delete verifier
142
+ - 'cancel': User cancelled
143
+ """
144
+ from prompt_toolkit import Application
145
+ from prompt_toolkit.key_binding import KeyBindings
146
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
147
+ from prompt_toolkit.styles import Style
148
+
149
+ manager = VerifierManager(Path.cwd())
150
+ verifiers = manager.verifiers
151
+
152
+ # Build menu items
153
+ menu_items = []
154
+
155
+ # Add "Run All" as first option if verifiers exist
156
+ if verifiers:
157
+ menu_items.append(("▶ Run All Verifiers", f"Run {len(verifiers)} verifier(s)", None, "run"))
158
+
159
+ # Add existing verifiers
160
+ for v in verifiers:
161
+ vtype = "[cmd]" if v.type == "command" else "[llm]"
162
+ desc = v.command[:40] if v.command else (v.prompt[:40] if v.prompt else "")
163
+ menu_items.append((v.name, f"{vtype} {desc}", v.name, "view"))
164
+
165
+ # Add create option
166
+ menu_items.append(("+ Create New Verifier", "Add a verifier with AI assistance", None, "create"))
167
+
168
+ selected_index = [0]
169
+ result = [("cancel", "")]
170
+
171
+ kb = KeyBindings()
172
+
173
+ @kb.add("up")
174
+ @kb.add("k")
175
+ def move_up(event):
176
+ selected_index[0] = (selected_index[0] - 1) % len(menu_items)
177
+
178
+ @kb.add("down")
179
+ @kb.add("j")
180
+ def move_down(event):
181
+ selected_index[0] = (selected_index[0] + 1) % len(menu_items)
182
+
183
+ @kb.add("enter")
184
+ def select(event):
185
+ item = menu_items[selected_index[0]]
186
+ name, desc, verifier_name, action = item
187
+ result[0] = (action, verifier_name or "")
188
+ event.app.exit()
189
+
190
+ @kb.add("d")
191
+ def delete_verifier(event):
192
+ item = menu_items[selected_index[0]]
193
+ name, desc, verifier_name, action = item
194
+ if verifier_name: # Can only delete actual verifiers
195
+ result[0] = ("delete", verifier_name)
196
+ event.app.exit()
197
+
198
+ @kb.add("n")
199
+ def new_verifier(event):
200
+ result[0] = ("create", "")
201
+ event.app.exit()
202
+
203
+ @kb.add("r")
204
+ def run_all(event):
205
+ if verifiers:
206
+ result[0] = ("run", "")
207
+ event.app.exit()
208
+
209
+ @kb.add("c-c")
210
+ @kb.add("escape")
211
+ @kb.add("q")
212
+ def cancel(event):
213
+ result[0] = ("cancel", "")
214
+ event.app.exit()
215
+
216
+ def get_formatted_menu():
217
+ lines = [("class:title", "Verifiers\n\n")]
218
+
219
+ if not verifiers:
220
+ lines.append(("class:dim", "No verifiers configured yet.\n\n"))
221
+
222
+ for i, (name, desc, vname, action) in enumerate(menu_items):
223
+ is_selected = i == selected_index[0]
224
+ prefix = "❯ " if is_selected else " "
225
+
226
+ if action in ("create", "run"):
227
+ if is_selected:
228
+ lines.append(("class:action-selected", f"{prefix}{name}\n"))
229
+ else:
230
+ lines.append(("class:action", f"{prefix}{name}\n"))
231
+ else:
232
+ if is_selected:
233
+ lines.append(("class:item-selected", f"{prefix}{name}"))
234
+ lines.append(("class:desc-selected", f" {desc}\n"))
235
+ else:
236
+ lines.append(("class:item", f"{prefix}{name}"))
237
+ lines.append(("class:desc", f" {desc}\n"))
238
+
239
+ lines.append(("class:hint", "\n↑/↓ navigate • Enter select • n new • d delete • r run • q quit"))
240
+ return lines
241
+
242
+ style = Style.from_dict({
243
+ "title": "#00ccff bold",
244
+ "dim": "#666666",
245
+ "item": "#00ccff",
246
+ "item-selected": "#00cc66 bold",
247
+ "action": "#ffcc00",
248
+ "action-selected": "#ffcc00 bold",
249
+ "desc": "#666666",
250
+ "desc-selected": "#00cc66",
251
+ "hint": "#888888 italic",
252
+ })
253
+
254
+ height = len(menu_items) + 5
255
+
256
+ layout = Layout(
257
+ HSplit([
258
+ Window(
259
+ FormattedTextControl(get_formatted_menu),
260
+ height=height,
261
+ ),
262
+ ])
263
+ )
264
+
265
+ app = Application(
266
+ layout=layout,
267
+ key_bindings=kb,
268
+ style=style,
269
+ full_screen=False,
270
+ )
271
+
272
+ console.print()
273
+
274
+ try:
275
+ app.run()
276
+ except (KeyboardInterrupt, EOFError):
277
+ result[0] = ("cancel", "")
278
+
279
+ console.print()
280
+ return result[0]
281
+
282
+
283
+ def chat_create_verifier(client, renderer, model, max_iterations, render_with_interrupt) -> None:
284
+ """Start a chat session to create a new verifier with AI assistance."""
285
+ from prompt_toolkit import PromptSession
286
+ from prompt_toolkit.styles import Style
287
+
288
+ verifiers_file = Path.cwd() / ".emdash" / "verifiers.json"
289
+
290
+ console.print()
291
+ console.print("[bold cyan]Create New Verifier[/bold cyan]")
292
+ console.print("[dim]Describe what verification you want. The AI will help you configure it.[/dim]")
293
+ console.print("[dim]Type 'done' to finish, Ctrl+C to cancel[/dim]")
294
+ console.print()
295
+
296
+ chat_style = Style.from_dict({
297
+ "prompt": "#00cc66 bold",
298
+ })
299
+
300
+ ps = PromptSession(style=chat_style)
301
+ chat_session_id = None
302
+ first_message = True
303
+
304
+ # Ensure .emdash directory exists
305
+ verifiers_file.parent.mkdir(parents=True, exist_ok=True)
306
+
307
+ # Get current config
308
+ manager = VerifierManager(Path.cwd())
309
+ current_config = manager.get_config()
310
+
311
+ # Chat loop
312
+ while True:
313
+ try:
314
+ user_input = ps.prompt([("class:prompt", "› ")]).strip()
315
+
316
+ if not user_input:
317
+ continue
318
+
319
+ if user_input.lower() in ("done", "quit", "exit", "q"):
320
+ console.print("[dim]Finished[/dim]")
321
+ break
322
+
323
+ # First message includes context
324
+ if first_message:
325
+ current_json = "{\n \"verifiers\": [],\n \"max_attempts\": 3\n}"
326
+ if current_config.get("verifiers"):
327
+ import json
328
+ current_json = json.dumps(current_config, indent=2)
329
+
330
+ message_with_context = f"""I want to add a new verifier to my project.
331
+
332
+ **Verifiers file:** `{verifiers_file}`
333
+
334
+ **Current configuration:**
335
+ ```json
336
+ {current_json}
337
+ ```
338
+
339
+ **Config options:**
340
+ - `max_attempts`: Maximum verification loop attempts (default: 3). Set to `0` for infinite attempts.
341
+
342
+ **Verifier types:**
343
+ 1. **Command verifier** - runs a shell command, passes if exit code is 0:
344
+ ```json
345
+ {{"type": "command", "name": "tests", "command": "npm test", "timeout": 120}}
346
+ ```
347
+
348
+ 2. **LLM verifier** - asks gpt-oss-120b to review, passes if AI says pass:
349
+ ```json
350
+ {{"type": "llm", "name": "review", "prompt": "Review the git diff for bugs"}}
351
+ ```
352
+
353
+ **My request:** {user_input}
354
+
355
+ Please help me create a verifier. Ask clarifying questions if needed, then use the Write tool to update `{verifiers_file}` with the new verifier added to the verifiers array."""
356
+ stream = client.agent_chat_stream(
357
+ message=message_with_context,
358
+ model=model,
359
+ max_iterations=max_iterations,
360
+ options={"mode": "code"},
361
+ )
362
+ first_message = False
363
+ elif chat_session_id:
364
+ stream = client.agent_continue_stream(
365
+ chat_session_id, user_input
366
+ )
367
+ else:
368
+ stream = client.agent_chat_stream(
369
+ message=user_input,
370
+ model=model,
371
+ max_iterations=max_iterations,
372
+ options={"mode": "code"},
373
+ )
374
+
375
+ result = render_with_interrupt(renderer, stream)
376
+ if result and result.get("session_id"):
377
+ chat_session_id = result["session_id"]
378
+
379
+ except (KeyboardInterrupt, EOFError):
380
+ console.print()
381
+ console.print("[dim]Cancelled[/dim]")
382
+ break
383
+ except Exception as e:
384
+ console.print(f"[red]Error: {e}[/red]")
385
+
386
+
387
+ def delete_verifier(name: str) -> bool:
388
+ """Delete a verifier by name."""
389
+ from prompt_toolkit import PromptSession
390
+ import json
391
+
392
+ manager = VerifierManager(Path.cwd())
393
+ config = manager.get_config()
394
+
395
+ # Find the verifier
396
+ verifiers = config.get("verifiers", [])
397
+ found = any(v.get("name") == name for v in verifiers)
398
+
399
+ if not found:
400
+ console.print(f"[yellow]Verifier '{name}' not found[/yellow]")
401
+ return False
402
+
403
+ # Confirm deletion
404
+ console.print()
405
+ console.print(f"[yellow]Delete verifier '{name}'?[/yellow]")
406
+ console.print("[dim]Type 'yes' to confirm.[/dim]")
407
+
408
+ try:
409
+ ps = PromptSession()
410
+ response = ps.prompt("Confirm > ").strip().lower()
411
+ if response not in ("yes", "y"):
412
+ console.print("[dim]Cancelled[/dim]")
413
+ return False
414
+ except (KeyboardInterrupt, EOFError):
415
+ return False
416
+
417
+ # Remove the verifier
418
+ config["verifiers"] = [v for v in verifiers if v.get("name") != name]
419
+ manager.save_config(config)
420
+ console.print(f"[green]Deleted verifier: {name}[/green]")
421
+ return True
422
+
423
+
424
+ def handle_verify(args: str, client=None, renderer=None, model=None, max_iterations=10, render_with_interrupt=None) -> None:
425
+ """Handle /verify command - run verification checks or manage verifiers."""
426
+ manager = VerifierManager(Path.cwd())
427
+
428
+ # If args provided, handle subcommands
429
+ if args:
430
+ subparts = args.split(maxsplit=1)
431
+ subcommand = subparts[0].lower()
432
+
433
+ if subcommand == "run":
434
+ # Run verifiers directly
435
+ _run_verifiers(manager)
436
+ elif subcommand in ("add", "create", "new"):
437
+ if client and renderer:
438
+ chat_create_verifier(client, renderer, model, max_iterations, render_with_interrupt)
439
+ else:
440
+ console.print("[yellow]AI assistance not available. Use /setup for guided creation.[/yellow]")
441
+ elif subcommand == "list":
442
+ _list_verifiers(manager)
443
+ else:
444
+ console.print("[yellow]Usage: /verify [run|add|list][/yellow]")
445
+ console.print("[dim]Or just /verify for interactive menu[/dim]")
446
+ return
447
+
448
+ # No args - show interactive menu if we have client, otherwise just run
449
+ if client and renderer:
450
+ while True:
451
+ action, verifier_name = show_verifiers_menu()
452
+
453
+ if action == "cancel":
454
+ break
455
+ elif action == "run":
456
+ _run_verifiers(manager)
457
+ break # Exit menu after running
458
+ elif action == "create":
459
+ chat_create_verifier(client, renderer, model, max_iterations, render_with_interrupt)
460
+ # Refresh manager after creating
461
+ manager = VerifierManager(Path.cwd())
462
+ elif action == "delete":
463
+ delete_verifier(verifier_name)
464
+ manager = VerifierManager(Path.cwd())
465
+ elif action == "view":
466
+ _show_verifier_details(manager, verifier_name)
467
+ else:
468
+ # No client - just run verifiers
469
+ _run_verifiers(manager)
470
+
471
+
472
+ def _run_verifiers(manager: VerifierManager) -> None:
473
+ """Run all verifiers and display results."""
474
+ console.print()
475
+
476
+ if not manager.verifiers:
477
+ console.print("[yellow]No verifiers configured.[/yellow]")
478
+ console.print("[dim]Use /verify add or /setup to create verifiers[/dim]")
479
+ console.print()
480
+ return
481
+
482
+ console.print(f"[bold]Running {len(manager.verifiers)} verifier(s)...[/bold]")
483
+ console.print()
484
+
485
+ context = {
486
+ "git_diff": get_git_diff(),
487
+ "files_changed": get_changed_files(),
488
+ }
489
+
490
+ report = manager.run_all(context)
491
+ display_report(report)
492
+ console.print()
493
+
494
+
495
+ def _list_verifiers(manager: VerifierManager) -> None:
496
+ """List all configured verifiers."""
497
+ console.print()
498
+ if not manager.verifiers:
499
+ console.print("[dim]No verifiers configured.[/dim]")
500
+ else:
501
+ console.print("[bold cyan]Verifiers[/bold cyan]\n")
502
+ for v in manager.verifiers:
503
+ vtype = "[cmd]" if v.type == "command" else "[llm]"
504
+ desc = v.command if v.command else v.prompt
505
+ console.print(f" [cyan]{v.name}[/cyan] {vtype} - {desc[:50]}")
506
+ console.print()
507
+
508
+
509
+ def _show_verifier_details(manager: VerifierManager, name: str) -> None:
510
+ """Show details of a specific verifier."""
511
+ console.print()
512
+ console.print("[dim]─" * 50 + "[/dim]")
513
+
514
+ for v in manager.verifiers:
515
+ if v.name == name:
516
+ console.print(f"\n[bold cyan]{v.name}[/bold cyan]\n")
517
+ console.print(f"[bold]Type:[/bold] {v.type}")
518
+ if v.type == "command":
519
+ console.print(f"[bold]Command:[/bold] {v.command}")
520
+ console.print(f"[bold]Timeout:[/bold] {v.timeout}s")
521
+ else:
522
+ console.print(f"[bold]Prompt:[/bold] {v.prompt}")
523
+ console.print(f"[bold]Model:[/bold] gpt-oss-120b")
524
+ console.print(f"[bold]Enabled:[/bold] {v.enabled}")
525
+ break
526
+ else:
527
+ console.print(f"\n[yellow]Verifier '{name}' not found[/yellow]")
528
+
529
+ console.print()
530
+ console.print("[dim]─" * 50 + "[/dim]")
531
+
532
+
533
+ def run_verification(goal: str | None = None) -> tuple[VerificationReport, bool]:
534
+ """Run verification and return report.
535
+
536
+ Args:
537
+ goal: Optional goal/task being verified
538
+
539
+ Returns:
540
+ Tuple of (report, should_continue_loop)
541
+ should_continue_loop is True if user wants to retry
542
+ """
543
+ manager = VerifierManager(Path.cwd())
544
+
545
+ if not manager.verifiers:
546
+ console.print("[yellow]No verifiers configured. Skipping verification.[/yellow]")
547
+ return VerificationReport(results=[], all_passed=True, summary="No verifiers"), False
548
+
549
+ console.print()
550
+ console.print(f"[bold cyan]Running {len(manager.verifiers)} verifier(s)...[/bold cyan]")
551
+ console.print()
552
+
553
+ # Build context
554
+ context = {
555
+ "goal": goal,
556
+ "git_diff": get_git_diff(),
557
+ "files_changed": get_changed_files(),
558
+ }
559
+
560
+ # Run verifiers
561
+ report = manager.run_all(context)
562
+
563
+ # Display results
564
+ display_report(report)
565
+
566
+ return report, not report.all_passed
567
+
568
+
569
+ def handle_verify_loop(
570
+ task: str,
571
+ run_task_fn,
572
+ max_attempts: int = 3,
573
+ ) -> bool:
574
+ """Run a task in a verification loop.
575
+
576
+ Args:
577
+ task: The task to run
578
+ run_task_fn: Function to run the task (takes task string, returns None)
579
+ max_attempts: Maximum number of attempts (0 = infinite)
580
+
581
+ Returns:
582
+ True if completed successfully, False if stopped
583
+ """
584
+ manager = VerifierManager(Path.cwd())
585
+ config = manager.get_config()
586
+ max_attempts = config.get("max_attempts", max_attempts)
587
+ is_infinite = max_attempts == 0
588
+
589
+ if not manager.verifiers:
590
+ console.print("[yellow]No verifiers configured.[/yellow]")
591
+ console.print("[dim]Configure .emdash/verifiers.json to use verify-loop[/dim]")
592
+ console.print()
593
+ console.print("[dim]Running task without verification...[/dim]")
594
+ run_task_fn(task)
595
+ return True
596
+
597
+ current_task = task
598
+ attempt_count = 0
599
+
600
+ while is_infinite or attempt_count < max_attempts:
601
+ attempt_count += 1
602
+
603
+ # Show attempt header
604
+ console.print()
605
+ if is_infinite:
606
+ console.print(f"[bold cyan]━━━ Attempt {attempt_count}/∞ ━━━[/bold cyan]")
607
+ else:
608
+ console.print(f"[bold cyan]━━━ Attempt {attempt_count}/{max_attempts} ━━━[/bold cyan]")
609
+ console.print()
610
+
611
+ # Run the task
612
+ run_task_fn(current_task)
613
+
614
+ # Run verification
615
+ report, has_failures = run_verification(task)
616
+
617
+ if not has_failures:
618
+ console.print()
619
+ console.print("[bold green]✓ All verifications passed! Task complete.[/bold green]")
620
+ console.print()
621
+ return True
622
+
623
+ # Failed - ask user what to do
624
+ choice = prompt_retry_menu()
625
+
626
+ if choice == 'retry':
627
+ if not is_infinite and attempt_count >= max_attempts:
628
+ console.print()
629
+ console.print(f"[red]Max attempts ({max_attempts}) reached.[/red]")
630
+ return False
631
+ # Build retry prompt with failure context
632
+ current_task = build_retry_prompt(task, report)
633
+ console.print()
634
+ console.print("[dim]Retrying with failure context...[/dim]")
635
+
636
+ elif choice == 'approve':
637
+ console.print()
638
+ console.print("[yellow]Approved with failing verifications.[/yellow]")
639
+ return True
640
+
641
+ else: # stop
642
+ console.print()
643
+ console.print("[red]Stopped by user.[/red]")
644
+ return False
645
+
646
+ console.print()
647
+ console.print(f"[red]Max attempts ({max_attempts}) reached.[/red]")
648
+ return False