zwarm 1.1.1__py3-none-any.whl → 1.3.2__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/core/config.py CHANGED
@@ -62,6 +62,12 @@ class OrchestratorConfig:
62
62
  sync_first: bool = True # prefer sync mode by default
63
63
  compaction: CompactionConfig = field(default_factory=CompactionConfig)
64
64
 
65
+ # Directory restrictions for agent delegations
66
+ # None = only working_dir allowed (most restrictive, default)
67
+ # ["*"] = any directory allowed (dangerous)
68
+ # ["/path/a", "/path/b"] = only these directories allowed
69
+ allowed_dirs: list[str] | None = None
70
+
65
71
 
66
72
  @dataclass
67
73
  class WatcherConfigItem:
@@ -188,9 +194,24 @@ def load_env(path: Path | None = None) -> None:
188
194
 
189
195
 
190
196
  def load_toml_config(path: Path | None = None) -> dict[str, Any]:
191
- """Load config.toml file."""
197
+ """
198
+ Load config.toml file.
199
+
200
+ Search order:
201
+ 1. Explicit path (if provided)
202
+ 2. .zwarm/config.toml (new standard location)
203
+ 3. config.toml (legacy location for backwards compat)
204
+ """
192
205
  if path is None:
193
- path = Path.cwd() / "config.toml"
206
+ # Try new location first
207
+ new_path = Path.cwd() / ".zwarm" / "config.toml"
208
+ legacy_path = Path.cwd() / "config.toml"
209
+ if new_path.exists():
210
+ path = new_path
211
+ elif legacy_path.exists():
212
+ path = legacy_path
213
+ else:
214
+ return {}
194
215
  if not path.exists():
195
216
  return {}
196
217
  with open(path, "rb") as f:
zwarm/core/state.py CHANGED
@@ -1,16 +1,25 @@
1
1
  """
2
2
  Flat-file state management for zwarm.
3
3
 
4
- State structure:
4
+ State structure (with instance isolation):
5
5
  .zwarm/
6
- ├── state.json # Current state (sessions, tasks)
7
- ├── events.jsonl # Append-only event log
8
- ├── sessions/
9
- │ └── <session-id>/
10
- ├── messages.json # Full conversation history
11
- │ └── output.log # Agent stdout/stderr
12
- └── orchestrator/
13
- └── messages.json # Orchestrator's message history (for resume)
6
+ ├── instances.json # Registry of all instances
7
+ └── instances/
8
+ └── <instance-id>/
9
+ ├── state.json # Current state (sessions, tasks)
10
+ ├── events.jsonl # Append-only event log
11
+ ├── sessions/
12
+ └── <session-id>/
13
+ │ ├── messages.json
14
+ │ └── output.log
15
+ └── orchestrator/
16
+ └── messages.json # Orchestrator's message history (for resume)
17
+
18
+ Legacy structure (single instance, for backwards compat):
19
+ .zwarm/
20
+ ├── state.json
21
+ ├── events.jsonl
22
+ └── ...
14
23
  """
15
24
 
16
25
  from __future__ import annotations
@@ -19,10 +28,116 @@ import json
19
28
  from datetime import datetime
20
29
  from pathlib import Path
21
30
  from typing import Any
31
+ from uuid import uuid4
22
32
 
23
33
  from .models import ConversationSession, Event, Task
24
34
 
25
35
 
