feat(qwen): add Qwen Code (qwen_cli) provider (#376) - #412
Conversation
Add Qwen Code (`qwen`) — Alibaba's Gemini-CLI-derived Ink TUI coding agent — as a first-class CAO provider, modeled on the sibling antigravity_cli provider. - QwenCliProvider: launches `qwen --approval-mode yolo` with `--append-system-prompt` role injection, `--model`, and a per-terminal `--mcp-config` file carrying CAO_TERMINAL_ID; footer-anchored status detection with pyte stale-footer resolution (get_status_from_screen). - wiring: ProviderType enum, manager branch, launch workspace set, terminal_service (runtime skill prompt + soft enforcement), tool_mapping (gemini-style native names), /agents/providers endpoint. - tests: unit tests at 100% provider coverage against real captured qwen TUI fixtures; TestQwenCli* e2e classes across handoff/assign/ send_message/allowed_tools/supervisor_orchestration/skills; provider manager test; dedicated CI workflow. - docs: docs/qwen-cli.md, README provider table + enumerations, CHANGELOG. Auth is user-managed (OpenAI-compatible env or qwen-oauth); tool restrictions are soft (SECURITY_PROMPT) under yolo. Verified live on a real cao-server (tmux backend): the qwen terminal reaches IDLE, launches with the correct command + per-terminal MCP config, transitions idle->processing->completed, its cao-mcp-server connects with the matching CAO_TERMINAL_ID, and a cross-provider inbox message from a claude_code terminal is delivered and submitted into the qwen worker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot silently off) Boolean env flags were parsed as `os.environ.get(...).lower() == "true"`, so the common truthy spellings `1` / `yes` / `on` evaluated to False. For CAO_PYTE_STATUS that silently DISABLED pyte screen-detection — which qwen_cli and antigravity_cli require (their raw pipe-pane retains a stale "esc to cancel" → false PROCESSING) — so a finished qwen turn was never detected COMPLETED and the blocking handoff/assign hung the full timeout. - add env_bool(name, default) in constants.py: accepts 1/true/yes/on and 0/false/no/off (case- and whitespace-insensitive); unset, empty, or unrecognized values fall back to the default rather than flipping the flag. - route all four boolean env sites through it: CAO_PYTE_STATUS, CAO_EAGER_INBOX_DELIVERY (constants.py), CAO_ENABLE_WORKING_DIRECTORY, CAO_ENABLE_SENDER_ID_INJECTION (mcp_server/server.py). - correct the CAO_PYTE_STATUS comment to list all opt-in providers (claude_code, kimi_cli, qwen_cli, antigravity_cli). TDD: 24 new tests in test/test_constants.py::TestEnvBool cover the truthy/falsy spellings, default fallback, and a CAO_PYTE_STATUS=1 regression. Full suite: 3841 passed / 21 skipped; black + isort clean. Verified live: a claude_code->qwen_cli handoff that hung on CAO_PYTE_STATUS=1 returns the worker's result in ~9s once pyte is actually enabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ints Reframe qwen auth around the simplest path: configure qwen once (like any standalone qwen user) and every CAO-spawned worker inherits it — no export before cao-server, no --env. qwen loads ~/.qwen/.env natively (bundled dotenv), and an already-set process env var still wins, so the existing --env / export flows remain as overrides. - primary path: a one-time ~/.qwen/.env with just OPENAI_API_KEY (+ base URL); model defaults via the profile `model:` field, so the only secret a user plugs in is the API key. - region tables: shared DashScope (China / International-Singapore) and the Model Studio workspace-gateway region codes (cn-beijing, ap-southeast-1, us-east-1, eu-central-1, ap-northeast-1). - fix stale guidance that said `CAO_PYTE_STATUS=1` (the value that silently disabled pyte): note pyte is required for qwen_cli, on by default, and the flag now accepts 1/true/yes/on. Verified live: with a credential-free cao-server (no OPENAI_* in env) and no --env, a qwen worker authenticated purely from ~/.qwen/.env and completed a real turn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er callbacks (awslabs#376) qwen-code registers a native `send_message` tool (its team / background-task messaging feature). Its bare name collides with cao-mcp-server's `send_message`, which qwen surfaces under the prefixed name `mcp__cao-mcp-server__send_message`. When a CAO worker is told to "send_message" its result back to the supervisor, the model matches the shorter native tool and calls it, failing with "No active team and no task_id provided" — so assign/handoff callbacks from a qwen worker never route back and the supervisor waits forever. Pass `--exclude-tools send_message` on launch so the colliding native tool is dropped, leaving `mcp__cao-mcp-server__send_message` as the only send-message-shaped tool the model can pick. CAO orchestration never uses qwen-code's native team messaging, so nothing is lost. Verified: with the flag, `/mcp` tool listing shows only the MCP send_message, and a qwen worker's assign result now routes back to the supervisor end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
anilkmr-a2z
left a comment
There was a problem hiding this comment.
Review summary: Well-structured provider that mirrors the Antigravity CLI sibling closely. 2 must-fix findings (correctness), 1 nit (test assertion imprecision).
| cfg = server_config.model_dump(exclude_none=True) | ||
| entry = { | ||
| "command": cfg.get("command", ""), | ||
| "args": cfg.get("args", []), |
There was a problem hiding this comment.
must-fix (correctness) -- The design doc (section 5) declares paste_enter_count = 1 (Ink single-Enter submit), and the unit test asserts in (1, 2), but the implementation never overrides the BaseProvider default of 2. For an Ink TUI where single-Enter submits after bracketed paste, sending a second Enter injects a stray newline that either starts a new turn (doubling input) or is silently consumed. The Antigravity CLI sibling (same Ink scaffold) also relies on the base default of 2, but the design doc here explicitly documents 1.
Fix: Add a class-level override:
@property
def paste_enter_count(self) -> int:
return 1Or, if live testing confirms double-Enter is correct for qwen, update the design doc to match reality and tighten the test assertion to == 2.
| r"|(?:trust (?:this )?folder|Do you trust the files in this folder)" | ||
| r"|(?:Get started|Sign in with)" | ||
| ) | ||
|
|
There was a problem hiding this comment.
must-fix (correctness) -- _handle_startup_dialog is synchronous (time.sleep in a while loop), but it is called from async initialize(). On the default asyncio event loop this blocks the entire event loop for up to startup_prompt_handler_timeout seconds (server setting, typically 30-60s). The sibling antigravity_cli has the same pattern (inherited tech debt), so this is not a regression introduced by this PR alone, but worth flagging because a qwen startup that hits the theme picker on first install will freeze the CAO server's request handling for up to 30s.
For parity with the sibling this is acceptable as-is, but please leave a # TODO: convert to async polling (same tech debt as antigravity_cli) comment so the next contributor knows the intent.
|
|
||
|
|
||
| def test_paste_enter_count_is_valid(): | ||
| assert make_provider().paste_enter_count in (1, 2) |
There was a problem hiding this comment.
nit (test precision) -- assert make_provider().paste_enter_count in (1, 2) passes for both possible values and therefore validates nothing -- it will never fail regardless of whether the provider overrides the base default. Either assert the exact expected value (== 1 per the design doc, or == 2 if double-Enter is validated), or remove the test if the value is intentionally unspecified.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #412 +/- ##
=======================================
Coverage ? 88.08%
=======================================
Files ? 124
Lines ? 15865
Branches ? 0
=======================================
Hits ? 13974
Misses ? 1891
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds Qwen Code (qwen_cli) as a first-class provider so CLI Agent Orchestrator can launch and orchestrate qwen terminals (supervisor/worker) with status detection, response extraction, MCP wiring, and documentation/testing support. It also introduces a more tolerant boolean env-var parser to prevent configuration “truthy” footguns (notably for pyte-based status detection needed by Gemini-CLI-derived TUIs).
Changes:
- Added
QwenCliProviderwith footer/screen-based status detection, response extraction, per-terminal--mcp-configgeneration, startup-dialog dismissal, andsend_messagenative-tool collision mitigation. - Introduced
env_bool()and migrated key boolean env flags to it (includingCAO_PYTE_STATUS), with regression tests. - Added unit + e2e coverage, fixtures, documentation, changelog entry, and a dedicated GitHub Actions workflow for the provider.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/cli_agent_orchestrator/providers/qwen_cli.py |
New Qwen provider implementation (launch, status detection, extraction, MCP temp config, cleanup). |
src/cli_agent_orchestrator/models/provider.py |
Adds ProviderType.QWEN_CLI. |
src/cli_agent_orchestrator/providers/manager.py |
Wires qwen_cli into provider factory. |
src/cli_agent_orchestrator/services/terminal_service.py |
Adds qwen_cli to skill-prompt + soft-enforcement provider sets. |
src/cli_agent_orchestrator/utils/tool_mapping.py |
Adds Qwen tool vocabulary mapping for launch-time summaries. |
src/cli_agent_orchestrator/cli/commands/launch.py |
Allows qwen_cli in workspace-access-required providers. |
src/cli_agent_orchestrator/api/main.py |
Exposes qwen_cli provider binary mapping (qwen) in provider listing. |
src/cli_agent_orchestrator/constants.py |
Adds env_bool() and uses it for CAO_PYTE_STATUS/EAGER_INBOX_DELIVERY. |
src/cli_agent_orchestrator/mcp_server/server.py |
Uses env_bool() for MCP server feature flags. |
README.md |
Documents Qwen Code in provider list/table and valid --provider values. |
docs/qwen-cli.md |
New provider guide (setup/auth, status detection, MCP, limitations). |
docs/issues/376-qwen-cli-provider/design.md |
Design doc for issue #376 implementation. |
CHANGELOG.md |
Adds Qwen provider entry. |
test/providers/test_qwen_cli_unit.py |
New unit tests for command building, status detection, extraction, MCP temp config, init/startup handling. |
test/providers/fixtures/qwen_cli_idle.txt |
Captured TUI fixture for IDLE detection. |
test/providers/fixtures/qwen_cli_processing.txt |
Captured TUI fixture for PROCESSING detection. |
test/providers/fixtures/qwen_cli_completed.txt |
Captured TUI fixture for COMPLETED detection. |
test/providers/fixtures/qwen_cli_error.txt |
Captured TUI fixture for retryable error-turn handling. |
test/providers/fixtures/qwen_cli_response.txt |
Captured TUI fixture for multiline response extraction. |
test/providers/fixtures/qwen_cli_waiting.txt |
Captured TUI fixture for WAITING_USER_ANSWER detection. |
test/providers/test_provider_manager_unit.py |
ProviderManager unit test for qwen_cli creation/mapping. |
test/test_constants.py |
Adds env_bool regression tests and CAO_PYTE_STATUS parsing tests. |
test/api/test_api_endpoints.py |
Updates provider list expectations to include qwen_cli. |
test/e2e/conftest.py |
Adds require_qwen_cli skip fixture. |
test/e2e/test_assign.py |
Adds Qwen e2e assign coverage. |
test/e2e/test_handoff.py |
Adds Qwen e2e handoff coverage. |
test/e2e/test_send_message.py |
Adds Qwen e2e send_message coverage. |
test/e2e/test_supervisor_orchestration.py |
Adds Qwen e2e supervisor orchestration coverage. |
test/e2e/test_skills.py |
Adds Qwen e2e skill injection coverage. |
test/e2e/test_allowed_tools.py |
Adds Qwen allowed-tools e2e coverage (with soft-enforcement xfail where appropriate). |
.github/workflows/test-qwen-cli-provider.yml |
New CI workflow to run Qwen provider unit tests + linting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| entry = { | ||
| "command": cfg.get("command", ""), | ||
| "args": cfg.get("args", []), | ||
| } | ||
| env = dict(cfg.get("env", {})) | ||
| env["CAO_TERMINAL_ID"] = self.terminal_id | ||
| entry["env"] = env | ||
| servers[server_name] = entry | ||
|
|
||
| fd, path_str = tempfile.mkstemp(prefix="cao_qwen_mcp_", suffix=".json") | ||
| with os.fdopen(fd, "w") as f: | ||
| json.dump({"mcpServers": servers}, f, indent=2) |
|
@SparkyWen thanks for the PR. It looks great to me, while i have two questions: (1) would you pls check whether any documents needs to be updated in |
gutosantos82
left a comment
There was a problem hiding this comment.
PR Review: #412 — feat(qwen): add Qwen Code (qwen_cli) provider (#376)
Summary
Adds a qwen_cli provider (619-line qwen_cli.py + full wiring, docs, and 132 passing unit tests) so CAO can drive qwen --approval-mode yolo as supervisor or worker, including cross-provider orchestration. The implementation is high quality — security-conscious command construction, 100% line coverage on the provider, black/isort/mypy clean, and registration correctly wired everywhere the PR body claims. However, two correctness issues sit on the critical orchestration path (response extraction leaks tool-call chrome; over-broad WAITING patterns can pin a terminal), and there are two concrete completeness gaps (examples/ provider list, CHANGELOG). Recommend Request changes — the mechanics are solid but the callback-corruption risk should be resolved before merge.
Blocking (must fix before merge)
- [correctness] providers/qwen_cli.py:570-590 (
_is_chrome/extract_last_message_from_script) 🆕 — Tool-call bullet lines leak into the extracted response. In qwen the●glyph marks both assistant prose and tool-call/picker lines (the waiting fixture shows● Shell run_shell_command(ls -la)and● 1. Yes, allow once)._is_chrome()filters separators/footers/tips/spinner/banner but has no tool-call pattern, and the extractor strips the●and keeps the text. The siblingantigravity_cli.py:145uses_TOOL_CALL_PATTERN = r"^\s*●"to drop every bullet line as chrome — qwen deliberately diverges to keep bullet prose, but that means for any tool-using turn (the common orchestration case) the "response" returned to a handoff/assign supervisor is prepended withShell run_shell_command(...)chrome, and a turn that only ran tools then replied viasend_messageextracts pure garbage. Why it matters: assign/handoff callbacks — the entire point of this PR — deliver corrupted content. Fix: add a qwen tool-call line pattern (the● <ToolName> <tool>(...)shape) to_is_chrome()so tool lines are dropped while genuine● <prose>bullets are kept; add a fixture/test for a completed tool-using turn. Path-weighted to blocking (providers/, core callback path). Confirmed against the code; the exact end-of-turn rendering wasn't reproduced against a live binary — repro: capture aqwenturn that runs a shell tool then answers, feed the frame toextract_last_message_from_script.
Important (should fix)
- [correctness] providers/qwen_cli.py:119-125, 457, 506 (
WAITING_USER_ANSWER_PATTERN) 🆕 — Over-broad WAITING patterns can false-positive on legitimate response prose and hang orchestration.Apply this changeandDo you want to proceedare phrases a coding agent routinely emits in a normal answer. WAITING is checked first (before COMPLETED) against the whole 2048-char tail (raw path) / bottom-15 rows (pyte path), which includes the response body; becauseblocks_orchestrated_input_while_waiting_user_answer=True, a benign completed turn matching one of these pins the terminal until timeout with no real dialog to clear it. Fix: gate the WAITING check on the ready-input-box footer being absent (a real qwen dialog replaces theType your message or @box), and/or narrow to picker structure ([y/n], numberedN. Yes, allow …) matched only in the bottom-most rows. Path-weighted (providers/, core path). - [security] providers/qwen_cli.py + terminal_service.py (
--approval-mode yolo+ SOFT_ENFORCEMENT) — A tool-restricted qwen profile (e.g. read-only reviewer) auto-approves every tool call and can still run arbitraryrun_shell_command; the only barrier is the advisory appended SECURITY_PROMPT. Restrictions are not actually enforced. This matches the established sibling pattern (kimi_cli/codex/antigravity) and is documented (docs/qwen-cli.md "Tool Restrictions" + xfail e2etest_restricted_supervisor_cannot_bash), so it's a conscious widening, not a regression — flagging so the choice is deliberate. - [consistency] examples/cross-provider/README.md:149 🆕 — The "Valid provider values" list (
kimi_cli, copilot_cli, opencode_cli, cursor_cli, …) is missingqwen_cli— the exact parallel enumeration the PR updated in the root README andcao launch --provider. This directly answers maintainer @haofeif's "does examples/ need updating?" — yes. (The list also pre-existingly omitshermes; add both.) Fix: appendqwen_cli(andhermes). - [conventions] CHANGELOG.md 🆕 — The PR also fixes env parsing (
env_bool()helper) soCAO_PYTE_STATUS=1/yes/onnow correctly enables pyte (previously silently parsed False, affecting all screen-detection providers). That's a user-facing behavior change with only an "Added" qwen entry and no### Fixedentry. Fix: add a### Fixedentry for the truthy/falsy env-parsing correction. - [tests] test/providers/test_qwen_cli_unit.py — FOOTER_TAIL_WINDOW eviction untested — The 2 KB tail window's stated motivation ("avoid flipping to IDLE mid-response when a long answer scrolls the old spinner out") has no test. Fix:
buffer = spinner + "x"*3000 + idle_footer; assertget_status == COMPLETEDafter a turn.
Nits (optional)
- [tests] test/providers/test_qwen_cli_unit.py:319 —
assert "--model" in cmd and "qwen3-coder-plus" in cmd— two independent substring checks that pass even if flag/value are non-adjacent. Use the adjacency formassert "--model qwen3-coder-plus" in cmd(as line 629 does). (This is the assertion-imprecision nit @anilkmr-a2z already raised — see Prior feedback.) - [correctness] providers/qwen_cli.py:1034-1063 (
_handle_startup_dialog) —STARTUP_DIALOG_PATTERNalso matches an auth picker (Sign in with/How would you like to … authenticate); blindly pressing Enter accepts the default auth method, which under misconfigured creds could select browser-onlyqwen-oauthand hang init. Consider matching only theme/trust dialogs and letting auth fail fast. - [security/conventions] providers/qwen_cli.py:270 — MCP config written via
os.fdopen(fd, "w")with platform-default encoding; preferencoding="utf-8"per repo checklist (content is ASCII JSON, near-zero impact). - [security] providers/qwen_cli.py:269 (temp-file leak) — the
mkstemp--mcp-configfile leaks in/tmpifinitialize()raises after writing it andcleanup()isn't called. 0600, no secrets (onlyCAO_TERMINAL_ID+ server command) — housekeeping. Wrap init in try/finally. - [consistency] docs/issues/376-qwen-cli-provider/design.md §5 — Design doc drifted from shipped code:
ERROR_PATTERN,paste_enter_count,get_statussignature, andIDLE_FOOTER_PATTERNall differ from the implementation (the code is correct and cross-provider consistent; the doc is stale). Add a "final impl differs" note. - [consistency] constants.py:122 — CAO_PYTE_STATUS comment lists screen-detection providers but omits
opencode_cli(also setssupports_screen_detection = True). The PR edits this exact list — natural place to complete it. - [conventions] skills/cao-session-management/SKILL.md:50 — provider enumeration not updated with
qwen_cli(also omitshermes; illustrative, not authoritative). - [tests] test_qwen_cli_unit.py —
PROCESSING_SPINNER_PATTERNbranch never independently covered (footer substring always matches first);_handle_startup_dialogtimeout-exhaustion path untested;_write_mcp_configempty-server-config edge untested. - [conventions/consistency] scope — the
env_boolrefactor touches shared files (constants.py, mcp_server/server.py: EAGER_INBOX_DELIVERY, ENABLE_WORKING_DIRECTORY, ENABLE_SENDER_ID_INJECTION) beyond the provider. Author already offered to split; a separatefix:PR/commit is preferable per CONTRIBUTING.
Prior feedback (already raised — not restating)
- ↩︎ Test assertion imprecision at test_qwen_cli_unit.py:319 — already raised by @anilkmr-a2z (nit); we concur (kept as a nit above with the concrete fix, credited).
- ↩︎ "2 must-fix correctness findings" — @anilkmr-a2z's review summary cites two must-fix correctness items whose specifics weren't in the fetched comment body; our two correctness findings above (tool-call leak, over-broad WAITING) likely correspond. Weigh toward Request changes regardless of overlap.
- ↩︎ "Does examples/ need updating?" — asked by @haofeif; answered: yes,
examples/cross-provider/README.md:149is missingqwen_cli(surfaced net-new above). - ↩︎ "Did you test examples/assign end-to-end?" — asked by @haofeif; not independently verifiable here (needs a live qwen CLI + auth) — see Verification.
Tests
Coverage is strong. 48 unit cases + 6 realistic captured-TUI ANSI fixtures (idle/processing/completed/error/response/waiting), registration test, env_bool/CAO_PYTE_STATUS regression, and qwen_cli parametrized across all 6 e2e suites (assign/handoff/send_message/skills/allowed_tools/supervisor_orchestration) behind a require_qwen_cli skip fixture. All hard areas covered: status detection on both raw + pyte paths, IDLE-vs-COMPLETED turn split, transient-API-error-is-COMPLETED, extraction failures, ANSI/bullet stripping, model precedence, security-prompt gating, MCP config for dict + pydantic forms, temp-file cleanup idempotency, full async initialize() paths. Gaps: the FOOTER_TAIL_WINDOW eviction path and (critically) no test for a completed tool-using turn, which is exactly where the Blocking extraction bug hides.
Verification
Ran in a clean uv/python3.12 container (uv sync succeeded — numpy built fine in-container):
- Baseline:
uv run pytest test/providers/test_qwen_cli_unit.py test/test_constants.py test/providers/test_provider_manager_unit.py -q→ 132 passed, 0 failed (2.11s); qwen_cli.py reported 100% line coverage. - ✓ VERIFIED
--exclude-tools send_messageis passed —_build_qwen_command()returned exactlyqwen --approval-mode yolo --exclude-tools send_message;test_build_command_excludes_native_send_messagepasses. - ✓ VERIFIED provider registered & resolvable —
ProviderType.QWEN_CLI.value == 'qwen_cli', inPROVIDERS, manager dispatches toQwenCliProvider. - ✓ VERIFIED
qwen_cliselectable via--provider— validated at runtime viaPROVIDERSmembership (not click.Choice), and listed in help/error output. - ✓ VERIFIED status detection on all 6 fixtures (idle→IDLE, processing→PROCESSING, completed/error→COMPLETED, waiting→WAITING_USER_ANSWER, response→IDLE + correct extraction).
- ✓ VERIFIED
black --check/isort --check/mypyall clean on qwen_cli.py. - ⁇ NOT VERIFIED — live end-to-end orchestration (real qwen session reaching IDLE, MCP
send_messagecallback routing back to supervisor, startup-dialog dismissal against a real TUI). Needs a liveqwenbinary + auth (not installed). Manual:npm install -g @qwen-code/qwen-code, launch a CAO session with--provider qwen_cli, confirm assign/handoff callbacks route viamcp__cao-mcp-server__send_message. This is also @haofeif'sexamples/assignend-to-end question — recommend the author confirm it, especially given the tool-call extraction concern above.
All runnable PR claims hold; no diff↔description contradictions in what could be exercised statically/dynamically.
Verdict
Request changes — excellent implementation, fully wired and tested, but the response-extraction tool-call leak and the over-broad WAITING patterns both sit on the core assign/handoff callback path this PR exists to enable, and examples/ + CHANGELOG completeness gaps remain. Resolve the extraction bug (with a tool-turn fixture), tighten WAITING gating, add the two enumerations/entry, and this is a clean approve. --approval-mode yolo soft-enforcement is acceptable as a documented, sibling-consistent choice.
Qwen Code (
qwen_cli) providerAdds the Qwen Code (
qwen_cli) provider so CAO can driveqwenas a first-class agent — supervisor or worker — including cross-provider orchestration (handoff/assign/send_message).Closes #376.
Provider
src/cli_agent_orchestrator/providers/qwen_cli.py— launchesqwen --approval-mode yolowith a per-terminal--mcp-config,--append-system-prompt(CAO role + skills + security prompt when tool-restricted), model selection, full status detection (PROCESSING / IDLE / COMPLETED / ERROR / WAITING_USER_ANSWER), response extraction, and first-run dialog handling (theme / folder-trust).~/.qwen/.envor~/.qwen/settings.json) orqwen-oauth.--providerCLI choices, cross-provider resolution, and the README provider table.Native
send_messagetool collision (fixed)qwen-code ships a native
send_messagetool (its team / background-task messaging feature) whose bare name shadows cao-mcp-server'ssend_message, which qwen surfaces under the prefixed namemcp__cao-mcp-server__send_message. A worker told to "send_message" its result back matched the native tool →No active team and no task_id provided→assign/handoffcallbacks from a qwen worker never routed back to the supervisor.Launch now passes
--exclude-tools send_message, dropping the colliding native tool somcp__cao-mcp-server__send_messageis the only send-message-shaped tool the model can pick. CAO never uses qwen-code's native team messaging, so nothing is lost. Full analysis in the #376 comment thread.Verified end-to-end: a qwen worker's
assignresult now routes back to the supervisor (supervisor receives[Message from terminal <qwen_id>]and combines it with the other worker's result).Tests
test/providers/test_qwen_cli_unit.py— command building, status detection, response extraction, per-terminal MCP config, andtest_build_command_excludes_native_send_message.Docs
docs/qwen-cli.md(provider guide), design doc, README provider table + valid--providervalues.Note on branch contents
The branch also carries a small related fix,
fix(config): tolerant env_bool()(soCAO_PYTE_STATUS=1reliably enables pyte), and the qwen docs commits. Happy to split any of these out if preferred.