emdash-cli 0.1.30__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.
- emdash_cli/__init__.py +15 -0
- emdash_cli/client.py +156 -0
- emdash_cli/clipboard.py +30 -61
- emdash_cli/commands/agent/__init__.py +14 -0
- emdash_cli/commands/agent/cli.py +100 -0
- emdash_cli/commands/agent/constants.py +53 -0
- emdash_cli/commands/agent/file_utils.py +178 -0
- emdash_cli/commands/agent/handlers/__init__.py +41 -0
- emdash_cli/commands/agent/handlers/agents.py +421 -0
- emdash_cli/commands/agent/handlers/auth.py +69 -0
- emdash_cli/commands/agent/handlers/doctor.py +319 -0
- emdash_cli/commands/agent/handlers/hooks.py +121 -0
- emdash_cli/commands/agent/handlers/mcp.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +200 -0
- emdash_cli/commands/agent/handlers/rules.py +394 -0
- emdash_cli/commands/agent/handlers/sessions.py +168 -0
- emdash_cli/commands/agent/handlers/setup.py +582 -0
- emdash_cli/commands/agent/handlers/skills.py +440 -0
- emdash_cli/commands/agent/handlers/todos.py +98 -0
- emdash_cli/commands/agent/handlers/verify.py +648 -0
- emdash_cli/commands/agent/interactive.py +657 -0
- emdash_cli/commands/agent/menus.py +728 -0
- emdash_cli/commands/agent.py +7 -856
- emdash_cli/commands/server.py +99 -40
- emdash_cli/server_manager.py +70 -10
- emdash_cli/session_store.py +321 -0
- emdash_cli/sse_renderer.py +256 -110
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
- emdash_cli-0.1.46.dist-info/RECORD +49 -0
- emdash_cli-0.1.30.dist-info/RECORD +0 -29
- {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.30.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
|