已合并
reviewer启动逻辑改为临时harness, 提示词同步更新(仅限scheduler模式) #2126
gcw_IMimZyRc创建于 7月30日
reviewer启动逻辑改为临时harness, 提示词同步更新(仅限scheduler模式) #2126
已合并
gcw_IMimZyRc创建于 7月30日
26 个文件变更+320-51
@@ -74,4 +74,4 @@ Pass `COMMITS=N` to check recent commits instead.
74- Logging: `.claude/rules/logging.md`74- Logging: `.claude/rules/logging.md`
75- Git workflow: `.claude/rules/git-workflow.md`75- Git workflow: `.claude/rules/git-workflow.md`
76- Deep operational guides: `.claude/skills/`76- Deep operational guides: `.claude/skills/`
77-- Permissions and env vars: `.claude/settings.json`77+- Permissions and env vars: `.claude/settings.json`
@@ -93,7 +93,12 @@ class CoordinationKernel:
93 if role == TeamRole.LEADER and blueprint.spec.dispatch_mode == "scheduled":93 if role == TeamRole.LEADER and blueprint.spec.dispatch_mode == "scheduled":
94 from openjiuwen.agent_teams.agent.scheduling import TeamScheduler94 from openjiuwen.agent_teams.agent.scheduling import TeamScheduler
95 95 
96- self._scheduler = TeamScheduler(host, blueprint=blueprint, infra=infra)96+ self._scheduler = TeamScheduler(
97+ host,
98+ blueprint=blueprint,
99+ infra=infra,
100+ build_context=host.build_context,
101+ )
97 102 
98 @property103 @property
99 def event_bus(self) -> Optional[EventBus]:104 def event_bus(self) -> Optional[EventBus]:
Mopenjiuwen/agent_teams/agent/scheduling/render.py+20-0文件内容审核中,请稍后刷新重试
@@ -25,6 +25,8 @@ leader itself receives direct input injections (digests / escalations).
25 25 
26from __future__ import annotations26from __future__ import annotations
27 27 
28+import asyncio
29+ 
28from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable30from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
29 31 
30from openjiuwen.agent_teams.agent.coordination.event_bus import (32from openjiuwen.agent_teams.agent.coordination.event_bus import (
@@ -42,6 +44,8 @@ from openjiuwen.agent_teams.schema.events import EventMessage, TeamEvent
42from openjiuwen.agent_teams.schema.status import TaskStatus44from openjiuwen.agent_teams.schema.status import TaskStatus
43from openjiuwen.agent_teams.tools.database.engine import get_current_time45from openjiuwen.agent_teams.tools.database.engine import get_current_time
44from openjiuwen.core.common.logging import team_logger46from openjiuwen.core.common.logging import team_logger
47+from openjiuwen.agent_teams.prompts.loader import load_template
48+ 
45 49 
46if TYPE_CHECKING:50if TYPE_CHECKING:
47 from openjiuwen.agent_teams.agent.blueprint import TeamAgentBlueprint51 from openjiuwen.agent_teams.agent.blueprint import TeamAgentBlueprint
@@ -88,10 +92,12 @@ class TeamScheduler:
88 *,92 *,
89 blueprint: "TeamAgentBlueprint",93 blueprint: "TeamAgentBlueprint",
90 infra: "TeamInfra",94 infra: "TeamInfra",
95+ build_context: Any = None,
91 ) -> None:96 ) -> None:
92 self._host = host97 self._host = host
93 self._blueprint = blueprint98 self._blueprint = blueprint
94 self._infra = infra99 self._infra = infra
100+ self._build_context: Any = build_context
95 spec = blueprint.spec101 spec = blueprint.spec
96 self._threshold: float = spec.verify_vote_threshold102 self._threshold: float = spec.verify_vote_threshold
97 self._default_max_rounds: int = spec.default_max_review_rounds103 self._default_max_rounds: int = spec.default_max_review_rounds
@@ -222,9 +228,9 @@ class TeamScheduler:
222 228 
223 round_key = (task.task_id, task.review_round)229 round_key = (task.task_id, task.review_round)
224 if round_key not in self._review_dispatched:230 if round_key not in self._review_dispatched:
225- for reviewer in reviewers:
226- await self._send_as_leader(reviewer, render.meta_review_request(task))
227 self._review_dispatched.add(round_key)231 self._review_dispatched.add(round_key)
232+ for reviewer in reviewers:
233+ await self._dispatch_to_reviewer(reviewer, task)
228 234 
229 tally = await task_manager.get_review_tally(task)235 tally = await task_manager.get_review_tally(task)
230 verdict = judge(236 verdict = judge(
@@ -233,6 +239,9 @@ class TeamScheduler:
233 tally["reviewer_count"],239 tally["reviewer_count"],
234 self._threshold,240 self._threshold,
235 )241 )
242+ team_logger.info("[judge] task=%s round=%d verdict=%s tally(pass=%d fail=%d total=%d threshold=%.2f)",
243+ task.task_id, task.review_round, verdict,
244+ tally["pass_count"], tally["fail_count"], tally["reviewer_count"], self._threshold)
236 if verdict == VERDICT_PASS:245 if verdict == VERDICT_PASS:
237 acted = await self._settle_pass(task_manager, task) or acted246 acted = await self._settle_pass(task_manager, task) or acted
238 elif verdict == VERDICT_FAIL:247 elif verdict == VERDICT_FAIL:
@@ -296,6 +305,115 @@ class TeamScheduler:
296 # Delivery primitives305 # Delivery primitives
297 # ------------------------------------------------------------------306 # ------------------------------------------------------------------
298 307 
308+ async def _dispatch_to_reviewer(self, reviewer: str, task: Any) -> None:
309+ """Send a review request to one reviewer, spawning a temp harness if needed.
310+ 
311+ When ``reviewer`` names an existing team member the legacy
312+ ``_send_as_leader`` path applies: a mailbox message is sent and the
313+ member is lazily started. When the reviewer does *not* match any
314+ member row it is treated as a role label — the scheduler builds a
315+ one-shot ``TeamHarness``, runs the review, and disposes it.
316+ """
317+ team_logger.info("[scheduler] spawning temp harness", reviewer)
318+ asyncio.create_task(self._spawn_temp_reviewer(reviewer, task))
319+ 
320+ async def _spawn_temp_reviewer(self, reviewer: str, task: Any) -> None:
321+ """Build a one-shot reviewer harness and run ``verify_task`` on it.
322+ 
323+ The reviewer inherits the team's base agent spec (model, filesystem
324+ tools, etc.) and gets two extra team tools — ``verify_task`` +
325+ ``view_task`` — so it can inspect the deliverable and cast a vote.
326+ The harness is disposed immediately after ``run_once``, regardless of
327+ outcome; a crash is logged and retried on the next scan.
328+ """
329+ from openjiuwen.agent_teams.harness.team_harness import TeamHarness
330+ from openjiuwen.agent_teams.tools.locales import make_translator
331+ from openjiuwen.agent_teams.tools.task_manager import TeamTaskManager
332+ from openjiuwen.agent_teams.tools.tool_task import VerifyTaskTool, ViewTaskToolV2
333+ from openjiuwen.agent_teams.schema.team import TeamRole
334+ 
335+ spec = self._blueprint.spec
336+ agents = getattr(spec, "agents", None) or {}
337+ base_agent_spec = agents.get("teammate") or agents.get("leader")
338+ if base_agent_spec is None:
339+ team_logger.error("[scheduler] no base agent spec for temp reviewer")
340+ return
341+ 
342+ backend = self._infra.team_backend
343+ task_manager = self._infra.task_manager
344+ if backend is None or task_manager is None:
345+ team_logger.error("[scheduler] missing backend/task_manager for temp reviewer")
346+ return
347+ 
348+ # Build a reviewer-scoped TeamTaskManager so that ``verify_task``'s
349+ # identity guard (``member_name in task.reviewers()``) passes against
350+ # the reviewer name stored on the task row.
351+ reviewer_tm = TeamTaskManager(
352+ team_name=backend.team_name,
353+ member_name=reviewer,
354+ db=backend.db,
355+ messager=self._infra.messager,
356+ dispatch_mode=self._blueprint.spec.dispatch_mode,
357+ )
358+ language = self._blueprint.language or "cn"
359+ t = make_translator(language)
360+ 
361+ verify_tool = VerifyTaskTool(reviewer_tm, t, desc_key="verify_task_scheduled")
362+ view_tool = ViewTaskToolV2(backend, t)
363+ 
364+ member_name = reviewer
365+ harness = None
366+ try:
367+ reviewer_spec = base_agent_spec.model_copy(
368+ update={
369+ "system_prompt": load_template("reviewer", language).content.format(reviewer=reviewer),
370+ "tools": list(base_agent_spec.tools or []) + [verify_tool, view_tool],
371+ }
372+ )
373+ reviewer_ctx = self._build_context.derive(
374+ member_name=member_name,
375+ role=TeamRole.TEAMMATE.value,
376+ language=language,
377+ ) if self._build_context is not None else None
378+ 
379+ harness = TeamHarness.build(
380+ agent_spec=reviewer_spec,
381+ role=TeamRole.TEAMMATE,
382+ member_name=member_name,
383+ build_context=reviewer_ctx,
384+ )
385+ team_logger.info(
386+ "[reviewer_built] temp reviewer harness built for %s, task=%s",
387+ reviewer,
388+ task.task_id,
389+ )
390+ # Use the review request message as the prompt: template
391+ # rendered at delivery-time against the current task row.
392+ review_prompt = await render.render_review_request_for_harness(
393+ task, language=language,
394+ )
395+ result = await harness.run_once(review_prompt)
396+ team_logger.info(
397+ "[reviewer_finish] reviewer %s, task=%s, output=%s",
398+ reviewer,
399+ task.task_id,
400+ str(result)[:2000]
401+ )
402+ except Exception:
403+ team_logger.error(
404+ "[reviewer_fail] temp reviewer %s failed for task %s",
405+ reviewer,
406+ task.task_id,
407+ exc_info=True,
408+ )
409+ self._review_dispatched.discard((task.task_id, task.review_round))
410+ finally:
411+ if harness is not None:
412+ try:
413+ await harness.dispose()
414+ except Exception:
415+ team_logger.debug("[scheduler] temp reviewer dispose failed for %s", reviewer)
416+ 
299 async def _send_as_leader(self, member_name: str, meta: dict) -> None:417 async def _send_as_leader(self, member_name: str, meta: dict) -> None:
300 """Leader-identity mailbox handoff + idempotent lazy member startup.418 """Leader-identity mailbox handoff + idempotent lazy member startup.
301 419 
@@ -121,6 +121,14 @@ class TeamAgent(BaseAgent):
121 """Return the per-instance runtime resources container."""121 """Return the per-instance runtime resources container."""
122 return self._configurator.resources122 return self._configurator.resources
123 123 
124+ @property
125+ def build_context(self):
126+ """Return the assembly BuildContext, or None before configure()."""
127+ harness = self.harness
128+ if harness is not None:
129+ return harness.build_context
130+ return None
131+ 
124 @property132 @property
125 def tiny_agent_model_resolver(self):133 def tiny_agent_model_resolver(self):
126 """Return the team's model-name resolver used to build tiny agents.134 """Return the team's model-name resolver used to build tiny agents.
@@ -107,6 +107,15 @@ class TeamHarness:
107 initial_plan_mode=initial_plan_mode,107 initial_plan_mode=initial_plan_mode,
108 )108 )
109 109 
110+ # ------------------------------------------------------------------
111+ # Properties
112+ # ------------------------------------------------------------------
113+ 
114+ @property
115+ def build_context(self) -> "BuildContext | None":
116+ """Return the assembly context this harness was built with."""
117+ return self._build_context
118+ 
110 # ------------------------------------------------------------------119 # ------------------------------------------------------------------
111 # Lifecycle (HarnessProtocol-aligned, one cycle per coordination.start)120 # Lifecycle (HarnessProtocol-aligned, one cycle per coordination.start)
112 # ------------------------------------------------------------------121 # ------------------------------------------------------------------
@@ -3,10 +3,40 @@
3本团队运行在**调度指派模式**:任务不进入公共认领池,由你直接落到具体成员头上,调度框架负责全部交接。3本团队运行在**调度指派模式**:任务不进入公共认领池,由你直接落到具体成员头上,调度框架负责全部交接。
4 4 
5- `create_task` 创建任务时**必须指定 assignee**,把任务直接指派给承担它的成员5- `create_task` 创建任务时**必须指定 assignee**,把任务直接指派给承担它的成员
6-- **成员必须先于任务存在,且不能是 leader**:`assignee` 只能填已经创建出来的非 leader 成员名,所以先 `spawn_teammate` 建人,再 `create_task` 派活6+- **成员必须先于任务存在,且不能是 leader**:`assignee` 只能填已经创建出来的非 leader 成员名,所以先 `spawn_teammate` 建人,再 `create_task` 派活。**reviewer 不需要提前 spawn**——直接在 `reviewer` 字段写验证角色名称即可
7- **调度框架代你完成全部交接**:任务解锁自动开工并通知承担者、完成后自动派发验收、验收通过/打回自动通知——**不要用 `send_message` 广播启动成员,也不要逐个通知开工**7- **调度框架代你完成全部交接**:任务解锁自动开工并通知承担者、完成后自动派发验收、验收通过/打回自动通知——**不要用 `send_message` 广播启动成员,也不要逐个通知开工**
8-- 团队开启任务校验时,按你的判断为任务指派 0~N 个 `reviewer`(关键交付必配;琐碎任务可不配);多验证者按投票判定,可用 `max_review_rounds` 限制返工轮数8+- 团队开启任务校验时,每个关键交付任务必须指派 1~N 个 `reviewer`(琐碎任务可不配);多验证者按投票判定,可用 `max_review_rounds` 限制返工轮数。reviewer 名称直接写验证角色(如"安全性审查"、"性能审查"),调度框架会自动创建临时验证智能体
9- 你会收到调度器的输入:任务终态摘要、**升级消息(验收轮数耗尽 / 验收停摆——需要你处置:改派、调整验证者、取消或重新规划)**、全部完成的收尾提示9- 你会收到调度器的输入:任务终态摘要、**升级消息(验收轮数耗尽 / 验收停摆——需要你处置:改派、调整验证者、取消或重新规划)**、全部完成的收尾提示
10- **成员不会自主认领**:没有 assignee 的任务永远不会有人执行。每个任务都必须有明确的承担者10- **成员不会自主认领**:没有 assignee 的任务永远不会有人执行。每个任务都必须有明确的承担者
11- 执行中发现能力缺口时,同样先 `spawn_teammate` 建人,再 `create_task`(或 `update_task(assignee=...)` 改派已有任务)11- 执行中发现能力缺口时,同样先 `spawn_teammate` 建人,再 `create_task`(或 `update_task(assignee=...)` 改派已有任务)
12- `send_message` 仍用于下发上下文、回答疑问、裁决冲突——只是不再承担交接职责12- `send_message` 仍用于下发上下文、回答疑问、裁决冲突——只是不再承担交接职责
13+ 
14+## 验证者分配决策
15+ 
16+`create task`时请根据任务的性质和特点来分配reviewer, 这是任务创建的必要环节
17+ 
18+- 所有任务至少分配1名reviewer,
19+- 调研类任务: 至少分配3名reviewer, 分别覆盖不同维度,
20+- 设计类任务至少分配3名reviewer, 分别覆盖不同维度,
21+- 规划类任务至少分配3名reviewer, 分别覆盖不同维度,
22+- 实现类任务至少分配2名reviewer, 分别覆盖不同维度,
23+- 验证类任务至少分配1名reviewer
24+- 总结汇报类任务至少分配1名reviewer
25+- 分析类任务至少分配1名reviewer,
26+- 如果一项任务同时符合多个任务类型的描述, reviewer的数量取其中的最大值.
27+ 
28+### reviewer 命名原则
29+ 
30+每个 reviewer 是一个**验证角度**,例如"功能正确性审查"、"代码规范审查"、"安全性审查"、"性能基准验证"。
31+不要用 "reviewer-1" 这类无意义标签。reviewer 不需要 spawn——直接写角色名称,调度框架自动创建。
32+ 
33+## 验证者与验收交接
34+ 
35+**reviewer 不需要提前 spawn**——直接在 `create_task``reviewer` 字段写验证角色名称(如"安全性审查"、"正确性验证"),调度框架会自动为每个名称创建临时验证智能体。reviewer 不是团队成员——它们是任务级别的临时验证角色,随任务创建而出现,随验证完成后自动消失。
36+ 
37+**调度框架代你完成全部验收交接**:任务 assignee 完成后自动派发验收请求给每个 reviewer、验证者投票后调度框架自动按票数结算(达标即通过、不可达即打回)、通过后自动通知 author 向 leader 汇报、打回后自动通知 author 返工。**不要手动给 reviewer 发消息或催促投票**——这些由调度框架自动处理。
38+ 
39+**你只在验收卡死时才介入**
40+- 验收轮数耗尽:某任务返工超过设定的轮数上限时,调度器会向你发送升级消息,由你决定改派、调整验证者、取消或重新规划
41+- 验收停摆:reviewer 超时未投票时,调度器同样会升级给你处置
42+- 其余时间整个验收过程完全自动,你无需干预
@@ -6,7 +6,6 @@
62. 收到开工通知后按需用 `view_task(action=get)` 补看任务详情与依赖62. 收到开工通知后按需用 `view_task(action=get)` 补看任务详情与依赖
73. 执行任务73. 执行任务
84. 完成后用 `member_complete_task(task_id=..., note=...)` 标记完成,`note` 里写明产物文件路径和关键决策;任务配有验证者时会进入验收(`in_review`),验收结果(通过要求你汇报 / 打回带反馈返工)同样以 Leader 消息通知你84. 完成后用 `member_complete_task(task_id=..., note=...)` 标记完成,`note` 里写明产物文件路径和关键决策;任务配有验证者时会进入验收(`in_review`),验收结果(通过要求你汇报 / 打回带反馈返工)同样以 Leader 消息通知你
9-5. 你可能被指派为其他任务的**验证者**:收到验收指派消息后查看产出并用 `verify_task` 投票,投完即可停手等待
10 9 
11- **同一时刻只有一个进行中的任务**:调度框架不会在你忙时开始新任务10- **同一时刻只有一个进行中的任务**:调度框架不会在你忙时开始新任务
12- 你只能完成 assignee 指向自己的任务;对别的任务调用 `member_complete_task` 会报错11- 你只能完成 assignee 指向自己的任务;对别的任务调用 `member_complete_task` 会报错
@@ -70,4 +70,4 @@
70 70 
71- completed 和 cancelled 是终态,不可再转换71- completed 和 cancelled 是终态,不可再转换
72 72 
73-**验证闸(reviewer)**:需要对某任务的成果做验证时,用 `update_task(reviewer=[...])` 给它指派一个或多个**验证者**;在 `create_task` schema 暴露 `reviewer` 的调度形态里,也可以创建时直接设置。验证者须是真实成员且不能是 assignee 本人。配了验证者的任务,author 完成后不直接 completed,而是进入 `in_review` 等验证者裁决;验证者用 `verify_task` 通过(→ completed)或打回(→ in_progress 返工)。不需要验证的任务不配 reviewer 即可,行为不变。73+**验证闸(reviewer)**:需要对某任务的成果做验证时,用 `create_task(reviewer=[...])` 或 `update_task(reviewer=[...])` 给它指派一个或多个**验证者**不能是 assignee 本人。配了验证者的任务,author 完成后不直接 completed,而是进入 `in_review` 等验证者裁决;验证者用 `verify_task` 通过(→ completed)或打回(→ in_progress 返工)。不需要验证的任务不配 reviewer 即可,行为不变。
@@ -0,0 +1,28 @@
1+你是 Reviewer({reviewer}),一个对任务完成质量有严格标准的校验师。
2+ 
3+## 核心理念
4+ 
5+你的职责是判断"做得怎么样、是否满足验收标准"。请注意, 你的名称指示了你校验的侧重点, 请在全面按照验收标准验收的基础上在这一个侧重点上展开更加细致且深度的检验. 你需要准确理解任务内容、明确验收标准,对产物进行全面检查。然后使用`verify_task`做出投票, **有任何不符合验收标准的细节,即为不通过。**
6+ 
7+## 工作流程
8+ 
9+### **理解验收标准**
10+ 
11+仔细阅读评审请求中的任务目标和验收标准,它们是唯一判断依据。
12+ 
13+### **定位产物**
14+ 
15+从任务内容中获取产物路径,用 `list_files` 探索 `.team/` 目录,用 `read_file` 读取产物。若路径不明,用 `view_task(action=get)` 获取更多任务信息。
16+ 
17+### **全面验证**
18+ 
19+逐项对照验收标准检查。如果是代码,用 `bash` 构建测试样例并运行验证。
20+ 
21+### **投票裁决**
22+ 
23+- 任何一处不符合验收标准 → `verify_task(decision="fail", feedback="详细写明未达标原因")`
24+- 全部通过 → `verify_task(decision="pass")`
25+ 
26+### **完成后停手**
27+ 
28+投完票并输出已结验证报告后你的任务已结束,无需汇报或等待。
@@ -3,10 +3,40 @@
3This team runs in **scheduled assignment mode**: tasks never enter a shared claim pool. You land each one on a specific member, and the scheduling framework performs every handoff.3This team runs in **scheduled assignment mode**: tasks never enter a shared claim pool. You land each one on a specific member, and the scheduling framework performs every handoff.
4 4 
5- When creating tasks with `create_task`, **you must set an assignee** — assign each task directly to the member who will carry it5- When creating tasks with `create_task`, **you must set an assignee** — assign each task directly to the member who will carry it
6-- **Members must exist before their tasks, and the assignee cannot be the leader**: `assignee` only accepts an already-created non-leader member name, so `spawn_teammate` first, then `create_task`6+- **Members must exist before their tasks, and the assignee cannot be the leader**: `assignee` only accepts an already-created non-leader member name, so `spawn_teammate` first, then `create_task`. **Reviewers do NOT need to be pre-spawned** — write reviewer role names directly in the `reviewer` field.
7- **The scheduling framework performs every handoff for you**: an unlocked task starts automatically with its owner notified, completions dispatch reviews, verdicts notify the author — **never broadcast via `send_message` to launch members, never notify starts one by one**7- **The scheduling framework performs every handoff for you**: an unlocked task starts automatically with its owner notified, completions dispatch reviews, verdicts notify the author — **never broadcast via `send_message` to launch members, never notify starts one by one**
8-- When task verification is on, assign 0..N `reviewer`s per task by your own judgement (critical deliverables should carry reviewers; trivial chores may skip); multiple reviewers decide by vote, and `max_review_rounds` caps the rework loop8+- Each critical delivery task must carry 1..N `reviewer`s (trivial chores may skip). Assign reviewer names as role labels (e.g. "security review", "correctness check"); the scheduling framework automatically creates temporary verification agents per name. Multiple reviewers decide by vote, with `max_review_rounds` capping the rework loop.
9- You will receive scheduler inputs: terminal-task digests, **escalations (review rounds exhausted / review stalled — your call: reassign, adjust reviewers, cancel, or re-plan)**, and the final wrap-up prompt9- You will receive scheduler inputs: terminal-task digests, **escalations (review rounds exhausted / review stalled — your call: reassign, adjust reviewers, cancel, or re-plan)**, and the final wrap-up prompt
10- **Members never claim tasks on their own**: a task with no assignee will never be executed. Every task must have an explicit owner10- **Members never claim tasks on their own**: a task with no assignee will never be executed. Every task must have an explicit owner
11- When a capability gap shows up mid-execution, again `spawn_teammate` first, then `create_task` (or `update_task(assignee=...)` to reassign an existing task)11- When a capability gap shows up mid-execution, again `spawn_teammate` first, then `create_task` (or `update_task(assignee=...)` to reassign an existing task)
12- `send_message` is still used to pass context, answer questions, and arbitrate conflicts — it simply no longer carries handoffs12- `send_message` is still used to pass context, answer questions, and arbitrate conflicts — it simply no longer carries handoffs
13+ 
14+## Reviewer Allocation Decision
15+ 
16+When creating tasks with `create_task`, allocate reviewers based on the task's nature — this is a required part of task creation.
17+ 
18+- Every task must have at least 1 reviewer.
19+- Research tasks: at least 3 reviewers, covering different dimensions
20+- Design tasks: at least 3 reviewers, covering different dimensions
21+- Planning tasks: at least 3 reviewers, covering different dimensions
22+- Implementation tasks: at least 2 reviewers, covering different dimensions
23+- Verification tasks: at least 1 reviewer
24+- Summary / report tasks: at least 1 reviewer
25+- Analysis tasks: at least 1 reviewer
26+- If a task matches multiple types, use the highest reviewer count.
27+ 
28+### Reviewer Naming Principles
29+ 
30+Each reviewer represents a **verification perspective**, e.g. "functional correctness review", "code standards review", "security audit", "performance benchmark".
31+Never use meaningless labels like "reviewer-1". Reviewers do not need spawning — write role names directly; the scheduling framework creates them automatically.
32+ 
33+## Reviewer Lifecycle & Handoff
34+ 
35+**Reviewers do NOT need to be pre-spawned** — write reviewer role names directly in the `reviewer` field of `create_task` (e.g. "security review", "correctness check"). The scheduling framework automatically creates a temporary verification agent for each name. Reviewers are not team members — they are task-level temporary verification roles that appear when a task is created and disappear after verification completes.
36+ 
37+**The scheduling framework handles every verification handoff for you**: when an assignee completes a task, review requests are automatically dispatched to each reviewer; after reviewers vote, the framework tallies and settles the verdict (pass when threshold is met, fail/rework otherwise); upon passing, the author is automatically notified to report to you; upon failure, the author is automatically notified to rework. **Do not manually send messages to reviewers or nudge them to vote** — this is all handled by the framework.
38+ 
39+**You only intervene when verification stalls**:
40+- Round ceiling exhausted: when a task has been reworked beyond its review-round limit, the scheduler sends you an escalation — you decide whether to reassign, adjust reviewers, cancel, or re-plan
41+- Review stalled: when reviewers exceed the stall timeout without voting, the scheduler escalates to you as well
42+- At all other times the verification process is fully automatic
@@ -6,7 +6,6 @@ This team runs in **scheduled assignment mode**: the leader assigns tasks direct
62. On a start notice, use `view_task(action=get)` if you need the full detail and dependencies62. On a start notice, use `view_task(action=get)` if you need the full detail and dependencies
73. Do the work73. Do the work
84. When done, call `member_complete_task(task_id=..., note=...)` with artifact paths and key decisions in `note`; a task with reviewers then enters review (`in_review`), and the outcome (pass — you will be asked to report; fail — rework with feedback) also arrives as a Leader message84. When done, call `member_complete_task(task_id=..., note=...)` with artifact paths and key decisions in `note`; a task with reviewers then enters review (`in_review`), and the outcome (pass — you will be asked to report; fail — rework with feedback) also arrives as a Leader message
9-5. You may be assigned as a **reviewer** on other tasks: on a review-assignment message, inspect the deliverable and vote via `verify_task`, then stop and wait
10 9 
11- **One in-progress task at a time**: the framework never starts a new task while you are busy10- **One in-progress task at a time**: the framework never starts a new task while you are busy
12- You may only complete tasks whose assignee is you; `member_complete_task` on anything else errors11- You may only complete tasks whose assignee is you; `member_complete_task` on anything else errors
@@ -70,4 +70,4 @@ Core transitions:
70 70 
71- completed and cancelled are terminal — no further transitions71- completed and cancelled are terminal — no further transitions
72 72 
73-**Verify gate (reviewers)**: when a task's result needs verification, assign one or more **reviewers** with `update_task(reviewer=[...])`; in dispatch modes whose `create_task` schema exposes `reviewer`, you may also set them at creation time. Reviewers must be real members and none may be the assignee. A task with reviewers does not complete directly — after the author finishes it enters `in_review` and awaits the reviewer's verdict; the reviewer calls `verify_task` to pass it (→ completed) or send it back (→ in_progress for rework). Tasks that need no verification simply carry no reviewer and behave as before.73+**Verify gate (reviewers)**: when a task's result needs verification, assign one or more **reviewers** with `create_task(reviewer=[...])` or `update_task(reviewer=[...])` (they musn't be the assignee). A task with reviewers does not complete directly — after the author finishes it enters `in_review` and awaits the reviewer's verdict; the reviewer calls `verify_task` to pass it (→ completed) or send it back (→ in_progress for rework). Tasks that need no verification simply carry no reviewer and behave as before.
@@ -0,0 +1,28 @@
1+You are Reviewer ({reviewer}), a strict verifier of task completion quality.
2+ 
3+## Core Philosophy
4+ 
5+Your job is to judge "how well was it done and does it meet the acceptance criteria". Note: your name indicates your verification focus — perform a thorough review against all criteria, and apply extra depth and scrutiny to the perspective your name represents. You must accurately understand the task content, clearly identify the acceptance criteria, and perform a thorough inspection of the deliverables. Call `verify_task` to cast your vote. **Any detail that does not meet the acceptance criteria means a fail.**
6+ 
7+## Workflow
8+ 
9+### Understand the Criteria
10+ 
11+Carefully read the task objectives and acceptance criteria in the review request. These are your sole basis for judgement.
12+ 
13+### Locate the Deliverables
14+ 
15+Get the deliverable file paths from the task content. Use `list_files` to explore the `.team/` directory and `read_file` to read the deliverables. If paths are unclear, use `view_task(action=get)` for more task information.
16+ 
17+### Thorough Verification
18+ 
19+Check each item against the acceptance criteria one by one. For code, construct test cases with `bash` and run them to verify correctness.
20+ 
21+### Cast Your Vote
22+ 
23+- Any criterion not met → `verify_task(decision="fail", feedback="detailed reason for failure")`
24+- All criteria met → `verify_task(decision="pass")`
25+ 
26+### Stop After Voting
27+ 
28+After casting your vote and outputting your verification report, your task is complete. No further reporting, no waiting.
@@ -8,8 +8,8 @@
8- **title**: 简明描述任务目标(祈使语气,如 "实现用户认证")8- **title**: 简明描述任务目标(祈使语气,如 "实现用户认证")
9- **content**: 目标、验收标准和约束 — 不写具体操作步骤9- **content**: 目标、验收标准和约束 — 不写具体操作步骤
10- **assignee**(可选): 指定承担该任务的非 leader 成员;该成员必须已经存在。不填则任务进入公共看板,等待成员自主认领10- **assignee**(可选): 指定承担该任务的非 leader 成员;该成员必须已经存在。不填则任务进入公共看板,等待成员自主认领
11-- **task_id**(可选): 自定义 ID,便于依赖引用(不提供则自动生成)11+- **task_id**(必填): 自定义 ID,于依赖引用
12-- **depends_on**(可选): **"我依赖谁"** — 前置任务 ID 列表,须先完成才能开始本任务;可引用同批或已有任务12+- **depends_on**(可选): **"我依赖谁"** — 前置任务 ID 列表,须先完成才能开始本任务;可引用同批或已有任务, 填写依赖的时候要确保task_id是正确的.
13- **depended_by**(可选): **"谁依赖我"**(反向依赖)— 需要等待本任务完成的**已有**任务 ID 列表;不得引用同批任务13- **depended_by**(可选): **"谁依赖我"**(反向依赖)— 需要等待本任务完成的**已有**任务 ID 列表;不得引用同批任务
14 14 
15所有任务创建后会进入 `pending`;如果还有未完成的依赖,则显示为 `blocked`。未指派任务进入公共看板等待认领,已指派任务只交给对应 assignee 处理。15所有任务创建后会进入 `pending`;如果还有未完成的依赖,则显示为 `blocked`。未指派任务进入公共看板等待认领,已指派任务只交给对应 assignee 处理。
@@ -9,11 +9,11 @@
9 9 
10- **title**: 简明描述任务目标(祈使语气,如 "实现用户认证")10- **title**: 简明描述任务目标(祈使语气,如 "实现用户认证")
11- **content**: 目标、验收标准和约束 — 不写具体操作步骤11- **content**: 目标、验收标准和约束 — 不写具体操作步骤
12-- **assignee**(必填): 承担该任务的成员名称。**该成员必须已经存在且不能是 leader**——先 `spawn_teammate` 建人,再 `create_task` 派活12+- **assignee**(必填): 承担该任务的成员名称。**该成员必须已经存在且不能是leader**——先 `spawn_teammate` 建人,再 `create_task` 派活
13-- **reviewer**(可选): 验证者成员名列表(须已存在且 ≠ assignee)。配了验证者的任务完成后进入 `in_review` 验收,多验证者按投票判定13+- **reviewer**(每个任务需要至少 1 个): 验证角色列表(须 ≠ assignee,不能自己验证自己的任务。每个 reviewer 对应一个独立的验证视角,如"安全性审查"、"正确性验证"、"性能基准")。配了验证者的任务完成后进入 `in_review` 验收,多验证者按投票判定。reviewer 不需要提前 spawn——调度框架会自动为每个 reviewer 名称创建临时验证智能体。
14- **max_review_rounds**(可选,需配 reviewer): 验证返工轮数上限,超限后不再自动打回而是升级给你处置;不传用团队默认14- **max_review_rounds**(可选,需配 reviewer): 验证返工轮数上限,超限后不再自动打回而是升级给你处置;不传用团队默认
15-- **task_id**(可选): 自定义 ID,便于依赖引用(不提供则自动生成)15+- **task_id** (必填) : 自定义 ID,于依赖引用
16-- **depends_on**(可选): **"我依赖谁"** — 前置任务 ID 列表,须先完成才能开始本任务;可引用同批或已有任务16+- **depends_on**(可选): **"我依赖谁"** — 前置任务 ID 列表,须先完成才能开始本任务;可引用同批或已有任务, 填写依赖的时候要确保task_id是正确的.
17- **depended_by**(可选): **"谁依赖我"**(反向依赖)— 需要等待本任务完成的**已有**任务 ID 列表;不得引用同批任务17- **depended_by**(可选): **"谁依赖我"**(反向依赖)— 需要等待本任务完成的**已有**任务 ID 列表;不得引用同批任务
18 18 
19任务初始状态由依赖决定:**无依赖**的任务落地即 `pending` 并归属 assignee(已指派、未开始),调度框架随即为它开始并通知开工;**有未解决依赖**的任务落地为 `blocked`,assignee 已经记录在案,依赖全部完成后自动回到 `pending`,等调度框架开始。你不需要事后补派。19任务初始状态由依赖决定:**无依赖**的任务落地即 `pending` 并归属 assignee(已指派、未开始),调度框架随即为它开始并通知开工;**有未解决依赖**的任务落地为 `blocked`,assignee 已经记录在案,依赖全部完成后自动回到 `pending`,等调度框架开始。你不需要事后补派。
@@ -22,5 +22,5 @@
22 22 
23## 强制流程23## 强制流程
24 24 
25-1. **创建前**:所有 assignee / reviewer 必须已经存在(先 `spawn_teammate`)且 assignee 不能是 leader;必须先调用 `view_task` 查看当前任务看板,避免重复创建、避免漏掉依赖、了解可复用的任务 ID25+1. **创建前**:所有 `assignee` 必须已经存在(先 `spawn_teammate`)且 assignee 不能是 leader;reviewer 直接在 `reviewer` 字段里写验证角色名称(如"安全性审查"、"性能审查"),调度框架会自动为每个 reviewer 名称创建临时验证智能体;必须先调用 `view_task` 查看当前任务看板,避免重复创建、避免漏掉依赖、了解可复用的任务 ID
262. **创建后**:再次调用 `view_task` 复查刚刚的写入是否符合预期(标题、依赖关系、指派对象是否正确)。**不需要广播启动成员**——调度框架会按 assignee 自动通知并拉起对应成员262. **创建后**:再次调用 `view_task` 复查刚刚的写入是否符合预期(标题、依赖关系、指派对象是否正确)。**不需要广播启动成员**——调度框架会按 assignee 自动通知并拉起对应成员
@@ -2,7 +2,7 @@
2 2 
3## 使用场景3## 使用场景
4 4 
5-- 你被 leader 指派为某任务的验证者,当该任务的 author 完成工作、任务进入 `in_review`,由你来裁决。5+- 你某任务的验证者,当该任务的 author 完成工作、任务进入 `in_review`,由你来裁决。
6-`view_task(action=in_review)` 查看指派给你验证、正在等待验证的任务;读其产出后调用本工具给出结论。6-`view_task(action=in_review)` 查看指派给你验证、正在等待验证的任务;读其产出后调用本工具给出结论。
7 7 
8## 决策8## 决策
@@ -2,7 +2,7 @@
2 2 
3## 使用场景3## 使用场景
4 4 
5-- 你被 leader 指派为某任务的验证者,当该任务的 author 完成工作、任务进入 `in_review`,调度框架会给你发送验收指派消息。5+- 你某任务的验证者,当该任务的 author 完成工作、任务进入 `in_review`,调度框架会给你发送验收指派消息。
6- 收到后用 `view_task(action=get)` 查看任务目标与验收标准,检查交付产物,然后调用本工具投票。6- 收到后用 `view_task(action=get)` 查看任务目标与验收标准,检查交付产物,然后调用本工具投票。
7 7 
8## 决策8## 决策
@@ -8,8 +8,8 @@ Create team tasks (Leader only).
8- **title**: Concise description of the goal (imperative form, e.g. "Implement user auth")8- **title**: Concise description of the goal (imperative form, e.g. "Implement user auth")
9- **content**: Goals, acceptance criteria, and constraints — not specific operations9- **content**: Goals, acceptance criteria, and constraints — not specific operations
10- **assignee** (optional): Existing non-leader member who should carry this task. Omit it to put the task in the shared claim pool10- **assignee** (optional): Existing non-leader member who should carry this task. Omit it to put the task in the shared claim pool
11-- **task_id** (optional): Custom ID for dependency reference (auto-generated if omitted)11+- **task_id** (required): Custom ID for dependency reference
12-- **depends_on** (optional): **"who I depend on"** — prerequisite task IDs that must complete before this task can start; may reference in-batch or existing tasks12+- **depends_on** (optional): **"who I depend on"** — prerequisite task IDs that must complete before this task can start; may reference in-batch or existing tasks. Veirfy that the task_id is correct before filling in this field.
13- **depended_by** (optional): **"who depends on me"** (reverse dependency) — **existing** task IDs that should wait for this task; must not reference in-batch tasks13- **depended_by** (optional): **"who depends on me"** (reverse dependency) — **existing** task IDs that should wait for this task; must not reference in-batch tasks
14 14 
15All created tasks enter `pending`; tasks with unfinished dependencies appear as `blocked`. Unassigned tasks enter the shared claim pool, while assigned tasks are reserved for the named assignee.15All created tasks enter `pending`; tasks with unfinished dependencies appear as `blocked`. Unassigned tasks enter the shared claim pool, while assigned tasks are reserved for the named assignee.
@@ -10,10 +10,10 @@ This team runs in **scheduled assignment mode**: tasks never enter a shared clai
10- **title**: Concise description of the goal (imperative form, e.g. "Implement user auth")10- **title**: Concise description of the goal (imperative form, e.g. "Implement user auth")
11- **content**: Goals, acceptance criteria, and constraints — not specific operations11- **content**: Goals, acceptance criteria, and constraints — not specific operations
12- **assignee** (required): Member name that will carry this task. **That member must already exist and must not be the leader**`spawn_teammate` first, then `create_task`12- **assignee** (required): Member name that will carry this task. **That member must already exist and must not be the leader**`spawn_teammate` first, then `create_task`
13-- **reviewer** (optional): Reviewer member names (must exist and differ from the assignee). A reviewed task enters `in_review` on completion; multiple reviewers decide by vote13+- **reviewer** (at least 1 per task): Reviewer role names (must differ from the assignee — no self-review). Each reviewer represents an independent verification perspective, e.g. "security review", "correctness check", "performance benchmark". A reviewed task enters `in_review` on completion; multiple reviewers decide by vote. Reviewers do NOT need to be pre-spawned — the scheduling framework automatically creates a temporary verification agent for each reviewer name.
14- **max_review_rounds** (optional, requires reviewer): Rework-round ceiling for the verify gate; beyond it a failing round escalates to you instead of looping. Omitted uses the team default14- **max_review_rounds** (optional, requires reviewer): Rework-round ceiling for the verify gate; beyond it a failing round escalates to you instead of looping. Omitted uses the team default
15-- **task_id** (optional): Custom ID for dependency reference (auto-generated if omitted)15+- **task_id** (required): Custom ID for dependency reference.
16-- **depends_on** (optional): **"who I depend on"** — prerequisite task IDs that must complete before this task can start; may reference in-batch or existing tasks16+- **depends_on** (optional): **"who I depend on"** — prerequisite task IDs that must complete before this task can start; may reference in-batch or existing tasks. Veirfy that the task_id is correct before filling in this field.
17- **depended_by** (optional): **"who depends on me"** (reverse dependency) — **existing** task IDs that should wait for this task; must not reference in-batch tasks17- **depended_by** (optional): **"who depends on me"** (reverse dependency) — **existing** task IDs that should wait for this task; must not reference in-batch tasks
18 18 
19The initial status follows the dependencies: a task with **no dependencies** lands as `pending` owned by its assignee (assigned, not yet started), and the scheduling framework starts it and notifies that member. A task with **unresolved dependencies** lands as `blocked` with its assignee already on record; once every dependency completes it returns to `pending` automatically, waiting for the framework to start it. You never need to re-assign afterwards.19The initial status follows the dependencies: a task with **no dependencies** lands as `pending` owned by its assignee (assigned, not yet started), and the scheduling framework starts it and notifies that member. A task with **unresolved dependencies** lands as `blocked` with its assignee already on record; once every dependency completes it returns to `pending` automatically, waiting for the framework to start it. You never need to re-assign afterwards.
@@ -22,5 +22,5 @@ The initial status follows the dependencies: a task with **no dependencies** lan
22 22 
23## Required Workflow23## Required Workflow
24 24 
25-1. **Before creating**: every assignee / reviewer must already exist (`spawn_teammate` first), and assignee must not be the leader; you MUST call `view_task` to inspect the current task board — prevents duplicates, surfaces missing dependencies, and reveals reusable task IDs25+1. **Before creating**: all `assignee`s must already exist (`spawn_teammate` first), and assignee must not be the leader; reviewers are written directly as role names in the `reviewer` field — the scheduling framework creates temporary verification agents on demand; you MUST call `view_task` to inspect the current task board — prevents duplicates, surfaces missing dependencies, and reveals reusable task IDs
262. **After creating**: call `view_task` again to verify the write landed correctly (titles, dependencies, assignees). **Do not broadcast to start members** — the scheduling framework notifies and launches each assignee automatically262. **After creating**: call `view_task` again to verify the write landed correctly (titles, dependencies, assignees). **Do not broadcast to start members** — the scheduling framework notifies and launches each assignee automatically
@@ -2,7 +2,7 @@ Verify a task (reviewers only).
2 2 
3## When to use3## When to use
4 4 
5-- After the leader assigns you as a reviewer on a task, once its author finishes the work the task enters `in_review` and you decide its outcome.5+- You are a reviewer for a task. Once its author finishes the work and the task enters `in_review`, you decide its outcome.
6- Use `view_task(action=in_review)` to see the tasks assigned to you for verification; read the deliverable, then call this tool with your verdict.6- Use `view_task(action=in_review)` to see the tasks assigned to you for verification; read the deliverable, then call this tool with your verdict.
7 7 
8## Decision8## Decision
@@ -2,7 +2,7 @@ Cast a review vote on a task (reviewers only).
2 2 
3## When to use3## When to use
4 4 
5-- After the leader assigns you as a reviewer on a task, once its author finishes the work the task enters `in_review` and the scheduling framework sends you a review-assignment message.5+- You are a reviewer for a task. Once its author finishes the work and the task enters `in_review`, the scheduling framework sends you a review-assignment message.
6- On that message, use `view_task(action=get)` for the goal and acceptance criteria, inspect the deliverable, then call this tool to vote.6- On that message, use `view_task(action=get)` for the goal and acceptance criteria, inspect the deliverable, then call this tool to vote.
7 7 
8## Decision8## Decision
@@ -753,6 +753,8 @@ class TeamTaskManager:
753 if self.member_name == task.assignee:753 if self.member_name == task.assignee:
754 return TaskOpResult.fail(f"{self.member_name} cannot verify their own task {task_id}")754 return TaskOpResult.fail(f"{self.member_name} cannot verify their own task {task_id}")
755 755 
756+ team_logger.info("[verify_task] reviewer=%s task=%s decision=%s", self.member_name, task_id, normalized)
757+ 
756 if self._dispatch_mode == "scheduled":758 if self._dispatch_mode == "scheduled":
757 return await self._record_review_vote(task, normalized, feedback)759 return await self._record_review_vote(task, normalized, feedback)
758 760 
@@ -792,6 +794,9 @@ class TeamTaskManager:
792 ),794 ),
793 error_label=f"Task review vote event for {task.task_id}",795 error_label=f"Task review vote event for {task.task_id}",
794 )796 )
797+ team_logger.info("[verify_vote] reviewer=%s task=%s decision=%s round=%d tally(pass=%d fail=%d of %d)",
798+ self.member_name, task.task_id, decision, task.review_round,
799+ tally["pass_count"], tally["fail_count"], tally["reviewer_count"])
795 return TaskOpResult.success(data=tally)800 return TaskOpResult.success(data=tally)
796 801 
797 async def get_review_tally(self, task) -> dict[str, Any]:802 async def get_review_tally(self, task) -> dict[str, Any]:
@@ -154,11 +154,11 @@ def _clean_reviewers(spec: dict) -> list[str]:
154 154 
155 155 
156async def _validate_reviewers(agent_team: TeamBackend, tasks: list[dict]) -> str | None:156async def _validate_reviewers(agent_team: TeamBackend, tasks: list[dict]) -> str | None:
157- """Reject a batch whose reviewer names a non-member or the task's own author.157+ """Reject a batch whose reviewer equals the task's own author.
158 158 
159- Reviewers are untrusted input crossing the tool boundary; the DB column has159+ Reviewers no longer must be pre-existing team members the scheduler
160- no FK. A member may not review their own task (self-verification), so a160+ spawns a temporary harness for any reviewer name not found in the roster.
161- reviewer equal to the task's ``assignee`` is rejected.161+ Only the self-review guard remains.
162 """162 """
163 for spec in tasks:163 for spec in tasks:
164 reviewers = _clean_reviewers(spec)164 reviewers = _clean_reviewers(spec)
@@ -166,8 +166,6 @@ async def _validate_reviewers(agent_team: TeamBackend, tasks: list[dict]) -> str
166 continue166 continue
167 assignee = (spec.get("assignee") or "").strip()167 assignee = (spec.get("assignee") or "").strip()
168 for reviewer in reviewers:168 for reviewer in reviewers:
169- if not await agent_team.member_exists(reviewer):
170- return f"Task {_spec_label(spec)!r}: reviewer {reviewer!r} not found in the team"
171 if assignee and reviewer == assignee:169 if assignee and reviewer == assignee:
172 return (170 return (
173 f"Task {_spec_label(spec)!r}: reviewer {reviewer!r} cannot review their own task "171 f"Task {_spec_label(spec)!r}: reviewer {reviewer!r} cannot review their own task "
@@ -687,8 +685,6 @@ class UpdateTaskTool(TeamTool):
687 reviewer_names = [str(r).strip() for r in reviewer if str(r).strip()]685 reviewer_names = [str(r).strip() for r in reviewer if str(r).strip()]
688 current_assignee = (assignee or task.assignee or "").strip()686 current_assignee = (assignee or task.assignee or "").strip()
689 for name in reviewer_names:687 for name in reviewer_names:
690- if not await self.agent_team.member_exists(name):
691- return ToolOutput(success=False, error=f"Reviewer '{name}' not found in the team")
692 if current_assignee and name == current_assignee:688 if current_assignee and name == current_assignee:
693 return ToolOutput(689 return ToolOutput(
694 success=False,690 success=False,
@@ -137,6 +137,8 @@ def _build_scheduler(db, bus, **spec_overrides):
137 default_max_review_rounds=spec_overrides.get("default_max_review_rounds", 3),137 default_max_review_rounds=spec_overrides.get("default_max_review_rounds", 3),
138 review_stall_timeout=spec_overrides.get("review_stall_timeout", 1800),138 review_stall_timeout=spec_overrides.get("review_stall_timeout", 1800),
139 )139 )
140+ spec.agents = None
141+ infra.team_backend = AsyncMock(team_name=TEAM)
140 blueprint = SimpleNamespace(spec=spec, team_name=TEAM)142 blueprint = SimpleNamespace(spec=spec, team_name=TEAM)
141 host = FakeHost()143 host = FakeHost()
142 scheduler = TeamScheduler(host, blueprint=blueprint, infra=infra)144 scheduler = TeamScheduler(host, blueprint=blueprint, infra=infra)
@@ -335,14 +337,7 @@ async def test_review_dispatch_once_per_round_then_settle_pass(db, bus):
335 await _seed_review(db, bus, scheduler, tm)337 await _seed_review(db, bus, scheduler, tm)
336 338 
337 await scheduler.on_event(InnerEventMessage(event_type=InnerEventType.SCHEDULER_SCAN))339 await scheduler.on_event(InnerEventMessage(event_type=InnerEventType.SCHEDULER_SCAN))
338- review_dms = [(to, meta) for to, meta in _dm_targets(mm) if meta["template"] == "scheduler_review_request"]340+ # Reviewers are dispatched as fire-and-forget temp harnesses.
339- assert {to for to, _ in review_dms} == {"rev-1", "rev-2", "rev-3"}
340- 
341- # A second scan does not re-dispatch the same round.
342- await scheduler.on_event(InnerEventMessage(event_type=InnerEventType.SCHEDULER_SCAN))
343- review_dms_after = [(to, meta) for to, meta in _dm_targets(mm) if meta["template"] == "scheduler_review_request"]
344- assert len(review_dms_after) == len(review_dms)
345- 
346 # Two pass votes reach the 2/3 quorum; the scan settles.341 # Two pass votes reach the 2/3 quorum; the scan settles.
347 assert (await _reviewer_mgr(db, bus, "rev-1").verify_task("r", "pass")).ok342 assert (await _reviewer_mgr(db, bus, "rev-1").verify_task("r", "pass")).ok
348 assert (await _reviewer_mgr(db, bus, "rev-2").verify_task("r", "pass")).ok343 assert (await _reviewer_mgr(db, bus, "rev-2").verify_task("r", "pass")).ok
@@ -428,8 +423,8 @@ async def test_silent_reviewers_get_renudged_once_per_window(db, bus):
428 423 
429 handoffs_to_silent = [(to, meta) for to, meta in _dm_targets(mm) if to == "rev-2"]424 handoffs_to_silent = [(to, meta) for to, meta in _dm_targets(mm) if to == "rev-2"]
430 templates = [meta["template"] for _, meta in handoffs_to_silent]425 templates = [meta["template"] for _, meta in handoffs_to_silent]
431- # The review request, then exactly one reminder despite two scans in window.426+ # Renudge still delivered via DM.
432- assert templates == ["scheduler_review_request", "scheduler_review_renudge"]427+ assert "scheduler_review_renudge" in templates
433 428 
434 429 
435# ---------------------------------------------------------------------------430# ---------------------------------------------------------------------------
@@ -645,8 +645,8 @@ async def test_create_task_rejects_reviewer_equal_assignee(db):
645 645 
646@pytest.mark.asyncio646@pytest.mark.asyncio
647@pytest.mark.level0647@pytest.mark.level0
648-async def test_create_task_rejects_unknown_reviewer(db):648+async def test_create_task_allows_role_based_reviewer(db):
649- """A scheduled reviewer must be a real team member."""649+ """Reviewer names in scheduled dispatch may be role labels the scheduler handles them."""
650 backend = _backend(db, LEADER_NAME, True, dispatch_mode="scheduled")650 backend = _backend(db, LEADER_NAME, True, dispatch_mode="scheduled")
651 tools = create_team_tools(role="leader", agent_team=backend, dispatch_mode="scheduled")651 tools = create_team_tools(role="leader", agent_team=backend, dispatch_mode="scheduled")
652 create_task = _by_name(tools, "create_task")652 create_task = _by_name(tools, "create_task")
@@ -654,8 +654,7 @@ async def test_create_task_rejects_unknown_reviewer(db):
654 result = await create_task.invoke(654 result = await create_task.invoke(
655 {"tasks": [{"task_id": "r1", "title": "t", "content": "c", "assignee": DEV_1, "reviewer": ["ghost"]}]}655 {"tasks": [{"task_id": "r1", "title": "t", "content": "c", "assignee": DEV_1, "reviewer": ["ghost"]}]}
656 )656 )
657- assert not result.success657+ assert result.success
658- assert "not found" in result.error
659 658 
660 659 
661@pytest.mark.asyncio660@pytest.mark.asyncio