diff --git a/README.md b/README.md
index aa3e1e6..282154d 100644
--- a/README.md
+++ b/README.md
@@ -490,6 +490,8 @@ COMA is part of the `ellmos-ai` orchestration architecture and the broader `open
| **Agent Bootstrap** | [`dev-bricks/safe-start-for-codex`](https://github.com/dev-bricks/safe-start-for-codex) | Safe startup, environment checks & preflight diagnostics |
| **Umbrella Ecosystem** | [`open-bricks`](https://github.com/open-bricks) | Open-source foundation for local-first developer tools |
+- [Swarm AI](https://github.com/ellmos-ai/swarm_ai) optionally uses COMA's Spawner and adapter APIs for provider-backed workflows.
+
---
diff --git a/README_de.md b/README_de.md
index 5bd98cf..7ed0a83 100644
--- a/README_de.md
+++ b/README_de.md
@@ -487,6 +487,8 @@ COMA ist integraler Bestandteil der `ellmos-ai`-Architektur und des `open-bricks
| **Agenten-Bootstrap** | [`dev-bricks/safe-start-for-codex`](https://github.com/dev-bricks/safe-start-for-codex) | Sicherer Start, Umgebungsprüfungen & Preflight-Diagnostik |
| **Dach-Ökosystem** | [`open-bricks`](https://github.com/open-bricks) | Open-Source-Fundament für lokale Entwicklerwerkzeuge |
+- [Swarm AI](https://github.com/ellmos-ai/swarm_ai) nutzt COMAs Spawner- und Adapter-APIs optional für anbieterbasierte Workflows.
+
---
diff --git a/llms.txt b/llms.txt
index a2a5dc5..1a31ec1 100644
--- a/llms.txt
+++ b/llms.txt
@@ -113,6 +113,7 @@ coma check .\coma-vendor.json
- [`README.md`](README.md): Primary documentation and 18-point quick navigation.
- [`README_de.md`](README_de.md): German documentation and architecture overview.
+- [Swarm AI](https://github.com/ellmos-ai/swarm_ai): Optional consumer of COMA's Spawner and adapter APIs for provider-backed workflows.
- [`NOTICE`](NOTICE): Formal copyright attribution (Lukas Geiger / ellmos-ai / open-bricks).
- [`KONZEPT.md`](KONZEPT.md): Architectural rationale, session-decoupling motivation, and offline design.
- [`SECURITY.md`](SECURITY.md): Process isolation, single-writer protocol boundaries, and 48h vulnerability reporting SLA.
diff --git a/tests/test_session.py b/tests/test_session.py
index f1965ce..d0fbbf8 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -126,10 +126,44 @@ def test_probe_accepts_token_from_bounded_fake_process(tmp_path):
assert probe(command, 5, cwd=tmp_path) == (True, f"{PROBE_TOKEN} erkannt")
-def test_cli_session_dry_run_never_starts_provider(prompt_file, tmp_path, capsys):
+def test_cli_session_dry_run_never_starts_provider(
+ prompt_file, tmp_path, capsys, monkeypatch
+):
+ monkeypatch.setattr(
+ "coma.session.shutil.which",
+ lambda provider: "codex-test" if provider == "codex" else None,
+ )
+
+ def fail_if_started(*args, **kwargs):
+ pytest.fail("dry-run darf keinen Provider-Prozess starten")
+
+ monkeypatch.setattr("coma.cli.subprocess.run", fail_if_started)
+ monkeypatch.setattr("coma.cli.subprocess.Popen", fail_if_started)
code = main([
"session", "--provider", "codex", "--prompt-file", str(prompt_file),
"--request", "Nur planen.", "--cwd", str(tmp_path), "--dry-run",
])
assert code == 0
- assert "Nur planen." in capsys.readouterr().out
+ output = capsys.readouterr().out
+ assert output.startswith("codex-test ")
+ assert "Nur planen." in output
+
+
+def test_cli_session_dry_run_reports_missing_cli_without_starting(
+ prompt_file, tmp_path, capsys, monkeypatch
+):
+ monkeypatch.setattr("coma.session.shutil.which", lambda _provider: None)
+
+ def fail_if_started(*args, **kwargs):
+ pytest.fail("fehlende Provider-CLI darf keinen Prozessstart auslösen")
+
+ monkeypatch.setattr("coma.cli.subprocess.run", fail_if_started)
+ monkeypatch.setattr("coma.cli.subprocess.Popen", fail_if_started)
+ code = main([
+ "session", "--provider", "codex", "--prompt-file", str(prompt_file),
+ "--request", "Nur planen.", "--cwd", str(tmp_path), "--dry-run",
+ ])
+ captured = capsys.readouterr()
+ assert code == 2
+ assert "Provider 'codex' wurde nicht gefunden" in captured.err
+ assert captured.out == ""
diff --git a/tests/test_skeleton_adapters.py b/tests/test_skeleton_adapters.py
index c2c9845..0aa7747 100644
--- a/tests/test_skeleton_adapters.py
+++ b/tests/test_skeleton_adapters.py
@@ -5,6 +5,8 @@
dass sie die dokumentierte Aufrufkonvention korrekt zusammenbauen — und dass die
Sicherung greift, die sie von einem unbeaufsichtigten Lauf trennt.
"""
+import os
+
import pytest
from coma import Spawner, UnverifiedAdapterError
@@ -66,13 +68,43 @@ def test_claude_needs_no_opt_in(self, monkeypatch, fake_run):
class TestCodex:
- def test_uses_native_exec_via_windows_node_entrypoint(self):
- cmd = CodexAdapter().build_cmd("Mach was")
+ def test_windows_node_entrypoint_contract(self):
+ entrypoint = (
+ r"C:\fixture\node_modules\@openai\codex\bin\codex.js"
+ )
+ cmd = CodexAdapter(
+ executable="node", entrypoint=entrypoint
+ ).build_cmd("Mach was")
assert cmd[0] == "node"
assert "@openai/codex/bin/codex.js" in cmd[1].replace("\\", "/")
assert cmd[2] == "exec"
assert cmd[-1] == "Mach was"
+ def test_native_executable_contract_has_no_node_entrypoint(self):
+ cmd = CodexAdapter(executable="codex").build_cmd(
+ "Mach was", entrypoint=None
+ )
+ assert cmd == [
+ "codex", "exec", "--sandbox", "read-only",
+ "--skip-git-repo-check", "--ephemeral", "Mach was",
+ ]
+
+ def test_host_default_launcher_matches_platform(self):
+ adapter = CodexAdapter()
+ cmd = adapter.build_cmd("Mach was")
+ if os.name == "nt":
+ assert adapter.executable == "node"
+ assert adapter.entrypoint is not None
+ assert "@openai/codex/bin/codex.js" in str(adapter.entrypoint).replace(
+ "\\", "/"
+ )
+ assert cmd[0] == "node"
+ assert cmd[2] == "exec"
+ else:
+ assert adapter.executable == "codex"
+ assert adapter.entrypoint is None
+ assert cmd[0:2] == ["codex", "exec"]
+
def test_read_only_by_default(self):
cmd = CodexAdapter().build_cmd("Mach was")
assert cmd[cmd.index("--sandbox") + 1] == "read-only"
@@ -138,10 +170,38 @@ def test_prompt_directly_after_p(self):
index = cmd.index("-p")
assert cmd[index:index + 2] == ["-p", "mach was"]
- def test_windows_uses_node_entrypoint(self):
- cmd = KimiAdapter().build_cmd("mach was")
+ def test_windows_node_entrypoint_contract(self):
+ entrypoint = (
+ r"C:\fixture\node_modules\@moonshot-ai\kimi-code\dist\main.mjs"
+ )
+ cmd = KimiAdapter(
+ executable="node", entrypoint=entrypoint
+ ).build_cmd("mach was")
assert cmd[0] == "node"
assert "@moonshot-ai/kimi-code" in cmd[1].replace("\\", "/")
+ assert cmd[2:] == ["-p", "mach was"]
+
+ def test_native_executable_contract_has_no_node_entrypoint(self):
+ cmd = KimiAdapter(executable="kimi").build_cmd(
+ "mach was", entrypoint=None
+ )
+ assert cmd == ["kimi", "-p", "mach was"]
+
+ def test_host_default_launcher_matches_platform(self):
+ adapter = KimiAdapter()
+ cmd = adapter.build_cmd("mach was")
+ if os.name == "nt":
+ assert adapter.executable == "node"
+ assert adapter.entrypoint is not None
+ assert "@moonshot-ai/kimi-code" in str(adapter.entrypoint).replace(
+ "\\", "/"
+ )
+ assert cmd[0] == "node"
+ assert cmd[2:] == ["-p", "mach was"]
+ else:
+ assert adapter.executable == "kimi"
+ assert adapter.entrypoint is None
+ assert cmd == ["kimi", "-p", "mach was"]
@pytest.mark.parametrize("flag", ["-y", "--yolo", "--auto"])
def test_incompatible_flags_are_refused(self, flag):