36
+ # --- Instance Registry ---
37
+
38
+ def get_instances_registry_path(base_dir: Path | str = ".zwarm") -> Path:
39
+ """Get path to the instances registry file."""
40
+ return Path(base_dir) / "instances.json"
41
+
42
+
43
+ def list_instances(base_dir: Path | str = ".zwarm") -> list[dict[str, Any]]:
44
+ """List all registered instances."""
45
+ registry_path = get_instances_registry_path(base_dir)
46
+ if not registry_path.exists():
47
+ return []
48
+ try:
49
+ return json.loads(registry_path.read_text()).get("instances", [])
50
+ except (json.JSONDecodeError, KeyError):
51
+ return []
52
+
53
+
54
+ def register_instance(
55
+ instance_id: str,
56
+ name: str | None = None,
57
+ task: str | None = None,
58
+ base_dir: Path | str = ".zwarm",
59
+ ) -> None:
60
+ """Register an instance in the global registry."""
61
+ base = Path(base_dir)
62
+ base.mkdir(parents=True, exist_ok=True)
63
+
64
+ registry_path = get_instances_registry_path(base_dir)
65
+
66
+ # Load existing registry
67
+ if registry_path.exists():
68
+ try:
69
+ registry = json.loads(registry_path.read_text())
70
+ except json.JSONDecodeError:
71
+ registry = {"instances": []}
72
+ else:
73
+ registry = {"instances": []}
74
+
75
+ # Check if instance already registered
76
+ existing_ids = {inst["id"] for inst in registry["instances"]}
77
+ if instance_id in existing_ids:
78
+ # Update existing entry
79
+ for inst in registry["instances"]:
80
+ if inst["id"] == instance_id:
81
+ inst["updated_at"] = datetime.now().isoformat()
82
+ inst["status"] = "active"
83
+ if name:
84
+ inst["name"] = name
85
+ if task:
86
+ inst["task"] = task[:100] # Truncate
87
+ break
88
+ else:
89
+ # Add new entry
90
+ registry["instances"].append({
91
+ "id": instance_id,
92
+ "name": name or instance_id[:8],
93
+ "task": (task[:100] if task else None),
94
+ "created_at": datetime.now().isoformat(),
95
+ "updated_at": datetime.now().isoformat(),
96
+ "status": "active",
97
+ })
98
+
99
+ registry_path.write_text(json.dumps(registry, indent=2))
100
+
101
+
102
+ def update_instance_status(
103
+ instance_id: str,
104
+ status: str,
105
+ base_dir: Path | str = ".zwarm",
106
+ ) -> None:
107
+ """Update an instance's status in the registry."""
108
+ registry_path = get_instances_registry_path(base_dir)
109
+ if not registry_path.exists():
110
+ return
111
+
112
+ try:
113
+ registry = json.loads(registry_path.read_text())
114
+ except json.JSONDecodeError:
115
+ return
116
+
117
+ for inst in registry.get("instances", []):
118
+ if inst["id"] == instance_id:
119
+ inst["status"] = status
120
+ inst["updated_at"] = datetime.now().isoformat()
121
+ break
122
+
123
+ registry_path.write_text(json.dumps(registry, indent=2))
124
+
125
+
126
+ def get_instance_state_dir(
127
+ instance_id: str | None = None,
128
+ base_dir: Path | str = ".zwarm",
129
+ ) -> Path:
130
+ """
131
+ Get the state directory for an instance.
132
+
133
+ If instance_id is None, returns the legacy path for backwards compat.
134
+ """
135
+ base = Path(base_dir)
136
+ if instance_id is None:
137
+ return base # Legacy: .zwarm/
138
+ return base / "instances" / instance_id
139
+
140
+
26
141
  def _json_serializer(obj: Any) -> Any:
27
142
  """Custom JSON serializer for non-standard types."""
28
143
  # Handle pydantic models
@@ -42,15 +157,31 @@ class StateManager:
42
157
  """
43
158
  Manages flat-file state for zwarm.
44
159
 
45
- All state is stored as JSON files in a directory (default: .zwarm/).
160
+ All state is stored as JSON files in a directory.
161
+ With instance isolation: .zwarm/instances/<instance-id>/
162
+ Legacy (no instance): .zwarm/
163
+
46
164
  This enables:
47
165
  - Git-backed history
48
166
  - Easy debugging (just read the files)
49
167
  - Resume from previous state
168
+ - Multiple concurrent orchestrators (with instance isolation)
50
169
  """
51
170
 
