trinity-lite 0.1.2__tar.gz → 0.1.4__tar.gz

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.
Files changed (33) hide show
  1. {trinity_lite-0.1.2/trinity_lite.egg-info → trinity_lite-0.1.4}/PKG-INFO +19 -2
  2. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/README.md +18 -1
  3. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/pyproject.toml +1 -1
  4. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/tests/test_bus.py +13 -0
  5. trinity_lite-0.1.4/tests/test_cli.py +323 -0
  6. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/bus.py +11 -0
  7. trinity_lite-0.1.4/trinity_lite/cli.py +440 -0
  8. {trinity_lite-0.1.2 → trinity_lite-0.1.4/trinity_lite.egg-info}/PKG-INFO +19 -2
  9. trinity_lite-0.1.2/tests/test_cli.py +0 -150
  10. trinity_lite-0.1.2/trinity_lite/cli.py +0 -175
  11. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/LICENSE +0 -0
  12. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/setup.cfg +0 -0
  13. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/tests/test_doctor.py +0 -0
  14. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/tests/test_guard.py +0 -0
  15. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/tests/test_orchestrator.py +0 -0
  16. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/tests/test_router.py +0 -0
  17. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/tests/test_worker.py +0 -0
  18. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/__init__.py +0 -0
  19. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/__main__.py +0 -0
  20. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/adapters.py +0 -0
  21. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/config.py +0 -0
  22. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/doctor.py +0 -0
  23. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/guard.py +0 -0
  24. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/orchestrator.py +0 -0
  25. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/paths.py +0 -0
  26. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/py.typed +0 -0
  27. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/router.py +0 -0
  28. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/validation.py +0 -0
  29. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite/worker.py +0 -0
  30. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite.egg-info/SOURCES.txt +0 -0
  31. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite.egg-info/dependency_links.txt +0 -0
  32. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite.egg-info/entry_points.txt +0 -0
  33. {trinity_lite-0.1.2 → trinity_lite-0.1.4}/trinity_lite.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trinity-lite
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A minimal public multi-agent task bus with routing, workers, and safety checks.
5
5
  Author: Yomiracle
6
6
  License-Expression: MIT
@@ -155,7 +155,24 @@ trinity-lite worker codex --once
155
155
  trinity-lite tasks
156
156
  ```
157
157
 
158
- For local development from source:
158
+ ## Upgrading
159
+
160
+ ```bash
161
+ python3 -m pip install --upgrade trinity-lite
162
+ ```
163
+
164
+ Trinity Lite follows [semantic versioning](https://semver.org). Patch releases (0.1.x) are backward-compatible
165
+ and require no configuration or data migration. Check your version:
166
+
167
+ ```bash
168
+ trinity-lite doctor
169
+ # or
170
+ python3 -m pip show trinity-lite | grep Version
171
+ ```
172
+
173
+ See [CHANGELOG.md](CHANGELOG.md) for what changed between versions.
174
+
175
+ ## Quick Start (from source)
159
176
 
160
177
  ```bash
161
178
  git clone https://github.com/Yomiracle/trinity-lite.git
@@ -134,7 +134,24 @@ trinity-lite worker codex --once
134
134
  trinity-lite tasks
135
135
  ```
136
136
 
137
- For local development from source:
137
+ ## Upgrading
138
+
139
+ ```bash
140
+ python3 -m pip install --upgrade trinity-lite
141
+ ```
142
+
143
+ Trinity Lite follows [semantic versioning](https://semver.org). Patch releases (0.1.x) are backward-compatible
144
+ and require no configuration or data migration. Check your version:
145
+
146
+ ```bash
147
+ trinity-lite doctor
148
+ # or
149
+ python3 -m pip show trinity-lite | grep Version
150
+ ```
151
+
152
+ See [CHANGELOG.md](CHANGELOG.md) for what changed between versions.
153
+
154
+ ## Quick Start (from source)
138
155
 
139
156
  ```bash
140
157
  git clone https://github.com/Yomiracle/trinity-lite.git
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trinity-lite"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "A minimal public multi-agent task bus with routing, workers, and safety checks."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -61,6 +61,19 @@ class BusTest(unittest.TestCase):
61
61
  with self.assertRaises(KeyError):
62
62
  self.bus.get_task("rollback-test")
63
63
 
64
+ def test_await_task_returns_on_completion(self):
65
+ task = self.bus.submit_task("user", "codex", "build it", "implementation", cwd=self.root)
66
+ self.bus.task_for_worker("codex")
67
+ self.bus.finish_worker(task["id"], result="ok")
68
+ result = self.bus.await_task(task["id"], timeout=5)
69
+ self.assertEqual(result["status"], "completed")
70
+ self.assertEqual(result["result"], "ok")
71
+
72
+ def test_await_task_timeout_raises(self):
73
+ task = self.bus.submit_task("user", "codex", "slow task", "implementation", cwd=self.root)
74
+ with self.assertRaises(TimeoutError):
75
+ self.bus.await_task(task["id"], timeout=0.1)
76
+
64
77
  def test_message_round_trip(self):
65
78
  msg = self.bus.send_message("codex", "claude_code", "please review")
66
79
  self.assertEqual(msg["read"], 0)
