zwarm 2.3.5__py3-none-any.whl → 3.2.1__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/cli/main.py CHANGED
@@ -79,6 +79,7 @@ app = typer.Typer(
79
79
  [cyan]init[/] Initialize zwarm (creates .zwarm/ with config)
80
80
  [cyan]reset[/] Reset state and optionally config files
81
81
  [cyan]orchestrate[/] Start orchestrator to delegate tasks to executors
82
+ [cyan]pilot[/] Conversational orchestrator REPL (interactive)
82
83
  [cyan]exec[/] Run a single executor directly (for testing)
83
84
  [cyan]status[/] Show current state (sessions, tasks, events)
84
85
  [cyan]history[/] Show event history log
@@ -274,6 +275,112 @@ def orchestrate(
274
275
  sys.exit(1)
275
276
 
276
277
 
278
+ class PilotLM(str, Enum):
279
+ """LM options for pilot mode."""
280
+ gpt5_mini = "gpt5-mini" # GPT5MiniTester - fast, cheap, good for testing
281
+ gpt5 = "gpt5" # GPT5Large - standard
282
+ gpt5_verbose = "gpt5-verbose" # GPT5LargeVerbose - with extended thinking
283
+
284
+
285
+ @app.command()
286
+ def pilot(
287
+ task: Annotated[Optional[str], typer.Option("--task", "-t", help="Initial task (optional)")] = None,
288
+ task_file: Annotated[Optional[Path], typer.Option("--task-file", "-f", help="Read task from file")] = None,
289
+ config: Annotated[Optional[Path], typer.Option("--config", "-c", help="Path to config YAML")] = None,
290
+ overrides: Annotated[Optional[list[str]], typer.Option("--set", help="Override config (key=value)")] = None,
291
+ working_dir: Annotated[Path, typer.Option("--working-dir", "-w", help="Working directory")] = Path("."),
292
+ instance: Annotated[Optional[str], typer.Option("--instance", "-i", help="Instance ID (for isolation)")] = None,
293
+ instance_name: Annotated[Optional[str], typer.Option("--name", "-n", help="Human-readable instance name")] = None,
294
+ model: Annotated[PilotLM, typer.Option("--model", "-m", help="LM to use")] = PilotLM.gpt5_verbose,
295
+ ):
296
+ """
297
+ Interactive conversational orchestrator REPL.
298
+
299
+ Like 'orchestrate' but conversational: give instructions, watch the
300
+ orchestrator work, course-correct in real-time, time-travel to checkpoints.
301
+
302
+ [bold]Features:[/]
303
+ - Streaming display of orchestrator thinking and tool calls
304
+ - Turn-by-turn execution with checkpoints
305
+ - Time travel (:goto T1) to return to previous states
306
+ - Session visibility (:sessions) and state inspection (:state)
307
+
308
+ [bold]Commands:[/]
309
+ :help Show help
310
+ :history [N|all] Show turn checkpoints
311
+ :goto <turn|root> Time travel (e.g., :goto T1)
312
+ :state Show orchestrator state
313
+ :sessions Show active executor sessions
314
+ :reasoning on|off Toggle reasoning display
315
+ :quit Exit
316
+
317
+ [bold]LM Options:[/]
318
+ gpt5-mini GPT5MiniTester - fast/cheap, good for testing
319
+ gpt5 GPT5Large - standard model
320
+ gpt5-verbose GPT5LargeVerbose - with extended thinking (default)
321
+
322
+ [bold]Examples:[/]
323
+ [dim]# Start fresh, give instructions interactively[/]
324
+ $ zwarm pilot
325
+
326
+ [dim]# Start with an initial task[/]
327
+ $ zwarm pilot --task "Build user authentication"
328
+
329
+ [dim]# Use faster model for testing[/]
330
+ $ zwarm pilot --model gpt5-mini
331
+
332
+ [dim]# Named instance[/]
333
+ $ zwarm pilot --name my-feature
334
+ """
335
+ from zwarm.cli.pilot import run_pilot, build_pilot_orchestrator
336
+
337
+ # Resolve task (optional for pilot)
338
+ resolved_task = _resolve_task(task, task_file)
339
+
340
+ console.print(f"[bold]Starting pilot session...[/]")
341
+ console.print(f" Working dir: {working_dir.absolute()}")
342
+ console.print(f" Model: {model.value}")
343
+ if resolved_task:
344
+ console.print(f" Initial task: {resolved_task[:60]}...")
345
+ if instance:
346
+ console.print(f" Instance: {instance}" + (f" ({instance_name})" if instance_name else ""))
347
+ console.print()
348
+
349
+ orchestrator = None
350
+ try:
351
+ orchestrator = build_pilot_orchestrator(
352
+ config_path=config,
353
+ working_dir=working_dir.absolute(),
354
+ overrides=list(overrides or []),
355
+ instance_id=instance,
356
+ instance_name=instance_name,
357
+ lm_choice=model.value,
358
+ )
359
+
360
+ # Show instance ID if auto-generated
361
+ if orchestrator.instance_id and not instance:
362
+ console.print(f" [dim]Instance: {orchestrator.instance_id[:8]}[/]")
363
+
364
+ # Run the pilot REPL
365
+ run_pilot(orchestrator, initial_task=resolved_task)
366
+
367
+ # Save state on exit
368
+ orchestrator.save_state()
369
+ console.print("\n[dim]State saved.[/]")
370
+
371
+ except KeyboardInterrupt:
372
+ console.print("\n\n[yellow]Interrupted.[/]")
373
+ if orchestrator:
374
+ orchestrator.save_state()
375
+ console.print("[dim]State saved.[/]")
376
+ sys.exit(1)
377
+ except Exception as e:
378
+ console.print(f"\n[red]Error:[/] {e}")
379
+ import traceback
380
+ traceback.print_exc()
381
+ sys.exit(1)
382
+
383
+
277
384
  @app.command()
278
385
  def exec(
279
386
  task: Annotated[str, typer.Option("--task", "-t", help="Task to execute")],
@@ -633,8 +740,14 @@ def init(
633
740
  [bold]Creates:[/]
634
741
  [cyan].zwarm/[/] State directory for sessions and events
635
742
  [cyan].zwarm/config.toml[/] Runtime settings (weave, adapter, watchers)
743
+ [cyan].zwarm/codex.toml[/] Codex CLI settings (model, reasoning effort)
636
744
  [cyan]zwarm.yaml[/] Project config (optional, with --with-project)
637
745
 
746
+ [bold]Configuration relationship:[/]
747
+ config.toml → Controls zwarm itself (tracing, which watchers run)
748
+ codex.toml → Controls the Codex CLI that runs executor sessions
749
+ zwarm.yaml → Project-specific context injected into orchestrator
750
+
638
751
  [bold]Examples:[/]
639
752
  [dim]# Interactive setup[/]
640
753
  $ zwarm init
@@ -681,6 +794,9 @@ def init(
681
794
  create_project_config = with_project
682
795
  project_description = ""
683
796
  project_context = ""
797
+ # Codex settings
798
+ codex_model = "gpt-5.1-codex-mini"
799
+ codex_reasoning = "high"
684
800
 
685
801
  if not non_interactive:
686
802
  console.print("[bold]Configuration[/]\n")
@@ -699,6 +815,42 @@ def init(
699
815
  type=str,
700
816
  )
701
817
 
818
+ # Codex model settings
819
+ console.print("\n [bold]Codex Model Settings[/] (.zwarm/codex.toml)")
820
+ console.print(" [dim]These control the underlying Codex CLI that runs executor sessions[/]\n")
821
+
822
+ console.print(" Available models:")
823
+ console.print(" [cyan]1[/] gpt-5.1-codex-mini [dim]- Fast, cheap, good for most tasks (Recommended)[/]")
824
+ console.print(" [cyan]2[/] gpt-5.1-codex [dim]- Balanced speed and capability[/]")
825
+ console.print(" [cyan]3[/] gpt-5.1-codex-max [dim]- Most capable, 400k context, expensive[/]")
826
+
827
+ model_choice = typer.prompt(
828
+ " Select model (1-3)",
829
+ default="1",
830
+ type=str,
831
+ )
832
+ model_map = {
833
+ "1": "gpt-5.1-codex-mini",
834
+ "2": "gpt-5.1-codex",
835
+ "3": "gpt-5.1-codex-max",
836
+ }
837
+ codex_model = model_map.get(model_choice, model_choice)
838
+ if model_choice not in model_map:
839
+ console.print(f" [dim]Using custom model: {codex_model}[/]")
840
+
841
+ console.print("\n Reasoning effort (how much the model \"thinks\" before responding):")
842
+ console.print(" [cyan]1[/] low [dim]- Minimal reasoning, fastest responses[/]")
843
+ console.print(" [cyan]2[/] medium [dim]- Balanced reasoning[/]")
844
+ console.print(" [cyan]3[/] high [dim]- Maximum reasoning, best for complex tasks (Recommended)[/]")
845
+
846
+ reasoning_choice = typer.prompt(
847
+ " Select reasoning effort (1-3)",
848
+ default="3",
849
+ type=str,
850
+ )
851
+ reasoning_map = {"1": "low", "2": "medium", "3": "high"}
852
+ codex_reasoning = reasoning_map.get(reasoning_choice, "high")
853
+
702
854
  # Watchers
703
855
  console.print("\n [bold]Watchers[/] (trajectory aligners)")
704
856
  available_watchers = ["progress", "budget", "delegation", "delegation_reminder", "scope", "pattern", "quality"]
@@ -750,10 +902,20 @@ def init(
750
902
 
751
903
  # Create codex.toml for isolated codex configuration
752
904
  codex_toml_path = state_dir / "codex.toml"
753
- if not codex_toml_path.exists():
754
- codex_content = _generate_codex_toml()
905
+ write_codex_toml = True
906
+ if codex_toml_path.exists():
907
+ if not non_interactive:
908
+ overwrite_codex = typer.confirm(" .zwarm/codex.toml exists. Overwrite?", default=False)
909
+ if not overwrite_codex:
910
+ write_codex_toml = False
911
+ console.print(" [dim]Skipping codex.toml[/]")
912
+ else:
913
+ write_codex_toml = False # Don't overwrite in non-interactive mode
914
+
915
+ if write_codex_toml:
916
+ codex_content = _generate_codex_toml(model=codex_model, reasoning_effort=codex_reasoning)
755
917
  codex_toml_path.write_text(codex_content)
756
- console.print(f" [green]✓[/] Created .zwarm/codex.toml (isolated codex config)")
918
+ console.print(f" [green]✓[/] Created .zwarm/codex.toml")
757
919
 
758
920
  # Create zwarm.yaml
759
921
  if create_project_config:
@@ -773,11 +935,23 @@ def init(
773
935
 
774
936
  # Summary
775
937
  console.print("\n[bold green]Done![/] zwarm is ready.\n")
938
+
939
+ # Explain config files
940
+ console.print("[bold]Configuration files:[/]")
941
+ console.print(" [cyan].zwarm/config.toml[/] - Runtime settings (Weave tracing, watchers)")
942
+ console.print(" [cyan].zwarm/codex.toml[/] - Codex CLI settings (model, reasoning effort)")
943
+ if create_project_config:
944
+ console.print(" [cyan]zwarm.yaml[/] - Project context and constraints")
945
+ console.print()
946
+ console.print(" [dim]Edit these files to customize behavior. Run 'zwarm init' again to reconfigure.[/]\n")
947
+
776
948
  console.print("[bold]Next steps:[/]")
777
949
  console.print(" [dim]# Run the orchestrator[/]")
778
950
  console.print(" $ zwarm orchestrate --task \"Your task here\"\n")
779
951
  console.print(" [dim]# Or test an executor directly[/]")
780
952
  console.print(" $ zwarm exec --task \"What is 2+2?\"\n")
953
+ console.print(" [dim]# Interactive session management[/]")
954
+ console.print(" $ zwarm interactive\n")
781
955
 
782
956
 
783
957
  def _generate_config_toml(
@@ -1133,897 +1307,183 @@ def clean(
1133
1307
  def interactive(
1134
1308
  default_dir: Annotated[Path, typer.Option("--dir", "-d", help="Default working directory")] = Path("."),
1135
1309
  model: Annotated[Optional[str], typer.Option("--model", help="Default model override")] = None,
1136
- adapter: Annotated[str, typer.Option("--adapter", "-a", help="Executor adapter")] = "codex_mcp",
1137
- state_dir: Annotated[Path, typer.Option("--state-dir", help="State directory for persistence")] = Path(".zwarm"),
1138
1310
  ):
1139
1311
  """
1140
- Universal multi-agent CLI for commanding coding agents.
1312
+ Interactive REPL for session management.
1141
1313
 
1142
- Spawn multiple agents across different directories, manage them interactively,
1143
- and view their outputs. You are the orchestrator.
1144
-
1145
- This uses the SAME code path as `zwarm orchestrate` - the adapter layer.
1146
- Interactive is the human-in-the-loop version, orchestrate is the LLM version.
1314
+ A clean, autocomplete-enabled interface for managing codex sessions.
1315
+ Tab-complete session IDs, watch live output, and manage the session lifecycle.
1147
1316
 
1148
1317
  [bold]Commands:[/]
1149
- [cyan]spawn[/] "task" [opts] Start a coding agent session (sync mode)
1150
- [cyan]async[/] "task" [opts] Start async session (fire-and-forget)
1151
- [cyan]ls[/] / [cyan]list[/] Dashboard of all sessions
1152
- [cyan]?[/] ID Quick peek (status + latest message)
1153
- [cyan]show[/] ID Full session details & history
1154
- [cyan]traj[/] ID Show trajectory (all steps taken)
1155
- [cyan]c[/] / [cyan]continue[/] ID "msg" Continue a sync conversation
1156
- [cyan]kill[/] ID Stop a session (keeps in history)
1157
- [cyan]rm[/] ID Delete session entirely
1158
- [cyan]killall[/] Stop all running sessions
1159
- [cyan]clean[/] Remove old sessions (>7 days)
1160
- [cyan]q[/] / [cyan]quit[/] Exit
1161
-
1162
- [bold]Spawn Options:[/]
1163
- spawn "task" --dir ~/project --model gpt-5.1-codex-max --async
1318
+ [cyan]spawn[/] "task" Start a new session
1319
+ [cyan]ls[/] Dashboard of all sessions
1320
+ [cyan]peek[/] ID / [cyan]?[/] ID Quick status check
1321
+ [cyan]show[/] ID Full session details
1322
+ [cyan]traj[/] ID Show trajectory (steps taken)
1323
+ [cyan]watch[/] ID Live follow session output
1324
+ [cyan]c[/] ID "msg" Continue conversation
1325
+ [cyan]kill[/] ID | all Stop session(s)
1326
+ [cyan]rm[/] ID | all Delete session(s)
1164
1327
 
1165
1328
  [bold]Examples:[/]
1166
1329
  $ zwarm interactive
1167
- > spawn "Build auth module" --dir ~/api
1168
- > spawn "Fix tests" --dir ~/api
1169
- > c abc123 "Now add error handling"
1330
+ > spawn "Build auth module"
1331
+ > watch abc123
1332
+ > c abc123 "Now add tests"
1170
1333
  > ls
1171
- > ? abc123
1172
1334
  """
1173
- from zwarm.adapters import get_adapter, list_adapters
1174
- from zwarm.core.models import ConversationSession, SessionStatus, SessionMode
1175
- import argparse
1335
+ from zwarm.cli.interactive import run_interactive
1176
1336
 
1177
- # Initialize adapter (same as orchestrator uses)
1178
1337
  default_model = model or "gpt-5.1-codex-mini"
1179
- default_adapter = adapter
1180
-
1181
- # Config path for isolated codex configuration
1182
- codex_config_path = state_dir / "codex.toml"
1183
- if not codex_config_path.exists():
1184
- # Try relative to working dir
1185
- codex_config_path = default_dir / ".zwarm" / "codex.toml"
1186
- if not codex_config_path.exists():
1187
- codex_config_path = None # Fall back to overrides
1188
-
1189
- # Session tracking - same pattern as orchestrator
1190
- sessions: dict[str, ConversationSession] = {}
1191
- adapters_cache: dict[str, Any] = {}
1192
-
1193
- def get_or_create_adapter(adapter_name: str, session_model: str | None = None) -> Any:
1194
- """Get or create an adapter instance with isolated config."""
1195
- cache_key = f"{adapter_name}:{session_model or default_model}"
1196
- if cache_key not in adapters_cache:
1197
- adapters_cache[cache_key] = get_adapter(
1198
- adapter_name,
1199
- model=session_model or default_model,
1200
- config_path=codex_config_path,
1201
- )
1202
- return adapters_cache[cache_key]
1203
-
1204
- def find_session(query: str) -> tuple[ConversationSession | None, str | None]:
1205
- """Find session by full or partial ID."""
1206
- if query in sessions:
1207
- return sessions[query], query
1208
- for sid, session in sessions.items():
1209
- if sid.startswith(query):
1210
- return session, sid
1211
- return None, None
1212
-
1213
- console.print("\n[bold cyan]zwarm interactive[/] - Multi-Agent Command Center\n")
1214
- console.print(f" Working dir: {default_dir.absolute()}")
1215
- console.print(f" Model: [cyan]{default_model}[/]")
1216
- console.print(f" Adapter: [cyan]{default_adapter}[/]")
1217
- if codex_config_path:
1218
- console.print(f" Config: [green]{codex_config_path}[/] (isolated)")
1219
- else:
1220
- console.print(f" Config: [yellow]using fallback overrides[/] (run 'zwarm init' for isolation)")
1221
- console.print(f" Available: {', '.join(list_adapters())}")
1222
- console.print("\n Type [cyan]help[/] for commands, [cyan]quit[/] to exit.\n")
1223
-
1224
- def show_help():
1225
- help_table = Table(show_header=False, box=None, padding=(0, 2))
1226
- help_table.add_column("Command", style="cyan", width=35)
1227
- help_table.add_column("Description")
1228
- help_table.add_row('spawn "task" [options]', "Start session (waits for completion)")
1229
- help_table.add_row(" --dir PATH", "Working directory")
1230
- help_table.add_row(" --model NAME", "Model override")
1231
- help_table.add_row(" --async", "Background mode (don't wait)")
1232
- help_table.add_row("", "")
1233
- help_table.add_row("ls / list", "Dashboard of all sessions")
1234
- help_table.add_row("? ID / peek ID", "Quick peek (status + latest message)")
1235
- help_table.add_row("show ID", "Full session details & messages")
1236
- help_table.add_row("traj ID [--full]", "Show trajectory (all steps taken)")
1237
- help_table.add_row('c ID "msg"', "Continue conversation (wait for response)")
1238
- help_table.add_row('ca ID "msg"', "Continue async (fire-and-forget)")
1239
- help_table.add_row("check ID", "Check session status")
1240
- help_table.add_row("kill ID", "Stop a running session")
1241
- help_table.add_row("rm ID", "Delete session entirely")
1242
- help_table.add_row("killall", "Stop all running sessions")
1243
- help_table.add_row("clean", "Remove old completed sessions")
1244
- help_table.add_row("q / quit", "Exit")
1245
- console.print(help_table)
1246
-
1247
- def show_sessions():
1248
- """List all sessions from CodexSessionManager (same as orchestrator)."""
1249
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1250
- from datetime import datetime
1251
-
1252
- manager = CodexSessionManager(default_dir / ".zwarm")
1253
- all_sessions = manager.list_sessions()
1254
-
1255
- if not all_sessions:
1256
- console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
1257
- return
1258
-
1259
- # Status summary
1260
- running = sum(1 for s in all_sessions if s.status == SessStatus.RUNNING)
1261
- completed = sum(1 for s in all_sessions if s.status == SessStatus.COMPLETED)
1262
- failed = sum(1 for s in all_sessions if s.status == SessStatus.FAILED)
1263
- killed = sum(1 for s in all_sessions if s.status == SessStatus.KILLED)
1264
-
1265
- summary_parts = []
1266
- if running:
1267
- summary_parts.append(f"[yellow]{running} running[/]")
1268
- if completed:
1269
- summary_parts.append(f"[green]{completed} done[/]")
1270
- if failed:
1271
- summary_parts.append(f"[red]{failed} failed[/]")
1272
- if killed:
1273
- summary_parts.append(f"[dim]{killed} killed[/]")
1274
- if summary_parts:
1275
- console.print(" | ".join(summary_parts))
1276
- console.print()
1277
-
1278
- def time_ago(iso_str: str) -> str:
1279
- """Convert ISO timestamp to human-readable 'X ago' format."""
1280
- try:
1281
- dt = datetime.fromisoformat(iso_str)
1282
- delta = datetime.now() - dt
1283
- secs = delta.total_seconds()
1284
- if secs < 60:
1285
- return f"{int(secs)}s"
1286
- elif secs < 3600:
1287
- return f"{int(secs/60)}m"
1288
- elif secs < 86400:
1289
- return f"{secs/3600:.1f}h"
1290
- else:
1291
- return f"{secs/86400:.1f}d"
1292
- except:
1293
- return "?"
1294
-
1295
- table = Table(box=None, show_header=True, header_style="bold dim")
1296
- table.add_column("ID", style="cyan", width=10)
1297
- table.add_column("", width=2) # Status icon
1298
- table.add_column("T", width=2) # Turn
1299
- table.add_column("Task", max_width=30)
1300
- table.add_column("Updated", justify="right", width=8)
1301
- table.add_column("Last Message", max_width=40)
1302
-
1303
- status_icons = {
1304
- "running": "[yellow]●[/]",
1305
- "completed": "[green]✓[/]",
1306
- "failed": "[red]✗[/]",
1307
- "killed": "[dim]○[/]",
1308
- "pending": "[dim]◌[/]",
1309
- }
1310
-
1311
- for s in all_sessions:
1312
- icon = status_icons.get(s.status.value, "?")
1313
- task_preview = s.task[:27] + "..." if len(s.task) > 30 else s.task
1314
- updated = time_ago(s.updated_at)
1315
-
1316
- # Get last assistant message preview
1317
- messages = manager.get_messages(s.id)
1318
- last_msg = ""
1319
- for msg in reversed(messages):
1320
- if msg.role == "assistant":
1321
- last_msg = msg.content.replace("\n", " ")[:37]
1322
- if len(msg.content) > 37:
1323
- last_msg += "..."
1324
- break
1325
-
1326
- # Style the last message based on recency
1327
- if s.status == SessStatus.RUNNING:
1328
- last_msg_styled = f"[yellow]{last_msg or '(working...)'}[/]"
1329
- updated_styled = f"[yellow]{updated}[/]"
1330
- elif s.status == SessStatus.COMPLETED:
1331
- # Highlight if recently completed (< 60s)
1332
- try:
1333
- dt = datetime.fromisoformat(s.updated_at)
1334
- is_recent = (datetime.now() - dt).total_seconds() < 60
1335
- except:
1336
- is_recent = False
1337
- if is_recent:
1338
- last_msg_styled = f"[green bold]{last_msg or '(done)'}[/]"
1339
- updated_styled = f"[green bold]{updated} ★[/]"
1340
- else:
1341
- last_msg_styled = f"[green]{last_msg or '(done)'}[/]"
1342
- updated_styled = f"[dim]{updated}[/]"
1343
- elif s.status == SessStatus.FAILED:
1344
- last_msg_styled = f"[red]{s.error[:37] if s.error else '(failed)'}...[/]"
1345
- updated_styled = f"[red]{updated}[/]"
1346
- else:
1347
- last_msg_styled = f"[dim]{last_msg or '-'}[/]"
1348
- updated_styled = f"[dim]{updated}[/]"
1349
-
1350
- table.add_row(
1351
- s.short_id,
1352
- icon,
1353
- str(s.turn),
1354
- task_preview,
1355
- updated_styled,
1356
- last_msg_styled,
1357
- )
1358
-
1359
- console.print(table)
1360
-
1361
- def parse_spawn_args(args: list[str]) -> dict:
1362
- """Parse spawn command arguments."""
1363
- parser = argparse.ArgumentParser(add_help=False)
1364
- parser.add_argument("task", nargs="*")
1365
- parser.add_argument("--dir", "-d", type=Path, default=None)
1366
- parser.add_argument("--model", "-m", default=None)
1367
- parser.add_argument("--async", dest="async_mode", action="store_true", default=False)
1368
-
1369
- try:
1370
- parsed, _ = parser.parse_known_args(args)
1371
- return {
1372
- "task": " ".join(parsed.task) if parsed.task else "",
1373
- "dir": parsed.dir,
1374
- "model": parsed.model,
1375
- "async_mode": parsed.async_mode,
1376
- }
1377
- except SystemExit:
1378
- return {"error": "Invalid spawn arguments"}
1379
-
1380
- def do_spawn(args: list[str]):
1381
- """Spawn a new coding agent session using CodexSessionManager (same as orchestrator)."""
1382
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1383
- import time
1384
-
1385
- parsed = parse_spawn_args(args)
1386
-
1387
- if "error" in parsed:
1388
- console.print(f" [red]{parsed['error']}[/]")
1389
- console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
1390
- return
1391
-
1392
- if not parsed["task"]:
1393
- console.print(" [red]Task required[/]")
1394
- console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
1395
- return
1396
-
1397
- task = parsed["task"]
1398
- work_dir = (parsed["dir"] or default_dir).absolute()
1399
- session_model = parsed["model"] or default_model
1400
- is_async = parsed.get("async_mode", False)
1401
-
1402
- if not work_dir.exists():
1403
- console.print(f" [red]Directory not found:[/] {work_dir}")
1404
- return
1405
-
1406
- mode_str = "async" if is_async else "sync"
1407
- console.print(f"\n[dim]Spawning {mode_str} session...[/]")
1408
- console.print(f" [dim]Dir: {work_dir}[/]")
1409
- console.print(f" [dim]Model: {session_model}[/]")
1410
-
1411
- try:
1412
- # Use CodexSessionManager - SAME as orchestrator's delegate()
1413
- manager = CodexSessionManager(work_dir / ".zwarm")
1414
- session = manager.start_session(
1415
- task=task,
1416
- working_dir=work_dir,
1417
- model=session_model,
1418
- sandbox="workspace-write",
1419
- source="user",
1420
- adapter="codex",
1421
- )
1422
-
1423
- console.print(f"\n[green]✓[/] Session: [cyan]{session.short_id}[/]")
1424
- console.print(f" [dim]PID: {session.pid}[/]")
1425
-
1426
- # For sync mode, wait for completion and show response
1427
- if not is_async:
1428
- console.print(f"\n[dim]Waiting for completion...[/]")
1429
- timeout = 300.0
1430
- start = time.time()
1431
- while time.time() - start < timeout:
1432
- # get_session() auto-updates status based on output completion
1433
- session = manager.get_session(session.id)
1434
- if session.status != SessStatus.RUNNING:
1435
- break
1436
- time.sleep(1.0)
1437
-
1438
- # Get all assistant responses
1439
- messages = manager.get_messages(session.id)
1440
- assistant_msgs = [m for m in messages if m.role == "assistant"]
1441
- if assistant_msgs:
1442
- console.print(f"\n[bold]Response ({len(assistant_msgs)} message{'s' if len(assistant_msgs) > 1 else ''}):[/]")
1443
- for msg in assistant_msgs:
1444
- preview = msg.content[:300]
1445
- if len(msg.content) > 300:
1446
- preview += "..."
1447
- console.print(preview)
1448
- if len(assistant_msgs) > 1:
1449
- console.print() # Blank line between multiple messages
1450
-
1451
- console.print(f"\n[dim]Use 'show {session.short_id}' to see full details[/]")
1452
- console.print(f"[dim]Use 'c {session.short_id} \"message\"' to continue[/]")
1453
-
1454
- except Exception as e:
1455
- console.print(f" [red]Error:[/] {e}")
1456
- import traceback
1457
- console.print(f" [dim]{traceback.format_exc()}[/]")
1458
-
1459
- def do_orchestrate(args: list[str]):
1460
- """Spawn an orchestrator agent that delegates to sub-sessions."""
1461
- parsed = parse_spawn_args(args)
1462
-
1463
- if "error" in parsed:
1464
- console.print(f" [red]{parsed['error']}[/]")
1465
- console.print(" [dim]Usage: orchestrate \"task\" [--dir PATH][/]")
1466
- return
1467
-
1468
- if not parsed["task"]:
1469
- console.print(" [red]Task required[/]")
1470
- console.print(" [dim]Usage: orchestrate \"task\" [--dir PATH][/]")
1471
- return
1472
-
1473
- task = parsed["task"]
1474
- work_dir = (parsed["dir"] or default_dir).absolute()
1475
-
1476
- if not work_dir.exists():
1477
- console.print(f" [red]Directory not found:[/] {work_dir}")
1478
- return
1479
-
1480
- console.print(f"\n[dim]Starting orchestrator...[/]")
1481
- console.print(f" [dim]Dir: {work_dir}[/]")
1482
- console.print(f" [dim]Task: {task[:60]}{'...' if len(task) > 60 else ''}[/]")
1338
+ run_interactive(working_dir=default_dir.absolute(), model=default_model)
1483
1339
 
1484
- import subprocess
1485
- from uuid import uuid4
1486
1340
 
1487
- # Generate instance ID for tracking
1488
- instance_id = str(uuid4())[:8]
1341
+ @app.command()
1342
+ def sessions(
1343
+ working_dir: Annotated[Path, typer.Option("--dir", "-d", help="Working directory")] = Path("."),
1344
+ clean: Annotated[bool, typer.Option("--clean", "-c", help="Delete all non-running sessions")] = False,
1345
+ kill_all: Annotated[bool, typer.Option("--kill-all", "-k", help="Kill all running sessions")] = False,
1346
+ rm: Annotated[Optional[str], typer.Option("--rm", help="Delete specific session ID")] = None,
1347
+ kill: Annotated[Optional[str], typer.Option("--kill", help="Kill specific session ID")] = None,
1348
+ ):
1349
+ """
1350
+ Quick session management.
1489
1351
 
1490
- # Build command to run orchestrator in background
1491
- cmd = [
1492
- sys.executable, "-m", "zwarm.cli.main", "orchestrate",
1493
- "--task", task,
1494
- "--working-dir", str(work_dir),
1495
- "--instance", instance_id,
1496
- ]
1352
+ By default, lists all sessions. Use flags for quick actions:
1497
1353
 
1498
- # Create log file for orchestrator output
1499
- log_dir = state_dir / "orchestrators"
1500
- log_dir.mkdir(parents=True, exist_ok=True)
1501
- log_file = log_dir / f"{instance_id}.log"
1354
+ [bold]Examples:[/]
1355
+ [dim]# List all sessions[/]
1356
+ $ zwarm sessions
1502
1357
 
1503
- try:
1504
- with open(log_file, "w") as f:
1505
- proc = subprocess.Popen(
1506
- cmd,
1507
- cwd=work_dir,
1508
- stdout=f,
1509
- stderr=subprocess.STDOUT,
1510
- start_new_session=True,
1511
- )
1358
+ [dim]# Delete all completed/failed sessions[/]
1359
+ $ zwarm sessions --clean
1360
+ $ zwarm sessions -c
1512
1361
 
1513
- console.print(f"\n[green]✓[/] Orchestrator started: [magenta]{instance_id}[/]")
1514
- console.print(f" PID: {proc.pid}")
1515
- console.print(f" Log: {log_file}")
1516
- console.print(f"\n[dim]Delegated sessions will appear with source 'orch:{instance_id[:4]}'[/]")
1517
- console.print(f"[dim]Use 'ls' to monitor progress[/]")
1362
+ [dim]# Kill all running sessions[/]
1363
+ $ zwarm sessions --kill-all
1364
+ $ zwarm sessions -k
1518
1365
 
1519
- except Exception as e:
1520
- console.print(f" [red]Error starting orchestrator:[/] {e}")
1366
+ [dim]# Delete a specific session[/]
1367
+ $ zwarm sessions --rm abc123
1521
1368
 
1522
- def do_peek(session_id: str):
1523
- """Quick peek at session - just status + latest message (for fast polling)."""
1524
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1369
+ [dim]# Kill a specific session[/]
1370
+ $ zwarm sessions --kill abc123
1371
+ """
1372
+ from zwarm.sessions import CodexSessionManager, SessionStatus
1373
+ from zwarm.core.costs import estimate_session_cost, format_cost
1525
1374
 
1526
- manager = CodexSessionManager(default_dir / ".zwarm")
1527
- session = manager.get_session(session_id)
1375
+ manager = CodexSessionManager(working_dir / ".zwarm")
1528
1376
 
1377
+ # Handle --kill (specific session)
1378
+ if kill:
1379
+ session = manager.get_session(kill)
1529
1380
  if not session:
1530
- console.print(f" [red]Not found:[/] {session_id}")
1531
- return
1532
-
1533
- # Status icon
1534
- icon = {
1535
- "running": "[yellow]●[/]",
1536
- "completed": "[green]✓[/]",
1537
- "failed": "[red]✗[/]",
1538
- "killed": "[dim]○[/]",
1539
- }.get(session.status.value, "?")
1540
-
1541
- # Get latest assistant message
1542
- messages = manager.get_messages(session.id)
1543
- latest = None
1544
- for msg in reversed(messages):
1545
- if msg.role == "assistant":
1546
- latest = msg.content.replace("\n", " ")
1547
- break
1548
-
1549
- if session.status == SessStatus.RUNNING:
1550
- console.print(f"{icon} [cyan]{session.short_id}[/] [yellow](running...)[/]")
1551
- elif latest:
1552
- # Truncate for one-liner
1553
- preview = latest[:120] + "..." if len(latest) > 120 else latest
1554
- console.print(f"{icon} [cyan]{session.short_id}[/] {preview}")
1555
- elif session.status == SessStatus.FAILED:
1556
- error = (session.error or "unknown")[:80]
1557
- console.print(f"{icon} [cyan]{session.short_id}[/] [red]{error}[/]")
1381
+ console.print(f"[red]Session not found:[/] {kill}")
1382
+ raise typer.Exit(1)
1383
+ if manager.kill_session(session.id):
1384
+ console.print(f"[green]✓[/] Killed {session.short_id}")
1558
1385
  else:
1559
- console.print(f"{icon} [cyan]{session.short_id}[/] [dim](no response)[/]")
1560
-
1561
- def do_show(session_id: str):
1562
- """Show full session details and messages using CodexSessionManager."""
1563
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1564
-
1565
- manager = CodexSessionManager(default_dir / ".zwarm")
1566
- session = manager.get_session(session_id)
1567
-
1568
- if not session:
1569
- console.print(f" [red]Session not found:[/] {session_id}")
1570
- console.print(f" [dim]Use 'ls' to see available sessions[/]")
1571
- return
1572
-
1573
- # Status styling
1574
- status_display = {
1575
- "running": "[yellow]● running[/]",
1576
- "completed": "[green]✓ completed[/]",
1577
- "failed": "[red]✗ failed[/]",
1578
- "killed": "[dim]○ killed[/]",
1579
- "pending": "[dim]◌ pending[/]",
1580
- }.get(session.status.value, str(session.status.value))
1581
-
1582
- console.print()
1583
- console.print(f"[bold cyan]Session {session.short_id}[/] {status_display}")
1584
- console.print(f"[dim]Adapter:[/] {session.adapter} [dim]│[/] [dim]Model:[/] {session.model}")
1585
- console.print(f"[dim]Task:[/] {session.task}")
1586
- console.print(f"[dim]Dir:[/] {session.working_dir} [dim]│[/] [dim]Turn:[/] {session.turn}")
1587
- console.print(f"[dim]Source:[/] {session.source_display} [dim]│[/] [dim]Runtime:[/] {session.runtime}")
1588
- if session.pid:
1589
- console.print(f"[dim]PID:[/] {session.pid}")
1590
-
1591
- # Show log file path
1592
- log_path = default_dir / ".zwarm" / "sessions" / session.id / "turns" / f"turn_{session.turn}.jsonl"
1593
- console.print(f"[dim]Log:[/] {log_path}")
1594
- console.print()
1595
-
1596
- # Get messages from manager
1597
- messages = manager.get_messages(session.id)
1598
-
1599
- if not messages:
1600
- if session.is_running:
1601
- console.print("[yellow]Session is still running...[/]")
1602
- else:
1603
- console.print("[dim]No messages captured.[/]")
1604
- return
1605
-
1606
- # Display messages
1607
- for msg in messages:
1608
- if msg.role == "user":
1609
- console.print(Panel(msg.content, title="[bold blue]User[/]", border_style="blue"))
1610
- elif msg.role == "assistant":
1611
- content = msg.content
1612
- if len(content) > 2000:
1613
- content = content[:2000] + "\n\n[dim]... (truncated)[/]"
1614
- console.print(Panel(content, title="[bold green]Assistant[/]", border_style="green"))
1615
- elif msg.role == "tool":
1616
- console.print(f" [dim]Tool: {msg.content[:100]}[/]")
1617
-
1618
- console.print()
1619
- if session.token_usage:
1620
- tokens = session.token_usage
1621
- console.print(f"[dim]Tokens: {tokens.get('input_tokens', 0):,} in / {tokens.get('output_tokens', 0):,} out[/]")
1622
-
1623
- if session.error:
1624
- console.print(f"[red]Error:[/] {session.error}")
1625
-
1626
- def do_trajectory(session_id: str, full: bool = False):
1627
- """Show the full trajectory of a session - all steps in order."""
1628
- from zwarm.sessions import CodexSessionManager
1629
-
1630
- manager = CodexSessionManager(default_dir / ".zwarm")
1631
- session = manager.get_session(session_id)
1632
-
1633
- if not session:
1634
- console.print(f" [red]Session not found:[/] {session_id}")
1635
- return
1636
-
1637
- trajectory = manager.get_trajectory(session_id, full=full)
1638
-
1639
- if not trajectory:
1640
- console.print("[dim]No trajectory data available.[/]")
1641
- return
1642
-
1643
- mode = "[bold](full)[/] " if full else ""
1644
- console.print(f"\n[bold cyan]Trajectory: {session.short_id}[/] {mode}({len(trajectory)} steps)")
1645
- console.print(f"[dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
1646
- console.print()
1647
-
1648
- # Display each step
1649
- for step in trajectory:
1650
- turn = step.get("turn", 1)
1651
- step_num = step.get("step", 0)
1652
- step_type = step.get("type", "unknown")
1653
-
1654
- prefix = f"[dim]T{turn}.{step_num:02d}[/]"
1655
-
1656
- if step_type == "reasoning":
1657
- if full and step.get("full_text"):
1658
- console.print(f"{prefix} [yellow]thinking:[/]")
1659
- console.print(f" {step['full_text']}")
1660
- else:
1661
- summary = step.get("summary", "")
1662
- console.print(f"{prefix} [yellow]thinking:[/] {summary}")
1663
-
1664
- elif step_type == "command":
1665
- cmd = step.get("command", "")
1666
- output = step.get("output", "")
1667
- exit_code = step.get("exit_code", "?")
1668
- # Show command
1669
- console.print(f"{prefix} [cyan]$ {cmd}[/]")
1670
- if output:
1671
- if full:
1672
- # Show all output
1673
- for line in output.split("\n"):
1674
- console.print(f" [dim]{line}[/]")
1675
- else:
1676
- # Indent output, max 5 lines
1677
- for line in output.split("\n")[:5]:
1678
- console.print(f" [dim]{line}[/]")
1679
- if output.count("\n") > 5:
1680
- console.print(f" [dim]... ({output.count(chr(10))} lines)[/]")
1681
- if exit_code != 0 and exit_code is not None:
1682
- console.print(f" [red]exit: {exit_code}[/]")
1683
-
1684
- elif step_type == "tool_call":
1685
- tool = step.get("tool", "unknown")
1686
- if full and step.get("full_args"):
1687
- import json
1688
- console.print(f"{prefix} [magenta]tool:[/] {tool}")
1689
- console.print(f" {json.dumps(step['full_args'], indent=2)}")
1690
- else:
1691
- args = step.get("args_preview", "")
1692
- console.print(f"{prefix} [magenta]tool:[/] {tool}({args})")
1693
-
1694
- elif step_type == "tool_output":
1695
- output = step.get("output", "")
1696
- if not full:
1697
- output = output[:100]
1698
- console.print(f"{prefix} [dim]→ {output}[/]")
1699
-
1700
- elif step_type == "message":
1701
- if full and step.get("full_text"):
1702
- console.print(f"{prefix} [green]response:[/]")
1703
- console.print(f" {step['full_text']}")
1704
- else:
1705
- summary = step.get("summary", "")
1706
- full_len = step.get("full_length", 0)
1707
- console.print(f"{prefix} [green]response:[/] {summary}")
1708
- if full_len > 200:
1709
- console.print(f" [dim]({full_len} chars total)[/]")
1710
-
1711
- console.print()
1712
-
1713
- def do_continue(session_id: str, message: str, wait: bool = True):
1714
- """
1715
- Continue a conversation using CodexSessionManager.inject_message().
1716
-
1717
- Works for both sync and async sessions:
1718
- - If session was sync (or wait=True): wait for response
1719
- - If session was async (or wait=False): fire-and-forget, check later with '?'
1720
- """
1721
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1722
- import time
1723
-
1724
- manager = CodexSessionManager(default_dir / ".zwarm")
1725
- session = manager.get_session(session_id)
1726
-
1727
- if not session:
1728
- console.print(f" [red]Session not found:[/] {session_id}")
1729
- console.print(f" [dim]Use 'ls' to see available sessions[/]")
1730
- return
1731
-
1732
- if session.status == SessStatus.RUNNING:
1733
- console.print("[yellow]Session is still running.[/]")
1734
- console.print("[dim]Wait for it to complete, or use '?' to check progress.[/]")
1735
- return
1736
-
1737
- if session.status == SessStatus.KILLED:
1738
- console.print("[yellow]Session was killed.[/]")
1739
- console.print("[dim]Start a new session with 'spawn'.[/]")
1740
- return
1741
-
1742
- # Determine if we should wait based on session source
1743
- # Sessions from 'user' with --async flag have source='user'
1744
- # We'll use wait parameter to control this
1745
- should_wait = wait
1746
-
1747
- console.print(f"\n[dim]Sending message to {session.short_id}...[/]")
1748
-
1749
- try:
1750
- # Inject message - spawns new background process
1751
- updated_session = manager.inject_message(session.id, message)
1752
-
1753
- if not updated_session:
1754
- console.print(f" [red]Failed to inject message[/]")
1755
- return
1756
-
1757
- console.print(f"[green]✓[/] Message sent (turn {updated_session.turn})")
1758
-
1759
- if should_wait:
1760
- # Sync mode: wait for completion
1761
- console.print(f"[dim]Waiting for response...[/]")
1762
- timeout = 300.0
1763
- start = time.time()
1764
- while time.time() - start < timeout:
1765
- # get_session() auto-updates status based on output completion
1766
- session = manager.get_session(session.id)
1767
- if session.status != SessStatus.RUNNING:
1768
- break
1769
- time.sleep(1.0)
1770
-
1771
- # Get the response (last assistant message)
1772
- messages = manager.get_messages(session.id)
1773
- response = ""
1774
- for msg in reversed(messages):
1775
- if msg.role == "assistant":
1776
- response = msg.content
1777
- break
1778
-
1779
- console.print(f"\n[bold]Response:[/]")
1780
- if len(response) > 500:
1781
- console.print(response[:500] + "...")
1782
- console.print(f"\n[dim]Use 'show {session.short_id}' to see full response[/]")
1783
- else:
1784
- console.print(response or "(no response captured)")
1785
- else:
1786
- # Async mode: return immediately
1787
- console.print(f"[dim]Running in background (PID: {updated_session.pid})[/]")
1788
- console.print(f"[dim]Use '? {session.short_id}' to check progress[/]")
1789
-
1790
- except Exception as e:
1791
- console.print(f" [red]Error:[/] {e}")
1792
- import traceback
1793
- console.print(f" [dim]{traceback.format_exc()}[/]")
1794
-
1795
- def do_check(session_id: str):
1796
- """Check status of a session using CodexSessionManager (same as orchestrator)."""
1797
- from zwarm.sessions import CodexSessionManager
1798
-
1799
- manager = CodexSessionManager(default_dir / ".zwarm")
1800
- session = manager.get_session(session_id)
1801
-
1802
- if not session:
1803
- console.print(f" [red]Session not found:[/] {session_id}")
1804
- console.print(f" [dim]Use 'ls' to see available sessions[/]")
1805
- return
1806
-
1807
- status_icon = {
1808
- "running": "●",
1809
- "completed": "✓",
1810
- "failed": "✗",
1811
- "killed": "○",
1812
- "pending": "◌",
1813
- }.get(session.status.value, "?")
1814
-
1815
- console.print(f"\n[bold]Session {session.short_id}[/]: {status_icon} {session.status.value}")
1816
- console.print(f" [dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
1817
- console.print(f" [dim]Turn: {session.turn} | Runtime: {session.runtime}[/]")
1818
-
1819
- if session.status.value == "completed":
1820
- messages = manager.get_messages(session.id)
1821
- for msg in reversed(messages):
1822
- if msg.role == "assistant":
1823
- console.print(f"\n[bold]Response:[/]")
1824
- if len(msg.content) > 500:
1825
- console.print(msg.content[:500] + "...")
1826
- else:
1827
- console.print(msg.content)
1828
- break
1829
- elif session.status.value == "failed":
1830
- console.print(f"[red]Error:[/] {session.error or 'Unknown error'}")
1831
- elif session.status.value == "running":
1832
- console.print("[dim]Session is still running...[/]")
1833
- console.print(f" [dim]PID: {session.pid}[/]")
1834
-
1835
- def do_kill(session_id: str):
1836
- """Kill a running session using CodexSessionManager (same as orchestrator)."""
1837
- from zwarm.sessions import CodexSessionManager
1838
-
1839
- manager = CodexSessionManager(default_dir / ".zwarm")
1840
- session = manager.get_session(session_id)
1386
+ console.print(f"[yellow]Session not running or already stopped[/]")
1387
+ return
1841
1388
 
1389
+ # Handle --rm (specific session)
1390
+ if rm:
1391
+ session = manager.get_session(rm)
1842
1392
  if not session:
1843
- console.print(f" [red]Session not found:[/] {session_id}")
1844
- return
1845
-
1846
- if not session.is_running:
1847
- console.print(f"[yellow]Session {session.short_id} is not running ({session.status.value}).[/]")
1848
- return
1849
-
1850
- try:
1851
- killed = manager.kill_session(session.id)
1852
- if killed:
1853
- console.print(f"[green]✓[/] Stopped session {session.short_id}")
1854
- else:
1855
- console.print(f"[red]Failed to stop session[/]")
1856
- except Exception as e:
1857
- console.print(f"[red]Failed to stop session:[/] {e}")
1858
-
1859
- def do_killall():
1860
- """Kill all running sessions using CodexSessionManager."""
1861
- from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
1862
-
1863
- manager = CodexSessionManager(default_dir / ".zwarm")
1864
- running_sessions = manager.list_sessions(status=SessStatus.RUNNING)
1393
+ console.print(f"[red]Session not found:[/] {rm}")
1394
+ raise typer.Exit(1)
1395
+ if manager.delete_session(session.id):
1396
+ console.print(f"[green]✓[/] Deleted {session.short_id}")
1397
+ else:
1398
+ console.print(f"[red]Failed to delete[/]")
1399
+ return
1865
1400
 
1866
- if not running_sessions:
1867
- console.print("[dim]No running sessions to kill.[/]")
1401
+ # Handle --kill-all
1402
+ if kill_all:
1403
+ running = manager.list_sessions(status=SessionStatus.RUNNING)
1404
+ if not running:
1405
+ console.print("[dim]No running sessions[/]")
1868
1406
  return
1869
-
1870
1407
  killed = 0
1871
- for session in running_sessions:
1872
- try:
1873
- if manager.kill_session(session.id):
1874
- killed += 1
1875
- except Exception as e:
1876
- console.print(f"[red]Failed to stop {session.short_id}:[/] {e}")
1877
-
1878
- console.print(f"[green]✓[/] Killed {killed} sessions")
1879
-
1880
- def do_clean():
1881
- """Remove old completed/failed sessions from disk."""
1882
- from zwarm.sessions import CodexSessionManager
1883
-
1884
- manager = CodexSessionManager(default_dir / ".zwarm")
1885
- cleaned = manager.cleanup_completed(keep_days=7)
1886
- console.print(f"[green]✓[/] Cleaned {cleaned} old sessions")
1887
-
1888
- def do_delete(session_id: str):
1889
- """Delete a session entirely (removes from disk and ls)."""
1890
- from zwarm.sessions import CodexSessionManager
1891
-
1892
- manager = CodexSessionManager(default_dir / ".zwarm")
1893
- session = manager.get_session(session_id)
1408
+ for s in running:
1409
+ if manager.kill_session(s.id):
1410
+ console.print(f" [green]✓[/] Killed {s.short_id}")
1411
+ killed += 1
1412
+ console.print(f"\n[green]Killed {killed} session(s)[/]")
1413
+ return
1894
1414
 
1895
- if not session:
1896
- console.print(f" [red]Session not found:[/] {session_id}")
1415
+ # Handle --clean
1416
+ if clean:
1417
+ all_sessions = manager.list_sessions()
1418
+ to_delete = [s for s in all_sessions if s.status != SessionStatus.RUNNING]
1419
+ if not to_delete:
1420
+ console.print("[dim]Nothing to clean[/]")
1897
1421
  return
1422
+ deleted = 0
1423
+ for s in to_delete:
1424
+ if manager.delete_session(s.id):
1425
+ deleted += 1
1426
+ console.print(f"[green]✓[/] Deleted {deleted} session(s)")
1427
+ return
1898
1428
 
1899
- if manager.delete_session(session.id):
1900
- console.print(f"[green]✓[/] Deleted session {session.short_id}")
1901
- else:
1902
- console.print(f"[red]Failed to delete session[/]")
1903
-
1904
- # REPL loop
1905
- import shlex
1906
-
1907
- while True:
1908
- try:
1909
- raw_input = console.input("[bold cyan]>[/] ").strip()
1910
- if not raw_input:
1911
- continue
1912
-
1913
- # Parse command
1914
- try:
1915
- parts = shlex.split(raw_input)
1916
- except ValueError:
1917
- parts = raw_input.split()
1918
-
1919
- cmd = parts[0].lower()
1920
- args = parts[1:]
1921
-
1922
- if cmd in ("q", "quit", "exit"):
1923
- active = [s for s in sessions.values() if s.status == SessionStatus.ACTIVE]
1924
- if active:
1925
- console.print(f" [yellow]Warning:[/] {len(active)} sessions still active")
1926
- console.print(" [dim]Use 'killall' first, or they'll continue in background[/]")
1927
-
1928
- # Cleanup adapters
1929
- for adapter_instance in adapters_cache.values():
1930
- try:
1931
- asyncio.run(adapter_instance.cleanup())
1932
- except Exception:
1933
- pass
1934
-
1935
- console.print("\n[dim]Goodbye![/]\n")
1936
- break
1937
-
1938
- elif cmd in ("h", "help"):
1939
- show_help()
1940
-
1941
- elif cmd in ("ls", "list"):
1942
- show_sessions()
1943
-
1944
- elif cmd == "spawn":
1945
- do_spawn(args)
1946
-
1947
- elif cmd == "async":
1948
- # Async spawn shorthand
1949
- do_spawn(args + ["--async"])
1950
-
1951
- elif cmd == "orchestrate":
1952
- do_orchestrate(args)
1953
-
1954
- elif cmd in ("?", "peek"):
1955
- if not args:
1956
- console.print(" [red]Usage:[/] ? SESSION_ID")
1957
- else:
1958
- do_peek(args[0])
1959
-
1960
- elif cmd in ("show", "check"):
1961
- if not args:
1962
- console.print(" [red]Usage:[/] show SESSION_ID")
1963
- else:
1964
- do_show(args[0])
1965
-
1966
- elif cmd in ("traj", "trajectory"):
1967
- if not args:
1968
- console.print(" [red]Usage:[/] traj SESSION_ID [--full]")
1969
- else:
1970
- full_mode = "--full" in args
1971
- session_arg = [a for a in args if a != "--full"]
1972
- if session_arg:
1973
- do_trajectory(session_arg[0], full=full_mode)
1974
- else:
1975
- console.print(" [red]Usage:[/] traj SESSION_ID [--full]")
1976
-
1977
- elif cmd in ("c", "continue"):
1978
- # Sync continue - waits for response
1979
- if len(args) < 2:
1980
- console.print(" [red]Usage:[/] c SESSION_ID \"message\"")
1981
- else:
1982
- do_continue(args[0], " ".join(args[1:]), wait=True)
1429
+ # Default: list all sessions
1430
+ all_sessions = manager.list_sessions()
1983
1431
 
1984
- elif cmd in ("ca", "async"):
1985
- # Async continue - fire and forget
1986
- if len(args) < 2:
1987
- console.print(" [red]Usage:[/] ca SESSION_ID \"message\"")
1988
- console.print(" [dim]Sends message and returns immediately (check with '?')[/]")
1989
- else:
1990
- do_continue(args[0], " ".join(args[1:]), wait=False)
1432
+ if not all_sessions:
1433
+ console.print("[dim]No sessions found.[/]")
1434
+ console.print("[dim]Start one with:[/] zwarm session start \"your task\"")
1435
+ return
1991
1436
 
1992
- elif cmd == "check":
1993
- if not args:
1994
- console.print(" [red]Usage:[/] check SESSION_ID")
1995
- else:
1996
- do_check(args[0])
1437
+ # Summary counts
1438
+ running = sum(1 for s in all_sessions if s.status == SessionStatus.RUNNING)
1439
+ completed = sum(1 for s in all_sessions if s.status == SessionStatus.COMPLETED)
1440
+ failed = sum(1 for s in all_sessions if s.status == SessionStatus.FAILED)
1441
+ killed_count = sum(1 for s in all_sessions if s.status == SessionStatus.KILLED)
1442
+
1443
+ parts = []
1444
+ if running:
1445
+ parts.append(f"[yellow]⟳ {running} running[/]")
1446
+ if completed:
1447
+ parts.append(f"[green]✓ {completed} completed[/]")
1448
+ if failed:
1449
+ parts.append(f"[red]✗ {failed} failed[/]")
1450
+ if killed_count:
1451
+ parts.append(f"[dim]⊘ {killed_count} killed[/]")
1452
+
1453
+ console.print(" │ ".join(parts))
1454
+ console.print()
1997
1455
 
1998
- elif cmd == "kill":
1999
- if not args:
2000
- console.print(" [red]Usage:[/] kill SESSION_ID")
2001
- else:
2002
- do_kill(args[0])
1456
+ # Table
1457
+ table = Table(box=None, show_header=True, header_style="bold dim")
1458
+ table.add_column("ID", style="cyan")
1459
+ table.add_column("", width=2)
1460
+ table.add_column("Task", max_width=45)
1461
+ table.add_column("Tokens", justify="right", style="dim")
1462
+ table.add_column("Cost", justify="right", style="dim")
2003
1463
 
2004
- elif cmd == "killall":
2005
- do_killall()
1464
+ status_icons = {
1465
+ SessionStatus.RUNNING: "[yellow]⟳[/]",
1466
+ SessionStatus.COMPLETED: "[green]✓[/]",
1467
+ SessionStatus.FAILED: "[red]✗[/]",
1468
+ SessionStatus.KILLED: "[dim]⊘[/]",
1469
+ SessionStatus.PENDING: "[dim]○[/]",
1470
+ }
2006
1471
 
2007
- elif cmd == "clean":
2008
- do_clean()
1472
+ for session in all_sessions:
1473
+ icon = status_icons.get(session.status, "?")
1474
+ task = session.task[:42] + "..." if len(session.task) > 45 else session.task
1475
+ tokens = session.token_usage.get("total_tokens", 0)
1476
+ tokens_str = f"{tokens:,}" if tokens else "-"
2009
1477
 
2010
- elif cmd in ("rm", "delete", "remove"):
2011
- if not args:
2012
- console.print(" [red]Usage:[/] rm SESSION_ID")
2013
- else:
2014
- do_delete(args[0])
1478
+ # Cost estimate
1479
+ cost_info = estimate_session_cost(session.model, session.token_usage)
1480
+ cost_str = format_cost(cost_info.get("cost"))
2015
1481
 
2016
- else:
2017
- console.print(f" [yellow]Unknown command:[/] {cmd}")
2018
- console.print(" [dim]Type 'help' for available commands[/]")
1482
+ table.add_row(session.short_id, icon, task, tokens_str, cost_str)
2019
1483
 
2020
- except KeyboardInterrupt:
2021
- console.print("\n[dim](Ctrl+C to exit, or type 'quit')[/]")
2022
- except EOFError:
2023
- console.print("\n[dim]Goodbye![/]\n")
2024
- break
2025
- except Exception as e:
2026
- console.print(f" [red]Error:[/] {e}")
1484
+ console.print(table)
1485
+ console.print()
1486
+ console.print("[dim]Quick actions: --clean (-c) delete old │ --kill-all (-k) stop running[/]")
2027
1487
 
2028
1488
 
2029
1489
  # =============================================================================