52
- def __init__(self, state_dir: Path | str = ".zwarm"):
53
- self.state_dir = Path(state_dir)
171
+ def __init__(
172
+ self,
173
+ state_dir: Path | str = ".zwarm",
174
+ instance_id: str | None = None,
175
+ ):
176
+ self.base_dir = Path(state_dir)
177
+ self.instance_id = instance_id
178
+
179
+ # Resolve actual state directory
180
+ if instance_id:
181
+ self.state_dir = get_instance_state_dir(instance_id, self.base_dir)
182
+ else:
183
+ self.state_dir = self.base_dir
184
+
54
185
  self._sessions: dict[str, ConversationSession] = {}
55
186
  self._tasks: dict[str, Task] = {}
56
187
  self._orchestrator_messages: list[dict[str, Any]] = []
zwarm/orchestrator.py CHANGED
@@ -21,9 +21,7 @@ from pydantic import Field, PrivateAttr
21
21
  from wbal.agents.yaml_agent import YamlAgent
22
22
  from wbal.helper import TOOL_CALL_TYPE, format_openai_tool_response
23
23
 
24
- from zwarm.adapters.base import ExecutorAdapter
25
- from zwarm.adapters.claude_code import ClaudeCodeAdapter
26
- from zwarm.adapters.codex_mcp import CodexMCPAdapter
24
+ from zwarm.adapters import ExecutorAdapter, get_adapter
27
25
  from zwarm.core.compact import compact_messages, should_compact
28
26
  from zwarm.core.config import ZwarmConfig, load_config
29
27
  from zwarm.core.environment import OrchestratorEnv
@@ -54,6 +52,10 @@ class Orchestrator(YamlAgent):
54
52
  config: ZwarmConfig = Field(default_factory=ZwarmConfig)
55
53
  working_dir: Path = Field(default_factory=Path.cwd)
56
54
 
55
+ # Instance identification (for multi-orchestrator isolation)
56
+ instance_id: str | None = Field(default=None)
57
+ instance_name: str | None = Field(default=None)
58
+
57
59
  # Load tools from modules (delegation + bash for verification)