@@ -0,0 +1,323 @@
1
+ import io
2
+ import json
3
+ import tempfile
4
+ import unittest
5
+ from contextlib import redirect_stdout
6
+ from pathlib import Path
7
+
8
+ from trinity_lite.cli import main
9
+
10
+
11
+ class CliTest(unittest.TestCase):
12
+ def test_global_options_work_after_subcommand(self):
13
+ with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
14
+ root = Path(tmp)
15
+ db = root / "bus.db"
16
+ output = io.StringIO()
17
+ with redirect_stdout(output):
18
+ code = main([
19
+ "dispatch-auto",
20
+ "implement a parser",
21
+ "--db",
22
+ str(db),
23
+ "--cwd",
24
+ str(root),
25
+ ])
26
+ self.assertEqual(code, 0)
27
+ code = main(["worker", "codex", "--once", "--db", str(db)])
28
+ self.assertEqual(code, 0)
29
+
30
+ def test_route_errors_are_structured(self):
31
+ output = io.StringIO()
32
+ with redirect_stdout(output):
33
+ code = main(["route", "review this patch"])
34
+ self.assertEqual(code, 2)
35
+ self.assertIn('"error"', output.getvalue())
36
+
37
+ def test_doctor_accepts_runtime_hygiene_options(self):
38
+ with tempfile.TemporaryDirectory() as tmp:
39
+ root = Path(tmp)
40
+ runtime = root / "runtime"
41
+ runtime.mkdir()
42
+ (runtime / "metrics.jsonl").write_text("", encoding="utf-8")
43
+ output = io.StringIO()
44
+ with redirect_stdout(output):
45
+ code = main([
46
+ "doctor",
47
+ "--db",
48
+ str(root / "bus.db"),
49
+ "--runtime-root",
50
+ str(runtime),
51
+ "--retired-port",
52
+ "70000",
53
+ ])
54
+ self.assertEqual(code, 0)
55
+ self.assertIn('"runtime_metrics"', output.getvalue())
56
+ self.assertIn('"retired_port:70000"', output.getvalue())
57
+
58
+ def test_dispatch_wait_blocks_until_complete(self):
59
+ with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
60
+ root = Path(tmp)
61
+ output = io.StringIO()
62
+ with redirect_stdout(output):
63
+ code = main([
64
+ "dispatch",
65
+ "codex",
66
+ "implement hello world",
67
+ "--db",
68
+ str(root / "bus.db"),
69
+ "--cwd",
70
+ str(root),
71
+ "--wait",
72
+ "--wait-timeout",
73
+ "5",
74
+ ])
75
+ self.assertEqual(code, 0)
76
+ data = json.loads(output.getvalue())
77
+ self.assertEqual(data["status"], "completed")
78
+ self.assertIn("target_agent", data)
79
+ self.assertEqual(data["target_agent"], "codex")
80
+
81
+ def test_dispatch_auto_wait_blocks_until_complete(self):
82
+ with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
83
+ root = Path(tmp)
84
+ agents_path = root / "agents.json"
85
+ routes_path = root / "routes.json"
86
+ agents_path.write_text(json.dumps({
87
+ "agents": {
88
+ "qwen_cli": {
89
+ "mode": "mock",
90
+ "roles": ["primary_engineer"],
91
+ "capabilities": ["code_edit"],
92
+ "priority": 80,
93
+ }
94
+ }
95
+ }), encoding="utf-8")
96
+ routes_path.write_text(json.dumps({
97
+ "routes": {
98
+ "implementation": {
99
+ "requires": ["code_edit"],
100
+ "prefer": ["primary_engineer"],
101
+ }
102
+ },
103
+ "opposites": {}
104
+ }), encoding="utf-8")
105
+ output = io.StringIO()
106
+ with redirect_stdout(output):
107
+ code = main([
108
+ "dispatch-auto",
109
+ "implement a parser",
110
+ "--db",
111
+ str(root / "bus.db"),
112
+ "--cwd",
113
+ str(root),
114
+ "--agents",
115
+ str(agents_path),
116
+ "--routes",
117
+ str(routes_path),
118
+ "--wait",
119
+ "--wait-timeout",
120
+ "5",
121
+ ])
122
+ self.assertEqual(code, 0)
123
+ data = json.loads(output.getvalue())
124
+ self.assertEqual(data["status"], "completed")
125
+ self.assertEqual(data["target_agent"], "qwen_cli")
126
+
127
+ def test_dispatch_auto_uses_capability_routes_and_agents(self):
128
+ with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
129
+ root = Path(tmp)
130
+ agents_path = root / "agents.json"
131
+ routes_path = root / "routes.json"
132
+ agents_path.write_text(json.dumps({
133
+ "agents": {
134
+ "qwen_cli": {
135
+ "mode": "mock",
136
+ "roles": ["primary_engineer"],
137
+ "capabilities": ["code_edit"],
138
+ "priority": 80,
139
+ }
140
+ }
141
+ }), encoding="utf-8")
142
+ routes_path.write_text(json.dumps({
143
+ "routes": {
144
+ "implementation": {
145
+ "requires": ["code_edit"],
146
+ "prefer": ["primary_engineer"],
147
+ }
148
+ },
149
+ "opposites": {}
150
+ }), encoding="utf-8")
151
+ output = io.StringIO()
152
+ with redirect_stdout(output):
153
+ code = main([
154
+ "dispatch-auto",
155
+ "implement a parser",
156
+ "--db",
157
+ str(root / "bus.db"),
158
+ "--cwd",
159
+ str(root),
160
+ "--agents",
161
+ str(agents_path),
162
+ "--routes",
163
+ str(routes_path),
164
+ ])
165
+ self.assertEqual(code, 0)
166
+ self.assertIn('"target_agent": "qwen_cli"', output.getvalue())
167
+
168
+ def test_orchestrate_uses_capability_routes_and_agents(self):
169
+ with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
170
+ root = Path(tmp)
171
+ agents_path = root / "agents.json"
172
+ routes_path = root / "routes.json"
173
+ agents_path.write_text(json.dumps({
174
+ "agents": {
175
+ "qwen_cli": {
176
+ "mode": "mock",
177
+ "capabilities": ["code_edit"],
178
+ },
179
+ "gemini_cli": {
180
+ "mode": "mock",
181
+ "capabilities": ["code_review"],
182
+ },
183
+ }
184
+ }), encoding="utf-8")
185
+ routes_path.write_text(json.dumps({
186
+ "routes": {
187
+ "implementation": {
188
+ "requires": ["code_edit"],
189
+ "review_required": True,
190
+ },
191
+ "code_review": {
192
+ "requires": ["code_review"],
193
+ },
194
+ },
195
+ "opposites": {}
196
+ }), encoding="utf-8")
197
+ output = io.StringIO()
198
+ with redirect_stdout(output):
199
+ code = main([
200
+ "orchestrate",
201
+ "implement parser",
202
+ "--db",
203
+ str(root / "bus.db"),
204
+ "--cwd",
205
+ str(root),
206
+ "--agents",
207
+ str(agents_path),
208
+ "--routes",
209
+ str(routes_path),
210
+ ])
211
+ self.assertEqual(code, 0)
212
+ text = output.getvalue()
213
+ self.assertIn('"target_agent": "qwen_cli"', text)
214
+ self.assertIn('"target_agent": "gemini_cli"', text)
215
+ self.assertIn('"acceptance_status": "accepted"', text)
216
+
217
+ def test_orchestrate_wait_runs_workers_and_completes(self):
218
+ with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
219
+ root = Path(tmp)
220
+ agents_path = root / "agents.json"
221
+ routes_path = root / "routes.json"
222
+ agents_path.write_text(json.dumps({
223
+ "agents": {
224
+ "qwen_cli": {
225
+ "mode": "mock",
226
+ "capabilities": ["code_edit"],
227
+ },
228
+ "gemini_cli": {
229
+ "mode": "mock",
230
+ "capabilities": ["code_review"],
231
+ },
232
+ }
233
+ }), encoding="utf-8")
234
+ routes_path.write_text(json.dumps({
235
+ "routes": {
236
+ "implementation": {
237
+ "requires": ["code_edit"],
238
+ "review_required": True,
239
+ },
240
+ "code_review": {
241
+ "requires": ["code_review"],
242
+ },
243
+ },
244
+ "opposites": {}
245
+ }), encoding="utf-8")
246
+ output = io.StringIO()
247
+ with redirect_stdout(output):
248
+ code = main([
249
+ "orchestrate",
250
+ "implement parser",
251
+ "--db",
252
+ str(root / "bus.db"),
253
+ "--cwd",
254
+ str(root),
255
+ "--agents",
256
+ str(agents_path),
257
+ "--routes",
258
+ str(routes_path),
259
+ "--wait",
260
+ "--wait-timeout",
261
+ "10",
262
+ ])
263
+ self.assertEqual(code, 0)
264
+ text = output.getvalue()
265
+ self.assertIn('"acceptance_status": "accepted"', text)
266
+
267
+
268
+ def test_demo_command_produces_friendly_output(self):
269
+ with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
270
+ root = Path(tmp)
271
+ output = io.StringIO()
272
+ with redirect_stdout(output):
273
+ code = main([
274
+ "demo",
275
+ "--db",
276
+ str(root / "bus.db"),
277
+ ])
278
+ self.assertEqual(code, 0)
279
+ text = output.getvalue()
280
+ self.assertIn("Trinity Lite Demo", text)
281
+ self.assertIn("Next steps:", text)
282
+ self.assertIn("trinity-lite tasks", text)
283
+ self.assertIn("docs/REAL_AGENTS.md", text)
284
+
285
+ def test_version_command_prints_version(self):
286
+ output = io.StringIO()
287
+ with redirect_stdout(output):
288
+ code = main(["version"])
289
+ self.assertEqual(code, 0)
290
+ self.assertIn("trinity-lite", output.getvalue())
291
+
292
+ def test_version_flag_prints_version(self):
293
+ output = io.StringIO()
294
+ with redirect_stdout(output):
295
+ code = main(["--version"])
296
+ self.assertEqual(code, 0)
297
+ self.assertIn("trinity-lite", output.getvalue())
298
+
299
+ def test_no_args_shows_banner(self):
300
+ output = io.StringIO()
301
+ with redirect_stdout(output):
302
+ code = main([])
303
+ self.assertEqual(code, 0)
304
+ text = output.getvalue()
305
+ self.assertIn("Trinity Lite", text)
306
+ self.assertIn("Quick demo", text)
307
+ self.assertIn("trinity-lite demo", text)
308
+
309
+ def test_help_flag_works(self):
310
+ import sys
311
+ output = io.StringIO()
312
+ with redirect_stdout(output):
313
+ try:
314
+ main(["--help"])
315
+ except SystemExit as e:
316
+ self.assertEqual(e.code, 0)
317
+ text = output.getvalue()
318
+ self.assertIn("demo", text)
319
+ self.assertIn("version", text)
320
+
321
+
322
+ if __name__ == "__main__":
323
+ unittest.main()
@@ -216,6 +216,17 @@ class TrinityBus:
216
216
  row = conn.execute("SELECT * FROM messages WHERE id = ?", (msg_id,)).fetchone()
