zwarm 3.2.1__py3-none-any.whl → 3.6.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/cli/main.py CHANGED
@@ -122,16 +122,6 @@ Manage zwarm configurations.
122
122
  app.add_typer(configs_app, name="configs")
123
123
 
124
124
 
125
- class AdapterType(str, Enum):
126
- codex_mcp = "codex_mcp"
127
- claude_code = "claude_code"
128
-
129
-
130
- class ModeType(str, Enum):
131
- sync = "sync"
132
- async_ = "async"
133
-
134
-
135
125
  @app.command()
136
126
  def orchestrate(
137
127
  task: Annotated[Optional[str], typer.Option("--task", "-t", help="The task to accomplish")] = None,
@@ -228,6 +218,26 @@ def orchestrate(
228
218
  if orchestrator.instance_id and not instance:
229
219
  console.print(f" [dim]Instance: {orchestrator.instance_id[:8]}[/]")
230
220
 
221
+ # Set up step callback for live progress display
222
+ def step_callback(step_num: int, tool_results: list) -> None:
223
+ """Print tool calls and results as they happen."""
224
+ if not tool_results:
225
+ return
226
+ for tool_info, result in tool_results:
227
+ name = tool_info.get("name", "?")
228
+ # Truncate args for display
229
+ args_str = str(tool_info.get("args", {}))
230
+ if len(args_str) > 80:
231
+ args_str = args_str[:77] + "..."
232
+ # Truncate result for display
233
+ result_str = str(result)
234
+ if len(result_str) > 100:
235
+ result_str = result_str[:97] + "..."
236
+ console.print(f"[dim]step {step_num}[/] → [cyan]{name}[/]({args_str})")
237
+ console.print(f" └ {result_str}")
238
+
239
+ orchestrator._step_callback = step_callback
240
+
231
241
  # Run the orchestrator loop
232
242
  console.print("[bold]--- Orchestrator running ---[/]\n")
233
243
  result = orchestrator.run(task=task)
@@ -289,7 +299,8 @@ def pilot(
289
299
  config: Annotated[Optional[Path], typer.Option("--config", "-c", help="Path to config YAML")] = None,
290
300
  overrides: Annotated[Optional[list[str]], typer.Option("--set", help="Override config (key=value)")] = None,
291
301
  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,
302
+ resume: Annotated[bool, typer.Option("--resume", help="Resume from previous state")] = False,
303
+ instance: Annotated[Optional[str], typer.Option("--instance", "-i", help="Instance ID (for isolation/resume)")] = None,
293
304
  instance_name: Annotated[Optional[str], typer.Option("--name", "-n", help="Human-readable instance name")] = None,
294
305
  model: Annotated[PilotLM, typer.Option("--model", "-m", help="LM to use")] = PilotLM.gpt5_verbose,
295
306
  ):
@@ -331,19 +342,30 @@ def pilot(
331
342
 
332
343
  [dim]# Named instance[/]
333
344
  $ zwarm pilot --name my-feature
345
+
346
+ [dim]# Resume a previous session[/]
347
+ $ zwarm pilot --resume --instance abc123
334
348
  """
335
349
  from zwarm.cli.pilot import run_pilot, build_pilot_orchestrator
336
350
 
337
351
  # Resolve task (optional for pilot)
338
352
  resolved_task = _resolve_task(task, task_file)
339
353
 
340
- console.print(f"[bold]Starting pilot session...[/]")
354
+ # Validate resume requirements
355
+ if resume and not instance:
356
+ console.print("[red]Error:[/] --resume requires --instance to specify which session to resume")
357
+ console.print(" [dim]Use 'zwarm instances' to list available instances[/]")
358
+ raise typer.Exit(1)
359
+
360
+ console.print(f"[bold]{'Resuming' if resume else 'Starting'} pilot session...[/]")
341
361
  console.print(f" Working dir: {working_dir.absolute()}")
342
362
  console.print(f" Model: {model.value}")
343
363
  if resolved_task:
344
364
  console.print(f" Initial task: {resolved_task[:60]}...")
345
365
  if instance:
346
366
  console.print(f" Instance: {instance}" + (f" ({instance_name})" if instance_name else ""))
367
+ if resume:
368
+ console.print(f" [yellow]Resuming from saved state...[/]")
347
369
  console.print()
348
370
 
349
371
  orchestrator = None
@@ -361,6 +383,12 @@ def pilot(
361
383
  if orchestrator.instance_id and not instance:
362
384
  console.print(f" [dim]Instance: {orchestrator.instance_id[:8]}[/]")
363
385
 
386
+ # Resume from saved state if requested
387
+ if resume:
388
+ orchestrator.load_state()
389
+ msg_count = len(orchestrator.messages)
390
+ console.print(f" [green]✓[/] Resumed with {msg_count} messages")
391
+
364
392
  # Run the pilot REPL
365
393
  run_pilot(orchestrator, initial_task=resolved_task)
366
394
 
@@ -384,78 +412,68 @@ def pilot(
384
412
  @app.command()
385
413
  def exec(
386
414
  task: Annotated[str, typer.Option("--task", "-t", help="Task to execute")],
387
- adapter: Annotated[AdapterType, typer.Option("--adapter", "-a", help="Executor adapter")] = AdapterType.codex_mcp,
388
- mode: Annotated[ModeType, typer.Option("--mode", "-m", help="Execution mode")] = ModeType.sync,
389
415
  working_dir: Annotated[Path, typer.Option("--working-dir", "-w", help="Working directory")] = Path("."),
390
416
  model: Annotated[Optional[str], typer.Option("--model", help="Model override")] = None,
417
+ wait: Annotated[bool, typer.Option("--wait", help="Wait for completion and show result")] = False,
391
418
  ):
392
419
  """
393
- Run a single executor directly (for testing).
420
+ Run a single Codex session directly (for testing).
394
421
 
395
- Useful for testing adapters without the full orchestrator loop.
422
+ Spawns a session using CodexSessionManager - same as interactive/pilot.
423
+ Web search is always enabled via .codex/config.toml (set up by `zwarm init`).
396
424
 
397
425
  [bold]Examples:[/]
398
- [dim]# Test Codex[/]
399
- $ zwarm exec --task "What is 2+2?"
426
+ [dim]# Quick test[/]
427
+ $ zwarm exec --task "What is 2+2?" --wait
400
428
 
401
- [dim]# Test Claude Code[/]
402
- $ zwarm exec -a claude_code --task "List files in current dir"
429
+ [dim]# Run in background[/]
430
+ $ zwarm exec --task "Build feature"
403
431
 
404
- [dim]# Async mode[/]
405
- $ zwarm exec --task "Build feature" --mode async
432
+ [dim]# Web search is always available[/]
433
+ $ zwarm exec --task "Find latest FastAPI docs" --wait
406
434
  """
407
- from zwarm.adapters import get_adapter
408
-
409
- console.print(f"[bold]Running executor directly...[/]")
410
- console.print(f" Adapter: [cyan]{adapter.value}[/]")
411
- console.print(f" Mode: {mode.value}")
412
- console.print(f" Task: {task}")
413
-
414
- # Use isolated codex config if available
415
- config_path = working_dir / ".zwarm" / "codex.toml"
416
- if not config_path.exists():
417
- config_path = None
418
-
419
- try:
420
- executor = get_adapter(adapter.value, model=model, config_path=config_path)
421
- except ValueError as e:
422
- console.print(f"[red]Error:[/] {e}")
423
- sys.exit(1)
424
-
425
- async def run():
426
- try:
427
- session = await executor.start_session(
428
- task=task,
429
- working_dir=working_dir.absolute(),
430
- mode=mode.value,
431
- model=model,
432
- )
433
-
434
- console.print(f"\n[green]Session started:[/] {session.id[:8]}")
435
-
436
- if mode == ModeType.sync:
437
- response = session.messages[-1].content if session.messages else "(no response)"
438
- console.print(f"\n[bold]Response:[/]\n{response}")
435
+ import time
436
+ from zwarm.sessions import CodexSessionManager, SessionStatus
439
437
 
440
- # Interactive loop for sync mode
441
- while True:
442
- try:
443
- user_input = console.input("\n[dim]> (type message or 'exit')[/] ")
444
- if user_input.lower() == "exit" or not user_input:
445
- break
438
+ console.print(f"[bold]Running Codex session...[/]")
439
+ console.print(f" Task: {task[:60]}{'...' if len(task) > 60 else ''}")
440
+ if model:
441
+ console.print(f" Model: {model}")
446
442
 
447
- response = await executor.send_message(session, user_input)
448
- console.print(f"\n[bold]Response:[/]\n{response}")
449
- except KeyboardInterrupt:
450
- break
451
- else:
452
- console.print("[dim]Async mode - session running in background.[/]")
453
- console.print("Use 'zwarm status' to check progress.")
443
+ manager = CodexSessionManager(working_dir / ".zwarm")
444
+ effective_model = model or "gpt-5.1-codex-mini"
454
445
 
455
- finally:
456
- await executor.cleanup()
446
+ session = manager.start_session(
447
+ task=task,
448
+ working_dir=working_dir.absolute(),
449
+ model=effective_model,
450
+ )
457
451
 
458
- asyncio.run(run())
452
+ console.print(f"\n[green]Session started:[/] {session.short_id}")
453
+
454
+ if wait:
455
+ console.print("[dim]Waiting for completion...[/]")
456
+ while True:
457
+ time.sleep(2)
458
+ session = manager.get_session(session.id)
459
+ if session.status != SessionStatus.RUNNING:
460
+ break
461
+
462
+ if session.status == SessionStatus.COMPLETED:
463
+ console.print(f"\n[green]✓ Completed[/]")
464
+ # Show last assistant message
465
+ for msg in reversed(session.messages):
466
+ if msg.role == "assistant":
467
+ console.print(f"\n[bold]Response:[/]\n{msg.content}")
468
+ break
469
+ else:
470
+ console.print(f"\n[red]Status:[/] {session.status.value}")
471
+ if session.error:
472
+ console.print(f"[red]Error:[/] {session.error}")
473
+ else:
474
+ console.print("[dim]Running in background. Check with:[/]")
475
+ console.print(f" zwarm sessions")
476
+ console.print(f" zwarm session show {session.short_id}")
459
477
 
460
478
 
461
479
  @app.command()
@@ -740,12 +758,12 @@ def init(
740
758
  [bold]Creates:[/]
741
759
  [cyan].zwarm/[/] State directory for sessions and events
742
760
  [cyan].zwarm/config.toml[/] Runtime settings (weave, adapter, watchers)
743
- [cyan].zwarm/codex.toml[/] Codex CLI settings (model, reasoning effort)
761
+ [cyan].zwarm/codex.toml[/] Codex CLI settings (model, web search, etc.)
744
762
  [cyan]zwarm.yaml[/] Project config (optional, with --with-project)
745
763
 
746
764
  [bold]Configuration relationship:[/]
747
765
  config.toml → Controls zwarm itself (tracing, which watchers run)
748
- codex.toml → Controls the Codex CLI that runs executor sessions
766
+ codex.toml → Codex settings, parsed by zwarm and passed via -c overrides
749
767
  zwarm.yaml → Project-specific context injected into orchestrator
750
768
 
751
769
  [bold]Examples:[/]
@@ -917,6 +935,23 @@ def init(
917
935
  codex_toml_path.write_text(codex_content)
918
936
  console.print(f" [green]✓[/] Created .zwarm/codex.toml")
919
937
 
938
+ # Create claude.toml for isolated Claude Code configuration
939
+ claude_toml_path = state_dir / "claude.toml"
940
+ write_claude_toml = True
941
+ if claude_toml_path.exists():
942
+ if not non_interactive:
943
+ overwrite_claude = typer.confirm(" .zwarm/claude.toml exists. Overwrite?", default=False)
944
+ if not overwrite_claude:
945
+ write_claude_toml = False
946
+ console.print(" [dim]Skipping claude.toml[/]")
947
+ else:
948
+ write_claude_toml = False # Don't overwrite in non-interactive mode
949
+
950
+ if write_claude_toml:
951
+ claude_content = _generate_claude_toml(model="sonnet")
952
+ claude_toml_path.write_text(claude_content)
953
+ console.print(f" [green]✓[/] Created .zwarm/claude.toml")
954
+
920
955
  # Create zwarm.yaml
921
956
  if create_project_config:
922
957
  if zwarm_yaml_path.exists() and not non_interactive:
@@ -939,7 +974,7 @@ def init(
939
974
  # Explain config files
940
975
  console.print("[bold]Configuration files:[/]")
941
976
  console.print(" [cyan].zwarm/config.toml[/] - Runtime settings (Weave tracing, watchers)")
942
- console.print(" [cyan].zwarm/codex.toml[/] - Codex CLI settings (model, reasoning effort)")
977
+ console.print(" [cyan].zwarm/codex.toml[/] - Codex CLI settings (model, web search, sandbox)")
943
978
  if create_project_config:
944
979
  console.print(" [cyan]zwarm.yaml[/] - Project context and constraints")
945
980
  console.print()
@@ -959,40 +994,87 @@ def _generate_config_toml(
959
994
  adapter: str = "codex_mcp",
960
995
  watchers: list[str] | None = None,
961
996
  ) -> str:
962
- """Generate config.toml content."""
997
+ """Generate config.toml content with all options at their defaults."""
963
998
  watchers = watchers or []
964
999
 
965
1000
  lines = [
966
1001
  "# zwarm configuration",
967
1002
  "# Generated by 'zwarm init'",
1003
+ "# All values shown are defaults - uncomment and modify as needed",
968
1004
  "",
1005
+ "# ============================================================================",
1006
+ "# Weave Integration (optional tracing/observability)",
1007
+ "# ============================================================================",
969
1008
  "[weave]",
970
1009
  ]
971
1010
 
972
1011
  if weave_project:
973
1012
  lines.append(f'project = "{weave_project}"')
974
1013
  else:
975
- lines.append("# project = \"your-entity/your-project\" # Uncomment to enable Weave tracing")
1014
+ lines.append('# project = "your-entity/your-project" # Uncomment to enable Weave tracing')
976
1015
 
977
1016
  lines.extend([
1017
+ "enabled = true",
978
1018
  "",
1019
+ "# ============================================================================",
1020
+ "# Orchestrator Settings",
1021
+ "# ============================================================================",
979
1022
  "[orchestrator]",
980
- "max_steps = 50",
1023
+ '# lm = "gpt-5-mini" # LLM for orchestrator (gpt-5-mini, gpt-5, claude-sonnet-4)',
1024
+ "max_steps = 50 # Max steps for orchestrate command",
1025
+ "max_steps_per_turn = 60 # Max steps per turn in pilot mode",
1026
+ "parallel_delegations = 4 # Max concurrent delegations",
1027
+ '# prompt = "path/to/prompt.yaml" # Custom prompt file (optional)',
1028
+ '# allowed_dirs = ["*"] # Directories agent can delegate to (default: working_dir only)',
1029
+ "",
1030
+ "# Context window compaction (prevents overflow on long tasks)",
1031
+ "[orchestrator.compaction]",
1032
+ "enabled = true",
1033
+ "max_tokens = 100000 # Trigger compaction above this",
1034
+ "threshold_pct = 0.85 # Compact when at this % of max_tokens",
1035
+ "target_pct = 0.7 # Target this % after compaction",
1036
+ "keep_first_n = 2 # Always keep first N messages (system + task)",
1037
+ "keep_last_n = 10 # Always keep last N messages (recent context)",
981
1038
  "",
1039
+ "# ============================================================================",
1040
+ "# Executor Settings (codex agent configuration)",
1041
+ "# ============================================================================",
982
1042
  "[executor]",
983
- f'adapter = "{adapter}"',
984
- "# model = \"\" # Optional model override",
1043
+ f'adapter = "{adapter}" # codex_mcp | codex_exec | claude_code',
1044
+ '# model = "gpt-5.1-codex-mini" # Model for delegated sessions (uses codex.toml default if not set)',
1045
+ 'sandbox = "workspace-write" # read-only | workspace-write | danger-full-access',
1046
+ "timeout = 3600 # Session timeout in seconds",
1047
+ 'reasoning_effort = "high" # low | medium | high',
985
1048
  "",
1049
+ "# ============================================================================",
1050
+ "# Watchers (automated monitoring and nudges)",
1051
+ "# ============================================================================",
986
1052
  "[watchers]",
987
- f"enabled = {watchers}",
1053
+ f"enabled = {str(bool(watchers)).lower()}",
1054
+ 'message_role = "user" # Role for nudge messages: user | assistant | system',
988
1055
  "",
989
- "# Watcher-specific configuration",
990
- "# [watchers.budget]",
991
- "# max_steps = 50",
1056
+ "# Default watchers: progress, budget, delegation_reminder",
1057
+ "# Uncomment below to customize:",
1058
+ "",
1059
+ "# [[watchers.watchers]]",
1060
+ '# name = "progress"',
1061
+ "# enabled = true",
1062
+ "",
1063
+ "# [[watchers.watchers]]",
1064
+ '# name = "budget"',
1065
+ "# enabled = true",
1066
+ "# [watchers.watchers.config]",
1067
+ "# max_sessions = 10",
992
1068
  "# warn_at_percent = 80",
993
1069
  "",
994
- "# [watchers.pattern]",
995
- "# patterns = [\"DROP TABLE\", \"rm -rf\"]",
1070
+ "# [[watchers.watchers]]",
1071
+ '# name = "delegation_reminder"',
1072
+ "# enabled = true",
1073
+ "",
1074
+ "# ============================================================================",
1075
+ "# State Directory",
1076
+ "# ============================================================================",
1077
+ '# state_dir = ".zwarm" # Where to store session data',
996
1078
  "",
997
1079
  ])
998
1080
 
@@ -1006,28 +1088,73 @@ def _generate_codex_toml(
1006
1088
  """
1007
1089
  Generate codex.toml for isolated codex configuration.
1008
1090
 
1009
- This file is used by zwarm instead of ~/.codex/config.toml to ensure
1010
- consistent behavior across different environments.
1091
+ This file is parsed by zwarm and settings are passed to codex via -c overrides.
1092
+ Each .zwarm directory has its own codex config, independent of ~/.codex/config.toml.
1011
1093
  """
1012
1094
  lines = [
1013
1095
  "# Codex configuration for zwarm",
1014
- "# This file isolates zwarm's codex settings from your global ~/.codex/config.toml",
1096
+ "# zwarm parses this file and passes settings to codex via -c overrides",
1097
+ "# Each .zwarm dir has its own config, independent of ~/.codex/config.toml",
1015
1098
  "# Generated by 'zwarm init'",
1016
1099
  "",
1017
1100
  "# Model settings",
1018
1101
  f'model = "{model}"',
1019
1102
  f'model_reasoning_effort = "{reasoning_effort}" # low | medium | high',
1020
1103
  "",
1021
- "# Approval settings - zwarm manages these automatically",
1022
- "# disable_response_storage = false",
1104
+ "# DANGER MODE - bypasses all safety controls",
1105
+ "# Set to true to use --dangerously-bypass-approvals-and-sandbox",
1106
+ "full_danger = true",
1107
+ "",
1108
+ "# Web search - enables web_search tool for agents",
1109
+ "[features]",
1110
+ "web_search_request = true",
1023
1111
  "",
1024
- "# You can override any codex setting here",
1112
+ "# Sandbox settings - network access required for web search",
1113
+ "[sandbox_workspace_write]",
1114
+ "network_access = true",
1115
+ "",
1116
+ "# Approval policy - 'never' means no human approval needed",
1117
+ "# approval_policy = \"never\"",
1118
+ "",
1119
+ "# You can add any codex config key here",
1025
1120
  "# See: https://github.com/openai/codex#configuration",
1026
1121
  "",
1027
1122
  ]
1028
1123
  return "\n".join(lines)
1029
1124
 
1030
1125
 
1126
+ def _generate_claude_toml(
1127
+ model: str = "sonnet",
1128
+ ) -> str:
1129
+ """
1130
+ Generate claude.toml for isolated Claude Code configuration.
1131
+
1132
+ This file is parsed by zwarm and settings are passed to claude via CLI flags.
1133
+ Each .zwarm directory has its own claude config.
1134
+ """
1135
+ lines = [
1136
+ "# Claude Code configuration for zwarm",
1137
+ "# zwarm parses this file and passes settings to claude via CLI flags",
1138
+ "# Each .zwarm dir has its own config",
1139
+ "# Generated by 'zwarm init'",
1140
+ "",
1141
+ "# Model settings",
1142
+ f'model = "{model}" # sonnet | opus | haiku',
1143
+ "",
1144
+ "# DANGER MODE - bypasses all permission checks",
1145
+ "# Set to true to use --dangerously-skip-permissions",
1146
+ "full_danger = true",
1147
+ "",
1148
+ "# Note: Claude Code uses different CLI flags than Codex",
1149
+ "# Common options:",
1150
+ "# --model <model> Model to use (sonnet, opus, haiku)",
1151
+ "# --add-dir <path> Additional directories to allow",
1152
+ "# --allowed-tools <tools> Restrict available tools",
1153
+ "",
1154
+ ]
1155
+ return "\n".join(lines)
1156
+
1157
+
1031
1158
  def _generate_zwarm_yaml(
1032
1159
  description: str = "",
1033
1160
  context: str = "",
@@ -1534,6 +1661,7 @@ def session_start(
1534
1661
  Start a new Codex session in the background.
1535
1662
 
1536
1663
  The session runs independently and you can check on it later.
1664
+ Web search is always enabled via .codex/config.toml (set up by `zwarm init`).
1537
1665
 
1538
1666
  [bold]Examples:[/]
1539
1667
  [dim]# Simple task[/]
@@ -1541,6 +1669,9 @@ def session_start(
1541
1669
 
1542
1670
  [dim]# With specific model[/]
1543
1671
  $ zwarm session start "Refactor the API" --model gpt-5.1-codex-max
1672
+
1673
+ [dim]# Web search is always available[/]
1674
+ $ zwarm session start "Research latest OAuth2 best practices"
1544
1675
  """
1545
1676
  from zwarm.sessions import CodexSessionManager
1546
1677