trinity-lite 0.7.0__tar.gz → 0.7.2__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.
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/PKG-INFO +6 -4
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/README.md +5 -3
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/pyproject.toml +1 -1
- trinity_lite-0.7.2/tests/test_init.py +238 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_mcp_server.py +13 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/__init__.py +1 -1
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/cli.py +35 -4
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/doctor.py +71 -1
- trinity_lite-0.7.2/trinity_lite/init.py +116 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/mcp_server.py +15 -4
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite.egg-info/PKG-INFO +6 -4
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite.egg-info/SOURCES.txt +2 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/LICENSE +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/setup.cfg +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_bus.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_cli.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_doctor.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_guard.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_orchestrator.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_pipeline.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_proof.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_router.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_worker.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/tests/test_worktree.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/__main__.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/adapters.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/bus.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/config.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/guard.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/model_autodetect.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/model_pool_wizard.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/model_selector.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/orchestrator.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/paths.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/pipeline.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/proof.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/py.typed +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/router.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/validation.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/worker.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite/worktree.py +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite.egg-info/dependency_links.txt +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite.egg-info/entry_points.txt +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/trinity_lite.egg-info/requires.txt +0 -0
- {trinity_lite-0.7.0 → trinity_lite-0.7.2}/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.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Local AgentOps for cross-vendor CLI agents: route work, recover state, and accept only with evidence.
|
|
5
5
|
Author: Yomiracle
|
|
6
6
|
License-Expression: MIT
|
|
@@ -99,7 +99,7 @@ LangGraph and CrewAI give you primitives for building agents from scratch — gr
|
|
|
99
99
|
- **Execute safely, no shell injection.** Agent commands are JSON arrays run with `shell=False`. No string interpolation into a shell. No surprises.
|
|
100
100
|
- **Test with mock agents.** Mock agents simulate the full cycle without real CLIs. Prototype routing, persistence, and review handoffs first. Wire up real agents later.
|
|
101
101
|
- **Guard against runaway delegation.** Self-delegation is blocked. Delegation depth is capped. Working directories are allowlisted. Safe by default.
|
|
102
|
-
- **Check health in one pass.** `trinity-lite doctor` verifies Python, SQLite, route config, agent config, and publish readiness.
|
|
102
|
+
- **Check health in one pass.** `trinity-lite doctor` verifies Python, SQLite, route config, agent config, and publish readiness. `trinity-lite doctor --onboarding` grades every check as blocker or optional, probes for agent CLIs, and fails only on blockers.
|
|
103
103
|
- **Zero core dependencies.** The default runtime is Python standard library only. YAML pipelines are available through an optional extra.
|
|
104
104
|
- **150+ tests guarding the surface area.** Mock workflows, safety checks, routing, persistence, MCP, and acceptance gates — all covered.
|
|
105
105
|
- **Optional model selection hints.** Select from your declared model pool with transparent task, tier, and capability rules; no claim of a universal best or cheapest model.
|
|
@@ -130,13 +130,15 @@ trinity-lite orchestrate "implement a rate limiter for the API"
|
|
|
130
130
|
|
|
131
131
|
The primary task row records `route_json`, `review_task_id`, `verification_json`, `acceptance_status`, `acceptance_reason`, and `accepted_at`.
|
|
132
132
|
|
|
133
|
-
Ready for real CLIs when you are:
|
|
133
|
+
Ready for real CLIs when you are — auto-detect what is installed and generate a safe starter config:
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
|
-
|
|
136
|
+
trinity-lite init
|
|
137
137
|
trinity-lite orchestrate "implement a rate limiter for the API" --agents agents.local.json
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
`trinity-lite init` probes PATH for Codex, Claude Code, and Hermes, writes `agents.local.json` with JSON-array commands for the CLIs it finds, keeps the rest in mock mode, and never writes credentials or overwrites an existing config without `--force`. Prefer hand-editing? `cp examples/agents.command.example.json agents.local.json` still works.
|
|
141
|
+
|
|
140
142
|
Prefer manual control? Use the lower-level bus commands:
|
|
141
143
|
|
|
142
144
|
```bash
|
|
@@ -69,7 +69,7 @@ LangGraph and CrewAI give you primitives for building agents from scratch — gr
|
|
|
69
69
|
- **Execute safely, no shell injection.** Agent commands are JSON arrays run with `shell=False`. No string interpolation into a shell. No surprises.
|
|
70
70
|
- **Test with mock agents.** Mock agents simulate the full cycle without real CLIs. Prototype routing, persistence, and review handoffs first. Wire up real agents later.
|
|
71
71
|
- **Guard against runaway delegation.** Self-delegation is blocked. Delegation depth is capped. Working directories are allowlisted. Safe by default.
|
|
72
|
-
- **Check health in one pass.** `trinity-lite doctor` verifies Python, SQLite, route config, agent config, and publish readiness.
|
|
72
|
+
- **Check health in one pass.** `trinity-lite doctor` verifies Python, SQLite, route config, agent config, and publish readiness. `trinity-lite doctor --onboarding` grades every check as blocker or optional, probes for agent CLIs, and fails only on blockers.
|
|
73
73
|
- **Zero core dependencies.** The default runtime is Python standard library only. YAML pipelines are available through an optional extra.
|
|
74
74
|
- **150+ tests guarding the surface area.** Mock workflows, safety checks, routing, persistence, MCP, and acceptance gates — all covered.
|
|
75
75
|
- **Optional model selection hints.** Select from your declared model pool with transparent task, tier, and capability rules; no claim of a universal best or cheapest model.
|
|
@@ -100,13 +100,15 @@ trinity-lite orchestrate "implement a rate limiter for the API"
|
|
|
100
100
|
|
|
101
101
|
The primary task row records `route_json`, `review_task_id`, `verification_json`, `acceptance_status`, `acceptance_reason`, and `accepted_at`.
|
|
102
102
|
|
|
103
|
-
Ready for real CLIs when you are:
|
|
103
|
+
Ready for real CLIs when you are — auto-detect what is installed and generate a safe starter config:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
-
|
|
106
|
+
trinity-lite init
|
|
107
107
|
trinity-lite orchestrate "implement a rate limiter for the API" --agents agents.local.json
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
`trinity-lite init` probes PATH for Codex, Claude Code, and Hermes, writes `agents.local.json` with JSON-array commands for the CLIs it finds, keeps the rest in mock mode, and never writes credentials or overwrites an existing config without `--force`. Prefer hand-editing? `cp examples/agents.command.example.json agents.local.json` still works.
|
|
111
|
+
|
|
110
112
|
Prefer manual control? Use the lower-level bus commands:
|
|
111
113
|
|
|
112
114
|
```bash
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trinity-lite"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
8
8
|
description = "Local AgentOps for cross-vendor CLI agents: route work, recover state, and accept only with evidence."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from contextlib import redirect_stdout
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest import mock
|
|
10
|
+
|
|
11
|
+
from trinity_lite.cli import main
|
|
12
|
+
from trinity_lite.doctor import run_doctor
|
|
13
|
+
from trinity_lite.init import (
|
|
14
|
+
AGENT_CLI_PRESETS,
|
|
15
|
+
build_starter_config,
|
|
16
|
+
detect_agent_clis,
|
|
17
|
+
run_init,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fake_path(dir_path: Path, names: list[str]) -> str:
|
|
22
|
+
"""Create executable stubs for the given CLI names in dir_path."""
|
|
23
|
+
for name in names:
|
|
24
|
+
stub = dir_path / name
|
|
25
|
+
stub.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
|
26
|
+
stub.chmod(stub.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
27
|
+
return str(dir_path)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DetectAgentClisTest(unittest.TestCase):
|
|
31
|
+
def test_detects_present_clis(self):
|
|
32
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
33
|
+
path = _fake_path(Path(tmp), ["codex", "claude"])
|
|
34
|
+
with mock.patch.dict(os.environ, {"PATH": path}):
|
|
35
|
+
detected = detect_agent_clis()
|
|
36
|
+
self.assertTrue(detected["codex"].endswith("codex"))
|
|
37
|
+
self.assertTrue(detected["claude_code"].endswith("claude"))
|
|
38
|
+
self.assertIsNone(detected["hermes"])
|
|
39
|
+
|
|
40
|
+
def test_detects_nothing_on_empty_path(self):
|
|
41
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
42
|
+
with mock.patch.dict(os.environ, {"PATH": str(Path(tmp))}):
|
|
43
|
+
detected = detect_agent_clis()
|
|
44
|
+
self.assertTrue(all(exe is None for exe in detected.values()))
|
|
45
|
+
self.assertEqual(set(detected), set(AGENT_CLI_PRESETS))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BuildStarterConfigTest(unittest.TestCase):
|
|
49
|
+
def test_detected_agents_use_command_mode(self):
|
|
50
|
+
config = build_starter_config({"codex": "/usr/bin/codex", "claude_code": None, "hermes": None})
|
|
51
|
+
agents = config["agents"]
|
|
52
|
+
self.assertEqual(agents["codex"]["mode"], "command")
|
|
53
|
+
self.assertEqual(agents["codex"]["command"], ["codex", "exec", "-C", "{cwd}", "{prompt}"])
|
|
54
|
+
self.assertEqual(agents["claude_code"]["mode"], "mock")
|
|
55
|
+
self.assertEqual(agents["hermes"]["mode"], "mock")
|
|
56
|
+
|
|
57
|
+
def test_commands_are_json_arrays_without_credentials(self):
|
|
58
|
+
config = build_starter_config({agent: f"/usr/bin/{agent}" for agent in AGENT_CLI_PRESETS})
|
|
59
|
+
text = json.dumps(config).lower()
|
|
60
|
+
for spec in config["agents"].values():
|
|
61
|
+
self.assertIsInstance(spec["command"], list)
|
|
62
|
+
self.assertTrue(all(isinstance(part, str) for part in spec["command"]))
|
|
63
|
+
for forbidden in ("api_key", "apikey", "token", "secret", "password"):
|
|
64
|
+
self.assertNotIn(forbidden, text)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RunInitTest(unittest.TestCase):
|
|
68
|
+
def test_writes_starter_config_with_detected_clis(self):
|
|
69
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
70
|
+
root = Path(tmp)
|
|
71
|
+
(root / "bin").mkdir()
|
|
72
|
+
path = _fake_path(root / "bin", ["codex"])
|
|
73
|
+
out = root / "agents.local.json"
|
|
74
|
+
with mock.patch.dict(os.environ, {"PATH": path}):
|
|
75
|
+
result = run_init(out_path=str(out))
|
|
76
|
+
self.assertTrue(result["written"])
|
|
77
|
+
self.assertFalse(result["skipped"])
|
|
78
|
+
self.assertFalse(result["mock_only"])
|
|
79
|
+
self.assertEqual([d["agent"] for d in result["detected"]], ["codex"])
|
|
80
|
+
self.assertEqual(result["missing"], ["claude_code", "hermes"])
|
|
81
|
+
config = json.loads(out.read_text(encoding="utf-8"))
|
|
82
|
+
self.assertEqual(config["agents"]["codex"]["mode"], "command")
|
|
83
|
+
self.assertEqual(config["agents"]["claude_code"]["mode"], "mock")
|
|
84
|
+
|
|
85
|
+
def test_mock_only_config_when_no_cli_found(self):
|
|
86
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
87
|
+
root = Path(tmp)
|
|
88
|
+
out = root / "agents.local.json"
|
|
89
|
+
with mock.patch.dict(os.environ, {"PATH": str(root)}):
|
|
90
|
+
result = run_init(out_path=str(out))
|
|
91
|
+
self.assertTrue(result["written"])
|
|
92
|
+
self.assertTrue(result["mock_only"])
|
|
93
|
+
self.assertEqual(result["detected"], [])
|
|
94
|
+
config = json.loads(out.read_text(encoding="utf-8"))
|
|
95
|
+
self.assertTrue(all(s["mode"] == "mock" for s in config["agents"].values()))
|
|
96
|
+
|
|
97
|
+
def test_never_overwrites_existing_config_without_force(self):
|
|
98
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
99
|
+
out = Path(tmp) / "agents.local.json"
|
|
100
|
+
out.write_text('{"agents": {"custom": {"mode": "mock"}}}\n', encoding="utf-8")
|
|
101
|
+
with mock.patch.dict(os.environ, {"PATH": str(Path(tmp))}):
|
|
102
|
+
result = run_init(out_path=str(out))
|
|
103
|
+
self.assertFalse(result["written"])
|
|
104
|
+
self.assertTrue(result["skipped"])
|
|
105
|
+
self.assertIn("exists", result["reason"])
|
|
106
|
+
self.assertIn("custom", out.read_text(encoding="utf-8"))
|
|
107
|
+
|
|
108
|
+
def test_force_overwrites_existing_config(self):
|
|
109
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
110
|
+
out = Path(tmp) / "agents.local.json"
|
|
111
|
+
out.write_text('{"agents": {"custom": {"mode": "mock"}}}\n', encoding="utf-8")
|
|
112
|
+
with mock.patch.dict(os.environ, {"PATH": str(Path(tmp))}):
|
|
113
|
+
result = run_init(out_path=str(out), force=True)
|
|
114
|
+
self.assertTrue(result["written"])
|
|
115
|
+
self.assertTrue(result["forced"])
|
|
116
|
+
config = json.loads(out.read_text(encoding="utf-8"))
|
|
117
|
+
self.assertNotIn("custom", config["agents"])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class InitCliTest(unittest.TestCase):
|
|
121
|
+
def test_cli_init_writes_config_and_prints_json(self):
|
|
122
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
123
|
+
out = Path(tmp) / "agents.local.json"
|
|
124
|
+
output = io.StringIO()
|
|
125
|
+
with mock.patch.dict(os.environ, {"PATH": str(Path(tmp))}):
|
|
126
|
+
with redirect_stdout(output):
|
|
127
|
+
code = main(["init", "--out", str(out)])
|
|
128
|
+
self.assertEqual(code, 0)
|
|
129
|
+
result = json.loads(output.getvalue())
|
|
130
|
+
self.assertTrue(result["written"])
|
|
131
|
+
self.assertTrue(out.exists())
|
|
132
|
+
|
|
133
|
+
def test_cli_init_skips_existing_without_force(self):
|
|
134
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
135
|
+
out = Path(tmp) / "agents.local.json"
|
|
136
|
+
out.write_text("{}", encoding="utf-8")
|
|
137
|
+
output = io.StringIO()
|
|
138
|
+
with redirect_stdout(output):
|
|
139
|
+
code = main(["init", "--out", str(out)])
|
|
140
|
+
self.assertEqual(code, 0)
|
|
141
|
+
result = json.loads(output.getvalue())
|
|
142
|
+
self.assertTrue(result["skipped"])
|
|
143
|
+
self.assertEqual(out.read_text(encoding="utf-8"), "{}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class OnboardingDoctorTest(unittest.TestCase):
|
|
147
|
+
def test_run_doctor_marks_levels_on_all_checks(self):
|
|
148
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
149
|
+
report = run_doctor(db_path=str(Path(tmp) / "bus.db"))
|
|
150
|
+
self.assertTrue(report["checks"])
|
|
151
|
+
for check in report["checks"]:
|
|
152
|
+
self.assertIn(check["level"], ("blocker", "optional"))
|
|
153
|
+
self.assertEqual(report["blockers_failed"], [])
|
|
154
|
+
self.assertEqual(report["optional_failed"], [])
|
|
155
|
+
|
|
156
|
+
def test_onboarding_adds_agent_cli_check_as_optional(self):
|
|
157
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
158
|
+
with mock.patch.dict(os.environ, {"PATH": str(Path(tmp))}):
|
|
159
|
+
report = run_doctor(db_path=str(Path(tmp) / "bus.db"), onboarding=True)
|
|
160
|
+
cli_check = next(c for c in report["checks"] if c["name"] == "agent_clis")
|
|
161
|
+
self.assertEqual(cli_check["level"], "optional")
|
|
162
|
+
self.assertFalse(cli_check["ok"])
|
|
163
|
+
self.assertIn("hint", cli_check)
|
|
164
|
+
# Optional failure alone does not make onboarding unhealthy.
|
|
165
|
+
self.assertEqual(report["status"], "healthy")
|
|
166
|
+
self.assertEqual(report["blockers_failed"], [])
|
|
167
|
+
self.assertEqual(report["optional_failed"], ["agent_clis"])
|
|
168
|
+
|
|
169
|
+
def test_onboarding_unhealthy_only_on_blocker_failure(self):
|
|
170
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
171
|
+
root = Path(tmp)
|
|
172
|
+
agents_path = root / "agents.json"
|
|
173
|
+
agents_path.write_text(json.dumps({"agents": {"bad": {"mode": "nope"}}}), encoding="utf-8")
|
|
174
|
+
with mock.patch.dict(os.environ, {"PATH": str(root)}):
|
|
175
|
+
report = run_doctor(
|
|
176
|
+
db_path=str(root / "bus.db"),
|
|
177
|
+
agents_path=str(agents_path),
|
|
178
|
+
onboarding=True,
|
|
179
|
+
)
|
|
180
|
+
self.assertEqual(report["status"], "unhealthy")
|
|
181
|
+
self.assertIn("agents", report["blockers_failed"])
|
|
182
|
+
self.assertIn("agent_clis", report["optional_failed"])
|
|
183
|
+
|
|
184
|
+
def test_default_doctor_status_unchanged_by_optional_failures(self):
|
|
185
|
+
# Default (non-onboarding) mode keeps the old semantics: any failed
|
|
186
|
+
# check makes the report unhealthy.
|
|
187
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
188
|
+
root = Path(tmp)
|
|
189
|
+
runtime = root / "runtime"
|
|
190
|
+
runtime.mkdir()
|
|
191
|
+
report = run_doctor(db_path=str(root / "bus.db"), runtime_root=str(runtime))
|
|
192
|
+
self.assertEqual(report["status"], "unhealthy")
|
|
193
|
+
self.assertIn("runtime_metrics", report["optional_failed"])
|
|
194
|
+
|
|
195
|
+
def test_cli_doctor_onboarding_exit_codes(self):
|
|
196
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
197
|
+
root = Path(tmp)
|
|
198
|
+
# Healthy blockers + missing CLIs (optional) -> exit 0.
|
|
199
|
+
output = io.StringIO()
|
|
200
|
+
with mock.patch.dict(os.environ, {"PATH": str(root)}):
|
|
201
|
+
with redirect_stdout(output):
|
|
202
|
+
code = main(["doctor", "--onboarding", "--db", str(root / "bus.db")])
|
|
203
|
+
self.assertEqual(code, 0)
|
|
204
|
+
report = json.loads(output.getvalue())
|
|
205
|
+
self.assertEqual(report["status"], "healthy")
|
|
206
|
+
|
|
207
|
+
# Blocker failure -> exit 1.
|
|
208
|
+
bad_agents = root / "agents.json"
|
|
209
|
+
bad_agents.write_text(json.dumps({"agents": {"bad": {"mode": "nope"}}}), encoding="utf-8")
|
|
210
|
+
output = io.StringIO()
|
|
211
|
+
with mock.patch.dict(os.environ, {"PATH": str(root)}):
|
|
212
|
+
with redirect_stdout(output):
|
|
213
|
+
code = main([
|
|
214
|
+
"doctor", "--onboarding",
|
|
215
|
+
"--db", str(root / "bus.db"),
|
|
216
|
+
"--agents", str(bad_agents),
|
|
217
|
+
])
|
|
218
|
+
self.assertEqual(code, 1)
|
|
219
|
+
report = json.loads(output.getvalue())
|
|
220
|
+
self.assertIn("agents", report["blockers_failed"])
|
|
221
|
+
|
|
222
|
+
def test_cli_doctor_default_still_exits_zero_on_failure(self):
|
|
223
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
224
|
+
root = Path(tmp)
|
|
225
|
+
bad_agents = root / "agents.json"
|
|
226
|
+
bad_agents.write_text(json.dumps({"agents": {"bad": {"mode": "nope"}}}), encoding="utf-8")
|
|
227
|
+
output = io.StringIO()
|
|
228
|
+
with redirect_stdout(output):
|
|
229
|
+
code = main([
|
|
230
|
+
"doctor",
|
|
231
|
+
"--db", str(root / "bus.db"),
|
|
232
|
+
"--agents", str(bad_agents),
|
|
233
|
+
])
|
|
234
|
+
self.assertEqual(code, 0)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
unittest.main()
|
|
@@ -75,6 +75,19 @@ class McpServerTest(unittest.TestCase):
|
|
|
75
75
|
resp = self._call(self._msg("initialized"))
|
|
76
76
|
self.assertIsNone(resp)
|
|
77
77
|
|
|
78
|
+
def test_notifications_initialized_returns_none(self):
|
|
79
|
+
# Real MCP clients send `notifications/initialized` (no id) after
|
|
80
|
+
# initialize. Must return None and never emit an id:null error.
|
|
81
|
+
resp = self._call({"jsonrpc": "2.0", "method": "notifications/initialized"})
|
|
82
|
+
self.assertIsNone(resp)
|
|
83
|
+
|
|
84
|
+
def test_unknown_notification_returns_none(self):
|
|
85
|
+
# Any notification (no id) must not receive a response, even if the
|
|
86
|
+
# method is unknown. Regression for id:null error responses that
|
|
87
|
+
# crashed the official mcp SDK >= 2.x stdio transport.
|
|
88
|
+
resp = self._call({"jsonrpc": "2.0", "method": "notifications/whatever"})
|
|
89
|
+
self.assertIsNone(resp)
|
|
90
|
+
|
|
78
91
|
def test_shutdown_returns_empty_result(self):
|
|
79
92
|
resp = self._call(self._msg("shutdown", 1, {}))
|
|
80
93
|
self.assertEqual(resp["id"], 1)
|
|
@@ -53,7 +53,7 @@ Trinity Lite — multi-agent task bus
|
|
|
53
53
|
Quick demo: trinity-lite demo
|
|
54
54
|
Full help: trinity-lite --help
|
|
55
55
|
|
|
56
|
-
Commands: demo, dispatch, dispatch-auto, worker, orchestrate, worktree,
|
|
56
|
+
Commands: demo, init, dispatch, dispatch-auto, worker, orchestrate, worktree,
|
|
57
57
|
status, latest, tasks, route, proof, doctor, send, inbox, mcp,
|
|
58
58
|
setup-models, detect-models"""
|
|
59
59
|
|
|
@@ -173,6 +173,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
173
173
|
default=[],
|
|
174
174
|
help="TCP port that should not be listening, repeatable",
|
|
175
175
|
)
|
|
176
|
+
doctor.add_argument(
|
|
177
|
+
"--onboarding",
|
|
178
|
+
action="store_true",
|
|
179
|
+
help="onboarding mode: also probe agent CLIs, grade checks as blocker/optional, "
|
|
180
|
+
"and exit non-zero only when a blocker fails",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
init_cmd = sub.add_parser(
|
|
184
|
+
"init",
|
|
185
|
+
parents=[common],
|
|
186
|
+
help="detect local agent CLIs and write a safe starter agents config",
|
|
187
|
+
)
|
|
188
|
+
init_cmd.add_argument(
|
|
189
|
+
"--out",
|
|
190
|
+
default=None,
|
|
191
|
+
help="output path (default: ./agents.local.json)",
|
|
192
|
+
)
|
|
193
|
+
init_cmd.add_argument(
|
|
194
|
+
"--force",
|
|
195
|
+
action="store_true",
|
|
196
|
+
help="overwrite an existing config file",
|
|
197
|
+
)
|
|
176
198
|
|
|
177
199
|
version_cmd = sub.add_parser("version", parents=[common], help="show version and exit")
|
|
178
200
|
|
|
@@ -228,7 +250,12 @@ def run_command(args: argparse.Namespace) -> int:
|
|
|
228
250
|
if args.command == "worktree":
|
|
229
251
|
return _worktree(args)
|
|
230
252
|
|
|
231
|
-
#
|
|
253
|
+
# Init and model pool commands do not use the task bus.
|
|
254
|
+
if args.command == "init":
|
|
255
|
+
from .init import run_init
|
|
256
|
+
|
|
257
|
+
print_json(run_init(out_path=args.out, force=args.force))
|
|
258
|
+
return 0
|
|
232
259
|
if args.command == "setup-models":
|
|
233
260
|
from .model_pool_wizard import main as wizard_main
|
|
234
261
|
wizard_main()
|
|
@@ -342,14 +369,18 @@ def run_command(args: argparse.Namespace) -> int:
|
|
|
342
369
|
print_json(bus.inbox(args.agent, unread_only=not args.all, mark_read=args.mark_read, limit=args.limit))
|
|
343
370
|
return 0
|
|
344
371
|
if args.command == "doctor":
|
|
345
|
-
|
|
372
|
+
report = run_doctor(
|
|
346
373
|
args.db,
|
|
347
374
|
args.routes,
|
|
348
375
|
args.agents,
|
|
349
376
|
args.scan_root,
|
|
350
377
|
args.runtime_root,
|
|
351
378
|
args.retired_port,
|
|
352
|
-
|
|
379
|
+
onboarding=getattr(args, "onboarding", False),
|
|
380
|
+
)
|
|
381
|
+
print_json(report)
|
|
382
|
+
if getattr(args, "onboarding", False):
|
|
383
|
+
return 1 if report["blockers_failed"] else 0
|
|
353
384
|
return 0
|
|
354
385
|
if args.command == "demo":
|
|
355
386
|
return _demo(args, bus)
|
|
@@ -24,6 +24,33 @@ RETIRED_RUNTIME_ARTIFACTS = {
|
|
|
24
24
|
"trinity_learn.db-shm",
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
# Checks that never block a first run. Everything else is a blocker:
|
|
28
|
+
# without it the bus, routes, or agent config cannot work at all.
|
|
29
|
+
OPTIONAL_CHECKS = {
|
|
30
|
+
"public_tree_scan",
|
|
31
|
+
"runtime_root",
|
|
32
|
+
"runtime_metrics",
|
|
33
|
+
"retired_runtime_artifacts",
|
|
34
|
+
"retired_port",
|
|
35
|
+
"agent_clis",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Hints attached to failed optional checks so onboarding users know the
|
|
39
|
+
# failure does not block the mock-agent path.
|
|
40
|
+
OPTIONAL_HINTS = {
|
|
41
|
+
"agent_clis": "no supported agent CLI on PATH; mock agents still work, or run `trinity-lite init` after installing one",
|
|
42
|
+
"public_tree_scan": "resolve before publishing this tree",
|
|
43
|
+
"runtime_root": "runtime state directory is not ready yet",
|
|
44
|
+
"runtime_metrics": "start the runtime once to generate metrics",
|
|
45
|
+
"retired_runtime_artifacts": "remove retired artifacts from the runtime directory",
|
|
46
|
+
"retired_port": "stop the listener on the retired port",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _check_level(name: str) -> str:
|
|
51
|
+
base = name.split(":", 1)[0]
|
|
52
|
+
return "optional" if base in OPTIONAL_CHECKS else "blocker"
|
|
53
|
+
|
|
27
54
|
|
|
28
55
|
def run_doctor(
|
|
29
56
|
db_path: str | None = None,
|
|
@@ -32,6 +59,7 @@ def run_doctor(
|
|
|
32
59
|
scan_root: str | None = None,
|
|
33
60
|
runtime_root: str | None = None,
|
|
34
61
|
retired_ports: list[int] | None = None,
|
|
62
|
+
onboarding: bool = False,
|
|
35
63
|
) -> dict[str, Any]:
|
|
36
64
|
checks: list[dict[str, Any]] = []
|
|
37
65
|
|
|
@@ -99,12 +127,54 @@ def run_doctor(
|
|
|
99
127
|
for port in retired_ports or []:
|
|
100
128
|
checks.append(_retired_port_check(port))
|
|
101
129
|
|
|
130
|
+
if onboarding:
|
|
131
|
+
checks.append(_agent_cli_check())
|
|
132
|
+
|
|
133
|
+
for check in checks:
|
|
134
|
+
check["level"] = _check_level(check["name"])
|
|
135
|
+
if not check["ok"] and check["level"] == "optional":
|
|
136
|
+
hint = OPTIONAL_HINTS.get(check["name"].split(":", 1)[0])
|
|
137
|
+
if hint:
|
|
138
|
+
check["hint"] = hint
|
|
139
|
+
|
|
140
|
+
blockers_failed = [
|
|
141
|
+
c["name"] for c in checks if not c["ok"] and c["level"] == "blocker"
|
|
142
|
+
]
|
|
143
|
+
optional_failed = [
|
|
144
|
+
c["name"] for c in checks if not c["ok"] and c["level"] == "optional"
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
if onboarding:
|
|
148
|
+
status = "healthy" if not blockers_failed else "unhealthy"
|
|
149
|
+
else:
|
|
150
|
+
status = "healthy" if all(c["ok"] for c in checks) else "unhealthy"
|
|
151
|
+
|
|
102
152
|
return {
|
|
103
|
-
"status":
|
|
153
|
+
"status": status,
|
|
154
|
+
"onboarding": bool(onboarding),
|
|
155
|
+
"blockers_failed": blockers_failed,
|
|
156
|
+
"optional_failed": optional_failed,
|
|
104
157
|
"checks": checks,
|
|
105
158
|
}
|
|
106
159
|
|
|
107
160
|
|
|
161
|
+
def _agent_cli_check() -> dict[str, Any]:
|
|
162
|
+
"""Probe PATH for supported agent CLIs (always optional)."""
|
|
163
|
+
from .init import detect_agent_clis
|
|
164
|
+
|
|
165
|
+
detected = detect_agent_clis()
|
|
166
|
+
found = sorted(agent for agent, exe in detected.items() if exe)
|
|
167
|
+
missing = sorted(agent for agent, exe in detected.items() if not exe)
|
|
168
|
+
return {
|
|
169
|
+
"name": "agent_clis",
|
|
170
|
+
"ok": bool(found),
|
|
171
|
+
"detail": {
|
|
172
|
+
"found": {agent: detected[agent] for agent in found},
|
|
173
|
+
"missing": missing,
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
108
178
|
def _bus_acceptance_checks(bus: TrinityBus) -> list[dict[str, Any]]:
|
|
109
179
|
with bus.connect() as conn:
|
|
110
180
|
columns = {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Detect local agent CLIs and generate a safe starter agents config.
|
|
2
|
+
|
|
3
|
+
This module supports the five-minute onboarding path: probe PATH for
|
|
4
|
+
supported agent CLIs, then write an ``agents.local.json`` that runs
|
|
5
|
+
detected CLIs in command mode and keeps everything else on mock agents.
|
|
6
|
+
|
|
7
|
+
Safety rules:
|
|
8
|
+
|
|
9
|
+
- Commands are JSON arrays only; Trinity Lite executes them with
|
|
10
|
+
``shell=False``.
|
|
11
|
+
- No credentials, API keys, tokens, or private paths are ever written.
|
|
12
|
+
- An existing config is never overwritten unless ``force=True``.
|
|
13
|
+
- When no supported CLI is found, the generated config is mock-only and
|
|
14
|
+
still fully usable.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import shutil
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
# Supported agent CLI presets. Each entry maps the Trinity Lite agent id to
|
|
25
|
+
# the executable probed on PATH and the safe command template aligned with
|
|
26
|
+
# docs/REAL_AGENTS.md and examples/agents.command.example.json.
|
|
27
|
+
AGENT_CLI_PRESETS: dict[str, dict[str, Any]] = {
|
|
28
|
+
"codex": {
|
|
29
|
+
"executable": "codex",
|
|
30
|
+
"command": ["codex", "exec", "-C", "{cwd}", "{prompt}"],
|
|
31
|
+
},
|
|
32
|
+
"claude_code": {
|
|
33
|
+
"executable": "claude",
|
|
34
|
+
"command": ["claude", "-p", "{prompt}"],
|
|
35
|
+
},
|
|
36
|
+
"hermes": {
|
|
37
|
+
"executable": "hermes",
|
|
38
|
+
"command": ["hermes", "-z", "{prompt}"],
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
DEFAULT_TIMEOUT = 1800
|
|
43
|
+
DEFAULT_CONFIG_NAME = "agents.local.json"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def detect_agent_clis() -> dict[str, str | None]:
|
|
47
|
+
"""Probe PATH for supported agent CLI executables.
|
|
48
|
+
|
|
49
|
+
Returns a mapping of agent id to the resolved executable path, or None
|
|
50
|
+
when the executable is not on PATH.
|
|
51
|
+
"""
|
|
52
|
+
found: dict[str, str | None] = {}
|
|
53
|
+
for agent_id, preset in AGENT_CLI_PRESETS.items():
|
|
54
|
+
found[agent_id] = shutil.which(preset["executable"])
|
|
55
|
+
return found
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_starter_config(detected: dict[str, str | None]) -> dict[str, Any]:
|
|
59
|
+
"""Build an agents config from detection results.
|
|
60
|
+
|
|
61
|
+
Detected CLIs become command-mode agents with safe JSON-array commands;
|
|
62
|
+
undetected presets stay in mock mode so the bus works out of the box.
|
|
63
|
+
"""
|
|
64
|
+
agents: dict[str, Any] = {}
|
|
65
|
+
for agent_id, preset in AGENT_CLI_PRESETS.items():
|
|
66
|
+
if detected.get(agent_id):
|
|
67
|
+
agents[agent_id] = {
|
|
68
|
+
"mode": "command",
|
|
69
|
+
"command": list(preset["command"]),
|
|
70
|
+
"timeout": DEFAULT_TIMEOUT,
|
|
71
|
+
}
|
|
72
|
+
else:
|
|
73
|
+
agents[agent_id] = {
|
|
74
|
+
"mode": "mock",
|
|
75
|
+
"timeout": DEFAULT_TIMEOUT,
|
|
76
|
+
}
|
|
77
|
+
return {"agents": agents}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_init(out_path: str | None = None, force: bool = False) -> dict[str, Any]:
|
|
81
|
+
"""Detect agent CLIs and write a starter config.
|
|
82
|
+
|
|
83
|
+
Returns a stable JSON-serializable result describing what was detected
|
|
84
|
+
and what was written. Never raises for a missing CLI; a mock-only
|
|
85
|
+
config is a successful outcome.
|
|
86
|
+
"""
|
|
87
|
+
target = Path(out_path) if out_path else Path.cwd() / DEFAULT_CONFIG_NAME
|
|
88
|
+
detected = detect_agent_clis()
|
|
89
|
+
found = sorted(agent for agent, exe in detected.items() if exe)
|
|
90
|
+
missing = sorted(agent for agent, exe in detected.items() if not exe)
|
|
91
|
+
|
|
92
|
+
result: dict[str, Any] = {
|
|
93
|
+
"config_path": str(target),
|
|
94
|
+
"detected": [
|
|
95
|
+
{"agent": agent, "executable": detected[agent]} for agent in found
|
|
96
|
+
],
|
|
97
|
+
"missing": missing,
|
|
98
|
+
"mock_only": not found,
|
|
99
|
+
"forced": bool(force),
|
|
100
|
+
"written": False,
|
|
101
|
+
"skipped": False,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if target.exists() and not force:
|
|
105
|
+
result["skipped"] = True
|
|
106
|
+
result["reason"] = f"exists: {target} (use --force to overwrite)"
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
config = build_starter_config(detected)
|
|
110
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
target.write_text(
|
|
112
|
+
json.dumps(config, ensure_ascii=False, indent=2) + "\n",
|
|
113
|
+
encoding="utf-8",
|
|
114
|
+
)
|
|
115
|
+
result["written"] = True
|
|
116
|
+
return result
|
|
@@ -374,6 +374,7 @@ RESOURCE_DEFINITIONS = [
|
|
|
374
374
|
ALLOWED_METHODS = {
|
|
375
375
|
"initialize",
|
|
376
376
|
"initialized",
|
|
377
|
+
"notifications/initialized",
|
|
377
378
|
"tools/list",
|
|
378
379
|
"tools/call",
|
|
379
380
|
"resources/list",
|
|
@@ -1073,6 +1074,13 @@ def handle_request(msg, bus, agents_path, routes_path):
|
|
|
1073
1074
|
id_ = msg.get("id")
|
|
1074
1075
|
params = msg.get("params", {})
|
|
1075
1076
|
|
|
1077
|
+
# JSON-RPC 2.0: notifications carry no `id` and must NOT receive a
|
|
1078
|
+
# response. Silently accept any notification (known or unknown) so we
|
|
1079
|
+
# never emit an `id: null` error response, which the official `mcp`
|
|
1080
|
+
# SDK >= 2.x stdio transport fails to validate and closes the stream.
|
|
1081
|
+
if id_ is None:
|
|
1082
|
+
return None
|
|
1083
|
+
|
|
1076
1084
|
if method not in ALLOWED_METHODS:
|
|
1077
1085
|
return jsonrpc_error(id_, -32601, "method not found: {}".format(method))
|
|
1078
1086
|
|
|
@@ -1083,7 +1091,7 @@ def handle_request(msg, bus, agents_path, routes_path):
|
|
|
1083
1091
|
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
|
|
1084
1092
|
})
|
|
1085
1093
|
|
|
1086
|
-
if method
|
|
1094
|
+
if method in ("initialized", "notifications/initialized"):
|
|
1087
1095
|
return None
|
|
1088
1096
|
|
|
1089
1097
|
if method == "tools/list":
|
|
@@ -1143,9 +1151,12 @@ def serve(bus, agents_path=None, routes_path=None):
|
|
|
1143
1151
|
except Exception:
|
|
1144
1152
|
tb = traceback.format_exc()
|
|
1145
1153
|
print("[mcp] unhandled exception:\n{}".format(tb), file=sys.stderr, flush=True)
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1154
|
+
# Don't emit an `id: null` error for notifications (no id).
|
|
1155
|
+
_err_id = msg.get("id")
|
|
1156
|
+
if _err_id is not None:
|
|
1157
|
+
error_resp = jsonrpc_error(_err_id, -32603, "internal server error")
|
|
1158
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
1159
|
+
sys.stdout.flush()
|
|
1149
1160
|
continue
|
|
1150
1161
|
|
|
1151
1162
|
# Write response
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trinity-lite
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Local AgentOps for cross-vendor CLI agents: route work, recover state, and accept only with evidence.
|
|
5
5
|
Author: Yomiracle
|
|
6
6
|
License-Expression: MIT
|
|
@@ -99,7 +99,7 @@ LangGraph and CrewAI give you primitives for building agents from scratch — gr
|
|
|
99
99
|
- **Execute safely, no shell injection.** Agent commands are JSON arrays run with `shell=False`. No string interpolation into a shell. No surprises.
|
|
100
100
|
- **Test with mock agents.** Mock agents simulate the full cycle without real CLIs. Prototype routing, persistence, and review handoffs first. Wire up real agents later.
|
|
101
101
|
- **Guard against runaway delegation.** Self-delegation is blocked. Delegation depth is capped. Working directories are allowlisted. Safe by default.
|
|
102
|
-
- **Check health in one pass.** `trinity-lite doctor` verifies Python, SQLite, route config, agent config, and publish readiness.
|
|
102
|
+
- **Check health in one pass.** `trinity-lite doctor` verifies Python, SQLite, route config, agent config, and publish readiness. `trinity-lite doctor --onboarding` grades every check as blocker or optional, probes for agent CLIs, and fails only on blockers.
|
|
103
103
|
- **Zero core dependencies.** The default runtime is Python standard library only. YAML pipelines are available through an optional extra.
|
|
104
104
|
- **150+ tests guarding the surface area.** Mock workflows, safety checks, routing, persistence, MCP, and acceptance gates — all covered.
|
|
105
105
|
- **Optional model selection hints.** Select from your declared model pool with transparent task, tier, and capability rules; no claim of a universal best or cheapest model.
|
|
@@ -130,13 +130,15 @@ trinity-lite orchestrate "implement a rate limiter for the API"
|
|
|
130
130
|
|
|
131
131
|
The primary task row records `route_json`, `review_task_id`, `verification_json`, `acceptance_status`, `acceptance_reason`, and `accepted_at`.
|
|
132
132
|
|
|
133
|
-
Ready for real CLIs when you are:
|
|
133
|
+
Ready for real CLIs when you are — auto-detect what is installed and generate a safe starter config:
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
|
-
|
|
136
|
+
trinity-lite init
|
|
137
137
|
trinity-lite orchestrate "implement a rate limiter for the API" --agents agents.local.json
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
`trinity-lite init` probes PATH for Codex, Claude Code, and Hermes, writes `agents.local.json` with JSON-array commands for the CLIs it finds, keeps the rest in mock mode, and never writes credentials or overwrites an existing config without `--force`. Prefer hand-editing? `cp examples/agents.command.example.json agents.local.json` still works.
|
|
141
|
+
|
|
140
142
|
Prefer manual control? Use the lower-level bus commands:
|
|
141
143
|
|
|
142
144
|
```bash
|
|
@@ -5,6 +5,7 @@ tests/test_bus.py
|
|
|
5
5
|
tests/test_cli.py
|
|
6
6
|
tests/test_doctor.py
|
|
7
7
|
tests/test_guard.py
|
|
8
|
+
tests/test_init.py
|
|
8
9
|
tests/test_mcp_server.py
|
|
9
10
|
tests/test_orchestrator.py
|
|
10
11
|
tests/test_pipeline.py
|
|
@@ -20,6 +21,7 @@ trinity_lite/cli.py
|
|
|
20
21
|
trinity_lite/config.py
|
|
21
22
|
trinity_lite/doctor.py
|
|
22
23
|
trinity_lite/guard.py
|
|
24
|
+
trinity_lite/init.py
|
|
23
25
|
trinity_lite/mcp_server.py
|
|
24
26
|
trinity_lite/model_autodetect.py
|
|
25
27
|
trinity_lite/model_pool_wizard.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|