zwarm 0.1.0__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.
- zwarm/__init__.py +38 -0
- zwarm/adapters/__init__.py +0 -0
- zwarm/adapters/base.py +109 -0
- zwarm/adapters/claude_code.py +303 -0
- zwarm/adapters/codex_mcp.py +428 -0
- zwarm/adapters/test_codex_mcp.py +224 -0
- zwarm/cli/__init__.py +0 -0
- zwarm/cli/main.py +534 -0
- zwarm/core/__init__.py +0 -0
- zwarm/core/config.py +271 -0
- zwarm/core/environment.py +83 -0
- zwarm/core/models.py +299 -0
- zwarm/core/state.py +224 -0
- zwarm/core/test_config.py +160 -0
- zwarm/core/test_models.py +265 -0
- zwarm/orchestrator.py +405 -0
- zwarm/prompts/__init__.py +10 -0
- zwarm/prompts/orchestrator.py +214 -0
- zwarm/tools/__init__.py +17 -0
- zwarm/tools/delegation.py +357 -0
- zwarm/watchers/__init__.py +26 -0
- zwarm/watchers/base.py +131 -0
- zwarm/watchers/builtin.py +256 -0
- zwarm/watchers/manager.py +143 -0
- zwarm/watchers/registry.py +57 -0
- zwarm/watchers/test_watchers.py +195 -0
- zwarm-0.1.0.dist-info/METADATA +382 -0
- zwarm-0.1.0.dist-info/RECORD +30 -0
- zwarm-0.1.0.dist-info/WHEEL +4 -0
- zwarm-0.1.0.dist-info/entry_points.txt +2 -0
zwarm/cli/main.py
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI for zwarm orchestration.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- zwarm orchestrate: Start an orchestrator session
|
|
6
|
+
- zwarm exec: Run a single executor directly (for testing)
|
|
7
|
+
- zwarm status: Show current state
|
|
8
|
+
- zwarm history: Show event history
|
|
9
|
+
- zwarm configs: Manage configurations
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import sys
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Annotated, Optional
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from rich import print as rprint
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
from rich.text import Text
|
|
26
|
+
|
|
27
|
+
# Create console for rich output
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_task(task: str | None, task_file: Path | None) -> str | None:
|
|
32
|
+
"""
|
|
33
|
+
Resolve task from multiple sources (priority order):
|
|
34
|
+
1. --task flag
|
|
35
|
+
2. --task-file flag
|
|
36
|
+
3. stdin (if not a tty)
|
|
37
|
+
"""
|
|
38
|
+
# Direct task takes priority
|
|
39
|
+
if task:
|
|
40
|
+
return task
|
|
41
|
+
|
|
42
|
+
# Then file
|
|
43
|
+
if task_file:
|
|
44
|
+
if not task_file.exists():
|
|
45
|
+
console.print(f"[red]Error:[/] Task file not found: {task_file}")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
return task_file.read_text().strip()
|
|
48
|
+
|
|
49
|
+
# Finally stdin (only if piped, not interactive)
|
|
50
|
+
if not sys.stdin.isatty():
|
|
51
|
+
stdin_content = sys.stdin.read().strip()
|
|
52
|
+
if stdin_content:
|
|
53
|
+
return stdin_content
|
|
54
|
+
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# Main app with rich help
|
|
58
|
+
app = typer.Typer(
|
|
59
|
+
name="zwarm",
|
|
60
|
+
help="""
|
|
61
|
+
[bold cyan]zwarm[/] - Multi-Agent CLI Orchestration Research Platform
|
|
62
|
+
|
|
63
|
+
[bold]DESCRIPTION[/]
|
|
64
|
+
Orchestrate multiple CLI coding agents (Codex, Claude Code) with
|
|
65
|
+
delegation, conversation, and trajectory alignment (watchers).
|
|
66
|
+
|
|
67
|
+
[bold]QUICK START[/]
|
|
68
|
+
[dim]# Test an executor directly[/]
|
|
69
|
+
$ zwarm exec --task "What is 2+2?"
|
|
70
|
+
|
|
71
|
+
[dim]# Run the orchestrator[/]
|
|
72
|
+
$ zwarm orchestrate --task "Build a hello world function"
|
|
73
|
+
|
|
74
|
+
[dim]# Check state after running[/]
|
|
75
|
+
$ zwarm status
|
|
76
|
+
|
|
77
|
+
[bold]COMMANDS[/]
|
|
78
|
+
[cyan]orchestrate[/] Start orchestrator to delegate tasks to executors
|
|
79
|
+
[cyan]exec[/] Run a single executor directly (for testing)
|
|
80
|
+
[cyan]status[/] Show current state (sessions, tasks, events)
|
|
81
|
+
[cyan]history[/] Show event history log
|
|
82
|
+
[cyan]configs[/] Manage configuration files
|
|
83
|
+
|
|
84
|
+
[bold]CONFIGURATION[/]
|
|
85
|
+
Create [cyan]config.toml[/] or use [cyan]--config[/] flag with YAML files.
|
|
86
|
+
See [cyan]zwarm configs list[/] for available configurations.
|
|
87
|
+
|
|
88
|
+
[bold]ADAPTERS[/]
|
|
89
|
+
[cyan]codex_mcp[/] Codex via MCP server (sync conversations)
|
|
90
|
+
[cyan]claude_code[/] Claude Code CLI
|
|
91
|
+
|
|
92
|
+
[bold]WATCHERS[/] (trajectory aligners)
|
|
93
|
+
[cyan]progress[/] Detects stuck/spinning agents
|
|
94
|
+
[cyan]budget[/] Monitors step/session limits
|
|
95
|
+
[cyan]scope[/] Detects scope creep
|
|
96
|
+
[cyan]pattern[/] Custom regex pattern matching
|
|
97
|
+
[cyan]quality[/] Code quality checks
|
|
98
|
+
""",
|
|
99
|
+
rich_markup_mode="rich",
|
|
100
|
+
no_args_is_help=True,
|
|
101
|
+
add_completion=False,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Configs subcommand group
|
|
105
|
+
configs_app = typer.Typer(
|
|
106
|
+
name="configs",
|
|
107
|
+
help="""
|
|
108
|
+
Manage zwarm configurations.
|
|
109
|
+
|
|
110
|
+
[bold]SUBCOMMANDS[/]
|
|
111
|
+
[cyan]list[/] List available configuration files
|
|
112
|
+
[cyan]show[/] Display a configuration file's contents
|
|
113
|
+
""",
|
|
114
|
+
rich_markup_mode="rich",
|
|
115
|
+
no_args_is_help=True,
|
|
116
|
+
)
|
|
117
|
+
app.add_typer(configs_app, name="configs")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class AdapterType(str, Enum):
|
|
121
|
+
codex_mcp = "codex_mcp"
|
|
122
|
+
claude_code = "claude_code"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ModeType(str, Enum):
|
|
126
|
+
sync = "sync"
|
|
127
|
+
async_ = "async"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command()
|
|
131
|
+
def orchestrate(
|
|
132
|
+
task: Annotated[Optional[str], typer.Option("--task", "-t", help="The task to accomplish")] = None,
|
|
133
|
+
task_file: Annotated[Optional[Path], typer.Option("--task-file", "-f", help="Read task from file")] = None,
|
|
134
|
+
config: Annotated[Optional[Path], typer.Option("--config", "-c", help="Path to config YAML")] = None,
|
|
135
|
+
overrides: Annotated[Optional[list[str]], typer.Option("--set", help="Override config (key=value)")] = None,
|
|
136
|
+
working_dir: Annotated[Path, typer.Option("--working-dir", "-w", help="Working directory")] = Path("."),
|
|
137
|
+
resume: Annotated[bool, typer.Option("--resume", help="Resume from previous state")] = False,
|
|
138
|
+
max_steps: Annotated[Optional[int], typer.Option("--max-steps", help="Maximum orchestrator steps")] = None,
|
|
139
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Show detailed output")] = False,
|
|
140
|
+
):
|
|
141
|
+
"""
|
|
142
|
+
Start an orchestrator session.
|
|
143
|
+
|
|
144
|
+
The orchestrator breaks down tasks and delegates to executor agents
|
|
145
|
+
(Codex, Claude Code). It can have sync conversations or fire-and-forget
|
|
146
|
+
async delegations.
|
|
147
|
+
|
|
148
|
+
[bold]Examples:[/]
|
|
149
|
+
[dim]# Simple task[/]
|
|
150
|
+
$ zwarm orchestrate --task "Add a logout button to the navbar"
|
|
151
|
+
|
|
152
|
+
[dim]# Task from file[/]
|
|
153
|
+
$ zwarm orchestrate -f task.md
|
|
154
|
+
|
|
155
|
+
[dim]# Task from stdin[/]
|
|
156
|
+
$ cat task.md | zwarm orchestrate
|
|
157
|
+
$ zwarm orchestrate < task.md
|
|
158
|
+
|
|
159
|
+
[dim]# With config file[/]
|
|
160
|
+
$ zwarm orchestrate -c configs/base.yaml --task "Refactor auth"
|
|
161
|
+
|
|
162
|
+
[dim]# Override settings[/]
|
|
163
|
+
$ zwarm orchestrate --task "Fix bug" --set executor.adapter=claude_code
|
|
164
|
+
|
|
165
|
+
[dim]# Resume interrupted session[/]
|
|
166
|
+
$ zwarm orchestrate --task "Continue work" --resume
|
|
167
|
+
"""
|
|
168
|
+
from zwarm.orchestrator import build_orchestrator
|
|
169
|
+
|
|
170
|
+
# Resolve task from: --task, --task-file, or stdin
|
|
171
|
+
resolved_task = _resolve_task(task, task_file)
|
|
172
|
+
if not resolved_task:
|
|
173
|
+
console.print("[red]Error:[/] No task provided. Use --task, --task-file, or pipe from stdin.")
|
|
174
|
+
raise typer.Exit(1)
|
|
175
|
+
|
|
176
|
+
task = resolved_task
|
|
177
|
+
|
|
178
|
+
# Build overrides list
|
|
179
|
+
override_list = list(overrides or [])
|
|
180
|
+
if max_steps:
|
|
181
|
+
override_list.append(f"orchestrator.max_steps={max_steps}")
|
|
182
|
+
|
|
183
|
+
console.print(f"[bold]Starting orchestrator...[/]")
|
|
184
|
+
console.print(f" Task: {task}")
|
|
185
|
+
console.print(f" Working dir: {working_dir.absolute()}")
|
|
186
|
+
console.print()
|
|
187
|
+
|
|
188
|
+
# Output handler to show orchestrator messages
|
|
189
|
+
def output_handler(msg: str) -> None:
|
|
190
|
+
if msg.strip():
|
|
191
|
+
console.print(f"[dim][orchestrator][/] {msg}")
|
|
192
|
+
|
|
193
|
+
orchestrator = None
|
|
194
|
+
try:
|
|
195
|
+
orchestrator = build_orchestrator(
|
|
196
|
+
config_path=config,
|
|
197
|
+
task=task,
|
|
198
|
+
working_dir=working_dir.absolute(),
|
|
199
|
+
overrides=override_list,
|
|
200
|
+
resume=resume,
|
|
201
|
+
output_handler=output_handler,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if resume:
|
|
205
|
+
console.print(" [dim]Resuming from previous state...[/]")
|
|
206
|
+
|
|
207
|
+
# Run the orchestrator loop
|
|
208
|
+
console.print("[bold]--- Orchestrator running ---[/]\n")
|
|
209
|
+
result = orchestrator.run(task=task)
|
|
210
|
+
|
|
211
|
+
console.print(f"\n[bold green]--- Orchestrator finished ---[/]")
|
|
212
|
+
console.print(f" Steps: {result.get('steps', 'unknown')}")
|
|
213
|
+
|
|
214
|
+
# Show exit message if any
|
|
215
|
+
exit_msg = getattr(orchestrator, "_exit_message", "")
|
|
216
|
+
if exit_msg:
|
|
217
|
+
console.print(f" Exit: {exit_msg[:200]}")
|
|
218
|
+
|
|
219
|
+
# Save state for potential resume
|
|
220
|
+
orchestrator.save_state()
|
|
221
|
+
|
|
222
|
+
except KeyboardInterrupt:
|
|
223
|
+
console.print("\n\n[yellow]Interrupted.[/]")
|
|
224
|
+
if orchestrator:
|
|
225
|
+
orchestrator.save_state()
|
|
226
|
+
console.print("[dim]State saved. Use --resume to continue.[/]")
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
console.print(f"\n[red]Error:[/] {e}")
|
|
230
|
+
if verbose:
|
|
231
|
+
console.print_exception()
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@app.command()
|
|
236
|
+
def exec(
|
|
237
|
+
task: Annotated[str, typer.Option("--task", "-t", help="Task to execute")],
|
|
238
|
+
adapter: Annotated[AdapterType, typer.Option("--adapter", "-a", help="Executor adapter")] = AdapterType.codex_mcp,
|
|
239
|
+
mode: Annotated[ModeType, typer.Option("--mode", "-m", help="Execution mode")] = ModeType.sync,
|
|
240
|
+
working_dir: Annotated[Path, typer.Option("--working-dir", "-w", help="Working directory")] = Path("."),
|
|
241
|
+
model: Annotated[Optional[str], typer.Option("--model", help="Model override")] = None,
|
|
242
|
+
):
|
|
243
|
+
"""
|
|
244
|
+
Run a single executor directly (for testing).
|
|
245
|
+
|
|
246
|
+
Useful for testing adapters without the full orchestrator loop.
|
|
247
|
+
|
|
248
|
+
[bold]Examples:[/]
|
|
249
|
+
[dim]# Test Codex[/]
|
|
250
|
+
$ zwarm exec --task "What is 2+2?"
|
|
251
|
+
|
|
252
|
+
[dim]# Test Claude Code[/]
|
|
253
|
+
$ zwarm exec -a claude_code --task "List files in current dir"
|
|
254
|
+
|
|
255
|
+
[dim]# Async mode[/]
|
|
256
|
+
$ zwarm exec --task "Build feature" --mode async
|
|
257
|
+
"""
|
|
258
|
+
from zwarm.adapters.codex_mcp import CodexMCPAdapter
|
|
259
|
+
from zwarm.adapters.claude_code import ClaudeCodeAdapter
|
|
260
|
+
|
|
261
|
+
console.print(f"[bold]Running executor directly...[/]")
|
|
262
|
+
console.print(f" Adapter: [cyan]{adapter.value}[/]")
|
|
263
|
+
console.print(f" Mode: {mode.value}")
|
|
264
|
+
console.print(f" Task: {task}")
|
|
265
|
+
|
|
266
|
+
if adapter == AdapterType.codex_mcp:
|
|
267
|
+
executor = CodexMCPAdapter()
|
|
268
|
+
elif adapter == AdapterType.claude_code:
|
|
269
|
+
executor = ClaudeCodeAdapter(model=model)
|
|
270
|
+
else:
|
|
271
|
+
console.print(f"[red]Unknown adapter:[/] {adapter}")
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
|
|
274
|
+
async def run():
|
|
275
|
+
try:
|
|
276
|
+
session = await executor.start_session(
|
|
277
|
+
task=task,
|
|
278
|
+
working_dir=working_dir.absolute(),
|
|
279
|
+
mode=mode.value,
|
|
280
|
+
model=model,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
console.print(f"\n[green]Session started:[/] {session.id[:8]}")
|
|
284
|
+
|
|
285
|
+
if mode == ModeType.sync:
|
|
286
|
+
response = session.messages[-1].content if session.messages else "(no response)"
|
|
287
|
+
console.print(f"\n[bold]Response:[/]\n{response}")
|
|
288
|
+
|
|
289
|
+
# Interactive loop for sync mode
|
|
290
|
+
while True:
|
|
291
|
+
try:
|
|
292
|
+
user_input = console.input("\n[dim]> (type message or 'exit')[/] ")
|
|
293
|
+
if user_input.lower() == "exit" or not user_input:
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
response = await executor.send_message(session, user_input)
|
|
297
|
+
console.print(f"\n[bold]Response:[/]\n{response}")
|
|
298
|
+
except KeyboardInterrupt:
|
|
299
|
+
break
|
|
300
|
+
else:
|
|
301
|
+
console.print("[dim]Async mode - session running in background.[/]")
|
|
302
|
+
console.print("Use 'zwarm status' to check progress.")
|
|
303
|
+
|
|
304
|
+
finally:
|
|
305
|
+
await executor.cleanup()
|
|
306
|
+
|
|
307
|
+
asyncio.run(run())
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@app.command()
|
|
311
|
+
def status(
|
|
312
|
+
working_dir: Annotated[Path, typer.Option("--working-dir", "-w", help="Working directory")] = Path("."),
|
|
313
|
+
):
|
|
314
|
+
"""
|
|
315
|
+
Show current state (sessions, tasks, events).
|
|
316
|
+
|
|
317
|
+
Displays active sessions, pending tasks, and recent events
|
|
318
|
+
from the .zwarm state directory.
|
|
319
|
+
|
|
320
|
+
[bold]Example:[/]
|
|
321
|
+
$ zwarm status
|
|
322
|
+
"""
|
|
323
|
+
from zwarm.core.state import StateManager
|
|
324
|
+
|
|
325
|
+
state_dir = working_dir / ".zwarm"
|
|
326
|
+
if not state_dir.exists():
|
|
327
|
+
console.print("[yellow]No zwarm state found in this directory.[/]")
|
|
328
|
+
console.print("[dim]Run 'zwarm orchestrate' to start.[/]")
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
state = StateManager(state_dir)
|
|
332
|
+
state.load()
|
|
333
|
+
|
|
334
|
+
# Sessions table
|
|
335
|
+
sessions = state.list_sessions()
|
|
336
|
+
console.print(f"\n[bold]Sessions[/] ({len(sessions)})")
|
|
337
|
+
if sessions:
|
|
338
|
+
table = Table(show_header=True, header_style="bold")
|
|
339
|
+
table.add_column("ID", style="dim")
|
|
340
|
+
table.add_column("Mode")
|
|
341
|
+
table.add_column("Status")
|
|
342
|
+
table.add_column("Task")
|
|
343
|
+
|
|
344
|
+
for s in sessions:
|
|
345
|
+
status_style = {"active": "green", "completed": "blue", "failed": "red"}.get(s.status.value, "white")
|
|
346
|
+
table.add_row(
|
|
347
|
+
s.id[:8],
|
|
348
|
+
s.mode.value,
|
|
349
|
+
f"[{status_style}]{s.status.value}[/]",
|
|
350
|
+
s.task_description[:50] + "..." if len(s.task_description) > 50 else s.task_description,
|
|
351
|
+
)
|
|
352
|
+
console.print(table)
|
|
353
|
+
else:
|
|
354
|
+
console.print(" [dim](none)[/]")
|
|
355
|
+
|
|
356
|
+
# Tasks table
|
|
357
|
+
tasks = state.list_tasks()
|
|
358
|
+
console.print(f"\n[bold]Tasks[/] ({len(tasks)})")
|
|
359
|
+
if tasks:
|
|
360
|
+
table = Table(show_header=True, header_style="bold")
|
|
361
|
+
table.add_column("ID", style="dim")
|
|
362
|
+
table.add_column("Status")
|
|
363
|
+
table.add_column("Description")
|
|
364
|
+
|
|
365
|
+
for t in tasks:
|
|
366
|
+
status_style = {"pending": "yellow", "in_progress": "cyan", "completed": "green", "failed": "red"}.get(t.status.value, "white")
|
|
367
|
+
table.add_row(
|
|
368
|
+
t.id[:8],
|
|
369
|
+
f"[{status_style}]{t.status.value}[/]",
|
|
370
|
+
t.description[:50] + "..." if len(t.description) > 50 else t.description,
|
|
371
|
+
)
|
|
372
|
+
console.print(table)
|
|
373
|
+
else:
|
|
374
|
+
console.print(" [dim](none)[/]")
|
|
375
|
+
|
|
376
|
+
# Recent events
|
|
377
|
+
events = state.get_events(limit=5)
|
|
378
|
+
console.print(f"\n[bold]Recent Events[/]")
|
|
379
|
+
if events:
|
|
380
|
+
for e in events:
|
|
381
|
+
console.print(f" [dim]{e.timestamp.strftime('%H:%M:%S')}[/] {e.kind}")
|
|
382
|
+
else:
|
|
383
|
+
console.print(" [dim](none)[/]")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@app.command()
|
|
387
|
+
def history(
|
|
388
|
+
working_dir: Annotated[Path, typer.Option("--working-dir", "-w", help="Working directory")] = Path("."),
|
|
389
|
+
kind: Annotated[Optional[str], typer.Option("--kind", "-k", help="Filter by event kind")] = None,
|
|
390
|
+
limit: Annotated[int, typer.Option("--limit", "-n", help="Number of events")] = 20,
|
|
391
|
+
):
|
|
392
|
+
"""
|
|
393
|
+
Show event history.
|
|
394
|
+
|
|
395
|
+
Displays the append-only event log with timestamps and details.
|
|
396
|
+
|
|
397
|
+
[bold]Examples:[/]
|
|
398
|
+
[dim]# Show last 20 events[/]
|
|
399
|
+
$ zwarm history
|
|
400
|
+
|
|
401
|
+
[dim]# Show more events[/]
|
|
402
|
+
$ zwarm history --limit 50
|
|
403
|
+
|
|
404
|
+
[dim]# Filter by kind[/]
|
|
405
|
+
$ zwarm history --kind session_started
|
|
406
|
+
"""
|
|
407
|
+
from zwarm.core.state import StateManager
|
|
408
|
+
|
|
409
|
+
state_dir = working_dir / ".zwarm"
|
|
410
|
+
if not state_dir.exists():
|
|
411
|
+
console.print("[yellow]No zwarm state found.[/]")
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
state = StateManager(state_dir)
|
|
415
|
+
events = state.get_events(kind=kind, limit=limit)
|
|
416
|
+
|
|
417
|
+
console.print(f"\n[bold]Event History[/] (last {limit})\n")
|
|
418
|
+
|
|
419
|
+
if not events:
|
|
420
|
+
console.print("[dim]No events found.[/]")
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
table = Table(show_header=True, header_style="bold")
|
|
424
|
+
table.add_column("Time", style="dim")
|
|
425
|
+
table.add_column("Event")
|
|
426
|
+
table.add_column("Session/Task")
|
|
427
|
+
table.add_column("Details")
|
|
428
|
+
|
|
429
|
+
for e in events:
|
|
430
|
+
details = ""
|
|
431
|
+
if e.payload:
|
|
432
|
+
details = ", ".join(f"{k}={str(v)[:30]}" for k, v in list(e.payload.items())[:2])
|
|
433
|
+
|
|
434
|
+
table.add_row(
|
|
435
|
+
e.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
|
|
436
|
+
e.kind,
|
|
437
|
+
(e.session_id or e.task_id or "-")[:8],
|
|
438
|
+
details[:60],
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
console.print(table)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@configs_app.command("list")
|
|
445
|
+
def configs_list(
|
|
446
|
+
config_dir: Annotated[Optional[Path], typer.Option("--dir", "-d", help="Directory to search")] = None,
|
|
447
|
+
):
|
|
448
|
+
"""
|
|
449
|
+
List available agent/experiment configuration files (YAML).
|
|
450
|
+
|
|
451
|
+
Note: config.toml is for user environment settings and is loaded
|
|
452
|
+
automatically - use YAML files for agent configurations.
|
|
453
|
+
|
|
454
|
+
[bold]Example:[/]
|
|
455
|
+
$ zwarm configs list
|
|
456
|
+
"""
|
|
457
|
+
search_dirs = [
|
|
458
|
+
config_dir or Path.cwd(),
|
|
459
|
+
Path.cwd() / "configs",
|
|
460
|
+
Path.cwd() / ".zwarm",
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
console.print("\n[bold]Available Configurations[/]\n")
|
|
464
|
+
found = False
|
|
465
|
+
|
|
466
|
+
for d in search_dirs:
|
|
467
|
+
if not d.exists():
|
|
468
|
+
continue
|
|
469
|
+
for pattern in ["*.yaml", "*.yml"]:
|
|
470
|
+
for f in d.glob(pattern):
|
|
471
|
+
found = True
|
|
472
|
+
try:
|
|
473
|
+
rel = f.relative_to(Path.cwd())
|
|
474
|
+
console.print(f" [cyan]{rel}[/]")
|
|
475
|
+
except ValueError:
|
|
476
|
+
console.print(f" [cyan]{f}[/]")
|
|
477
|
+
|
|
478
|
+
if not found:
|
|
479
|
+
console.print(" [dim]No configuration files found.[/]")
|
|
480
|
+
console.print("\n [dim]Create a YAML config in configs/ to get started.[/]")
|
|
481
|
+
|
|
482
|
+
# Check for config.toml and mention it
|
|
483
|
+
config_toml = Path.cwd() / "config.toml"
|
|
484
|
+
if config_toml.exists():
|
|
485
|
+
console.print(f"\n[dim]Environment: config.toml (loaded automatically)[/]")
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@configs_app.command("show")
|
|
489
|
+
def configs_show(
|
|
490
|
+
config_path: Annotated[Path, typer.Argument(help="Path to configuration file")],
|
|
491
|
+
):
|
|
492
|
+
"""
|
|
493
|
+
Show a configuration file's contents.
|
|
494
|
+
|
|
495
|
+
Loads and displays the resolved configuration including
|
|
496
|
+
any inherited values from 'extends:' directives.
|
|
497
|
+
|
|
498
|
+
[bold]Example:[/]
|
|
499
|
+
$ zwarm configs show configs/base.yaml
|
|
500
|
+
"""
|
|
501
|
+
from zwarm.core.config import load_config
|
|
502
|
+
import json
|
|
503
|
+
|
|
504
|
+
if not config_path.exists():
|
|
505
|
+
console.print(f"[red]File not found:[/] {config_path}")
|
|
506
|
+
raise typer.Exit(1)
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
config = load_config(config_path=config_path)
|
|
510
|
+
console.print(f"\n[bold]Configuration:[/] {config_path}\n")
|
|
511
|
+
console.print_json(json.dumps(config.to_dict(), indent=2))
|
|
512
|
+
except Exception as e:
|
|
513
|
+
console.print(f"[red]Error loading config:[/] {e}")
|
|
514
|
+
raise typer.Exit(1)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@app.callback(invoke_without_command=True)
|
|
518
|
+
def main_callback(
|
|
519
|
+
ctx: typer.Context,
|
|
520
|
+
version: Annotated[bool, typer.Option("--version", "-V", help="Show version")] = False,
|
|
521
|
+
):
|
|
522
|
+
"""Main callback for version flag."""
|
|
523
|
+
if version:
|
|
524
|
+
console.print("[bold cyan]zwarm[/] version [green]0.1.0[/]")
|
|
525
|
+
raise typer.Exit()
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def main():
|
|
529
|
+
"""Entry point for the CLI."""
|
|
530
|
+
app()
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
if __name__ == "__main__":
|
|
534
|
+
main()
|
zwarm/core/__init__.py
ADDED
|
File without changes
|