zwarm 2.3.5__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.
@@ -0,0 +1,237 @@
1
+ """Tests for the watcher system."""
2
+
3
+ import pytest
4
+
5
+ from zwarm.watchers import (
6
+ Watcher,
7
+ WatcherContext,
8
+ WatcherResult,
9
+ WatcherAction,
10
+ WatcherManager,
11
+ WatcherConfig,
12
+ get_watcher,
13
+ list_watchers,
14
+ )
15
+
16
+
17
+ class TestWatcherRegistry:
18
+ def test_list_watchers(self):
19
+ """Built-in watchers should be registered."""
20
+ watchers = list_watchers()
21
+ assert "progress" in watchers
22
+ assert "budget" in watchers
23
+ assert "scope" in watchers
24
+ assert "pattern" in watchers
25
+ assert "quality" in watchers
26
+
27
+ def test_get_watcher(self):
28
+ """Can get watcher by name."""
29
+ watcher = get_watcher("progress")
30
+ assert watcher.name == "progress"
31
+
32
+ def test_get_unknown_watcher(self):
33
+ """Unknown watcher raises error."""
34
+ with pytest.raises(ValueError, match="Unknown watcher"):
35
+ get_watcher("nonexistent")
36
+
37
+
38
+ class TestProgressWatcher:
39
+ @pytest.mark.asyncio
40
+ async def test_continues_on_normal_progress(self):
41
+ """Normal progress should continue."""
42
+ watcher = get_watcher("progress")
43
+ ctx = WatcherContext(
44
+ task="Test task",
45
+ step=2,
46
+ max_steps=10,
47
+ messages=[
48
+ {"role": "user", "content": "Start"},
49
+ {"role": "assistant", "content": "Working on it"},
50
+ ],
51
+ )
52
+ result = await watcher.observe(ctx)
53
+ assert result.action == WatcherAction.CONTINUE
54
+
55
+
56
+ class TestBudgetWatcher:
57
+ @pytest.mark.asyncio
58
+ async def test_warns_at_budget_threshold(self):
59
+ """Should warn when approaching step limit."""
60
+ watcher = get_watcher("budget", {"warn_at_percent": 80})
61
+ ctx = WatcherContext(
62
+ task="Test task",
63
+ step=9, # 90% of max
64
+ max_steps=10,
65
+ messages=[],
66
+ )
67
+ result = await watcher.observe(ctx)
68
+ assert result.action == WatcherAction.NUDGE
69
+ assert "remaining" in result.guidance.lower()
70
+
71
+ @pytest.mark.asyncio
72
+ async def test_continues_when_under_budget(self):
73
+ """Should continue when well under budget."""
74
+ watcher = get_watcher("budget")
75
+ ctx = WatcherContext(
76
+ task="Test task",
77
+ step=2,
78
+ max_steps=10,
79
+ messages=[],
80
+ )
81
+ result = await watcher.observe(ctx)
82
+ assert result.action == WatcherAction.CONTINUE
83
+
84
+ @pytest.mark.asyncio
85
+ async def test_only_counts_active_sessions(self):
86
+ """Should only count active sessions, not completed/failed ones."""
87
+ watcher = get_watcher("budget", {"max_sessions": 2})
88
+ # Create 5 sessions: 1 active, 2 completed, 2 failed
89
+ ctx = WatcherContext(
90
+ task="Test task",
91
+ step=2,
92
+ max_steps=10,
93
+ messages=[],
94
+ sessions=[
95
+ {"id": "s1", "status": "active"},
96
+ {"id": "s2", "status": "completed"},
97
+ {"id": "s3", "status": "completed"},
98
+ {"id": "s4", "status": "failed"},
99
+ {"id": "s5", "status": "failed"},
100
+ ],
101
+ )
102
+ # Should continue because only 1 active session (limit is 2)
103
+ result = await watcher.observe(ctx)
104
+ assert result.action == WatcherAction.CONTINUE
105
+
106
+ @pytest.mark.asyncio
107
+ async def test_warns_when_active_sessions_at_limit(self):
108
+ """Should warn when active sessions reach the limit."""
109
+ watcher = get_watcher("budget", {"max_sessions": 2})
110
+ ctx = WatcherContext(
111
+ task="Test task",
112
+ step=2,
113
+ max_steps=10,
114
+ messages=[],
115
+ sessions=[
116
+ {"id": "s1", "status": "active"},
117
+ {"id": "s2", "status": "active"},
118
+ {"id": "s3", "status": "completed"},
119
+ ],
120
+ )
121
+ # Should nudge because 2 active sessions (at limit)
122
+ result = await watcher.observe(ctx)
123
+ assert result.action == WatcherAction.NUDGE
124
+ assert "2 active sessions" in result.guidance
125
+
126
+
127
+ class TestPatternWatcher:
128
+ @pytest.mark.asyncio
129
+ async def test_detects_pattern(self):
130
+ """Should detect configured patterns."""
131
+ watcher = get_watcher("pattern", {
132
+ "patterns": [
133
+ {"regex": r"ERROR", "action": "nudge", "message": "Error detected!"}
134
+ ]
135
+ })
136
+ ctx = WatcherContext(
137
+ task="Test task",
138
+ step=1,
139
+ max_steps=10,
140
+ messages=[
141
+ {"role": "assistant", "content": "Got ERROR in the build"}
142
+ ],
143
+ )
144
+ result = await watcher.observe(ctx)
145
+ assert result.action == WatcherAction.NUDGE
146
+ assert "Error detected" in result.guidance
147
+
148
+ @pytest.mark.asyncio
149
+ async def test_abort_pattern(self):
150
+ """Should abort on critical patterns."""
151
+ watcher = get_watcher("pattern", {
152
+ "patterns": [
153
+ {"regex": r"rm -rf /", "action": "abort", "message": "Dangerous command!"}
154
+ ]
155
+ })
156
+ ctx = WatcherContext(
157
+ task="Test task",
158
+ step=1,
159
+ max_steps=10,
160
+ messages=[
161
+ {"role": "assistant", "content": "Running rm -rf /"}
162
+ ],
163
+ )
164
+ result = await watcher.observe(ctx)
165
+ assert result.action == WatcherAction.ABORT
166
+
167
+
168
+ class TestWatcherManager:
169
+ @pytest.mark.asyncio
170
+ async def test_runs_multiple_watchers(self):
171
+ """Manager runs all watchers."""
172
+ manager = WatcherManager([
173
+ WatcherConfig(name="progress"),
174
+ WatcherConfig(name="budget"),
175
+ ])
176
+ ctx = WatcherContext(
177
+ task="Test task",
178
+ step=2,
179
+ max_steps=10,
180
+ messages=[],
181
+ )
182
+ result = await manager.observe(ctx)
183
+ assert isinstance(result, WatcherResult)
184
+
185
+ @pytest.mark.asyncio
186
+ async def test_highest_priority_wins(self):
187
+ """Most severe action should win."""
188
+ manager = WatcherManager([
189
+ WatcherConfig(name="budget", config={"warn_at_percent": 50}), # Will nudge
190
+ WatcherConfig(name="pattern", config={
191
+ "patterns": [{"regex": "ABORT", "action": "abort", "message": "Abort!"}]
192
+ }),
193
+ ])
194
+ ctx = WatcherContext(
195
+ task="Test task",
196
+ step=6, # 60% - triggers budget nudge
197
+ max_steps=10,
198
+ messages=[
199
+ {"role": "assistant", "content": "Must ABORT now"}
200
+ ],
201
+ )
202
+ result = await manager.observe(ctx)
203
+ # Abort should take precedence over nudge
204
+ assert result.action == WatcherAction.ABORT
205
+
206
+ @pytest.mark.asyncio
207
+ async def test_empty_manager_continues(self):
208
+ """Manager with no watchers should continue."""
209
+ manager = WatcherManager([])
210
+ ctx = WatcherContext(
211
+ task="Test task",
212
+ step=1,
213
+ max_steps=10,
214
+ messages=[],
215
+ )
216
+ result = await manager.observe(ctx)
217
+ assert result.action == WatcherAction.CONTINUE
218
+
219
+ @pytest.mark.asyncio
220
+ async def test_disabled_watcher_skipped(self):
221
+ """Disabled watchers should be skipped."""
222
+ manager = WatcherManager([
223
+ WatcherConfig(name="pattern", enabled=False, config={
224
+ "patterns": [{"regex": ".*", "action": "abort", "message": "Always abort"}]
225
+ }),
226
+ ])
227
+ ctx = WatcherContext(
228
+ task="Test task",
229
+ step=1,
230
+ max_steps=10,
231
+ messages=[
232
+ {"role": "assistant", "content": "This would normally trigger abort"}
233
+ ],
234
+ )
235
+ result = await manager.observe(ctx)
236
+ # Since the pattern watcher is disabled, should continue
237
+ assert result.action == WatcherAction.CONTINUE
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.4
2
+ Name: zwarm
3
+ Version: 2.3.5
4
+ Summary: Multi-Agent CLI Orchestration Research Platform
5
+ Requires-Python: <3.14,>=3.13
6
+ Requires-Dist: python-dotenv>=1.0.0
7
+ Requires-Dist: pyyaml>=6.0
8
+ Requires-Dist: rich>=13.0.0
9
+ Requires-Dist: typer>=0.9.0
10
+ Requires-Dist: wbal>=0.5.8
11
+ Description-Content-Type: text/markdown
12
+
13
+ # zwarm
14
+
15
+ Multi-agent CLI for orchestrating coding agents. Spawn, manage, and converse with multiple Codex sessions in parallel.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ # From the workspace
21
+ cd /path/to/labs
22
+ uv sync
23
+
24
+ # Or install directly
25
+ uv pip install -e ./zwarm
26
+ ```
27
+
28
+ **Requirements:**
29
+ - Python 3.13+
30
+ - `codex` CLI installed and authenticated
31
+
32
+ **Environment:**
33
+ ```bash
34
+ export OPENAI_API_KEY="sk-..." # Required for Codex
35
+ export WEAVE_PROJECT="entity/zwarm" # Optional: Weave tracing
36
+ ```
37
+
38
+ ## Two Modes
39
+
40
+ zwarm has two ways to orchestrate coding agents:
41
+
42
+ | Mode | Who's in charge | Use case |
43
+ |------|-----------------|----------|
44
+ | `zwarm interactive` | **You** | Manual control, experimentation |
45
+ | `zwarm orchestrate` | **LLM** | Autonomous task execution |
46
+
47
+ Both use the **same underlying session manager** - the orchestrator LLM has access to the exact same tools you do.
48
+
49
+ ---
50
+
51
+ ## Interactive Mode
52
+
53
+ **You are the orchestrator.** Spawn sessions, check on them, continue conversations.
54
+
55
+ ```bash
56
+ zwarm interactive
57
+ ```
58
+
59
+ ### Commands
60
+
61
+ | Command | Description |
62
+ |---------|-------------|
63
+ | `spawn "task"` | Start a session (waits for completion) |
64
+ | `spawn --async "task"` | Start async (returns immediately) |
65
+ | `spawn -d /path "task"` | Start in specific directory |
66
+ | `ls` | List all sessions |
67
+ | `? <id>` | Quick peek: status + latest message |
68
+ | `show <id>` | Full details: all messages, tokens, etc. |
69
+ | `c <id> "msg"` | Continue conversation (waits) |
70
+ | `ca <id> "msg"` | Continue async (returns immediately) |
71
+ | `kill <id>` | Stop a running session |
72
+ | `rm <id>` | Delete session entirely |
73
+ | `killall` | Stop all running sessions |
74
+ | `clean` | Remove sessions older than 7 days |
75
+ | `q` | Quit |
76
+
77
+ ### Example Session
78
+
79
+ ```
80
+ $ zwarm interactive
81
+
82
+ > spawn "Add a login function to auth.py"
83
+ ✓ Started session a1b2c3d4, waiting...
84
+ [a1b2c3d4] codex (completed) - 32s
85
+ Response: I've added a login function with JWT support...
86
+
87
+ > spawn --async "Fix the type errors in utils.py"
88
+ ✓ Session: b2c3d4e5 (running in background)
89
+
90
+ > spawn --async "Add unit tests for auth.py"
91
+ ✓ Session: c3d4e5f6 (running in background)
92
+
93
+ > ls
94
+ 1 running | 2 done
95
+
96
+ ID │ │ T │ Task │ Updated │ Last Message
97
+ a1b2c3d4 │ ✓ │ 1 │ Add a login function... │ 2m │ I've added a login function...
98
+ b2c3d4e5 │ ✓ │ 1 │ Fix the type errors... │ 30s ★ │ Fixed 3 type errors in...
99
+ c3d4e5f6 │ ● │ 1 │ Add unit tests... │ 5s │ (working...)
100
+
101
+ > ? b2c3d4e5
102
+ ✓ b2c3d4e5 Fixed 3 type errors: Optional[str] -> str | None, added missing...
103
+
104
+ > c a1b2c3d4 "Now add password hashing with bcrypt"
105
+ Continuing session a1b2c3d4...
106
+ [a1b2c3d4] codex (completed) - 28s
107
+ Response: Done! I've updated the login function to use bcrypt...
108
+
109
+ > rm b2c3d4e5
110
+ ✓ Deleted session b2c3d4e5
111
+
112
+ > q
113
+ ```
114
+
115
+ ### Session Status Icons
116
+
117
+ | Icon | Status |
118
+ |------|--------|
119
+ | `●` | Running |
120
+ | `✓` | Completed |
121
+ | `✗` | Failed |
122
+ | `○` | Killed |
123
+ | `★` | Recently completed (< 60s) |
124
+
125
+ ---
126
+
127
+ ## Orchestrate Mode
128
+
129
+ **An LLM is the orchestrator.** Give it a task and it delegates to coding agents.
130
+
131
+ ```bash
132
+ zwarm orchestrate --task "Build a REST API with authentication"
133
+ ```
134
+
135
+ The orchestrator LLM uses the same tools available in interactive mode:
136
+
137
+ | Tool | Description |
138
+ |------|-------------|
139
+ | `delegate(task, ...)` | Start a new session |
140
+ | `converse(id, msg)` | Continue a conversation |
141
+ | `peek_session(id)` | Quick status check |
142
+ | `check_session(id)` | Full session details |
143
+ | `list_sessions()` | List all sessions with `needs_attention` flags |
144
+ | `end_session(id, delete=False)` | Kill/delete a session |
145
+
146
+ ### Task Input
147
+
148
+ ```bash
149
+ # Direct
150
+ zwarm orchestrate --task "Build a REST API"
151
+
152
+ # From file
153
+ zwarm orchestrate --task-file task.md
154
+
155
+ # From stdin
156
+ echo "Fix the bug in auth.py" | zwarm orchestrate
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Configuration
162
+
163
+ zwarm looks for config in this order:
164
+ 1. `--config` flag
165
+ 2. `.zwarm/config.toml`
166
+ 3. `config.toml` in working directory
167
+
168
+ ### Minimal Config
169
+
170
+ ```toml
171
+ [weave]
172
+ enabled = true
173
+ project = "your-entity/zwarm"
174
+
175
+ [executor]
176
+ adapter = "codex_mcp"
177
+ model = "gpt-5.1-codex-mini"
178
+ ```
179
+
180
+ ### Full Config Reference
181
+
182
+ ```toml
183
+ [orchestrator]
184
+ lm = "gpt-5-mini"
185
+ max_steps = 100
186
+
187
+ [orchestrator.compaction]
188
+ enabled = true
189
+ max_tokens = 100000
190
+ threshold_pct = 0.85
191
+ target_pct = 0.7
192
+
193
+ [executor]
194
+ adapter = "codex_mcp"
195
+ model = "gpt-5.1-codex-mini"
196
+ sandbox = "workspace-write"
197
+ timeout = 300
198
+
199
+ [weave]
200
+ enabled = true
201
+ project = "your-entity/zwarm"
202
+
203
+ [watchers]
204
+ enabled = true
205
+ watchers = [
206
+ { name = "progress" },
207
+ { name = "budget", config = { max_steps = 50, max_sessions = 10 } },
208
+ { name = "delegation_reminder", config = { threshold = 10 } },
209
+ ]
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Session Management
215
+
216
+ Sessions are the core abstraction. Each session is a conversation with a Codex agent.
217
+
218
+ ### Lifecycle
219
+
220
+ ```
221
+ spawn → running → completed/failed
222
+
223
+ continue → running → completed
224
+
225
+ continue → ...
226
+ ```
227
+
228
+ ### Storage
229
+
230
+ ```
231
+ .zwarm/sessions/<uuid>/
232
+ ├── meta.json # Status, task, model, messages, tokens
233
+ └── turns/
234
+ ├── turn_1.jsonl # Raw codex output for turn 1
235
+ ├── turn_2.jsonl # Output after first continue
236
+ └── ...
237
+ ```
238
+
239
+ ### Sync vs Async
240
+
241
+ | Mode | Spawn | Continue | Use case |
242
+ |------|-------|----------|----------|
243
+ | **Sync** | `spawn "task"` | `c id "msg"` | Sequential work, immediate feedback |
244
+ | **Async** | `spawn --async "task"` | `ca id "msg"` | Parallel work, batch processing |
245
+
246
+ Async sessions return immediately. Poll with `ls` or `?` to check status.
247
+
248
+ ---
249
+
250
+ ## Watchers
251
+
252
+ Watchers monitor agent behavior and intervene when needed.
253
+
254
+ | Watcher | Purpose |
255
+ |---------|---------|
256
+ | `progress` | Detects stuck/spinning agents |
257
+ | `budget` | Enforces step/session limits |
258
+ | `scope` | Detects scope creep |
259
+ | `delegation_reminder` | Nudges orchestrator to delegate |
260
+
261
+ Configure in `config.toml`:
262
+
263
+ ```toml
264
+ [watchers]
265
+ enabled = true
266
+ watchers = [
267
+ { name = "progress" },
268
+ { name = "budget", config = { max_steps = 50 } },
269
+ ]
270
+ ```
271
+
272
+ ---
273
+
274
+ ## CLI Reference
275
+
276
+ ```bash
277
+ zwarm init # Initialize .zwarm/ in current directory
278
+ zwarm interactive # Start interactive REPL
279
+ zwarm orchestrate # Start LLM orchestrator
280
+ zwarm exec # Run single executor directly (testing)
281
+ zwarm status # Show current state
282
+ zwarm history # Show event history
283
+ zwarm clean # Remove old sessions
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Project Structure
289
+
290
+ ```
291
+ zwarm/
292
+ ├── src/zwarm/
293
+ │ ├── sessions/ # Session management (core)
294
+ │ │ ├── manager.py # CodexSessionManager
295
+ │ │ └── __init__.py
296
+ │ ├── tools/
297
+ │ │ └── delegation.py # Orchestrator tools (delegate, converse, etc.)
298
+ │ ├── cli/
299
+ │ │ └── main.py # CLI commands and interactive REPL
300
+ │ ├── core/
301
+ │ │ ├── config.py # Configuration loading
302
+ │ │ ├── compact.py # Context window management
303
+ │ │ └── state.py # State persistence
304
+ │ ├── watchers/ # Trajectory alignment
305
+ │ └── orchestrator.py # Orchestrator agent
306
+ ├── docs/
307
+ │ └── INTERNALS.md # Technical architecture
308
+ └── README.md
309
+ ```
@@ -0,0 +1,38 @@
1
+ zwarm/__init__.py,sha256=3i3LMjHwIzE-LFIS2aUrwv3EZmpkvVMe-xj1h97rcSM,837
2
+ zwarm/orchestrator.py,sha256=JGRGuJP05Nf5QibuWytjQAC_NuGGaGUR3G-tLq4SVxY,23624
3
+ zwarm/test_orchestrator_watchers.py,sha256=QpoaehPU7ekT4XshbTOWnJ2H0wRveV3QOZjxbgyJJLY,807
4
+ zwarm/adapters/__init__.py,sha256=O0b-SfZpb6txeNqFkXZ2aaf34yLFYreznyrAV25jF_Q,656
5
+ zwarm/adapters/base.py,sha256=fZlQviTgVvOcwnxduTla6WuM6FzQJ_yoHMW5SxwVgQg,2527
6
+ zwarm/adapters/claude_code.py,sha256=vAjsjD-_JjARmC4_FBSILQZmQCBrk_oNHo18a9ubuqk,11481
7
+ zwarm/adapters/codex_mcp.py,sha256=EhdkM3gj5hc01AcM1ERhtfZbydK390yN4Pg3dawKIGU,48791
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
11
+ zwarm/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ zwarm/cli/main.py,sha256=cSJ--IHJQv5o3Stb4PMKGIsiMNJn8s-xXvvm6DCjdmA,93294
13
+ zwarm/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ zwarm/core/compact.py,sha256=Y8C7Gs-5-WOU43WRvQ863Qzd5xtuEqR6Aw3r2p8_-i8,10907
15
+ zwarm/core/config.py,sha256=331i4io9uEnloFwUMjTPJ5_lQFKJR1nhTpA4SPfSpiI,11748
16
+ zwarm/core/environment.py,sha256=zrgh0N3Ng4HI2F1gCYkcQVGzjQPKiIFWuRe1OPRuRn0,6558
17
+ zwarm/core/models.py,sha256=PrC3okRBVJxISUa1Fax4KkagqLT6Xub-kTxC9drN0sY,10083
18
+ zwarm/core/state.py,sha256=MzrvODKEiJovI7YI1jajW4uukineZ3ezmW5oQinMgjg,11563
19
+ zwarm/core/test_compact.py,sha256=WSdjCB5t4YMcknsrkmJIUsVOPY28s4y9GnDmu3Z4BFw,11878
20
+ zwarm/core/test_config.py,sha256=26ozyiFOdjFF2c9Q-HDfFM6GOLfgw_5FZ55nTDMNYA8,4888
21
+ zwarm/core/test_models.py,sha256=sWTIhMZvuLP5AooGR6y8OR2EyWydqVfhmGrE7NPBBnk,8450
22
+ zwarm/prompts/__init__.py,sha256=FiaIOniLrIyfD3_osxT6I7FfyKjtctbf8jNs5QTPs_s,213
23
+ zwarm/prompts/orchestrator.py,sha256=-VZ3B5t-2ALOTpdZyNZGSjjzaHiTufAuLzrTLgwg70M,15442
24
+ zwarm/sessions/__init__.py,sha256=jRibY8IfmNcnkgNmrgK2T81oa1w71wP_KQp9A1hPL7Q,568
25
+ zwarm/sessions/manager.py,sha256=Aq7Wh-WW7ZMP8LgGa3g70wfGg6E2GYjJOBucy6HUfGc,27700
26
+ zwarm/tools/__init__.py,sha256=FpqxwXJA6-fQ7C-oLj30jjK_0qqcE7MbI0dQuaB56kU,290
27
+ zwarm/tools/delegation.py,sha256=kNvc7YISAEUWhlGYCvacxfDVfGA0a4P2kuWgMN9rP0Y,25294
28
+ zwarm/watchers/__init__.py,sha256=a96s7X6ruYkF2ItWWOZ3Q5QUOMOoeCW4Vz8XXcYLXPM,956
29
+ zwarm/watchers/base.py,sha256=r1GoPlj06nOT2xp4fghfSjxbRyFFFQUB6HpZbEyO2OY,3834
30
+ zwarm/watchers/builtin.py,sha256=IL5QwwKOIqWEfJ_uQWb321Px4i5OLtI_vnWQMudqKoA,19064
31
+ zwarm/watchers/llm_watcher.py,sha256=yJGpE3BGKNZX3qgPsiNtJ5d3UJpiTT1V-A-Rh4AiMYM,11029
32
+ zwarm/watchers/manager.py,sha256=XZjBVeHjgCUlkTUeHqdvBvHoBC862U1ik0fG6nlRGog,5587
33
+ zwarm/watchers/registry.py,sha256=A9iBIVIFNtO7KPX0kLpUaP8dAK7ozqWLA44ocJGnOw4,1219
34
+ zwarm/watchers/test_watchers.py,sha256=zOsxumBqKfR5ZVGxrNlxz6KcWjkcdp0QhW9WB0_20zM,7855
35
+ zwarm-2.3.5.dist-info/METADATA,sha256=HAscgpL1b-0D0fBJxqTEJM0APjE-hijsy8G6Lozyr7M,7680
36
+ zwarm-2.3.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
+ zwarm-2.3.5.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
38
+ zwarm-2.3.5.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zwarm = zwarm.cli.main:main