feat: agent board — share work across independently started agents - #9402
feat: agent board — share work across independently started agents#9402yiliang114 wants to merge 46 commits into
Conversation
|
Re-run note: this pass reviews Template: all required headings present and filled in ✓. The Chinese Problem: unchanged — real and tracked. #8724 asks for same-machine sessions to share work; design discussion #9399 scopes it. Independently started agents still have no shared work surface today, and peer-agent collaboration remains an active, validated area in the ecosystem. Direction: aligned, unchanged. The feature is purely additive — a new Size: core paths are touched ( Approach: the two commits are the minimal fix set round 5 asked for, with no drive-by changes. R5-1 is the validation hoist above Risk: no elevated signals — none of the changed files match the revert-correlated path list. Moving on to code review. 🔍 中文说明Re-run 说明:本次审查的是 模板:所有必需标题齐全且已填写 ✓。PR 正文仍缺中文 问题:不变——真实且有跟踪。#8724 请求同机 session 共享工作;设计讨论 #9399 界定范围。独立启动的 agent 今天仍没有共享工作表面,peer-agent 协作在生态中仍是活跃且已被验证的方向。 方向:对齐,不变。功能纯新增——新的 规模:触及核心路径( 方案:两个 commit 正是第 5 轮要求的最小修复集,无夹带改动。R5-1 是把校验提前到 风险:无升级信号——变更文件均未命中 revert 相关路径列表。 进入代码审查。🔍 — Qwen Code · qwen3.8-max Reviewed at |
|
Re-run note: replaces the Stage 2 review of Code reviewMy independent proposal for the round-5 blockers, written before reading the fix commits: R5-1 is a hoist of the What I verified by reading the tree at this head:
The rest of the surface is unchanged from the head the previous pass verified end to end (storage discipline, two-tier locking, hostile-record handling, exit-code contract), so nothing else was re-litigated. The cross-process handshake the feature is built around: sequenceDiagram
participant P1 as Agent A process
participant P2 as board files under .qwen boards
participant P3 as Agent B process
P1->>P2: task create (exclusive write, uuid)
P3->>P2: show, then claim id (cross-process lock)
P3->>P2: done id with note
P1->>P2: ask web question --wait (poll loop)
P3->>P2: answer id (addressee only, locked settle)
P2-->>P1: wait loop sees answered, exit 0
Testing evidenceThis is an unattended CI run — I did not build or execute PR code (the agent env holds a write PAT, and the PR's own CI already ran the suite in isolation). Evidence carried: the check results below for the exact reviewed commit, plus static verification of every claim above. Not verified: live terminal behavior (no tmux on the CI path) and Windows/macOS runtime behavior — the author's smoke was macOS-only per the PR body; the platform unit jobs and CLI integration suite are merge-queue-gated by repository design, so the merge queue provides that coverage before landing. All checks on
The green suite genuinely pins the fixes this time — the drain-before-resolve and validate-before-write tests fail if the fixes are reverted — but it mocks the core API and stdout, so it proves the handler contract, not a real OS pipe. The round-5 measured reproduction (700-task board, 153 KB through a pipe) remains the witness for the failure mode the drain addresses. Sandboxed verification would settle the remainder: 中文说明Re-run 说明:替换对 代码审查:读修复 diff 之前的独立方案——R5-1 把 在本 head 树上核实:R5-1 已修复且有测试钉住 —— 其余表面与上一轮端到端核实过的 head 完全一致(存储纪律、双层锁、坏记录处理、退出码契约),未重复审查。 测试证据:无人值守 CI 运行——未构建或执行 PR 代码。所携证据为被审 commit 的真实 check 结果与上述静态核实。 沙箱验证可以收尾其余部分: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — everything round 5 blocked on is fixed at this exact head and pinned by regression tests, round 6 found nothing new, and CI is fully green. The remaining items are probe-level deferrals and platform runtime coverage, not blockers. Stepping back: my independent proposal for the two Criticals and the suggestion was, line for line, what landed — the The honest reservations, none of them blocking:
Verdict: approve. The standing round-5 中文说明置信度:4/5 —— 第 5 轮阻塞合并的所有问题都已在当前 head 修复并由回归测试钉住,第 6 轮没有新发现,CI 全绿。剩余事项是 probe 级延期项与平台运行时覆盖,不构成阻塞。 退一步看:我为两个 Critical 与建议准备的独立方案,与落地的修复逐行同形—— 诚实的保留意见,均不阻塞:mocked-stdout 套件证明的是 handler 契约而非真实 OS 管道;排空修复是标准做法(在 裁决:批准。 当前挂起的第 5 轮 — Qwen Code · qwen3.8-max Reviewed at |
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. |
|
Closing — this PR is wrong. It landed in #7799 ("feat(cli): Add agent view supervisor runtime", 2026-08-01) as the base of a five-PR series, and four of those are open, non-draft, and were updated today:
Deleting the directory would conflict with three of them directly and strand the fourth. What the verification got wrong. I checked static reachability — no path import, no import by module name, all 87 exported symbols unreferenced, no CLI flag, no build config, and a leaf dependency graph. Every one of those is true, and none of them was the right question. "Nothing in The PR description also asserted that nothing would acquire a consumer later, citing #8869's closure. That was wrong: #8869 was a separate fleet attempt, and closing it says nothing about the #7799–#7803 series, which is the actual owner of this code and is still moving. Apologies to @ZijianZhang989 for the noise. No files were removed from |
Stage 1 of docs/plans/2026-08-18-peer-session-collaboration.md. The board
holds task today; these are its two peers, under the same lock and
atomic-write discipline.
An ask is a question with terminal states — answered, declined, timeout — so
a sender always learns which and can wait, reroute, or escalate. That is what
a plain message cannot offer, and it is why the design has no general-purpose
message.
Timeout settles lazily. expiresAt is written at creation and any reader past
the deadline reports timeout; no sweeper walks the directory. A fetch-based
system has no daemon guaranteed to be running, so the only process that can be
relied on is the one currently asking. The deadline is re-checked under the
lock, since answering a lapsed ask would silently resurrect it.
A decision is something awaiting human authority — approval, acceptance,
adjudication unified, because each needs authority and no agent has more of it
than another. It has no expiry on purpose: silent expiry converts "nobody
looked" into "the system decided", which is the one authority nothing but a
human may hold.
Items land at ~/.qwen/boards/{board}/, the layout 2.8 settles, so they never
need the migration the existing split roots do.
board-lock.ts extracts the two-tier lock rather than adding a third copy of
it; folding tasks.ts and mailbox.ts onto it is a follow-up, kept out so this
change does not also rewrite those paths.
Refs #8724
d300730 to
5b45e0a
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
Stage 2 — the step where the requirement is actually met. With a command
line over the board, a Codex, a shell script, a scheduled job and a Qwen
session participate on identical terms, because running a command is the one
thing all of them can do.
qwen board show | watch | task | claim | done | ask | answer | decline
| raise | resolve
Verbs are flat rather than nested under a noun — board claim t-3, not board
task claim t-3 — because the id prefix already carries the noun. Two levels
instead of three matters for a surface an agent types every turn.
board watch polls rather than watching the directory: fs.watch differs across
platforms and network filesystems, and a directory this small is not the
bottleneck. The panel leads with what needs a human, then what is blocked on
a peer, then work in flight — a panel that leads with "3 agents working" is a
process monitor, this one leads with what will not move until someone acts.
ask --wait is bounded (30s default) with distinct exit codes, since a foreign
agent running it is blocking its own turn.
fleet up writes no terminal code. tmux already gives panes, per-pane cwd,
keyboard switching, zoom, detach and a server that outlives the client, and
core already wraps its CLI. The command sequences those calls and adds the
one thing tmux cannot know about: a pane showing the board. Agents start via
the command passed at split time, so no keystroke can land before a shell is
ready. --with runs any other binary in its own pane, which is how a foreign
agent joins.
board-tasks.ts is deliberately not agents/team/tasks.ts: that module is Agent
Team's in-session list with dependency edges and an in-process change emitter
its scheduler subscribes to, keyed under a different storage root. Keeping
them apart lets this ship without rewriting that scheduler.
Refs #8724
Without this the feature does not work at all. fleet up starts each pane with QWEN_BOARD in its environment, but a plain session ignored it — never claiming a task, never looking for a question addressed to it, never raising anything for the user. Panes of agents that cannot see each other, and a board pane that stays empty. The section is about when to reach for the board, not a command reference. A model given verbs without boundaries uses ask for things it could read itself, or narrates progress instead of moving a task. So it states that completing a task is how you report, that an ask is for what only another participant can answer, and that anything needing authority is a decision no agent resolves — including itself. It also pins the boundaries the design depends on and that prose would drift away from: no general-purpose message, a named owner is a proposal rather than an assignment, and your own output is not visible to anyone else. Absent unless QWEN_BOARD is set, so the common single-agent prompt pays nothing for it. The CLI now re-exports the env var names from core instead of redeclaring them. Two copies of a constant that must agree is the same failure mode this whole design exists to remove. Refs #8724
Written for someone who wants several agents working side by side, so it opens with the command that produces that and a picture of the result rather than with the model. Three things it states plainly because they are the parts that surprise people: the panel is ordered by what needs you first and hides anything settled; --wait always settles, so a caller never hangs; and nothing is pushed to any participant, which is exactly why a tool we did not write can take part. The limits section is honest about the two that matter — participation is cooperative and cannot be enforced on an agent we did not write, and a named owner is a proposal rather than an assignment.
The audit found the command surface was unreachable and the layout broken, which between them meant nothing in this PR could actually be exercised. Blocker: qwen board and qwen fleet were registered but not in parseArguments' exit list, so every invocation printed its output and then booted a full interactive Qwen session on the same stdin. In a fleet pane that is a TUI takeover; from a tool call it is a hung turn. It also made run()'s exit codes meaningless, so the branching the ask contract promises did not work. Layout: tmux -l sizes the *new* pane, and the new pane was the board — so the board took 70% and sat on the right, the opposite of both the intent and the comment. Splitting N ways by hand also computed -l 100% for the last pane, which tmux rejects, so the default --agents 2 failed on a half-built session. Now the window's own pane is respawned as the board and main-vertical does the arithmetic. Running inside tmux also used the configured session name rather than the one the user is actually in, which fails whenever those differ. Ownership: claiming a completed task silently reopened and reassigned it, and anyone could complete work someone else was mid-way through. Completion now requires ownership; notes and hand-offs stay open to anyone, since those are how a stuck board gets unstuck and neither destroys work. Identity: the participant fallback included the pid, so every invocation was a different participant — claim and done on one task came from two identities and an ask could never be addressed back. Stable per-user now. Also: --ttl on ask, since a recipient that only looks between long turns could not answer within the fixed 15 minutes; --wait reads one item instead of listing the board every 500ms; and the asks header no longer claims the timeout is persisted, because it never was and persisting it would mean a read path that writes.
The audit was right that --with was hollow: it set QWEN_BOARD in a foreign pane, but a Codex never reads that variable and nothing we control can inject into its prompt. The env var alone made heterogeneous participation look supported while leaving the operator to reconstruct the protocol by hand — the exact thing an access layer exists to avoid. qwen board protocol prints the same instructions a Qwen session gets, filled in with the real board and participant name, in a form meant to be pasted. --with panes now run it before exec'ing the command, so the text is already in the pane the agent lives in. This does not make participation enforceable, and the docs say so. It makes it possible without a human reading our source. Also adds show --mine, which narrows tasks and asks to one participant while deliberately keeping decisions: those are the human's, and a view that hides them cannot tell the user what is blocking the board.
…caught The second audit found this branch did not typecheck: fleet.ts passed tmuxListPanes' result — already TmuxPaneInfo[] — back into parseTmuxListPanes. Same failure mode as the round before: a path written against an imagined API and never exercised. Worse was what it did when it ran. Inside tmux, the code asked tmux for "the current window" after creating one. For a non-attached command client that resolves from the inherited $TMUX_PANE — the window that invoked us — so fleet up respawned its own pane and killed itself mid-build. tmuxNewWindow now returns the new window's id via -P -F, which is the only thing that can name the window we just made. Also from the audit: the layout ran out of rows past three or four panes because every split halved the same pane, so it now rebalances after each; re-running outside tmux reused an existing window and destroyed the earlier board, so it always makes a fresh one; tmux failures after verifyTmux escaped as a raw stack trace through the yargs handler, now contained to one line; and show --mine filtered only the human render, making the flag a no-op for --json, which is the branch a foreign agent uses. The --with pane no longer prints the protocol before exec'ing: agent TUIs enter the alternate screen and discard it, so the banner was never readable. The operator is told once, after the layout is up, to paste qwen board protocol in. The docs said the environment variables were enough for a foreign tool to join, which was never true; they now say what actually works, and record --ttl, --mine and the running-session limitation.
The last requirement still unmet. Board awareness came only from QWEN_BOARD, which fleet up sets in each pane's environment — and an environment is fixed at launch. A session that was already deep in a problem when coordination started could never participate, which is precisely the case this design exists for. resolveBoardPromptContext now checks a runtime context before the environment, and /board sets it and refreshes the system instruction, so the board section is present from the next turn. getMainSessionSystemInstruction rebuilds the base prompt on every refresh, so no cache had to be invalidated. /board reports where you are /board <name> [as <who>] /board off The point is that the joining session keeps its context. Briefing a fresh teammate is what spawning already does; bringing in an agent that has been working for an hour is what this is for. Docs record it, and the limits section now says what is actually true — a running session can join, but nothing is pushed to it, so it sees an item the next time it looks.
prune was specified in the design and referenced by a comment in asks.ts for a command that did not exist, so settled items accumulated forever. It is manual rather than scheduled: deleting a record another participant may be mid-read on is a concurrency problem worth not having, and a fetch-based system has no daemon that could be trusted to sweep. It takes the item lock so a concurrent settle cannot be lost between the read and the unlink. decisions.ts claimed no agent-visible tool routes to resolveDecision. That was false — qwen board resolve is on the same CLI the prompt teaches agents to use, so anything with a shell could settle its own decision. Enforcing it needs a surface the agent panes do not carry; until then the code, and now the user docs, say "by agreement" rather than pretending it is an invariant. Two smaller repairs from the audit: the lock map grew one mutex per touched path for the process lifetime, which a long-lived board watch would accumulate indefinitely, and a file removed between taking the lock and reading it surfaced as a raw ENOENT instead of the not-found the onMissing contract promises — reachable now that prune exists. Also names the board pane, which showed the hostname before.
Until now the only participants visible were those who had already owned a
task or been party to an ask. An agent that joined a minute ago could not be
addressed, because nothing knew its name — and `board ask <name>` had no way
to learn one, so callers were left guessing.
participants/{name}.json records who is on a board. It does not duplicate the
machine-wide session registry: that one is keyed by pid and answers what is
alive, this one is keyed by declared name and answers who is here. Liveness is
read from the former, so a record never has to be heartbeated — a crashed
agent stops being listed because its pid is gone.
A name held by a live process is suffixed rather than refused: a caller made
to retry with a different name will either pick badly or give up. A name whose
holder is gone is reclaimed, or a crash loop would exhaust every reasonable
one.
Panes join before the agent starts, and /board joins on the way in, rather
than instructing the model to run it. A participant nobody registered cannot
be addressed, which is too load-bearing to leave to whether an agent follows
its prompt.
kind — interactive | daemon | spawned | foreign — is the field that separates
an agent a leader started, which exists to do what it is given, from an
independent one with its own work, for which an assignment is a proposal.
A session mid-turn reads the board at its turn boundary, but one sitting idle at a prompt has no boundary — so an ask addressed to it would sit untouched until its TTL lapsed. That gap is the strongest argument for push, and it does not need push to close. useBoardPending polls every five seconds: below what a person notices, and one readdir plus a few small reads, the same order as sessions ps. The session still chose the moment and nothing can arrive unbidden, so none of the machinery a delivery path would need comes with it. Decisions lead the indicator and carry the mark — an ask blocks one peer, a decision blocks everyone until a person acts. Deliberately not wired into Footer.tsx. That is a 320-line ternary chain whose rendered output I cannot see from here, and blind edits to unverifiable code produced both of this branch's earlier blockers. The hook and its formatter are tested; wiring is one branch in leftBottomContent and wants a visual check. An unreadable board — removed, permissions changed — drops the indicator rather than interrupting the session.
|
Sandboxed verification: ✅ passed — merge-ready (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: 137 passed · 0 failed · 137 total Flakiness gate: ✅ 4 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:137 通过 · 0 失败 · 137 总计 抖动门:✅ 4 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9402 Deep Verification — feat: agent boardVerdict: 中文摘要结论:
ScopeCentral claim: independently started agents (separate CLI processes, declared actors) can share tasks, asks, and decisions through a named filesystem board, with addressee-only settlement, a stable JSON contract, and the The PR is purely additive (+2236 / 18 files, no dependency changes — the A/B: the wiring is load-bearingBase control:
10/10 scripted cells passed — see Behavioral harnesses (real processes, isolated
|
| step | harness | result | witness |
|---|---|---|---|
| 1. task → claim → done → show across invocations; ownership errors; one-line ids | H1 | 23/23 | 02-task-lifecycle-h1.png |
2. ask addressee-only settlement; --wait exits 0/2/3/4; settle-after-timeout rejected |
H2 | 23/23 | 03-ask-exit-codes-h2.png |
3. raise/resolve records raiser + resolver; --approve/--reject exclusivity; --about id validation |
H3 | 12/12 | logs/h3-decisions.log |
| 4. malformed neighbours skipped by listing; targeted mutations fail with sha256-identical bytes | H4 | 15/15 | logs/h4-malformed.log |
| 5. prune-vs-reopen under cross-process lock, with delete-control | H5 | 15/15 | 04-prune-race-h5.png |
| 6. perms 0700/0600; unsafe names (path escape, device names, length) rejected; 8-way concurrent id uniqueness; ESC/BEL sanitized in human output, JSON escapes control chars; 65536/65537-char boundary | H6 | 39/39 | logs/h6-security.log |
Notes on the race cell (H5 E/F): a holder process takes the real proper-lockfile item lock, a real prune CLI process blocks on it, the record is rewritten settled→open while the lock is held, then the lock releases — prune re-reads and preserves (E); the identical run without the reopen deletes (F). Two earlier iterations of this cell were race-flawed (rewrite racing CLI startup) and were discarded; the final design gates the rewrite inside the hold window and asserts it (H5.7a).
Mutation / vacuity matrix
Unmutated control green: core board suites 11/11, cli board suite 14/14 (06-gate-suites.png).
| mutant | oracle | verdict |
|---|---|---|
| M1 asks.ts: drop addressee check | core suite | killed — authz test fails at addressed to "web" |
| M2 board-lock.ts: drop device-name clause | core suite | killed — unsafe-name test fails on con |
| M3 board-tasks.ts: parse errors rethrow | core suite | killed — 2 tests fail |
| M4 board.ts: drop exit-code 2/3 mapping | cli suite | killed — both mapping tests fail |
| M5 pruneCollection reads before lock | core suite | survived (11/11 green) |
| M5 same mutant | H5 cross-process harness | killed (H5.8/9 flip to FAIL) |
| M5b pruneCollection unlinks unconditionally | core suite | killed — prune-eligibility test fails |
M1–M4 are the positive controls proving the suites and harness can go red. See 05-mutation-matrix.png.
Findings
- Suggestion (completeness, non-blocking):
board-items.test.ts"re-checks prune eligibility while holding the item lock" pins a weaker property than its name claims. Mutant M5 (read the record before taking the lock, unlink under the lock) survives the suite because the test's in-process rewrite lands on disk before the mutant's early read executes — the fixture cannot distinguish "re-read under lock" from "read early, delete under lock". The property itself is load-bearing: the shipped code passed H5 cell E while the same mutant failed it (H5.8/9), against the F control that deletes without the reopen. A fixture that would go red is the cross-process variant shipped in this artifact (harness/h5-prune-race.mjscells E/F, signal-based lock holder); alternatively the in-process fixture would need to force the rewrite to occur after prune's first read and before lock release, which the lock itself is what guarantees. No shipped-code change needed — behavior is proven correct.
No other findings: every claim in the Reviewer Test Plan held, the docs page (docs/users/features/agent-board.md) matches observed behavior on every checkable point (exit codes, --as filtering semantics, prune cutoff in days, recheck-under-lock), and the boundary probes (65536-char text, 64-char names, empty/blank inputs, self-ask, settle-after-timeout, double-settle) all behaved as specified.
Not covered
- Windows runtime behavior. Verified on Linux only; device-name rejection is portable by construction, but Windows file-lock/permission semantics were not exercised (the PR itself marks Windows/Linux smoke as unvalidated).
- Per-commit attribution. The checkout is depth 2 (
git rev-list HEAD^1..HEAD^2reaches 1 commit; the metadata snapshot lists 40), so only the aggregateHEAD^1..HEADdiff was verified. - Repo-wide test suite / lint / full typecheck. Gates were scoped to the affected files: the three new test files + the touched
cli.test.ts(55/55) andtsc --noEmitonpackages/coreandpackages/cli(both clean). The PR's own CI covers the rest. - Bundle entry (
bundle/qwen.js). Not produced by this CI run; harnesses drove the package binpackages/cli/dist/index.jsdirectly. The rootscripts/cli-entry.jsfast-path wrapper therefore was not part of the exercised path. - Multi-writer same-checkout, slash commands, footer polling, push delivery, remote boards — explicitly out of scope in the PR description.
- The one residual type error in the base worktree rebuild (
TS7016for@lydell/node-ptypulled in via cli→core source mapping) is an artifact of rebuilding in a bare worktree:tsc --noEmiton head'spackages/cliis clean, and the base cell's oracle is behavioral (yargs rejecting an unknown command), which the emitted base JS ran correctly.
Methodology
One container (node:22-bookworm, shared runner), working tree at refs/pull/9402/merge; npm ci + npm run build completed before this round. The PR's CLI was driven as real child processes (node packages/cli/dist/index.js) with a unique QWEN_HOME per harness so boards live under the artifact dir; wait cells used real background waiter processes answered/declined from separate invocations; the prune race used a lock-holder process importing the compiled withItemLock from packages/core/dist (same implementation as the CLI). Mutations were applied in place to head sources (or, for the dist-level M5 run, to a scratch-patched copy of the compiled board-lock.js with backup/restore verified), each followed by the focused vitest suite and a git checkout restore confirmed by a clean git status. Base side: worktree at HEAD^1 with nested node_modules symlinked from the head tree (valid because the PR leaves package-lock.json untouched — realpath asserted), packages/cli rebuilt there. Evidence images were produced by scripts/verify-capture.mjs; raw per-cell logs live in logs/, harness sources in harness/.
Flakiness gate log
rounds=5 files=4 skipped=0
file packages/cli/src/cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/cli.test.ts
file packages/cli/src/commands/board/board-cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/board/board-cli.test.ts
file packages/core/src/agents/team/board-items.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-items.test.ts
file packages/core/src/agents/team/board-tasks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-tasks.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/cli.test.ts: PPPPP
packages/cli/src/commands/board/board-cli.test.ts: PPPPP
packages/core/src/agents/team/board-items.test.ts: PPPPP
packages/core/src/agents/team/board-tasks.test.ts: PPPPP
verdict: pass
summary: 4 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/cli.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/cli.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/cli.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 4 · packages/cli/src/cli.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 5 · packages/cli/src/cli.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
What this enables todayThis PR provides a durable, pull-based board for independently started processes on the same machine. It does not depend on #8724 or the Agent View PRs. A minimal task flow looks like this: # Session A creates work
qwen board task "check the API response" --board orders --as api
# Session B reads, claims, and completes it
qwen board show --board orders --as web
qwen board claim <task-id> --board orders --as web
qwen board done <task-id> --board orders --as web --note "status is numeric"
# Session A reads the board-wide result
qwen board show --board ordersQuestions use the same model: # Session A leaves a question
qwen board ask web "does the client parse status as text?" --board orders --as api
# Session B reads and answers it
qwen board show --board orders --as web
qwen board answer <ask-id> "no, it is numeric" --board orders --as webThe observable result is shared state under The important boundary is that this version is manual pull, not live delivery. |
yiliang114
left a comment
There was a problem hiding this comment.
Correctness and security are clean at the current head: I found no P0/P1, and there are no active unresolved review threads. Normal interactive behavior is unchanged beyond registering and loading the new top-level command; no board directory, background polling, model input, or Agent Team state is touched unless qwen board is invoked.
The ponytail pass found one meaningful scope reduction and three small local simplifications inline. All are non-blocking. The main decision is whether the human-decision workflow belongs in the first public contract; the remaining items are optional cleanup. Net: roughly -400 lines possible if that workflow is deferred.
|
Narrowed the Stage 1/2 surface in Verification on the final diff: Core Board tests 10/10, CLI + routing tests 68/68, Core build, Core/CLI typecheck, and a real isolated-HOME CLI smoke all passed. The smoke confirmed |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI at the reviewed commit and its suite did not run locally; the new qwen board CLI surface is exactly what that suite exercises.
Not reviewed: build-and-test — Test (macos-latest / windows-latest, Node 22.x) platform suites were skipped in CI at the reviewed commit and did not run locally; the board code is filesystem-sensitive (proper-lockfile cross-process locks, Windows device-name rejection).
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/cli/src/commands/board.ts:210 — [probe] ask --wait deadline branch bypasses emit() — --json leaves stdout empty on exit 4packages/cli/src/commands/board.ts:199 — [probe] ask --wait human mode prints the answer when answered but only the word 'declined' when declined — the decline reason is never shownpackages/core/src/agents/team/board-tasks.ts:198 — [probe] claimBoardTask silently overwrites an owner explicitly assigned at creation while the task is still pendingpackages/core/src/agents/team/asks.ts:144 — [probe] createAsk never validates expiresAt > createdAt, while parseAsk enforces it — sub-ulp ttlMs persists a record every reader rejects
— qwen3.8-max via Qwen Code /review (v0.21.14)
|
@qwen-code /triage Patrol recheck: current head b04948c has 0 unresolved review threads. Resolved Criticals have author evidence replies or explicit outdated/design rationale. The latest bot review (09:48Z) lists only deferred non-blocking findings; please refresh the stale CHANGES_REQUESTED decision on this head. |
|
Sandboxed verification: ✅ passed — merge-ready (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: 121 passed · 0 failed · 121 total Flakiness gate: ✅ 4 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:121 通过 · 0 失败 · 121 总计 抖动门:✅ 4 changed test file(s) x 5 identical rounds, no divergence Verification reportSandboxed verification: ✅ passed — merge-ready (agent verdict) Follow-up round at the new head. 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: 121 passed · 0 failed · 121 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)本轮为跟进验证:PR 在上轮(head
Verification reportPR #9402 Deep Verification (round 2) — feat: agent boardVerdict: Previous-round findings at the new head
No other round-1 findings existed. All round-1 measurements were re-run at the new head (none carried by the identical-closure shortcut — the refactor touched every production file the harnesses exercise). ScopeCentral claim (unchanged): independently started agents (separate CLI processes, declared actors) share tasks and asks through a named filesystem board, with addressee-only settlement, a stable JSON contract, and the A/B: the wiring is load-bearing at the new headBase control:
17/17 scripted cells passed — witness Behavioral harnesses (real processes, isolated
|
| step | harness | result | witness |
|---|---|---|---|
1. task → claim → done → show across invocations; ownership errors; one-line ids; --as filtering |
H1 | 17/17 | 02-task-lifecycle-h1.png |
2. ask addressee-only settlement; --wait exits 0/2/3/4 via real background waiters; settle-after-timeout and double-settle rejected; self-ask rejected; --ttl abc / --timeout 1e308 / negative --ttl rejected without side effects |
H2 | 20/20 | 03-ask-exit-codes-h2.png |
| 3. malformed neighbours (broken JSON, schema 99, id-mismatch) skipped by listing; targeted mutations fail with sha256-identical bytes; prune skips malformed too | H4 | 13/13 | 04-malformed-h4.png |
4. prune-vs-reopen under a real cross-process lock: E preserves the reopened record, F control deletes; holder acquires/releases the compiled withItemLock cleanly |
H5 | 9/9 | 05-prune-race-h5.png |
| 5. perms 0700/0600; 13 unsafe board names + 4 actor names rejected (64-char boundary accepted); 8-way concurrent id uniqueness; ESC/BEL sanitized in human output while JSON round-trips raw; 65536/65537-char boundary | H6 | 32/32 | 06-security-h6.png |
(The round-1 H3 decisions harness was retired with the feature; its coverage cell is now the delta probe in H0: raise/resolve rejected on head.)
Mutation / vacuity matrix at the new head
Unmutated controls green: core board suites 10/10, cli board suite 13/13. Witness 07-mutation-matrix.png; per-mutant vitest logs in logs/mut-*.vitest.log.
| mutant | oracle | verdict |
|---|---|---|
| M1 asks.ts: drop addressee check | core suite | killed — lets only the addressed actor answer or decline red |
| M2 board-lock.ts: drop device-name clause | core suite | killed — rejects unsafe board directory names red |
| M3 board-tasks.ts: rethrow parse errors | core suite | killed — both malformed-record tests red |
| M4 board.ts: drop exit-code 2/3 mapping | cli suite | killed — both mapping tests red |
| M5b pruneCollection unlinks unconditionally | core suite | killed — the rewritten prune re-check test red (positive control: it can go red) |
| M5 pruneCollection reads before lock | core suite ×10 | survived 10/10 — known coverage gap (finding #1), not dead code |
| M5 same mutant, compiled into dist | H5 cross-process ×3 | killed 3/3 — cell E flips to FAIL (reopened record deleted) |
M5's survival is classified as an ordinary coverage gap (behavior correct, in-process fixture cannot assert it), mirrored from round 1; the H5 column is the load-bearing pin. No mutant regressed from killed to survived versus round 1.
Findings
- Suggestion (completeness, non-blocking; carried from round 1, stands): the rewritten
re-checks prune eligibility while holding the item locktest still does not pin re-read-under-lock — M5 survives it 10/10 while H5 kills the same mutant 3/3. A fixture that would go red is the cross-process variant shipped in this artifact (harness/h5-prune-race.mjscells E/F) or an in-process fixture that forces the rewrite between the mutant's early read and the lock acquisition. No shipped-code change needed.
No new findings. Delta-specific checks all held: no dangling references to the removed decisions API anywhere in packages/ (grep census); docs (user page + new design doc) match the narrowed surface on every checkable point (exit codes, --as filter semantics, prune cutoff in days, recheck-under-lock, item table without decisions); render's removed text() coercion has no reachable unvalidated path (the CLI only renders listAsks/listBoardTasks output, which parse-and-skip malformed records — verified by H4); the finiteNumber(a.timeout * 1000) simplification preserves rejection of NaN/Infinity/negative inputs (H2 cells).
Not covered
- Windows runtime behavior — Linux-only verification; the PR itself marks Windows smoke as pending.
- Per-commit attribution — depth-2 checkout reaches 1 commit of the PR's 40+; only the aggregate
HEAD^1..HEADdiff was verified, plus the PR-scoped delta6986db42..HEAD^2(the previous verified head is locally reachable, so the refactor's own hunks were verified commit-accurately). - Repo-wide test suite / lint — gates scoped to the affected files: board suites (10+13),
cli.test.ts(55/55, includes the bootstrap import-boundary test that now pinsboardCommand),tsc --noEmiton core and cli (both clean). The PR's own CI covers the rest. - Bundle entry — not produced by this run; harnesses drove
packages/cli/dist/index.js. - M5-vs-unit-suite cell is reported as a measurement (10/10 survived), not counted in
assertions.json; it carries no encoded expectation. - Flakiness gate on changed test files is run by the workflow, not this round.
Methodology
One container (node:22-bookworm, shared runner), working tree at refs/pull/9402/merge; npm ci + npm run build completed before the round. The PR's CLI ran as real child processes with a unique QWEN_HOME per harness; wait cells used real background waiter processes settled from separate invocations; the prune race used a lock-holder process importing compiled withItemLock from packages/core/dist. Mutations were applied to head sources by harness/run-mutations.mjs (exact-string replace, restore verified by clean git status), each followed by the focused vitest suite; the M5 dist experiment rebuilt packages/core twice (mutant then shipped), with the shipped ordering re-asserted in the rebuilt dist (withItemLock( precedes readFile(full) and H5 re-run green afterwards. Base side: worktree at HEAD^1, packages/cli rebuilt there after generating the gitignored git-commit.ts. Assertion counts come exclusively from logs/*.assert.json plus the matrix/gate rows listed above (108 harness + 8 matrix + 5 gates = 121). Evidence images produced by scripts/verify-capture.mjs; raw logs in logs/.
Flakiness gate log
rounds=5 files=4 skipped=0
file packages/cli/src/cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/cli.test.ts
file packages/cli/src/commands/board/board-cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/board/board-cli.test.ts
file packages/core/src/agents/team/board-items.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-items.test.ts
file packages/core/src/agents/team/board-tasks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-tasks.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/cli.test.ts: PPPPP
packages/cli/src/commands/board/board-cli.test.ts: PPPPP
packages/core/src/agents/team/board-items.test.ts: PPPPP
packages/core/src/agents/team/board-tasks.test.ts: PPPPP
verdict: pass
summary: 4 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/cli.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/cli.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/cli.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 4 · packages/cli/src/cli.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 5 · packages/cli/src/cli.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
Triage re-run completed without a new review. The bot already has a review of its own on 机器人在 The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
…iting `parseArguments` calls `process.exit` as soon as a board handler resolves. `emit` wrote to stdout without waiting for the flush, so a `--json` snapshot larger than the pipe buffer was truncated mid-record while the command still exited 0. Waiting for the write callback also means a reader that closes early has to be handled, since a closed pipe is reported both to the callback and as a stream error event. `ask --wait` validated `--timeout` only after `createAsk` had already persisted the ask, so a rejected timeout left an orphaned open ask on the board. The local wait is now validated before anything is written, matching how `--ttl` already behaved.
`prune --json` listed `a-<uuid>.json` / `t-<uuid>.json` while every other machine-readable board surface identifies items by bare id, so a script joining prune output against ids from `show --json` matched nothing.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI at the reviewed commit and its suite did not run locally; the new qwen board CLI surface is exactly what that suite exercises.
Not reviewed: build-and-test — Test (macos-latest / windows-latest, Node 22.x) platform suites were skipped in CI at the reviewed commit and did not run locally; the board code is filesystem-sensitive (proper-lockfile cross-process locks, Windows device-name rejection).
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/cli/src/commands/board.ts:187 — [review] numeric flag units undeclared in --help (seconds vs days)packages/core/src/agents/team/board-tasks.ts:225 — [probe] pruneBoardTasks (the only delete path) has no real-behavior testpackages/core/src/agents/team/board-tasks.test.ts:78 — [probe] claimBoardTask conflict branches never exercisedpackages/core/src/agents/team/board-tasks.ts:178 (+2 locations) — [probe] self-produced records rejected under a backwards wall clock (updatedAt/settledAt re-validation)packages/core/src/agents/team/asks.ts:121 — [probe] settleAsk timeout settlement untested in corepackages/cli/src/commands/board.ts:85 — [probe] show --as identity filter has no testpackages/cli/src/commands/board/render.ts:26 — [probe] renderBoard ask line (from -> to direction) has zero test coveragepackages/core/src/agents/team/board-items.test.ts:76 — [probe] double-settlement guard untested (silent answer overwrite)packages/core/src/agents/team/board-items.test.ts:57 — [probe] self-ask guard never exercised (sole enforcement point)packages/core/src/agents/team/board-tasks.test.ts:75 — [probe] completeBoardTask ownership half never exercised on in-progress taskspackages/core/src/agents/team/board-tasks.test.ts:110 (+2 locations) — [probe] createdAt sorts asserted only with single-element listspackages/core/src/agents/team/board-tasks.ts:174 (+2 locations) — [probe] corrupt/schema-bumped records unusable, invisible, and immortalpackages/cli/src/commands/board.ts:202 — [probe] --timeout without --wait silently discardedpackages/core/src/agents/team/board-tasks.test.ts:124 — [probe] assertText blank/over-length branches never exercisedpackages/core/src/agents/team/board-tasks.ts:145 — [probe] listBoardTasks ENOENT->[] guard never exercisedpackages/core/src/agents/team/asks.ts:230 (+2 locations) — [probe] board write sites omit noFollow:true (symlink write-through; house-hardening divergence)packages/core/src/agents/team/board-items.test.ts:149 — [probe] prune re-check test cannot discriminate the under-lock re-check (ordering)docs/plans/2026-08-18-peer-session-collaboration.md:71 — [review] plan doc contracts 'reports and skips' malformed records; implementation skips silentlydocs/plans/2026-08-18-peer-session-collaboration.md:97 — [review] plan doc ask contract omits --ttl/--aboutpackages/cli/src/commands/board.ts:291 — [probe] prune CLI success path has zero coverage (days->ms conversion and actor gate)- …and 3 more (see the run report)
— qwen3.8-max via Qwen Code /review (v0.21.15)
|
Patrol closeout for head 4f474e6 (round: 2026-08-21). The two fix commits pushed at 17:45Z (271c169, 4f474e6) were verified against the round-5 findings and all 7 remaining unresolved threads are now replied with evidence and resolved (0/138 unresolved across both thread pages):
CI on 4f474e6: all green (Test ubuntu-latest Node 22.x SUCCESS, Dependency CVE audit SUCCESS), MERGEABLE. The round-6 auto-review (21:27Z) is COMMENTED with zero new findings on this head (ledger The remaining CHANGES_REQUESTED is the bot's stale 09:48Z review on the previous head b04948c — all its findings are fixed in the current head. No human reviewer holds a blocking state. |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (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: 164 passed · 0 failed · 164 total Flakiness gate: ✅ 4 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:164 通过 · 0 失败 · 164 总计 抖动门:✅ 4 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9402 Deep Verification (round 3) — feat: agent boardVerdict: 中文 — 判定:✅ 通过 · 可合入(agent 判定)本轮为第三次跟进验证:PR 在上轮(head
Previous-round findings at the new head
No other round-2 findings existed. All round-2 measurements were re-run at the new head (none carried by the identical-closure shortcut — the two delta commits touch ScopeCentral claim (unchanged): independently started agents (separate CLI processes, declared actors) share tasks and asks through a named filesystem board, with addressee-only settlement, a stable JSON contract, and the A/B: the wiring is load-bearing at the new headBase control:
17/17 scripted cells passed. Delta A/B: each delta commit's fix flips a measured bug (H7)Control:
15/15 scripted cells passed. The D2 control truncates to exactly the 64 KiB pipe buffer — the truncation is total for anything past one buffer, and silent (exit 0), which is precisely what the drain-await closes. D4 is asserted on head only: the control exits before the EPIPE can surface, so the cell would not discriminate. Behavioral harnesses (real processes, isolated
|
| step | harness | result | witness |
|---|---|---|---|
1. task → claim → done → show across invocations; ownership errors; one-line ids; --as filtering; --owner hand-off |
H1 | 20/20 | 02-task-lifecycle-h1.png |
2. ask addressee-only settlement; --wait exits 0/2/3/4 via real background waiters (incl. two concurrent); settle-after-timeout and double-settle rejected; self-ask rejected; --ttl abc/-5/1e308 rejected with no record; --about validation |
H2 | 24/24 | 03-ask-exit-codes-h2.png |
| 3. malformed neighbours (broken JSON, schema 99, id-mismatch, broken task) skipped by listing; targeted mutations fail with sha256-identical bytes; prune skips malformed and removes settled healthy items by id | H4 | 15/15 | 04-malformed-h4.png |
| 4. prune-vs-reopen under a real cross-process lock: shipped preserves the freshened record 3/3; M5 mutant build deletes it 3/3; uncontended prune sanity | H5 | 29/29 | 05-prune-race-h5.png |
| 5. perms 0700/0600; 13 unsafe board names + 4 actor names rejected; 64-char boundary accepted; 8-way concurrent id uniqueness; ESC/BEL sanitized in human output while JSON round-trips raw; 65536/65537-char boundary | H6 | 30/30 | 06-security-h6.png |
Mutation / vacuity matrix at the new head
Unmutated controls green: core board suites 11/11 (5 board-items + 6 board-tasks), cli board suite 18/18, cli.test.ts 55/55. Witness 08-mutation-matrix.png; per-mutant vitest logs in logs/mut-*.vitest.log; each revert verified by clean git status.
| mutant | oracle | verdict |
|---|---|---|
| M1 asks.ts: drop addressee check | core suite | killed — lets only the addressed actor answer or decline red (promise resolved … instead of rejecting) |
| M2 board-lock.ts: drop device-name clause | core suite | killed — rejects unsafe board directory names red |
| M3 board-tasks.ts: rethrow parse errors | core suite | killed — both malformed-record tests red (promise rejected "SyntaxError…" ) |
| M4 board.ts: drop exit-code 2/3 mapping | cli suite | killed — both mapping tests red (expected undefined to be 2/3) |
| M5 board-lock.ts: reads before lock | core suite ×10 | survived 10/10 — carried coverage gap (finding #1), not dead code |
| M5 same mutant, compiled into dist | H5 cross-process ×3 | killed 3/3 — cell flips to DELETED |
| M5b pruneCollection unlinks unconditionally | core suite | killed — re-check test red (positive control: the suite can go red) |
M6 board.ts: revert emit to fire-and-forget |
cli suite | killed — drain test, expect.any(Function) cells, multi-line answer cell all red |
M7 board.ts: validate --timeout after createAsk |
cli suite | killed — both rejects --timeout … before creating the ask cells red (expected spy to not be called … called 1 times) |
| M8 board-lock.ts: prune pushes filename | core suite | killed — reports pruned items by id, not by filename red |
M5's survival is classified as an ordinary coverage gap (behavior correct, in-process fixture cannot assert it), mirrored from rounds 1–2; the H5 column is the load-bearing pin. No mutant regressed from killed to survived versus round 2. The three delta-specific mutants (M6–M8) prove the round-3 additions' new tests are not vacuous — each new test pins exactly the hunk it was added with.
Delta-specific sweeps
- All 9
emitcall sites awaited (show/task/claim/done/ask×2/answer/decline/prune) — the drain fix covers the whole command surface, verified by grep census. - Prune return consumers: only the CLI
prunehandler consumespruneAsks/pruneBoardTasksresults (grep census); nothing re-derives a path from the entries, so switching filename→id has no downstream reader to break. - Docs: user page documents exit codes 0/2/3/4 (matches H2), cutoff in days (matches
* 86_400_000), and recheck-under-lock (proven by H5); it makes no claim about prune's output shape, so D1 contradicts nothing. The "JSON without ANSI formatting" guarantee is now true even for slow pipes (D2). - Sibling of D3:
--ttlvalidation happens inside thecreateAskargument list (pre-write) and--older-thanis validated beforepruneAsksruns — the existing unit testrejects a negative prune cutoff before deletingpins the latter; no orphan path remains on either flag. - stderr truncation: the same drain race theoretically applies to
run()'sprocess.stderr.writeon the error path, but those messages are bounded one-liners (oneLine(err.message)) that fit any pipe buffer — observation only, not a finding.
Findings
- Suggestion (completeness, non-blocking; carried from rounds 1–2, stands): the in-process
re-checks prune eligibility while holding the item locktest still does not pin re-read-under-lock — M5 survives it 10/10 while H5 kills the same mutant 3/3 cross-process. A fixture that would go red is the cross-process variant shipped in this artifact (harness/h5-prune-race.mjs+harness/holder.mjs) or an in-process fixture that forces the rewrite between the mutant's early read and lock acquisition. No shipped-code change needed.
No new findings.
Not covered
- Windows runtime behavior — Linux-only verification; the PR itself marks Windows smoke as pending.
- Per-commit attribution inside the delta — the depth-2 checkout reaches
b04948c6(as an ancestor object) and4f474e6e, but271c1695is not present locally and4f474e6e's parent is grafted at the shallow boundary, so the two delta commits were verified as the aggregateb04948c6..HEAD^2diff and behavior, not commit-by-commit. The aggregate covers exactly those two commits per the metadata snapshot. (The repo-widegit rev-list --count HEAD^1..HEAD^2= 1 vs 45 commits in metadata is the same shallow-boundary artifact; per-commit attribution for the whole PR remains out of reach, as in rounds 1–2.) - Base ref note: the metadata snapshot's
baseRefOid(d4b54a46…) differs from the local merge-ref baseHEAD^1(5715782279); per the merge-ref contract the local checkout is authoritative and was used throughout. - Repo-wide test suite / lint — gates scoped to the affected files: board suites (11+18),
cli.test.ts(55/55, includes the bootstrap registration test pinningboardCommand),tsc --noEmiton core and cli (both clean; liveness proven by a planted type error that tsc caught and that was reverted). The PR's own CI covers the rest. - Bundle entry — not produced by this run; harnesses drove
packages/cli/dist/index.js. - Control-build typecheck noise: the delta/mutant worktree builds emit a
channel-registry.tstype conflict (ChannelPlugin types mixed between worktree and main-treechannels/basedists). This is a worktree-build artifact — channels are outside this PR's diff and byte-identical across all arms — and the emitted JS was proven functionally sound by smoke-testing the board surface in every tree before use. The base-tree build compiled fully clean. - M5-vs-unit-suite cell is reported as a measurement (10/10 survived), not counted in
assertions.json; it carries no encoded expectation. - Flakiness gate on changed test files is run by the workflow, not this round.
Methodology
One container (node:22-bookworm, shared runner), working tree at refs/pull/9402/merge; npm ci + npm run build completed before the round. The PR's CLI ran as real child processes with a unique QWEN_HOME per harness; wait cells used real background waiter processes settled from separate invocations; the prune race used a lock-holder process importing compiled withItemLock from the tree under test, with the rewrite scheduled at +1.5 s inside a 2.5 s hold so the mutant's early read (≤ ~1.2 s after prune spawn) provably precedes it. Three control trees: base at HEAD^1, delta at b04948c6, and a mutant tree at HEAD^2 with the M5 hunk applied to source before building — each with tree-local node_modules/@qwen-code symlinks (realpath asserted from inside each tree) and package-local node_modules linked from the main tree (lockfile unchanged across all three commits, so versions are identical). Compiled markers were verified in every control dist before use (removed.push(file) vs path.basename, write-callback presence, --timeout validation position, readFile-vs-lock ordering). Mutations were applied to head sources by harness/run-mutations.mjs (exact-string replace, unique-occurrence asserted, restore verified by content comparison and final clean git status). Pipe cells used real kernel pipes via bash -c with PIPESTATUS, a 700 ms-stalled reader modeling a slow consumer, and a 10-byte early-exit reader modeling | head. Assertion counts come exclusively from logs/*.assert.json plus the 8 matrix kill-expectations and 6 gate checks (150 + 8 + 6 = 164). Evidence images produced by scripts/verify-capture.mjs; raw logs in logs/.
Flakiness gate log
rounds=5 files=4 skipped=0
file packages/cli/src/cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/cli.test.ts
file packages/cli/src/commands/board/board-cli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/board/board-cli.test.ts
file packages/core/src/agents/team/board-items.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-items.test.ts
file packages/core/src/agents/team/board-tasks.test.ts: (cd packages/core) npx --no-install vitest run ./src/agents/team/board-tasks.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/cli.test.ts: PPPPP
packages/cli/src/commands/board/board-cli.test.ts: PPPPP
packages/core/src/agents/team/board-items.test.ts: PPPPP
packages/core/src/agents/team/board-tasks.test.ts: PPPPP
verdict: pass
summary: 4 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/cli.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 1 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 2 · packages/cli/src/cli.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 2 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 3 · packages/cli/src/cli.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 3 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 4 · packages/cli/src/cli.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 4 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
round 5 · packages/cli/src/cli.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/board/board-cli.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-items.test.ts: P (exit 0)
round 5 · packages/core/src/agents/team/board-tasks.test.ts: P (exit 0)
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. ✅
|
Route corrected in b47569f. The Board remains a portable storage/CLI layer, but it is no longer described as completion of the user-visible collaboration product. The design now restores the original #8724 sequence: Agent Team for spawned Qwen teammates, registry + inbound gate + sender addressing for already-running Qwen sessions, then optional process launch and foreign-runtime runners. No Board runtime behavior changed in this follow-up. |
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not explored to full depth (tool budget reached): chunk 5: None — my ~6 reads stayed well within budget.; You are review agent reverse-audit — Reverse audit agent (round 1).: None — I completed the full walk within the allocated budget.; You are review agent reverse-audit — Reverse audit agent (round 2).: None — the full 241-line chunk was within one read_file call with no truncation, and my analysis covered all functions and code paths..
— deepseek-v4-flash via Qwen Code /review (v0.21.10)
| return { ...ask, state: 'timeout', settledAt: ask.expiresAt }; | ||
| } | ||
|
|
||
| export async function createAsk(opts: { |
There was a problem hiding this comment.
[Suggestion] asks.ts has 288 lines of production logic with no dedicated unit test file (asks.test.ts does not exist). The CLI tests mock the core module, so a regression in parseAsk, settleAsk, settleOnDisk, answerAsk, declineAsk, or pruneAsks would not be caught by the existing test suite.
| export async function createAsk(opts: { | |
| Add packages/core/src/agents/team/asks.test.ts covering parseAsk with valid/invalid records, createAsk→reads back via getAsk, answerAsk/declineAsk→reads back with correct state, settleOnDisk rejects by wrong actor, settleOnDisk rejects already-settled asks, listAsks returns sorted results, pruneAsks removes old settled asks, pruneAsks preserves open unexpired asks. |
— deepseek-v4-flash via Qwen Code /review (v0.21.10)
| }); | ||
| } | ||
|
|
||
| export function pruneBoardTasks( |
There was a problem hiding this comment.
[Suggestion] pruneBoardTasks has task-specific pruning logic (only prunes completed status, uses updatedAt timestamp) that is not tested. The pruneAsks tests in board-items.test.ts exercise a different code path with different settled-at logic, so they cannot substitute.
| export function pruneBoardTasks( | |
| Add a test in board-tasks.test.ts that creates tasks, completes them, and calls pruneBoardTasks to verify: (a) only completed tasks are pruned, (b) pending/in-progress tasks survive, (c) the returned ids match the pruned items, and (d) the list after pruning excludes the removed items. |
— deepseek-v4-flash via Qwen Code /review (v0.21.10)
| return ask as AskRecord; | ||
| } | ||
|
|
||
| function settleAsk(ask: AskRecord, now = Date.now()): AskRecord { |
There was a problem hiding this comment.
[Suggestion] settleAsk auto-transition to timeout is not tested. A mutation that removes the settleAsk call from getAsk would cause expired asks to remain 'open' forever. The CLI --wait loop reads current.state and sets exit code 3 only when state === 'timeout'; without the auto-transition, the loop would only exit with code 4 (local wait timeout) instead of code 3 (ask TTL expired).
| function settleAsk(ask: AskRecord, now = Date.now()): AskRecord { | |
| Add a test that creates an ask with a short TTL, waits for it to expire, then calls getAsk and expects state: 'timeout' and settledAt to be set. A second test case could verify that listAsks also returns the expired ask as timeout. |
— deepseek-v4-flash via Qwen Code /review (v0.21.10)
| }): Promise<AskRecord> { | ||
| assertSafeName('actor name', opts.from); | ||
| assertSafeName('actor name', opts.to); | ||
| if (opts.from === opts.to) |
There was a problem hiding this comment.
[Suggestion] createAsk's from===to guard is not tested. A refactoring that weakens or removes this guard would allow an actor to ask themself, which the design doc explicitly excludes. All existing tests pass because no test supplies from === to.
| if (opts.from === opts.to) | |
| Add a test: expect(createAsk({ board:'demo', from:'same', to:'same', question:'self?' })).rejects.toThrow('An ask must target another actor.') |
— deepseek-v4-flash via Qwen Code /review (v0.21.10)
| } | ||
| assertItemId('task id', ask.aboutTask, 't'); | ||
| } | ||
| if (!['open', 'answered', 'declined'].includes(ask.state ?? '')) { |
There was a problem hiding this comment.
[Suggestion] parseAsk rejects 'timeout' state but AskState type includes it and settleAsk produces it in-memory. A future code path that writes state: 'timeout' to disk would cause that record to be silently dropped from listAsks and treated as missing by getAsk/answerAsk/declineAsk.
| if (!['open', 'answered', 'declined'].includes(ask.state ?? '')) { | |
| Either add 'timeout' to the parseAsk validation list, or add an explicit parseAsk guard that maps timeout→'open' before validation, or add a comment explaining why 'timeout' is deliberately excluded. |
— deepseek-v4-flash via Qwen Code /review (v0.21.10)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) failed in CI at the reviewed commit and the suite did not run locally; the new qwen board CLI surface is exactly what that suite exercises.
Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:
packages/cli/src/commands/board.ts:39 — [probe] emit() --json output passes Unicode bidi override/isolate characters from untrusted board text raw to the terminal (sanitizer bypass)
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI at the reviewed commit and its suite did not run locally; the new qwen board CLI surface is exactly what that suite exercises.
Not reviewed: build-and-test — Test (macos-latest / windows-latest, Node 22.x) platform suites were skipped in CI at the reviewed commit and did not run locally; the board code is filesystem-sensitive (proper-lockfile cross-process locks, Windows device-name rejection).
Not explored to full depth (tool budget reached): "agent reverse-audit (round 5)": none — no check was cut short.; chunk 5: executing board-items.test.ts under vitest — the review worktree has no node_modules ( Cannot find package 'vitest' ), and a monorepo install + build was no….
Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:
packages/core/src/agents/team/board-tasks.test.ts:25 (+2 locations) — [probe] vi.mock Storage spread copies no methods — latent TypeError on a second Storage memberpackages/core/src/agents/team/asks.ts:121 — [probe] settleAsk lazy timeout (exit-code-3 mechanism) has zero test coveragepackages/core/src/agents/team/board-tasks.ts:235 — [probe] pruneBoardTasks (the only task delete path) is completely untestedpackages/cli/src/commands/board.ts:78 — [review] documented show --as filter has no testpackages/core/src/agents/team/board-tasks.ts:198 — [probe] claim contention guards untested — ownership theft ships greenpackages/cli/src/commands/board.ts:289 — [review] prune CLI happy path (days→ms conversion) has no test at any layerpackages/core/src/agents/team/board-tasks.ts:198 — [probe] abandoned in_progress tasks are permanently stuck — no release/reassign/prune pathpackages/core/src/agents/team/board-items.test.ts:93 — [probe] malformed-record fixtures: false comment, one check exercised twice, filter-rejection branch untested
— qwen3.8-max via Qwen Code /review (v0.22.0)
Verdict: FINDINGS — 40/41 scripted assertions passed at
|
| Cell | Exact commit | Scenario | Oracle | Result |
|---|---|---|---|---|
| Base | d4b54a46ff2949d0bcadb7feb48b404d8b3b1af6 |
Import the Board API and create a pending task | packages/core/src/board.ts is absent |
Expected unavailable — pass |
| Head | b47569f6da76e11f81ed4880937287aa004748bf |
Import the same API shape and create a pending task under an isolated HOME |
A real t-<uuid> record is created with status=pending |
Available — pass |
The A/B harness reported 2/2 expected cells. The head-only mock-free harness then reported 27/27 assertions across real child processes and the real filesystem:
- exactly one of two different actors won a concurrent claim;
- exactly one of concurrent answer/decline transitions won;
- losers failed loudly and the stored record remained valid;
- only the addressed actor could answer an ask;
- item TTL and local wait represented the states used by CLI exit codes 3 and 4;
- malformed matching records were skipped by listing, rejected by targeted mutation, and remained byte-identical;
- Unix collection/file modes were
0700/0600; - unsafe and Windows-device path names were rejected;
- 30 independent creator processes produced 30 unique ids.
The PR's two focused Core suites also passed: 2 files, 11 tests.
Finding
Suggestion — the different-owner claim guard is not pinned by the new unit suite
I applied an interface-preserving mutation in the scratch head tree that disabled only this condition in claimBoardTask:
if (task.status === 'in_progress' && task.owner !== by) {
throw new Error(`Task "${id}" is already claimed by "${task.owner}".`);
}With that protection disabled, the exact command below still passed 6/6 tests:
docker run --rm --network none -v "$HEAD_TREE:/work" -w /work/packages/core \
node:22-bookworm npx vitest run src/agents/team/board-tasks.test.ts \
--coverage.enabled=falseThe mutation was restored and the worktree was clean afterward. This is a coverage gap rather than an implementation defect: the separate real two-process harness did prove that the unmodified head rejects the losing actor. A focused fixture should claim as worker-a, then assert that a claim by worker-b rejects with already claimed by "worker-a" and leaves the owner unchanged.
Reviewer Test Plan coverage
| Author step | Local result |
|---|---|
| Task create → claim → done → show contract | Covered through real Core records and cross-process claim/completion assertions |
| Ask actor enforcement and terminal outcomes | Covered at the real state-machine/process layer; full yargs 0/2/3/4 bootstrap was not completed |
| Malformed matching record beside a healthy record | Covered, including byte-for-byte preservation after targeted mutation |
| Prune re-reads eligibility while holding the item lock | Covered by the PR's focused Core test; a separate two-process prune/reopen harness was not run |
| Private modes, unsafe names, concurrent ids, actionable ids | Covered for Linux container modes/names/ids; Windows native behavior was not run |
Not covered
- Windows native execution. Device-name validation was exercised on Linux, but Windows locking, rename, case-insensitive path behavior, and ACL semantics were not measured.
- Full CLI bootstrap and exact process exit codes 0/2/3/4. The Board command module itself loaded, but the standard dev bootstrap required unrelated workspace
distartifacts and the reduced yargs runner still hit acliui/strip-ansiCJS/ESM interoperability failure after the repository'spostinstallpatch step. Those harnesses executed no Board assertions and are excluded from counts. - Full Core/CLI build. Two initial parallel
npm ciruns were OOM-killed by the 4 GiB Docker VM while other verification containers were active. A later reduced install could execute Board source and focused tests, but its full TypeScript build surfaced unrelated dependency declaration generation gaps. No build result is claimed. - Three-process waiter/responder/pruner race, prune partial-success semantics across asks/tasks, stale-lock behavior after a >5 s pause, large-output/EPIPE behavior, and board-scale resource limits.
- Visual evidence. Two independent local screenshot renderers produced blank PNGs on this machine and were rejected after visual inspection; no misleading images are attached. Raw text logs and rerunnable harnesses are retained instead.
- Qwen-to-Qwen delivery, launch/wake behavior, participant discovery, and runtime addressing are outside the final diff and were not treated as claims of this PR.
Methodology
Metadata was resolved with gh pr view 9402 --repo QwenLM/qwen-code; exact detached worktrees used the PR snapshot's base and head OIDs. All PR code execution occurred in credential-free node:22-bookworm containers with --network none after dependency installation. The A/B imported exact commit source trees; the behavioral harness used real child processes, proper-lockfile, atomic writes, and isolated temporary homes without mocking the Board implementation. Artifacts are under tmp/pr9402-verify-20260824-171449/, including harnesses/board-ab.mjs, harnesses/board-real.mts, board-ab.log, board-real.log, mutation-claim.log, and the focused test log.






















What this PR does
Adds a filesystem-backed board through which independently started agents can share tasks and questions. Every command names the board explicitly with
--board; every mutation declares its actor with--as. There is no membership, join/leave flow, participant record, heartbeat, roster, ambient identity, process-global board context, launcher, wake path, or runtime addressing.The storage layer uses private directories/files, UUID item ids, exclusive creation, per-item in-process and cross-process locks, validated records, atomic updates, and lock-scoped prune eligibility checks. Malformed foreign records are skipped by listings and rejected without being rewritten by targeted mutations.
The CLI supports
show,task,claim,done,ask,answer,decline, andprune, with stable JSON output. Bounded ask waits distinguish answered, declined, item-TTL timeout, and local-wait timeout outcomes by exit code.This PR also includes the design from #9399. It implements the Board storage and CLI contract. A standalone merge is a maintainer decision to ship that low-level experimental surface; otherwise it should land with a concrete native consumer or runner.
Why it's needed
Agent Team coordinates Qwen workers spawned by one session, while #8724 owns discovery and messaging between already-running Qwen Code sessions. Neither gives a non-Qwen process a durable shared work surface. A pull-based CLI is the smallest storage contract shared by Qwen Code, Codex, shell scripts, and scheduled jobs, but it is not itself the user-facing scheduler.
Reviewer Test Plan
How to verify
qwen board task "check the API" --board demo --as api --json; claim and complete the returned id as another actor. Expectshow --board demo --jsonto report the completed task, owner, and note.apitoweb; confirm another actor cannot answer it,webcan answer or decline it, and--waitreturns exit codes 0/2/3/4 for answered/declined/item timeout/local wait timeout.Evidence (Before & After)
Before: independently started agents had no durable shared task/question surface.
After: a real CLI smoke test completed task → concurrent cross-process claim → done and ask → answer → show using one named board. Only one of two concurrent claim processes succeeded. This evidence validates the Board contract; it does not demonstrate agent launch or message delivery.
Tested on
Environment
Node.js 22. After merging the latest
main,npm install/prepare/build completed successfully; focused Core and CLI tests passed (78 tests total), the Core build passed, and both package typechecks passed. The route clarification commit changes documentation only and passed the pre-commit formatting gate.Risk & Scope
Linked Issues
Tracking: #8724. The design from #9399 is included in this PR. The original #8724 Qwen-to-Qwen flow continues through #9576 and a sender/addressing follow-up rather than through this Board CLI.