feat(cli): add native multi-agent coordination - #8804
Conversation
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
中文说明
已审查——无阻断问题。 建议见行内评论。
— deepseek-v4-flash via Qwen Code /review (v0.21.8)
|
Changed: persist the teammate Verified: 35 focused coordination tests, ESLint, Prettier, and Core typecheck passed. Intentionally not changed: additional workflow-documentation and validation-test expansion outside the writer-task deadlock. Pending: exact-head CI and automatic review. 中文摘要已阻止只读调查员自动领取写任务;聚焦测试、lint 与 Core typecheck 通过,等待 CI。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): PR #8804 turns /coordinate into a native Agent Team workf...: none — all planned checks completed within budget.; PR #8804 turns /coordinate into a native Agent Team workf...: none — all checks above completed within budget..
中文说明
已审查。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):PR #8804 turns /coordinate into a native Agent Team workf...:none — all planned checks completed within budget.;PR #8804 turns /coordinate into a native Agent Team workf...:none — all checks above completed within budget.。
— qwen3.8-max via Qwen Code /review (v0.21.9)
|
Changed: no code changes in this round. Verified: all four current threads were checked against the exact head; no current execution bypass or failing behavior remains, and exact-head checks are green. Intentionally not changed: late product-contract and mutation-only test hardening after six review rounds. Pending: none from this closeout; maintainer review state may remain stale. |
Local real-stack verification — PR #8804 (native
|
| Check | Result |
|---|---|
| Read-only tool surface (both teammates, model-visible and execution allowlist) | exactly 9: read_file, grep_search, glob, list_directory, read_mcp_resource, tool_search, send_message, task_list, task_update |
run_shell_command attempt |
blocked: Tool "run_shell_command" not found |
write_file attempt |
blocked: Tool "write_file" not found |
tool_search select:run_shell_command → then call it |
schema is revealed, but the follow-up call is still blocked at the execution layer |
| Peer messages A→B and B→A | both delivered and observed in each teammate's context |
| Normal final answers → leader | auto-forwarded with no explicit send_message (reader-a sent none) |
Leader shutdown + team_delete |
happened with no user turn |
| Filesystem side effects of blocked writes | none (/tmp/pr8804-pwned*.txt, HACK.txt absent) |
The tool_search escape result is worth calling out: it confirms the PR's claim that "arbitrary deferred tools are unavailable even if a prompt asks for them" holds even when the model actively reveals the schema first — the executionAllowedTools backstop rejects the call regardless.
3. Scenario B — phased investigation → single worktree writer
/coordinate asked to investigate then make a one-line change. Ledger + external Git inspection confirm:
- The read-only
scoutfinished first and reported; the IMPL task was created while scout was idle, and the leader's owntask_listshows it stayed#2 [pending]with no owner — the read-only teammate did not auto-claim the writer task (this is precisely the last commit's fix). - Exactly one
general-purposewriter was spawned, pinned to the leader-created worktree viaworking_dir. - Main checkout
src/hello.jswas byte-identical before and after (sha f57de236…unchanged,git statusclean); the worktree copy was the only thing changed toHello, PR8804(M src/hello.jsthere).
MAIN src/hello.js -> console.log('Hello, world'); (sha unchanged, status clean)
WRITER src/hello.js -> console.log('Hello, PR8804'); ( M src/hello.js)
4. Direct Agent validation guards
All rejections fire with the intended messages:
| Attempt | Rejection |
|---|---|
read_only without name |
Parameter "read_only" requires a named teammate via "name". |
read_only with no active team |
Parameter "read_only" requires an active team. |
read_only + plan_mode_required |
Parameters "read_only" and "plan_mode_required" cannot be used together. |
working_dir = main checkout |
… is not a registered linked worktree of this repository … |
working_dir = arbitrary dir |
… is not a registered linked worktree of this repository … |
isolation for a named teammate |
Parameter "isolation" cannot be used for a named teammate. Create a leader-owned worktree first, then pass it with "working_dir". |
One clarification on the auto-forward semantics
While probing the "no duplicate wake" behavior I confirmed the actual rule (worth documenting for reviewers): the runtime forwards a teammate's final visible answer on each non-cancelled idle turn; an explicit send_message(to: leader) only suppresses the fallback "completed a turn" notification for a tool-only turn (no visible final text). So a turn that both calls send_message to the leader and ends with visible final text will deliver both — this matches "wake the leader after every non-cancelled idle turn," and is not a duplicate of the same content. No code issue; just noting the precise contract.
Verdict: enforced read-only surface (declaration + execution backstop, incl. deferred-tool escape), peer messaging, automatic leader delivery, phased shutdown-before-writer, worktree isolation, and all Agent parameter guards all reproduce on a real stack. LGTM.
Evidence: recorded provider JSONL ledgers + external git state; screenshots on branch pr-assets/8804-verify under verify/pr8804-manual-local/. Non-deterministic model behavior was replaced by a scripted provider so the tool declarations/results are the judgement, not model quality.
中文版本(点击展开)
本地真实环境验证 —— PR #8804(原生 /coordinate)
我从源码构建了本 PR,并在真实 bundle CLI + 真实 TUI + 真实 Agent Team 上运行,通过一个记录型 OpenAI 兼容 provider 驱动模型,因此每一条判据都以线协议层面的工具声明数组和工具返回结果为准,而不是依赖提示词遵守。结论:Reviewer 测试计划的四项全部复现,且只读强制比描述更强(能扛住 tool_search 绕过尝试)。建议合并。
环境
- Head
b2614ae,源码构建npm ci && npm run build && npm run bundle→dist/cli.jsv0.21.8,Node 22,macOS。 - 隔离
QWEN_HOME、QWEN_CODE_ENABLE_AGENT_TEAM=1、--approval-mode yolo、真实 tmux TUI。 - Fixture:一个干净的临时 Git 仓库(
README.md、package.json、src/hello.js)。 - Provider:本地记录型服务,按系统提示识别每个调用方并返回脚本化工具调用;每个请求(完整
tools[]+ messages)写入 JSONL 账本,作为判据来源。
1. feature commit 上的单测
受影响的两个测试文件加上新的协作 harness 全部通过:agent.test.ts(258)+ coordination-harness.test.ts + promptAddendum.test.ts = 296 通过。harness 里有一条直接断言:readOnly 队友的 tools 等于 executionAllowedTools,且不含 run_shell_command / save_memory / create_sub_session。
2. 场景 A —— 两个强制只读队友(无人工干预)
/coordinate 跑两个独立只读检查任务并要求互发消息。账本判据:
| 检查项 | 结果 |
|---|---|
| 只读工具面(两个队友,模型可见 且 执行 allowlist) | 恰好 9 个:read_file, grep_search, glob, list_directory, read_mcp_resource, tool_search, send_message, task_list, task_update |
尝试 run_shell_command |
被拦:Tool "run_shell_command" not found |
尝试 write_file |
被拦:Tool "write_file" not found |
tool_search select:run_shell_command → 再调用 |
schema 能被拉回,但后续调用仍在执行层被拦 |
| peer 消息 A→B、B→A | 双向送达,并在各自上下文中可见 |
| 普通最终回答 → leader | 自动转发,无需显式 send_message(reader-a 一次都没发) |
leader shutdown + team_delete |
无需任何用户轮次自动完成 |
| 被拦写操作的文件副作用 | 无(/tmp/pr8804-pwned*.txt、HACK.txt 均不存在) |
tool_search 绕过这条值得强调:它证明 PR 的说法 "即使提示词要求,任意延迟加载工具也不可用" 即便模型主动先把 schema 揭示出来也成立 —— executionAllowedTools 兜底会无条件拒绝该调用。
3. 场景 B —— 分阶段调查 → 唯一 worktree writer
/coordinate 要求先调查再改一行。账本 + 外部 Git 检查确认:
- 只读
scout先完成并汇报;IMPL 任务在 scout 空闲时创建,leader 自己的task_list显示它保持#2 [pending]且无 owner —— 只读队友没有自动领取 writer 任务(正是最后一个 commit 的修复点)。 - 恰好 spawn 一个
general-purposewriter,通过working_dir绑定 leader 创建的 worktree。 - 主 checkout
src/hello.js前后逐字节相同(sha f57de236…未变,git status干净);只有 worktree 副本被改成Hello, PR8804(该处M src/hello.js)。
4. 直接 Agent 参数守卫
所有拒绝都按预期文案触发:
| 尝试 | 拒绝信息 |
|---|---|
read_only 缺 name |
Parameter "read_only" requires a named teammate via "name". |
read_only 无 active team |
Parameter "read_only" requires an active team. |
read_only + plan_mode_required |
Parameters "read_only" and "plan_mode_required" cannot be used together. |
working_dir = 主 checkout |
… is not a registered linked worktree of this repository … |
working_dir = 任意目录 |
… is not a registered linked worktree of this repository … |
具名队友用 isolation |
Parameter "isolation" cannot be used for a named teammate… |
关于自动转发语义的一点澄清
在探查"不重复唤醒"行为时确认了真实规则(值得给 reviewer 记录):runtime 在每个未取消的 idle 轮次转发队友的最终可见回答;显式 send_message(to: leader) 只会抑制纯工具轮次(无可见最终文本)的 fallback "completed a turn" 通知。因此一个既调用 send_message 给 leader、又以可见最终文本结束的轮次会两者都送达 —— 这与"每个未取消的 idle 轮次都唤醒 leader"一致,并非同一内容的重复。不是代码问题,只是把精确契约写清楚。
结论: 只读强制面(声明 + 执行兜底,含延迟工具绕过)、peer 消息、自动 leader 回传、writer 前的分阶段 shutdown、worktree 隔离、以及所有 Agent 参数守卫,在真实栈上全部复现。LGTM。
证据:记录型 provider JSONL 账本 + 外部 git 状态;截图在分支 pr-assets/8804-verify 的 verify/pr8804-manual-local/ 下。非确定性模型行为已用脚本化 provider 替换,判据是工具声明/结果本身,而非模型质量。
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 634 passed · 0 failed · 634 total 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:634 通过 · 0 失败 · 634 总计 Verification reportPR 8804 Deep Verification (follow-up round) — feat(cli): add native multi-agent coordinationVerdict: This is a follow-up round: 5 commits landed since the previous verification (head was Previous-finding status
中文摘要结论: 本轮为复核轮:上一轮之后新增 5 个 commit(核心增量是“让只读队友不领取 writer 任务”),base 也前进到
Central claim + A/B tablesCentral claim (carried over): Delta claim (this round): read-only teammates are kept off writer tasks — they never auto-claim, on either claim path. A/B 1 — read-only enforcement (harness
|
| cell | head | base control |
|---|---|---|
spawn {readOnly:true} → captured toolConfig |
tools === executionAllowedTools === exactly the 10-entry canonical list; addendum in system prompt; member.readOnly persisted |
toolConfig === undefined, no addendum (param ignored) |
| hostile batch, tools declared | 10/10 denied at the execution allowlist | 10/10 authorized (reach scheduling; cancelled only by the harness abort) |
| backstop: shell declared but off the allowlist | denied by execution allowlist |
n/a |
positive control: read_file |
passes both gates (cancelled by abort after the gate) | n/a |
Head 19 assertions, base 14 — all pass.
A/B 2 — final-answer forwarding (harness harness/verify-forwarding.test.ts; witness 03-forwarding-ab-head-vs-base.png)
| cell | head | base control |
|---|---|---|
| final text on IDLE | forwarded verbatim | mailbox empty for 600 ms (the stall this PR fixes) |
| tool-only turn | fallback “completed a turn without a model-visible final answer” | empty |
explicit send_message then IDLE |
exactly 1 message, no fallback duplicate | 1 message (explicit send unchanged — regression guard) |
| round cancelled by user | not forwarded; next round still forwards | nothing (no bridge) |
| round 1 forwarded, round 2 tool-only | fallback for round 2; round-1 text NOT re-sent | empty |
Head 9 assertions, base 6 — all pass. Note: getLeaderMessages() drains the inbox; the harness accumulates drains.
A/B 3 — validation + worktree pinning (harness harness/verify-agenttool.test.ts; witness 04-agenttool-validation-ab.png)
Real AgentTool against real git fixtures (real GitWorktreeService registry checks: in-repo worktree, sibling worktree outside the repo dir, foreign-repo worktree, unregistered dir, main checkout). SubagentManager is a capture stub (spawn sink); Config is a stub.
| case | head | base control |
|---|---|---|
read_only matrix (non-boolean / no name / blank name / +plan_mode_required / no team / valid / false) |
7 documented outcomes (4 rejections, 3 accepts) | all silently accepted (param unknown) |
empty name + isolation |
accepted (empty name = unnamed routing) | accepted |
named + isolation, no working_dir, active team |
rejected: “Create a leader-owned worktree first…” | accepted by validation |
named + isolation at execute |
blocked at validation (never reaches execute) | validation passes and execute silently ignores isolation — teammate spawns in the parent tree |
working_dir + run_in_background unnamed / NAMED |
unnamed rejected; named allowed (delta) | both rejected |
named teammate + registered in-repo worktree through execute() |
spawn cwd pinned to the worktree; response names the pin |
rejected: “working_dir is not supported for a named teammate” |
| named teammate + registered worktree outside the repo | accepted; cwd pinned to the sibling path (F5) | unreachable (named+working_dir rejected outright) |
| unnamed + registered worktree outside the repo | accepted; child Config getProjectRoot() rebound to it |
rejected: “resolves outside this repository” (containment check) |
| unregistered dir / main checkout / foreign-repo worktree (named path) | all rejected by the registry gate | all rejected (via the named rejection) |
read_only through execute() |
spawnTeammate receives readOnly: true |
param silently dropped |
read_only from a subagent context |
rejected: “from the team leader” | no teammate spawn either — falls through to a regular subagent (pre-existing top-level gate) |
| abort after working_dir validation | “spawn aborted before … was registered”; no spawn | n/a at base |
Head 28 assertions, base 25 — all pass.
A/B 4 — auto-claim guards, the delta commit (harness harness/verify-autoclaim.test.ts; witness 02-autoclaim-ab-head-vs-base.png)
| cell | head | base control |
|---|---|---|
| positive control: normal teammate + pending task | claims (task prompt received) | claims — pipeline observable on both arms |
| read-only teammate + pending task (scan path) | received nothing | claims the task (readOnly ignored) |
| shutdown-pending teammate, RUNNING→IDLE flush path | received nothing | claims the task — base's _shutdownPending check in flushNextMessage only consumes the shutdown_request message; tryAutoClaimTask was unguarded |
| read-only teammate after processing a leader message (flush path) | got ONLY the leader message, never a task prompt | claims the task after the round |
Head 5 assertions, base 5 (each including the explicit “reader is IDLE” premise check) — all pass. This is the load-bearing proof of b2614ae1: on base, the same teammate that head treats as read-only claims the “writer task” on both claim paths.
Corrections
None.
Findings
All findings are non-blocking: every behavior below was measured correct at head by this round's harnesses; the findings are about what would (not) catch a future regression, plus one deliberate design change that deserves explicit reviewer sign-off.
F1 — Suggestion (stands): three forwarding-bridge guards remain unpinned
Mutants M3 (drop !explicitlyReported), M4 (drop !event.roundCancelledByUser), M5 (drop the RUNNING clear of pendingFinalReports/explicitLeaderReports) all survive the full 35-test coordination-harness.test.ts at the new head (each run: 35 passed). This round's H2 cells 3/4/5 prove all three behaviors currently correct, so these remain coverage gaps, not defects. The new forwarding tests pin adjacent axes instead — M6 (drop !automatic from shutdown classification) is killed by does not treat an automatic final report as a shutdown response, and M8 (change the fallback text) is killed by two tests — so the suite is not vacuous, just not covering these three guards.
Repro: python3 harness/mutate.py M3-drop-explicit-report-guard tmp/mutant-tree && cd tmp/mutant-tree/packages/core && npx vitest run src/agents/team/test-utils/coordination-harness.test.ts --coverage.enabled=false → green.
F2 — Suggestion (stands): the four read_only validation branches remain unpinned
Mutant M7 (delete the entire read_only block in AgentTool.validateToolParams) survives agent.test.ts (filtered run: 11 passed | 247 skipped); the suite contains no validation-level test for read_only — only the two execute-path tests (passes enforced read-only mode through to TeamManager, rejects read_only direct execution from a subagent context). The four branches (boolean type, name requirement incl. blank-name, active-team requirement, plan_mode_required conflict) are each verified correct at head by A/B 3. Four small rejection tests would close it.
F6 — Suggestion (new): the flush-path read-only guard is the delta's load-bearing piece and is pinned by nothing
The delta commit added two read-only guards: the scanIdleAgentsForTasks filter and the tryAutoClaimTask entry guard. Mutants M1 (drop the entry guard) and M2 (drop the scan filter) each survive individually — the PR's test does not auto-claim tasks for read-only teammates only pins the disjunction, because the scan path is double-guarded. But the flush path (message processed → IDLE → flushNextMessage → tryAutoClaimTask) is protected only by the entry guard — A/B 4's last two cells prove this is the path base leaks (the “readOnly” teammate claims the task after handling one leader message). If a refactor drops the entry guard while keeping the scan filter, the whole suite stays green while the flush path regresses. Porting A/B 4 cell D (send a message to a read-only teammate with a pending task; assert no task prompt) into coordination-harness.test.ts would pin it; the shape is ready-made (H4 cell D in this round's harness).
F5 — Suggestion (new, design change to ratify): working_dir containment check removed — registered worktrees outside the repo are now accepted
The delta removed the canonical-path containment check from resolveExternalWorktreeDir; the “single authoritative gate” is now isRegisteredLinkedWorktree alone. Measured consequence (A/B 3): a worktree registered against this repo but living outside the repo directory (git worktree add ../wt-out) is accepted at head and binds the child's workspace boundary there (getProjectRoot() rebound), while base rejected it with “resolves outside this repository”. The change is intentional (code comment + tool description updated to drop “must live inside it”) and consistent with the PR description's stated position that the pin is “a validated cwd/workspace pin, not a filesystem sandbox”. Blast radius is bounded: only the top-level leader can spawn teammates (isTopLevelSession() && !isTeammate(), verified), and a writer teammate can already reach outside via explicit absolute paths. Still, this removes a defense-in-depth layer that previously kept the relative-path file/search boundary inside the repository — a reviewer should sign off on that trade explicitly. What it is NOT: no exploit was demonstrated; isRegisteredLinkedWorktree's realpath comparison still rejects unregistered, foreign-repo, and primary-tree paths (all measured).
F3 — Nit (stands): read-only spawn test still nested under describe('spawn cap')
gives read-only teammates only inspection and coordination tools (coordination-harness.test.ts:682) asserts tool-surface enforcement but sits inside the spawn cap describe. Cosmetic grouping only.
Not covered
- Per-commit attribution: checkout is depth 2; the metadata snapshot lists 41 commits but only the merge commit, base tip, and PR head are reachable (
git rev-list HEAD^1..HEAD^2returns 1 at the shallow boundary — the known artifact). Verified the aggregateHEAD^1..HEADdiff only; the five delta commits' behaviors were verified as a bundle via the delta probes, not attributed per commit. - Live interactive
/coordinateruns (Reviewer Test Plan steps 1–3 with a real model): no API credentials in this container. The machinery those steps observe was driven through real runtime components (real TeamManager, real AgentCore gate seam, real git, real AgentTool execute path). This reproduces the shape of each step's assertions, not the model-side trigger. - Symlink-straddle robustness of
isRegisteredLinkedWorktreeunder the new single-gate regime: the repo shipsgitWorktreeService.symlinks.integ.test.ts; not executed this round (outside the affected-file set). - Windows/Linux TUI validation (PR marks both untested; enforcement paths verified here are platform-independent core code, run on Linux).
- Repo-wide test suite and other workspaces: ran the 13 affected/sibling core files (510 tests) + core
tsc --noEmitonly.packages/clicode is untouched by this diff. - The TOCTOU re-check inside the
tryAutoClaimTaskclaim loop (_shutdownPendingre-read after the asynclistTasks) is not deterministically reachable by a scripted cell; verified by code read only. - Docs lint/build not executed; docs diff (new
multi-agent-coordination.md, arena/commands updates, newSKILL.md) reviewed against verified behavior — claims match.
Methodology
Environment: CI merge-ref checkout (HEAD = merge commit, base tip a64d1291 already includes #8834), node:22-bookworm, npm ci + npm run build pre-run. Base control: git worktree add tmp/base-tree HEAD^1 with per-package node_modules symlinked from the head tree (lockfile untouched by the PR — git diff HEAD^1..HEAD -- package.json package-lock.json 'packages/*/package.json' is empty); harnesses import tree sources by relative file URL, and each spec asserts via realpathSync that its own location resolves inside $VERIFY_TREE, so the base arm executed base code (the root node_modules/@qwen-code/* workspace links were never imported by the harness closure). Harnesses (harness/verify-*.test.ts, rerunnable with VERIFY_ARM=… VERIFY_TREE=… npx vitest run …) drove: (1) real TeamManager.spawnTeammate with spawn-config capture via FakeBackend, (2) real AgentCore.processFunctionCalls with a pre-aborted controller observing the gate seam, (3) real AgentTool.validateToolParams/execute() against real git fixtures, (4) real TeamManager auto-claim paths via the repo's coordination harness with per-cell isolated QWEN_HOME and team names (an early version of the harness contaminated the shared default task store — fixed and re-measured; all reported numbers come from isolated runs). Mutation matrix: harness/mutate.py applied 10 single-point mutants in scratch worktree tmp/mutant-tree (unmutated controls green: coordination 35/35, agent filter 11/11); killed mutants M6/M9/M10 were attributed to their exact failing tests (expected 'cancelled' not to be 'cancelled'; missing createAgentHeadless call; expected "spy" to not be called). Raw per-arm JSON counts in logs/h*-*.json, matrix transcript in logs/mutation-matrix.txt, gate outputs in logs/gate-vitest.txt / logs/gate-typecheck.txt.
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
No merge blocker found. One non-blocking correctness issue remains:
Reviewed against |
|
Released in v0.21.11. |











What this PR does
This PR turns
/coordinatefrom a prompt-only wrapper into a usable native Agent Team workflow while reusing the Agent View tabs already in Qwen Code. It adds an enforcedread_only: truemode for named teammates, lets one named writer run from a leader-created registered Git worktree viaworking_dir, and automatically forwards each teammate's final answer—or a completion notification for a tool-only turn—to the leader.Read-only teammates receive one canonical positive allowlist: repository inspection plus
send_message,task_list, andtask_update. The same list is used both for model-visible tool declarations and the execution-layer backstop, so shell execution, file writes, memory, schedules, nested agents, and arbitrary deferred tools are unavailable even if a prompt asks for them.The bundled workflow keeps investigation parallel and bounded, shuts down investigators before queuing implementation so they cannot auto-claim the writer task, creates one worktree, and starts exactly one writer there. Existing shared tasks, peer messages, lifecycle handling, approval routing, and Agent View tabs remain the coordination plane; this PR adds no second roster, PTY supervisor, daemon, or UI.
Why it's needed
Before this change,
/coordinatecould describe a team workflow but could not enforce its two most important safety and usability claims. Agent-type names such as Explore or Plan were treated as if they implied read-only behavior even though their effective tool configuration could differ; named teammates also rejected an existingworking_dir, and a teammate that returned a normal final answer without explicitly callingsend_messagecould leave the leader waiting indefinitely.The corrected design keeps the existing Agent Team and Agent View architecture and fills those concrete gaps. It also uses the pre-existing inspection allowlist as the single source of truth, addressing the RFC feedback that a second divergent list would undermine the security model.
Reviewer Test Plan
How to verify
QWEN_CODE_ENABLE_AGENT_TEAM=1, start the interactive CLI, and run/coordinatewith two independent repository inspection tasks that require a peer message.Mainplus both named teammates, both teammates report thatrun_shell_commandis absent, their peer messages cross, their normal final answers reach the leader automatically, and the leader shuts them down and deletes the team without user intervention./coordinatewith an explicit request to create a Git worktree and make a one-line change. Confirm the investigation teammate is shut down before the implementation task is created, exactly one general-purpose writer is spawned with the returnedworking_dir, the main checkout remains unchanged, and only the worktree copy changes.read_onlywithout a named teammate or active team, rejectread_onlytogether with plan-required mode, and validate thatworking_diris a registered linked worktree rather than the main checkout or an arbitrary directory.Evidence (Before & After)
Before: the skill depended on prompt compliance for read-only behavior, a named teammate could not use a caller-owned worktree, and normal teammate final text was not guaranteed to wake the leader. The earlier draft also duplicated Agent View and PTY lifecycle code; that direction was removed.
After: a real no-intervention TUI run created two enforced read-only teammates, rendered
Main │ reader-b ● │ reader-a ●, completed two shared tasks, exchanged A→B evidence and B→A acknowledgement, delivered both reports automatically, shut down both teammates, and deleted the team in 51.7 seconds from prompt to final answer. A second real run completed the phased read-only-investigator → shutdown-pending → worktree → single-writer flow in 73.5 seconds from prompt to final answer; external Git inspection confirmed the main file stayed unchanged while the worktree contained only the requested one-line edit. The CLI summary for that fresh process reported 1m53s including startup and exit UI.Focused verification at the feature commit passed 281 tests across the two existing affected test files, repository typechecking, the full JavaScript/TypeScript build, the focused core build after the final cleanup, bundle generation, Prettier validation, and
git diff --check. The final merge from currentmainwas conflict-free and did not touch the coordination paths.For context, the earlier matched Herdr 0.8.0 baseline used the same checkout, Qwen Code bundle, and
DeepSeek/deepseek-v4-pro: two external PTY workers took 17.2s and a third aggregation invocation took 66.9s, or 84.1s when joined immediately. The final native read-only run was 38.5% shorter at 51.7s while also performing shared task state, peer messaging, automatic leader delivery, live Agent View status, directed shutdown, and team deletion. This is one small dogfood scenario, not a general throughput claim. Herdr remains stronger for persistent or remote terminals and heterogeneous CLI products; native/coordinateis stronger for collaboration inside one Qwen Code session.Tested on
Environment (optional)
macOS, Node.js 22, local bundled CLI,
QWEN_CODE_ENABLE_AGENT_TEAM=1, YOLO approval mode,DeepSeek/deepseek-v4-pro, sandbox disabled, fixture Git repository with a clean main checkout.Risk & Scope
working_diris a validated cwd/workspace pin, not a filesystem sandbox; explicit absolute paths remain possible, so the leader-owned single-writer rule is enforced by the bundled workflow rather than an OS boundary./coordinateexplicitly.Linked Issues
Refs #8718.
中文说明
本 PR 做了什么
这个 PR 在复用 Qwen Code 现有 Agent View 标签页的前提下,把
/coordinate从仅靠提示词描述的包装层补成可直接使用的原生 Agent Team 工作流。它为具名队友增加运行时强制的read_only: true模式,允许唯一 writer 通过working_dir使用 leader 创建并注册的 Git worktree,并在队友完成一轮时自动把最终回答回传给 leader;如果该轮只有工具调用而没有可见最终文本,则发送一条完成通知。只读队友使用唯一的一份正向 allowlist:仓库只读检查能力,加上
send_message、task_list和task_update。模型可见的工具声明与执行层拦截使用同一份列表,因此即使提示词要求执行,shell、文件写入、memory、定时任务、嵌套 agent 和任意延迟加载工具也不可用。内置工作流会保持调查阶段并行且有界,在创建 implementation task 之前先请求调查者 shutdown,避免只读调查者自动领取 writer 任务;随后只创建一个 worktree,并在其中启动唯一 writer。共享任务、队友消息、生命周期、审批路由和 Agent View 标签页仍然完全复用现有实现;本 PR 没有增加第二套路由表、PTY supervisor、daemon 或 UI。
为什么需要
修改前,
/coordinate可以描述团队协作流程,但无法兑现两个最重要的安全与可用性承诺。Explore、Plan 等 agent 类型名称被当作天然只读,但它们的实际工具配置可能不同;具名队友不能使用已有的working_dir;而且队友如果正常返回最终回答、却没有显式调用send_message,leader 可能一直等待。修正后的设计保留现有 Agent Team 与 Agent View 架构,只补齐这些具体缺口。它还直接复用已有检查工具 allowlist 作为唯一事实来源,回应 RFC 中“第二份分叉列表会破坏安全模型”的反馈。
Reviewer 测试计划
如何验证
QWEN_CODE_ENABLE_AGENT_TEAM=1启用 Agent Team,启动交互式 CLI,然后使用/coordinate执行两个独立的仓库只读检查任务,并要求队友互发消息。Main与两个具名队友;两个队友都报告没有run_shell_command;peer message 能双向到达;普通最终回答会自动送达 leader;leader 无需用户干预即可 shutdown 两个队友并删除 team。/coordinate明确要求创建 Git worktree 并完成一行修改。确认只读调查者在 implementation task 创建前进入 shutdown pending,只启动一个绑定返回working_dir的 general-purpose writer,主 checkout 不变,并且只有 worktree 副本被修改。read_only会被拒绝,read_only与 plan-required 模式不能同时使用,并且working_dir必须是已注册的 linked worktree,不能是主 checkout 或任意目录。证据(修改前与修改后)
修改前:skill 只能依赖提示词遵守只读约束;具名队友不能使用 caller-owned worktree;普通队友最终文本也不保证唤醒 leader。更早的草稿还重复实现了 Agent View 和 PTY 生命周期;该错误方向已全部移除。
修改后:一次真实、无人工干预的 TUI 运行创建了两个强制只读队友,显示
Main │ reader-b ● │ reader-a ●,完成两个共享任务,交换 A→B 证据和 B→A 确认,自动把两份结果送达 leader,随后 shutdown 两个队友并删除 team;从输入提示到最终回答耗时 51.7 秒。第二次真实运行完成了“只读调查者 → shutdown pending → worktree → 唯一 writer”的分阶段流程,从提示到最终回答耗时 73.5 秒;外部 Git 检查确认主目录文件保持不变,worktree 中只有要求的一行修改。该全新进程的 CLI 汇总耗时为 1 分 53 秒,其中包含启动和退出 UI。在 feature commit 上,受影响的两个现有测试文件共 281 个测试通过,仓库 typecheck、完整 JavaScript/TypeScript build、最终清理后的 core 聚焦 build、bundle 生成、Prettier 校验和
git diff --check全部通过。最后一次合入当前main没有冲突,也没有触及协调链路。作为对照,之前匹配的 Herdr 0.8.0 baseline 使用相同 checkout、Qwen Code bundle 和
DeepSeek/deepseek-v4-pro:两个外部 PTY worker 用时 17.2 秒,第三次聚合调用用时 66.9 秒,立即串接后的总时间为 84.1 秒。最终原生只读流程为 51.7 秒,短 38.5%,同时还完成了共享任务状态、peer messaging、自动 leader 回传、实时 Agent View 状态、定向 shutdown 和 team 删除。这只是一个小型 dogfood 场景,不代表通用吞吐结论。Herdr 仍然更适合持久化或远程终端以及异构 CLI 产品;原生/coordinate更适合一个 Qwen Code 会话内的协作。测试平台
环境(可选)
macOS、Node.js 22、本地 bundle CLI、
QWEN_CODE_ENABLE_AGENT_TEAM=1、YOLO approval mode、DeepSeek/deepseek-v4-pro、关闭 sandbox、使用主 checkout 干净的 fixture Git 仓库。风险与范围
working_dir是经过验证的 cwd/workspace pin,不是文件系统 sandbox;显式绝对路径仍然可能访问外部,因此 leader-owned single-writer 规则由内置工作流执行,而不是 OS 隔离边界。/coordinate。关联 Issue
Refs #8718。