58
60
  agent_tool_modules: list[str] = Field(
59
61
  default=[
@@ -79,11 +81,25 @@ class Orchestrator(YamlAgent):
79
81
  """Initialize state and adapters after model creation."""
80
82
  super().model_post_init(__context)
81
83
 
82
- # Initialize state manager
83
- self._state = StateManager(self.working_dir / self.config.state_dir)
84
+ # Initialize state manager with instance isolation
85
+ base_state_dir = self.working_dir / self.config.state_dir
86
+ self._state = StateManager(
87
+ state_dir=base_state_dir,
88
+ instance_id=self.instance_id,
89
+ )
84
90
  self._state.init()
85
91
  self._state.load()
86
92
 
93
+ # Register instance if using instance isolation
94
+ if self.instance_id:
95
+ from zwarm.core.state import register_instance
96
+ register_instance(
97
+ instance_id=self.instance_id,
98
+ name=self.instance_name,
99
+ task=None, # Will be updated when task is set
100
+ base_dir=base_state_dir,
101
+ )
102
+
87
103
  # Load existing sessions
88
104
  for session in self._state.list_sessions():
89
105
  self._sessions[session.id] = session
@@ -123,16 +139,11 @@ class Orchestrator(YamlAgent):
123
139
  return self._state
124
140
 
125
141
  def _get_adapter(self, name: str) -> ExecutorAdapter:
126
- """Get or create an adapter by name."""
142
+ """Get or create an adapter by name using the adapter registry."""
127
143
  if name not in self._adapters:
128
144
  # Get model from config (adapters have their own defaults if None)
129
145
  model = self.config.executor.model
130
- if name == "codex_mcp":
131
- self._adapters[name] = CodexMCPAdapter(model=model)
132
- elif name == "claude_code":
133
- self._adapters[name] = ClaudeCodeAdapter(model=model)
134
- else:
135
- raise ValueError(f"Unknown adapter: {name}")
146
+ self._adapters[name] = get_adapter(name, model=model)
136
147
  return self._adapters[name]
137
148
 
138
149
  def get_executor_usage(self) -> dict[str, int]:
@@ -222,12 +233,18 @@ class Orchestrator(YamlAgent):
222
233
  if not self._resumed:
223
234
  return
224
235
 
225
- # Build list of old sessions
236
+ # Build list of old sessions and INVALIDATE their conversation IDs
237
+ # The MCP server was restarted, so all conversation IDs are now stale
226
238
  old_sessions = []
239
+ invalidated_count = 0
227
240
  for sid, session in self._sessions.items():
228
241
  old_sessions.append(
229
242
  f" - {sid[:8]}... ({session.adapter}, {session.status.value})"
230
243
  )
244
+ # Clear stale conversation_id to prevent converse() from trying to use it
245
+ if session.conversation_id:
246
+ session.conversation_id = None
247
+ invalidated_count += 1
231
248
 
232
249
  session_info = "\n".join(old_sessions) if old_sessions else " (none)"
233
250
 
@@ -235,14 +252,14 @@ class Orchestrator(YamlAgent):
235
252
  "role": "user",
236
253
  "content": f"""[SYSTEM NOTICE] You have been resumed from a previous session.
237
254
 
238
- IMPORTANT: Your previous executor sessions are NO LONGER ACTIVE. The MCP connections and subprocess handles were lost when the previous session ended.
255
+ CRITICAL: Your previous executor sessions are NO LONGER USABLE. The MCP server was restarted, so all conversation state was lost. {invalidated_count} conversation ID(s) have been invalidated.
239
256
 
240
- Previous sessions (now stale):
257
+ Previous sessions (conversation IDs cleared):
241
258
  {session_info}
242
259
 
243
- You must start NEW sessions with delegate() if you need to continue work. Do NOT try to use converse() or check_session() with the old session IDs - they will fail.
260
+ You MUST start NEW sessions with delegate() to continue any work. The converse() tool will fail on these old sessions because they have no active conversation.
244
261
 
245
- Continue with your task from where you left off.""",
262
+ Review what was accomplished in the previous session and delegate new tasks as needed.""",
246
263
  }
247
264
 
248
265
  self.messages.append(resume_msg)
@@ -528,6 +545,8 @@ def build_orchestrator(
528
545
  overrides: list[str] | None = None,
529
546
  resume: bool = False,
530
547
  output_handler: Callable[[str], None] | None = None,
548
+ instance_id: str | None = None,
549
+ instance_name: str | None = None,
531
550
  ) -> Orchestrator:
532
551
  """
533
552
  Build an orchestrator from configuration.
@@ -539,10 +558,14 @@ def build_orchestrator(
539
558
  overrides: CLI overrides (--set key=value)
540
559
  resume: Whether to resume from previous state
541
560
  output_handler: Function to handle orchestrator output
561
+ instance_id: Unique ID for this instance (enables multi-orchestrator isolation)
562
+ instance_name: Human-readable name for this instance
542
563
 
543
564
  Returns:
544
565
  Configured Orchestrator instance
545
566
  """
567
+ from uuid import uuid4
568
+
546
569
  # Load configuration
547
570
  config = load_config(
548
571
  config_path=config_path,
@@ -552,6 +575,11 @@ def build_orchestrator(
552
575
  # Resolve working directory
553
576
  working_dir = working_dir or Path.cwd()
554
577
 
578
+ # Generate instance ID if not provided (enables isolation by default for new runs)
579
+ # For resume, instance_id should be provided explicitly
580
+ if instance_id is None and not resume:
581
+ instance_id = str(uuid4())
582
+
555
583
  # Build system prompt
556
584
  system_prompt = _build_system_prompt(config, working_dir)
557
585
 
@@ -572,6 +600,8 @@ def build_orchestrator(
572
600
  system_prompt=system_prompt,
573
601
  maxSteps=config.orchestrator.max_steps,
574
602
  env=env,
603
+ instance_id=instance_id,
604
+ instance_name=instance_name,
575
605
  )
576
606
 
577
607
  # Resume if requested
zwarm/tools/delegation.py CHANGED
@@ -11,6 +11,7 @@ These are the core tools that orchestrators use to delegate work to executors:
11
11
  from __future__ import annotations
12
12
 
13
13
  import asyncio
14
+ from pathlib import Path
14
15
  from typing import TYPE_CHECKING, Any, Literal
15
16
 
16
17
  from wbal.helper import weaveTool
@@ -31,6 +32,65 @@ def _format_session_header(session_id: str, adapter: str, mode: str) -> str:
31
32
  return f"[{session_id[:8]}] {adapter} ({mode})"
32
33
 
33
34
 
35
+ def _validate_working_dir(
36
+ requested_dir: Path | str | None,
37
+ default_dir: Path,
38
+ allowed_dirs: list[str] | None,
39
+ ) -> tuple[Path, str | None]:
40
+ """
41
+ Validate requested working directory against allowed_dirs config.
42
+
43
+ Args:
44
+ requested_dir: Directory requested by the agent (or None for default)
45
+ default_dir: The orchestrator's working directory
46
+ allowed_dirs: Config setting - None means only default allowed,
47
+ ["*"] means any, or list of allowed paths
48
+
49
+ Returns:
50
+ (validated_path, error_message) - error is None if valid
51
+ """
52
+ if requested_dir is None:
53
+ return default_dir, None
54
+
55
+ requested = Path(requested_dir).resolve()
56
+
57
+ # Check if directory exists
58
+ if not requested.exists():
59
+ return default_dir, f"Directory does not exist: {requested}"
60
+
61
+ if not requested.is_dir():
62
+ return default_dir, f"Not a directory: {requested}"
63
+
64
+ # If allowed_dirs is None, only default is allowed
65
+ if allowed_dirs is None:
66
+ if requested == default_dir.resolve():
67
+ return requested, None
68
+ return default_dir, (
69
+ f"Directory not allowed: {requested}. "
70
+ f"Agent can only delegate to working directory ({default_dir}). "
71
+ "Set orchestrator.allowed_dirs in config to allow other directories."
72
+ )
73
+
74
+ # If ["*"], any directory is allowed
75
+ if allowed_dirs == ["*"]:
76
+ return requested, None
77
+
78
+ # Check against allowed list
79
+ for allowed in allowed_dirs:
80
+ allowed_path = Path(allowed).resolve()
81
+ # Allow if requested is the allowed path or a subdirectory of it
82
+ try:
83
+ requested.relative_to(allowed_path)
84
+ return requested, None
85
+ except ValueError:
86
+ continue
87
+
88
+ return default_dir, (
89
+ f"Directory not allowed: {requested}. "
90
+ f"Allowed directories: {allowed_dirs}"
91
+ )
92
+
93
+
34
94
  @weaveTool
35
95
  def delegate(
36
96
  self: "Orchestrator",
@@ -38,6 +98,7 @@ def delegate(
38
98
  mode: Literal["sync", "async"] = "sync",
39
99
  adapter: str | None = None,
40
100
  model: str | None = None,
101
+ working_dir: str | None = None,
41
102
  ) -> dict[str, Any]:
42
103
  """
43
104
  Delegate work to an executor agent.
@@ -57,6 +118,8 @@ def delegate(
57
118
  mode: "sync" for conversational, "async" for fire-and-forget.
58
119
  adapter: Which executor adapter to use (default: config setting).
59
120
  model: Model override for the executor.
121
+ working_dir: Directory for the executor to work in (default: orchestrator's dir).
122
+ NOTE: May be restricted by orchestrator.allowed_dirs config.
60
123
 
61
124
  Returns:
62
125
  {session_id, status, response (if sync)}
@@ -65,6 +128,20 @@ def delegate(
65
128
  delegate(task="Add a logout button to the navbar", mode="sync")
66
129
  # Then use converse() to refine: "Also add a confirmation dialog"
67
130
  """
131
+ # Validate working directory against allowed_dirs config
132
+ effective_dir, dir_error = _validate_working_dir(
133
+ working_dir,
134
+ self.working_dir,
135
+ self.config.orchestrator.allowed_dirs,
136
+ )
137
+
138
+ if dir_error:
139
+ return {
140
+ "success": False,
141
+ "error": dir_error,
142
+ "hint": "Use the default working directory or ask user to update allowed_dirs config",
143
+ }
144
+
68
145
  # Get adapter (use default from config if not specified)
69
146
  adapter_name = adapter or self.config.executor.adapter
70
147
  executor = self._get_adapter(adapter_name)
@@ -73,7 +150,7 @@ def delegate(
73
150
  session = asyncio.run(
74
151
  executor.start_session(
75
152
  task=task,
76
- working_dir=self.working_dir,
153
+ working_dir=effective_dir,
77
154
  mode=mode,
78
155
  model=model or self.config.executor.model,
79
156
  sandbox=self.config.executor.sandbox,
@@ -117,7 +194,7 @@ def delegate(
117
194
  header = _format_session_header(session.id, adapter_name, mode)
118
195
 
119
196
  if mode == "sync":
120
- return {
197
+ result = {
121
198
  "success": True,
122
199
  "session": header,
123
200
  "session_id": session.id,
@@ -127,6 +204,14 @@ def delegate(
127
204
  "tokens": session.token_usage.get("total_tokens", 0),
128
205
  "hint": "Use converse(session_id, message) to continue this conversation",
129
206
  }
207
+ # Warn if no conversation ID - converse() won't work
208
+ if not session.conversation_id:
209
+ result["warning"] = "no_conversation_id"
210
+ result["hint"] = (
211
+ "WARNING: MCP didn't return a conversation ID. "
212
+ "You cannot use converse() - send all instructions upfront or use async mode."
213
+ )
214
+ return result
130
215
  else:
131
216
  return {
132
217
  "success": True,
@@ -186,6 +271,18 @@ def converse(
186
271
  "hint": "Start a new session with delegate()",
187
272
  }
188
273
 
274
+ # Check for stale/missing conversation_id (common after resume)
275
+ if not session.conversation_id:
276
+ return {
277
+ "success": False,
278
+ "error": "Session has no conversation ID (likely stale after resume)",
279
+ "hint": (
280
+ "This session's conversation was lost (MCP server restarted). "
281
+ "Use end_session() to close it, then delegate() a new task."
282
+ ),
283
+ "session_id": session_id,
284
+ }
285
+
189
286
  # Get adapter and send message
190
287
  executor = self._get_adapter(session.adapter)
191
288
  try:
@@ -211,7 +308,13 @@ def converse(
211
308
  turn = len([m for m in session.messages if m.role == "user"])
212
309
  header = _format_session_header(session.id, session.adapter, session.mode.value)
213
310
 
214
- return {
311
+ # Check for conversation loss (indicated by error in response)
312
+ conversation_lost = (
313
+ "[ERROR] Conversation lost" in response
314
+ or session.conversation_id is None
315
+ )
316
+
317
+ result = {
215
318
  "success": True,
216
319
  "session": header,
217
320
  "session_id": session_id,
@@ -221,6 +324,15 @@ def converse(
221
324
  "tokens": session.token_usage.get("total_tokens", 0),
222
325
  }
223
326
 
327
+ if conversation_lost:
328
+ result["warning"] = "conversation_lost"
329
+ result["hint"] = (
330
+ "The MCP server lost this conversation. You should end_session() "
331
+ "and delegate() a new task with the full context."
332
+ )
333
+
334
+ return result
335
+
224
336
 
225
337
  @weaveTool
226
338
  def check_session(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zwarm
3
- Version: 1.1.1
3
+ Version: 1.3.2
4
4
  Summary: Multi-Agent CLI Orchestration Research Platform
5
5
  Requires-Python: <3.14,>=3.13
6
6
  Requires-Dist: python-dotenv>=1.0.0
@@ -76,10 +76,11 @@ echo "Fix the bug in auth.py" | zwarm orchestrate
76
76
 
77
77
  zwarm looks for configuration in this order:
78
78
  1. `--config` flag (YAML file)
79
- 2. `config.toml` in working directory
80
- 3. Default settings
79
+ 2. `.zwarm/config.toml` (created by `zwarm init`)
80
+ 3. `config.toml` in working directory (legacy, for backwards compat)
81
+ 4. Default settings
81
82
 
82
- ### Minimal config.toml
83
+ ### Minimal .zwarm/config.toml
83
84
 
84
85
  ```toml
85
86
  [weave]
@@ -430,10 +431,11 @@ zwarm clean --events
430
431
 
431
432
  ### State Management
432
433
 
433
- All state is stored in flat files under `.zwarm/`:
434
+ All state and config is stored in flat files under `.zwarm/`:
434
435
 
435
436
  ```
436
437
  .zwarm/
438
+ ├── config.toml # Runtime settings (weave, adapter, watchers)
437
439
  ├── state.json # Current state
438
440
  ├── events.jsonl # Append-only event log
439
441
  ├── sessions/
@@ -1,33 +1,35 @@
1
1
  zwarm/__init__.py,sha256=3i3LMjHwIzE-LFIS2aUrwv3EZmpkvVMe-xj1h97rcSM,837
2
- zwarm/orchestrator.py,sha256=O3HGjMenQE-HF5xo0WQNMAHRS_vLsUrFfVp6oI_WpKU,19732
2
+ zwarm/orchestrator.py,sha256=wCvK4RVBDC6T_vrJSJBhPfaDpTorwLAXpOUYHDjowrs,21112
3
3
  zwarm/test_orchestrator_watchers.py,sha256=QpoaehPU7ekT4XshbTOWnJ2H0wRveV3QOZjxbgyJJLY,807
4
- zwarm/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ zwarm/adapters/__init__.py,sha256=O0b-SfZpb6txeNqFkXZ2aaf34yLFYreznyrAV25jF_Q,656
5
5
  zwarm/adapters/base.py,sha256=fZlQviTgVvOcwnxduTla6WuM6FzQJ_yoHMW5SxwVgQg,2527
6
- zwarm/adapters/claude_code.py,sha256=Z6f_jydiIUuZiR8fNipvvlcv16rP4BK6jNtkUaH3M4c,11368
7
- zwarm/adapters/codex_mcp.py,sha256=Wp4PExZlhqfLV5CHxBqmIBOm-pH7k8cJy93fyxRbCgw,27495
8
- zwarm/adapters/test_codex_mcp.py,sha256=vodQF5VUrM_F1GygUADtXYrA0kvAc7dWpT_Ff-Uoo0A,8383
6
+ zwarm/adapters/claude_code.py,sha256=vAjsjD-_JjARmC4_FBSILQZmQCBrk_oNHo18a9ubuqk,11481
7
+ zwarm/adapters/codex_mcp.py,sha256=olgR8mfgjn0cbrD8Gc11DX4yVzWXqE64fQHsMAq_xAQ,32771
8
+ zwarm/adapters/registry.py,sha256=EdyHECaNA5Kv1od64pYFBJyA_r_6I1r_eJTNP1XYLr4,1781
9
+ zwarm/adapters/test_codex_mcp.py,sha256=0qhVzxn_KF-XUS30gXSJKwMdR3kWGsDY9iPk1Ihqn3w,10698
10
+ zwarm/adapters/test_registry.py,sha256=otxcVDONwFCMisyANToF3iy7Y8dSbCL8bTmZNhxNuF4,2383
9
11
  zwarm/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- zwarm/cli/main.py,sha256=dU_8XLIYzcxikl_MgT6BmChNbqYq94dJ4Ra5STzm_KI,33109
12
+ zwarm/cli/main.py,sha256=9kHUw897580fQtBii1BqoJrUBYMaKmYjC6CM4AuOsdQ,59013
11
13
  zwarm/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
14
  zwarm/core/compact.py,sha256=Y8C7Gs-5-WOU43WRvQ863Qzd5xtuEqR6Aw3r2p8_-i8,10907
13
- zwarm/core/config.py,sha256=H8XFsWeEehZnUsMf7nsM_YqjljFYaC43DLs8Xw4sOuQ,10360
15
+ zwarm/core/config.py,sha256=J-TAwIQFLY4x_G9qdvP_16DAYndqg_zhakCW5TLEMN4,11059
14
16
  zwarm/core/environment.py,sha256=HVDpDZEpDSfyh9-wHZMzMKVUPKvioBkPVWeiME2JmFo,5435
15
17
  zwarm/core/models.py,sha256=PrC3okRBVJxISUa1Fax4KkagqLT6Xub-kTxC9drN0sY,10083
16
- zwarm/core/state.py,sha256=wMryIvXP-VDLh2b76A5taeL_9dm5k4jk4HnvHWgLqGE,7658
18
+ zwarm/core/state.py,sha256=MzrvODKEiJovI7YI1jajW4uukineZ3ezmW5oQinMgjg,11563
17
19
  zwarm/core/test_compact.py,sha256=WSdjCB5t4YMcknsrkmJIUsVOPY28s4y9GnDmu3Z4BFw,11878
18
20
  zwarm/core/test_config.py,sha256=26ozyiFOdjFF2c9Q-HDfFM6GOLfgw_5FZ55nTDMNYA8,4888
19
21
  zwarm/core/test_models.py,sha256=sWTIhMZvuLP5AooGR6y8OR2EyWydqVfhmGrE7NPBBnk,8450
20
22
  zwarm/prompts/__init__.py,sha256=FiaIOniLrIyfD3_osxT6I7FfyKjtctbf8jNs5QTPs_s,213
21
23
  zwarm/prompts/orchestrator.py,sha256=R-ym3nlspYNspf085Qwwfi96Yh3K6-Csb6vfBg29K2c,14228
22
24
  zwarm/tools/__init__.py,sha256=FpqxwXJA6-fQ7C-oLj30jjK_0qqcE7MbI0dQuaB56kU,290
23
- zwarm/tools/delegation.py,sha256=wf43jxmGvEy_c8jVx301YQvslIS2F80Qnq4Py2feVvg,11435
25
+ zwarm/tools/delegation.py,sha256=PeB0W7x2TcGCZ9rGBYDlIIFr7AT1TOdc1SLe7BKCUoM,15332
24
26
  zwarm/watchers/__init__.py,sha256=yYGTbhuImQLESUdtfrYbHYBJNvCNX3B-Ei-vY5BizX8,760
25
27
  zwarm/watchers/base.py,sha256=r1GoPlj06nOT2xp4fghfSjxbRyFFFQUB6HpZbEyO2OY,3834
26
28
  zwarm/watchers/builtin.py,sha256=52hyRREYYDsSuG-YKElXViSTyMmGySZaFreHc0pz-A4,12482
27
29
  zwarm/watchers/manager.py,sha256=XZjBVeHjgCUlkTUeHqdvBvHoBC862U1ik0fG6nlRGog,5587
28
30
  zwarm/watchers/registry.py,sha256=A9iBIVIFNtO7KPX0kLpUaP8dAK7ozqWLA44ocJGnOw4,1219
29
31
  zwarm/watchers/test_watchers.py,sha256=zOsxumBqKfR5ZVGxrNlxz6KcWjkcdp0QhW9WB0_20zM,7855
30
- zwarm-1.1.1.dist-info/METADATA,sha256=9GoAYbxRjMSdbHIwrZPS_zdrrDSz7-Avnwy_8AqNEBM,14995
31
- zwarm-1.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
32
- zwarm-1.1.1.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
33
- zwarm-1.1.1.dist-info/RECORD,,
32
+ zwarm-1.3.2.dist-info/METADATA,sha256=Xf0dFQrOuhMhlbVQsRWw1uh65uCIVWMB-Hv99-rZ7Xc,15174
33
+ zwarm-1.3.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ zwarm-1.3.2.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
35
+ zwarm-1.3.2.dist-info/RECORD,,
File without changes