217
217
  return dict(row)
218
218
 
219
+ def await_task(self, task_id: str, timeout: float = 300) -> dict[str, Any]:
220
+ """Block until task reaches terminal status or timeout."""
221
+ import time
222
+ deadline = time.monotonic() + timeout
223
+ while time.monotonic() < deadline:
224
+ task = self.get_task(task_id)
225
+ if task["status"] in TERMINAL_STATUSES:
226
+ return task
227
+ time.sleep(0.2)
228
+ raise TimeoutError(f"task {task_id} did not finish within {timeout}s")
229
+
219
230
  def inbox(
220
231
  self,
221
232
  agent: str,
@@ -0,0 +1,440 @@
1
+ """Command line interface for Trinity Lite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .bus import TrinityBus
13
+ from .doctor import run_doctor
14
+ from .orchestrator import run_review_flow
15
+ from .router import resolve_route
16
+ from .worker import run_loop, run_once
17
+
18
+
19
+ def print_json(data: Any) -> None:
20
+ print(json.dumps(data, ensure_ascii=False, indent=2))
21
+
22
+
23
+ def _version() -> str:
24
+ """Return the installed version, favouring importlib.metadata."""
25
+ try:
26
+ from importlib.metadata import PackageNotFoundError, version # Python >= 3.8
27
+
28
+ return version("trinity-lite")
29
+ except (ImportError, PackageNotFoundError):
30
+ from . import __version__
31
+
32
+ return __version__ # pragma: no cover — fallback for development
33
+
34
+
35
+ BANNER = """\
36
+ Trinity Lite — multi-agent task bus
37
+ ====================================
38
+
39
+ Quick demo: trinity-lite demo
40
+ Full help: trinity-lite --help
41
+
42
+ Commands: demo, dispatch, dispatch-auto, worker, orchestrate,
43
+ status, tasks, route, doctor, send, inbox"""
44
+
45
+
46
+ def build_parser() -> argparse.ArgumentParser:
47
+ parser = argparse.ArgumentParser(prog="trinity-lite")
48
+ parser.add_argument("--version", action="store_true", help="show version and exit")
49
+ common = argparse.ArgumentParser(add_help=False)
50
+ common.add_argument("--db", default=None, help="SQLite database path")
51
+ common.add_argument("--routes", default=None, help="routes JSON path")
52
+ common.add_argument("--agents", default=None, help="agents JSON path")
53
+ sub = parser.add_subparsers(dest="command")
54
+
55
+ route = sub.add_parser("route", parents=[common], help="resolve a route without dispatching")
56
+ route.add_argument("task")
57
+ route.add_argument("--type", dest="task_type")
58
+ route.add_argument("--previous-agent")
59
+
60
+ dispatch = sub.add_parser("dispatch", parents=[common], help="dispatch to an explicit agent")
61
+ dispatch.add_argument("target_agent")
62
+ dispatch.add_argument("task")
63
+ dispatch.add_argument("--source", default="user")
64
+ dispatch.add_argument("--type", dest="task_type")
65
+ dispatch.add_argument("--cwd", default=os.getcwd())
66
+ dispatch.add_argument("--wait", action="store_true", help="block until task completes and print final result")
67
+ dispatch.add_argument("--wait-timeout", type=float, default=300, help="timeout in seconds for --wait (default: 300)")
68
+
69
+ auto = sub.add_parser("dispatch-auto", parents=[common], help="resolve route then dispatch")
70
+ auto.add_argument("task")
71
+ auto.add_argument("--source", default="user")
72
+ auto.add_argument("--type", dest="task_type")
73
+ auto.add_argument("--previous-agent")
74
+ auto.add_argument("--cwd", default=os.getcwd())
75
+ auto.add_argument("--wait", action="store_true", help="block until task completes and print final result")
76
+ auto.add_argument("--wait-timeout", type=float, default=300, help="timeout in seconds for --wait (default: 300)")
77
+
78
+ orchestrate = sub.add_parser("orchestrate", parents=[common], help="dispatch and run a primary task with optional review")
79
+ orchestrate.add_argument("task")
80
+ orchestrate.add_argument("--source", default="user")
81
+ orchestrate.add_argument("--type", dest="task_type")
82
+ orchestrate.add_argument("--previous-agent")
83
+ orchestrate.add_argument("--cwd", default=os.getcwd())
84
+ orchestrate.add_argument("--no-run", action="store_true", help="dispatch the primary task without running workers")
85
+ orchestrate.add_argument("--wait", action="store_true", help="block until orchestrated flow completes")
86
+ orchestrate.add_argument("--wait-timeout", type=float, default=300, help="timeout in seconds for --wait (default: 300)")
87
+
88
+ status = sub.add_parser("status", parents=[common], help="show task status")
89
+ status.add_argument("task_id")
90
+
91
+ tasks = sub.add_parser("tasks", parents=[common], help="list recent tasks")
92
+ tasks.add_argument("--agent")
93
+ tasks.add_argument("--limit", type=int, default=20)
94
+
95
+ worker = sub.add_parser("worker", parents=[common], help="run a worker for one agent")
96
+ worker.add_argument("agent")
97
+ worker.add_argument("--once", action="store_true")
98
+ worker.add_argument("--poll", type=float, default=2.0)
99
+
100
+ send = sub.add_parser("send", parents=[common], help="send a durable message")
101
+ send.add_argument("target_agent")
102
+ send.add_argument("message")
103
+ send.add_argument("--source", default="user")
104
+ send.add_argument("--task-id")
105
+
106
+ inbox = sub.add_parser("inbox", parents=[common], help="read durable messages")
107
+ inbox.add_argument("agent")
108
+ inbox.add_argument("--all", action="store_true")
109
+ inbox.add_argument("--mark-read", action="store_true")
110
+ inbox.add_argument("--limit", type=int, default=20)
111
+
112
+ doctor = sub.add_parser("doctor", parents=[common], help="run environment checks")
113
+ doctor.add_argument("--scan-root")
114
+ doctor.add_argument("--runtime-root", help="runtime state directory for hygiene checks")
115
+ doctor.add_argument(
116
+ "--retired-port",
117
+ action="append",
118
+ type=int,
119
+ default=[],
120
+ help="TCP port that should not be listening, repeatable",
121
+ )
122
+
123
+ version_cmd = sub.add_parser("version", parents=[common], help="show version and exit")
124
+
125
+ demo = sub.add_parser("demo", parents=[common], help="run a guided first-run demo")
126
+
127
+ return parser
128
+
129
+
130
+ def main(argv: list[str] | None = None) -> int:
131
+ parser = build_parser()
132
+
133
+ # No-args: show friendly intro
134
+ if argv is None:
135
+ argv = sys.argv[1:]
136
+ if not argv:
137
+ print(BANNER)
138
+ return 0
139
+
140
+ args = parser.parse_args(argv)
141
+
142
+ # --version flag (before subcommand)
143
+ if args.version:
144
+ print(f"trinity-lite {_version()}")
145
+ return 0
146
+
147
+ # No subcommand but unknown args → show friendly intro
148
+ if args.command is None:
149
+ print(BANNER)
150
+ return 0
151
+
152
+ try:
153
+ return run_command(args)
154
+ except (KeyError, OSError, ValueError) as exc:
155
+ print_json({"error": str(exc)})
156
+ return 2
157
+
158
+
159
+ def run_command(args: argparse.Namespace) -> int:
160
+ bus = TrinityBus(args.db)
161
+
162
+ if args.command == "version":
163
+ print(f"trinity-lite {_version()}")
164
+ return 0
165
+
166
+ if args.command == "route":
167
+ print_json(resolve_route(args.task, args.task_type, args.previous_agent, args.routes, args.agents))
168
+ return 0
169
+ if args.command == "dispatch":
170
+ task = bus.submit_task(
171
+ source_agent=args.source,
172
+ target_agent=args.target_agent,
173
+ prompt=args.task,
174
+ task_type=args.task_type,
175
+ cwd=args.cwd,
176
+ )
177
+ if getattr(args, "wait", False):
178
+ task = _wait_for_task(bus, task["id"], args.target_agent, args.agents, args.wait_timeout)
179
+ print_json(task)
180
+ return 0
181
+ if args.command == "dispatch-auto":
182
+ route = resolve_route(args.task, args.task_type, args.previous_agent, args.routes, args.agents)
183
+ task = bus.submit_task(
184
+ source_agent=args.source,
185
+ target_agent=route["agent"],
186
+ prompt=args.task,
187
+ task_type=route["task_type"],
188
+ cwd=args.cwd,
189
+ )
190
+ task["route"] = route
191
+ if getattr(args, "wait", False):
192
+ task = _wait_for_task(bus, task["id"], route["agent"], args.agents, args.wait_timeout)
193
+ print_json(task)
194
+ return 0
195
+ if args.command == "orchestrate":
196
+ result = run_review_flow(
197
+ args.task,
198
+ bus,
199
+ args.routes,
200
+ args.agents,
201
+ args.source,
202
+ args.task_type,
203
+ args.previous_agent,
204
+ args.cwd,
205
+ run_workers=not args.no_run and not getattr(args, "wait", False),
206
+ )
207
+ if getattr(args, "wait", False):
208
+ result = _wait_for_flow(bus, result, args.agents, args.routes, args.wait_timeout)
209
+ print_json(result)
210
+ return 0
211
+ if args.command == "status":
212
+ print_json(bus.get_task(args.task_id))
213
+ return 0
214
+ if args.command == "tasks":
215
+ print_json(bus.list_tasks(args.agent, args.limit))
216
+ return 0
217
+ if args.command == "worker":
218
+ if args.once:
219
+ print_json(run_once(args.agent, bus, args.agents))
220
+ return 0
221
+ run_loop(args.agent, bus, args.agents, args.poll)
222
+ return 0
223
+ if args.command == "send":
224
+ print_json(bus.send_message(args.source, args.target_agent, args.message, args.task_id))
225
+ return 0
226
+ if args.command == "inbox":
227
+ print_json(bus.inbox(args.agent, unread_only=not args.all, mark_read=args.mark_read, limit=args.limit))
228
+ return 0
229
+ if args.command == "doctor":
230
+ print_json(run_doctor(
231
+ args.db,
232
+ args.routes,
233
+ args.agents,
234
+ args.scan_root,
235
+ args.runtime_root,
236
+ args.retired_port,
237
+ ))
238
+ return 0
239
+ if args.command == "demo":
240
+ return _demo(args, bus)
241
+
242
+ raise AssertionError(f"unhandled command: {args.command}")
243
+
244
+
245
+ def _wait_for_task(
246
+ bus: TrinityBus,
247
+ task_id: str,
248
+ agent: str,
249
+ agents_path: str | None,
250
+ timeout: float,
251
+ ) -> dict[str, Any]:
252
+ """Run worker cycles for agent until task completes or timeout."""
253
+ import time as _time
254
+ from .bus import TERMINAL_STATUSES
255
+ deadline = _time.monotonic() + timeout
256
+ while _time.monotonic() < deadline:
257
+ task = bus.get_task(task_id)
258
+ if task["status"] in TERMINAL_STATUSES:
259
+ return task
260
+ # Try to process one queued/running task
261
+ run_once(agent, bus, agents_path, task_id=task_id)
262
+ _time.sleep(0.1)
263
+ raise TimeoutError(f"task {task_id} did not finish within {timeout}s")
264
+
265
+
266
+ def _wait_for_flow(
267
+ bus: TrinityBus,
268
+ flow: dict[str, Any],
269
+ agents_path: str | None,
270
+ routes_path: str | None,
271
+ timeout: float,
272
+ ) -> dict[str, Any]:
273
+ """Run workers to complete a review flow until terminal."""
274
+ import time as _time
275
+ from .bus import TERMINAL_STATUSES
276
+ from .orchestrator import _review_prompt
277
+
278
+ primary_id = flow["primary_task"]["id"]
279
+ primary_agent = flow["route"]["agent"]
280
+ route = flow["route"]
281
+
282
+ # Review may not have been dispatched yet (if run_workers=False was used)
283
+ review_id = None
284
+ review_agent = None
285
+ review = flow.get("review_task")
286
+ if review:
287
+ review_id = review["id"]
288
+ review_agent = flow.get("review_route", {}).get("agent") if flow.get("review_route") else None
289
+
290
+ deadline = _time.monotonic() + timeout
291
+ review_dispatched = review_id is not None
292
+
293
+ while _time.monotonic() < deadline:
294
+ primary = bus.get_task(primary_id)
295
+
296
+ # If primary just completed and review is required but not yet dispatched
297
+ if (primary["status"] == "completed"
298
+ and route["review_required"]
299
+ and not review_dispatched):
300
+ review_prompt_text = _review_prompt(primary)
301
+ review_route = resolve_route(
302
+ review_prompt_text,
303
+ "code_review",
304
+ route["agent"],
305
+ routes_path,
306
+ agents_path,
307
+ )
308
+ review_task = bus.submit_task(
309
+ source_agent=route["agent"],
310
+ target_agent=review_route["agent"],
311
+ prompt=review_prompt_text,
312
+ task_type=review_route["task_type"],
313
+ cwd=primary["cwd"],
314
+ depth=int(primary["depth"]) + 1,
315
+ )
316
+ review_id = review_task["id"]
317
+ review_agent = review_route["agent"]
318
+ review_dispatched = True
319
+ flow["review_route"] = review_route
320
+ flow["review_task"] = review_task
321
+
322
+ review_done = True
323
+ if review_id is not None:
324
+ review_task = bus.get_task(review_id)
325
+ review_done = review_task["status"] in TERMINAL_STATUSES
326
+
327
+ if primary["status"] in TERMINAL_STATUSES and review_done:
328
+ flow["primary_task"] = primary
329
+ if review_id is not None:
330
+ flow["review_task"] = bus.get_task(review_id)
331
+ flow["acceptance_status"] = _acceptance_status_from_flow(
332
+ flow["route"], primary, flow.get("review_task")
333
+ )
334
+ return flow
335
+
336
+ if primary["status"] not in TERMINAL_STATUSES:
337
+ run_once(primary_agent, bus, agents_path, task_id=primary_id)
338
+ elif review_id is not None and review_agent:
339
+ run_once(review_agent, bus, agents_path, task_id=review_id)
340
+ _time.sleep(0.1)
341
+ raise TimeoutError(f"flow for {primary_id} did not finish within {timeout}s")
342
+
343
+
344
+ def _acceptance_status_from_flow(
345
+ route: dict[str, Any],
346
+ primary_task: dict[str, Any],
347
+ review_task: dict[str, Any] | None,
348
+ ) -> str:
349
+ from .orchestrator import _acceptance_status
350
+ return _acceptance_status(route, primary_task, review_task)
351
+
352
+
353
+ def _demo(args: argparse.Namespace, bus: TrinityBus) -> int:
354
+ """Run a guided demo flow with human-friendly output."""
355
+
356
+ print("Trinity Lite Demo")
357
+ print("=================")
358
+
359
+ # ---- 1. Doctor ----
360
+ print("→ Running environment checks...")
361
+ try:
362
+ health = run_doctor(args.db, args.routes, args.agents)
363
+ status_label = "healthy" if health["status"] == "healthy" else "unhealthy"
364
+ icon = "✓" if health["status"] == "healthy" else "✗"
365
+ print(f" {icon} Doctor: {status_label}")
366
+ for check in health.get("checks", []):
367
+ mark = "✓" if check["ok"] else "✗"
368
+ print(f" {mark} {check['name']}: {str(check.get('detail', '')).strip()}")
369
+ except Exception as exc:
370
+ print(f" ✗ Doctor: {exc}")
371
+ print()
372
+
373
+ # ---- 2. Dispatch + worker cycle (full orchestrated flow) ----
374
+ demo_prompt = "write a hello world function in Python"
375
+ print("→ Dispatching demo task...")
376
+ flow = run_review_flow(
377
+ demo_prompt,
378
+ bus,
379
+ args.routes,
380
+ args.agents,
381
+ source_agent="user",
382
+ cwd=str(Path.home()),
383
+ run_workers=True,
384
+ )
385
+ primary = flow["primary_task"]
386
+ route = flow["route"]
387
+
388
+ print(f" Task ID: {primary['id']}")
389
+ print(f" Routed to: {route['agent']}")
390
+ print()
391
+
392
+ # ---- 3. Codex worker already ran in run_review_flow ----
393
+ print(f"→ Running mock worker ({route['agent']})...")
394
+ icon = "✓" if primary["status"] == "completed" else "✗"
395
+ print(f" {icon} Task {primary['id']} {primary['status']}")
396
+ print()
397
+
398
+ # ---- 4. Claude Code review (already ran via run_review_flow) ----
399
+ print("→ Running mock worker (claude_code)...")
400
+ review_task = flow.get("review_task")
401
+ if review_task:
402
+ icon = "✓" if review_task.get("status") == "completed" else "✗"
403
+ print(f" {icon} Review {review_task.get('status')}")
404
+ else:
405
+ print(" - No review task required for this demo")
406
+ print()
407
+
408
+ # ---- 5. Results ----
409
+ print("→ Results:")
410
+ final = bus.get_task(primary["id"])
411
+ print(f" Task: {final['prompt']}")
412
+ print(f" Status: {final['status']}")
413
+ if final.get("result"):
414
+ result_text = final["result"]
415
+ if len(result_text) > 200:
416
+ result_text = result_text[:200] + "..."
417
+ print(f" Result: {result_text}")
418
+ if final.get("error"):
419
+ print(f" Error: {final['error']}")
420
+ if review_task:
421
+ review_final = bus.get_task(review_task["id"])
422
+ print(f" Review: {review_final.get('status')}")
423
+ if review_final.get("result"):
424
+ r_text = review_final["result"]
425
+ if len(r_text) > 200:
426
+ r_text = r_text[:200] + "..."
427
+ print(f" Review result: {r_text}")
428
+ print()
429
+
430
+ # ---- 6. Next steps ----
431
+ print("Next steps:")
432
+ print(" • See all tasks: trinity-lite tasks")
433
+ print(" • Connect real agents: docs/REAL_AGENTS.md")
434
+ print(" • Full docs: https://github.com/Yomiracle/trinity-lite")
435
+
436
+ return 0
437
+
438
+
439
+ if __name__ == "__main__":
440
+ raise SystemExit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trinity-lite
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A minimal public multi-agent task bus with routing, workers, and safety checks.
5
5
  Author: Yomiracle
6
6
  License-Expression: MIT
@@ -155,7 +155,24 @@ trinity-lite worker codex --once
155
155
  trinity-lite tasks
156
156
  ```
157
157
 
158
- For local development from source:
158
+ ## Upgrading
159
+
160
+ ```bash
161
+ python3 -m pip install --upgrade trinity-lite
162
+ ```
163
+
164
+ Trinity Lite follows [semantic versioning](https://semver.org). Patch releases (0.1.x) are backward-compatible
165
+ and require no configuration or data migration. Check your version:
166
+
167
+ ```bash
168
+ trinity-lite doctor
169
+ # or
170
+ python3 -m pip show trinity-lite | grep Version
171
+ ```
172
+
173
+ See [CHANGELOG.md](CHANGELOG.md) for what changed between versions.
174
+
175
+ ## Quick Start (from source)
159
176
 
160
177
  ```bash
161
178
  git clone https://github.com/Yomiracle/trinity-lite.git
@@ -1,150 +0,0 @@
1
- import io
2
- import json
3
- import tempfile
4
- import unittest
5
- from contextlib import redirect_stdout
6
- from pathlib import Path
7
-
8
- from trinity_lite.cli import main
9
-
10
-
11
- class CliTest(unittest.TestCase):
12
- def test_global_options_work_after_subcommand(self):
13
- with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
14
- root = Path(tmp)
15
- db = root / "bus.db"
16
- output = io.StringIO()
17
- with redirect_stdout(output):
18
- code = main([
19
- "dispatch-auto",
20
- "implement a parser",
21
- "--db",
22
- str(db),
23
- "--cwd",
24
- str(root),
25
- ])
26
- self.assertEqual(code, 0)
27
- code = main(["worker", "codex", "--once", "--db", str(db)])
28
- self.assertEqual(code, 0)
29
-
30
- def test_route_errors_are_structured(self):
31
- output = io.StringIO()
32
- with redirect_stdout(output):
33
- code = main(["route", "review this patch"])
34
- self.assertEqual(code, 2)
35
- self.assertIn('"error"', output.getvalue())
36
-
37
- def test_doctor_accepts_runtime_hygiene_options(self):
38
- with tempfile.TemporaryDirectory() as tmp:
39
- root = Path(tmp)
40
- runtime = root / "runtime"
41
- runtime.mkdir()
42
- (runtime / "metrics.jsonl").write_text("", encoding="utf-8")
43
- output = io.StringIO()
44
- with redirect_stdout(output):
45
- code = main([
46
- "doctor",
47
- "--db",
48
- str(root / "bus.db"),
49
- "--runtime-root",
50
- str(runtime),
51
- "--retired-port",
52
- "70000",
53
- ])
54
- self.assertEqual(code, 0)
55
- self.assertIn('"runtime_metrics"', output.getvalue())
56
- self.assertIn('"retired_port:70000"', output.getvalue())
57
-
58
- def test_dispatch_auto_uses_capability_routes_and_agents(self):
59
- with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
60
- root = Path(tmp)
61
- agents_path = root / "agents.json"
62
- routes_path = root / "routes.json"
63
- agents_path.write_text(json.dumps({
64
- "agents": {
65
- "qwen_cli": {
66
- "mode": "mock",
67
- "roles": ["primary_engineer"],
68
- "capabilities": ["code_edit"],
69
- "priority": 80,
70
- }
71
- }
72
- }), encoding="utf-8")
73
- routes_path.write_text(json.dumps({
74
- "routes": {
75
- "implementation": {
76
- "requires": ["code_edit"],
77
- "prefer": ["primary_engineer"],
78
- }
79
- },
80
- "opposites": {}
81
- }), encoding="utf-8")
82
- output = io.StringIO()
83
- with redirect_stdout(output):
84
- code = main([
85
- "dispatch-auto",
86
- "implement a parser",
87
- "--db",
88
- str(root / "bus.db"),
89
- "--cwd",
90
- str(root),
91
- "--agents",
92
- str(agents_path),
93
- "--routes",
94
- str(routes_path),
95
- ])
96
- self.assertEqual(code, 0)
97
- self.assertIn('"target_agent": "qwen_cli"', output.getvalue())
98
-
99
- def test_orchestrate_uses_capability_routes_and_agents(self):
100
- with tempfile.TemporaryDirectory(dir=str(Path.home())) as tmp:
101
- root = Path(tmp)
102
- agents_path = root / "agents.json"
103
- routes_path = root / "routes.json"
104
- agents_path.write_text(json.dumps({
105
- "agents": {
106
- "qwen_cli": {
107
- "mode": "mock",
108
- "capabilities": ["code_edit"],
109
- },
110
- "gemini_cli": {
111
- "mode": "mock",
112
- "capabilities": ["code_review"],
113
- },
114
- }
115
- }), encoding="utf-8")
116
- routes_path.write_text(json.dumps({
117
- "routes": {
118
- "implementation": {
119
- "requires": ["code_edit"],
120
- "review_required": True,
121
- },
122
- "code_review": {
123
- "requires": ["code_review"],
124
- },
125
- },
126
- "opposites": {}
127
- }), encoding="utf-8")
128
- output = io.StringIO()
129
- with redirect_stdout(output):
130
- code = main([
131
- "orchestrate",
132
- "implement parser",
133
- "--db",
134
- str(root / "bus.db"),
135
- "--cwd",
136
- str(root),
137
- "--agents",
138
- str(agents_path),
139
- "--routes",
140
- str(routes_path),
141
- ])
142
- self.assertEqual(code, 0)
143
- text = output.getvalue()
144
- self.assertIn('"target_agent": "qwen_cli"', text)
145
- self.assertIn('"target_agent": "gemini_cli"', text)
146
- self.assertIn('"acceptance_status": "accepted"', text)
147
-
148
-
149
- if __name__ == "__main__":
150
- unittest.main()
@@ -1,175 +0,0 @@
1
- """Command line interface for Trinity Lite."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import os
8
- from typing import Any
9
-
10
- from .bus import TrinityBus
11
- from .doctor import run_doctor
12
- from .orchestrator import run_review_flow
13
- from .router import resolve_route
14
- from .worker import run_loop, run_once
15
-
16
-
17
- def print_json(data: Any) -> None:
18
- print(json.dumps(data, ensure_ascii=False, indent=2))
19
-
20
-
21
- def build_parser() -> argparse.ArgumentParser:
22
- parser = argparse.ArgumentParser(prog="trinity-lite")
23
- common = argparse.ArgumentParser(add_help=False)
24
- common.add_argument("--db", default=None, help="SQLite database path")
25
- common.add_argument("--routes", default=None, help="routes JSON path")
26
- common.add_argument("--agents", default=None, help="agents JSON path")
27
- sub = parser.add_subparsers(dest="command", required=True)
28
-
29
- route = sub.add_parser("route", parents=[common], help="resolve a route without dispatching")
30
- route.add_argument("task")
31
- route.add_argument("--type", dest="task_type")
32
- route.add_argument("--previous-agent")
33
-
34
- dispatch = sub.add_parser("dispatch", parents=[common], help="dispatch to an explicit agent")
35
- dispatch.add_argument("target_agent")
36
- dispatch.add_argument("task")
37
- dispatch.add_argument("--source", default="user")
38
- dispatch.add_argument("--type", dest="task_type")
39
- dispatch.add_argument("--cwd", default=os.getcwd())
40
-
41
- auto = sub.add_parser("dispatch-auto", parents=[common], help="resolve route then dispatch")
42
- auto.add_argument("task")
43
- auto.add_argument("--source", default="user")
44
- auto.add_argument("--type", dest="task_type")
45
- auto.add_argument("--previous-agent")
46
- auto.add_argument("--cwd", default=os.getcwd())
47
-
48
- orchestrate = sub.add_parser("orchestrate", parents=[common], help="dispatch and run a primary task with optional review")
49
- orchestrate.add_argument("task")
50
- orchestrate.add_argument("--source", default="user")
51
- orchestrate.add_argument("--type", dest="task_type")
52
- orchestrate.add_argument("--previous-agent")
53
- orchestrate.add_argument("--cwd", default=os.getcwd())
54
- orchestrate.add_argument("--no-run", action="store_true", help="dispatch the primary task without running workers")
55
-
56
- status = sub.add_parser("status", parents=[common], help="show task status")
57
- status.add_argument("task_id")
58
-
59
- tasks = sub.add_parser("tasks", parents=[common], help="list recent tasks")
60
- tasks.add_argument("--agent")
61
- tasks.add_argument("--limit", type=int, default=20)
62
-
63
- worker = sub.add_parser("worker", parents=[common], help="run a worker for one agent")
64
- worker.add_argument("agent")
65
- worker.add_argument("--once", action="store_true")
66
- worker.add_argument("--poll", type=float, default=2.0)
67
-
68
- send = sub.add_parser("send", parents=[common], help="send a durable message")
69
- send.add_argument("target_agent")
70
- send.add_argument("message")
71
- send.add_argument("--source", default="user")
72
- send.add_argument("--task-id")
73
-
74
- inbox = sub.add_parser("inbox", parents=[common], help="read durable messages")
75
- inbox.add_argument("agent")
76
- inbox.add_argument("--all", action="store_true")
77
- inbox.add_argument("--mark-read", action="store_true")
78
- inbox.add_argument("--limit", type=int, default=20)
79
-
80
- doctor = sub.add_parser("doctor", parents=[common], help="run environment checks")
81
- doctor.add_argument("--scan-root")
82
- doctor.add_argument("--runtime-root", help="runtime state directory for hygiene checks")
83
- doctor.add_argument(
84
- "--retired-port",
85
- action="append",
86
- type=int,
87
- default=[],
88
- help="TCP port that should not be listening, repeatable",
89
- )
90
-
91
- return parser
92
-
93
-
94
- def main(argv: list[str] | None = None) -> int:
95
- args = build_parser().parse_args(argv)
96
- try:
97
- return run_command(args)
98
- except (KeyError, OSError, ValueError) as exc:
99
- print_json({"error": str(exc)})
100
- return 2
101
-
102
-
103
- def run_command(args: argparse.Namespace) -> int:
104
- bus = TrinityBus(args.db)
105
-
106
- if args.command == "route":
107
- print_json(resolve_route(args.task, args.task_type, args.previous_agent, args.routes, args.agents))
108
- return 0
109
- if args.command == "dispatch":
110
- print_json(bus.submit_task(
111
- source_agent=args.source,
112
- target_agent=args.target_agent,
113
- prompt=args.task,
114
- task_type=args.task_type,
115
- cwd=args.cwd,
116
- ))
117
- return 0
118
- if args.command == "dispatch-auto":
119
- route = resolve_route(args.task, args.task_type, args.previous_agent, args.routes, args.agents)
120
- task = bus.submit_task(
121
- source_agent=args.source,
122
- target_agent=route["agent"],
123
- prompt=args.task,
124
- task_type=route["task_type"],
125
- cwd=args.cwd,
126
- )
127
- task["route"] = route
128
- print_json(task)
129
- return 0
130
- if args.command == "orchestrate":
131
- print_json(run_review_flow(
132
- args.task,
133
- bus,
134
- args.routes,
135
- args.agents,
136
- args.source,
137
- args.task_type,
138
- args.previous_agent,
139
- args.cwd,
140
- run_workers=not args.no_run,
141
- ))
142
- return 0
143
- if args.command == "status":
144
- print_json(bus.get_task(args.task_id))
145
- return 0
146
- if args.command == "tasks":
147
- print_json(bus.list_tasks(args.agent, args.limit))
148
- return 0
149
- if args.command == "worker":
150
- if args.once:
151
- print_json(run_once(args.agent, bus, args.agents))
152
- return 0
153
- run_loop(args.agent, bus, args.agents, args.poll)
154
- return 0
155
- if args.command == "send":
156
- print_json(bus.send_message(args.source, args.target_agent, args.message, args.task_id))
157
- return 0
158
- if args.command == "inbox":
159
- print_json(bus.inbox(args.agent, unread_only=not args.all, mark_read=args.mark_read, limit=args.limit))
160
- return 0
161
- if args.command == "doctor":
162
- print_json(run_doctor(
163
- args.db,
164
- args.routes,
165
- args.agents,
166
- args.scan_root,
167
- args.runtime_root,
168
- args.retired_port,
169
- ))
170
- return 0
171
- raise AssertionError(f"unhandled command: {args.command}")
172
-
173
-
174
- if __name__ == "__main__":
175
- raise SystemExit(main())
File without changes
File without changes