Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

- Tighten the internal `dyro-board` Record protocol: declare a
production-line baseline (not `master`/`main`), close prior P0/P1 as
`已闭环` / `未闭环` / `须人工核` (or `已检索·无先前会审` when none),
re-check seat claims, start the wait window at first dispatch, let a
prior-board directory win over a conflicting repo convention, and
record the full test command plus whether a pipeline/redirect hid the
exit code. Pins lock those polarities. The `/dyro-review-board`
wrapper still follows that file only and refuses same-turn delivery.
Version unchanged.
- Hide the internal `dyro-board` protocol from slash / `$` skill pickers.
`/dyro-review-board` is the only human-facing 会审 command. The protocol
asset still installs as a non-invocable companion of `review-board` (and
Expand Down
16 changes: 1 addition & 15 deletions docs/agent-orchestration-discipline.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,7 @@ Agent 输出永远是**建议**;进入交付链必须经过 Dyro 契约与独

## 3. 对抗评审记录协议(摘要)

第一方座位 `dyro-board` 在用户提出会审 / 对抗 / Go/No-Go 时自动戴上协议;人类命令是 `/dyro-review-board`。记录仍不是 Proof。完整模板见 [可选本地 Agent 派发设计 §6](designs/optional-local-agent-dispatch.md#6-对抗评审记录协议)。

硬规则:

1. 单一共享评审文件
2. 每人仅写自己的签名区
3. **源码 / 线上契约 > 计划 / 旧评审**
4. 无法证明 → 标 `须人工核`
5. 终裁合并重复、仲裁冲突、输出 P0/P1/P2 + Go/No-Go + 执行交接

建议路径:

```text
docs/reviews/YYYY-MM-DD-<topic>-adversarial-board.md
```
第一方座位 `dyro-board` 在用户提出会审 / 对抗 / Go/No-Go 时自动戴上协议;人类命令是 `/dyro-review-board`。记录仍不是 Proof。完整 Record 协议只以 [`dyro-board/SKILL.md`](../src/dyro/integrations/assets/dyro-board/SKILL.md) 为准;不要在此另写一套,也不要把设计稿或旧五条摘要当成现行模板。

## 4. 写冲突与脏工作区

Expand Down
39 changes: 35 additions & 4 deletions src/dyro/integrations/assets/dyro-board/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,44 @@ Do not scan personal host skill directories.

## Record protocol

1. One shared review file. Prefer
`docs/reviews/YYYY-MM-DD-<topic>-adversarial-board.md`.
0. Before review starts, declare the diff baseline per repo:
`<repo> <ref> <sha> <date>`. Default the **production line** (usually
`origin/release`). **Must not default to `master`/`main`.** If
`master`/`main` has diverged from production, record how many commits.
If a prior board on the same development line used a different
baseline, say why.
0b. Search for prior boards on the same development line / same topic. If
one exists, the record **must** include a section that closes each
prior P0/P1 as `已闭环` / `未闭环` / `须人工核` with evidence. If the
search finds none, the record must say `已检索·无先前会审`.
1. One shared review file. Prefer the repo's **existing**
review-directory convention. If none,
`docs/reviews/YYYY-MM-DD-<topic>-adversarial-board.md`. If a prior
board exists on this line, use the same directory. If the repo
convention and a prior-board directory differ, the prior-board
directory wins (same line).
2. Each reviewer writes only in a signed section. Do not edit, rewrite, or
summarize another section.
3. Source code and live contracts outrank plans and prior reviews.
3. Source code and live contracts outrank plans, prior reviews, **and
same-batch seat opinions**. The chair must re-check every seat claim
before it enters final arbitration. Unchecked items must be marked
`未复核·转述`.
4. Unprovable claims are `须人工核`.
5. Final arbitration deduplicates, resolves conflicts, and emits P0/P1/P2,
5. Empty `ListAgents` / `TaskOutput` is **not** enough to declare a seat
dead — those APIs can be empty while the seat is still running. The
chair must set an explicit wait window (≥ 10 minutes). The clock
starts at **first dispatch** of the seats, not the first empty poll,
and **must not publish arbitration** during that window.
6. A seat that misses the window is `逾期未交`, not "lost". Record who
covers that dimension.
7. Seat reports that arrive after arbitration require a **revised**
record that lists what changed. No silent merge into the published
verdict.
8. Any test verdict must record: the full command, the summary line from
raw output, and whether a pipeline/redirect hid the exit code. Exit
code or a task-completed notification alone must not assert pass.
Unrun targets are `未执行` plus why.
9. Final arbitration deduplicates, resolves conflicts, emits P0/P1/P2,
Go/No-Go, and the next human Dyro command if one already exists.

## Authority
Expand Down
7 changes: 5 additions & 2 deletions src/dyro/integrations/assets/dyro-review-board/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ metadata:

命令名 `/dyro-review-board`、内部 id `dyro-board`、`dyro integration status board`、记录文件名里的 `adversarial-board` 都可以说。

内部 id 仍是 `dyro-board`,不要改,也不要另写一套协议。
内部 id 仍是 `dyro-board`,不要改,也不要另写一套协议。 Follow
`dyro-board` exactly; do not invent a second protocol. The protocol now
requires baseline + seat lifecycle.

## Same-turn delivery

Expand All @@ -33,7 +35,8 @@ If this turn also asks to 提交、commit、push、发布、publish、`task merg
- `dyro-board/SKILL.md` next to this skill's parent directory
- the installed `dyro-board` skill already on this host
2. If none of those can be read, stop. Say 会审协议未安装;下一步是 `dyro integration status board`.
3. Follow that file exactly. Do not start 会审 from this wrapper alone.
3. Follow `dyro-board` exactly. Do not invent a second protocol. Do not
start 会审 from this wrapper alone.
4. In chat, call the activity 会审 or 对抗审查. Call the output a 记录.

## Say this to developers
Expand Down
130 changes: 130 additions & 0 deletions tests/test_integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,37 @@
from dyro.integrations import manager


def _record_protocol_section(skill: str) -> str:
start = skill.index("## Record protocol")
rest = skill[start:]
match = re.search(r"\n## ", rest[1:])
return rest if match is None else rest[: match.start() + 1]


def _plain_record(skill: str) -> str:
return re.sub(r"\s+", " ", re.sub(r"[*_]", "", _record_protocol_section(skill)))


def _has_prior_closeout(plain: str) -> bool:
return (
re.search(r"(?:^|\s)0b\.", plain) is not None
and "same development line / same topic" in plain
and "已闭环" in plain
and "未闭环" in plain
)


def _has_empty_api_polarity(plain: str) -> bool:
return "is not enough to declare a seat dead" in plain


def _has_test_verdict_polarity(plain: str) -> bool:
return (
"pipeline/redirect hid the exit code" in plain
and "must not assert pass" in plain
)


class IntegrationManagerTests(unittest.TestCase):
def setUp(self) -> None:
self.tmp = tempfile.TemporaryDirectory(prefix="dyro-integrations-")
Expand Down Expand Up @@ -296,12 +327,111 @@ def test_review_board_is_the_only_invocable_board_slash(self) -> None:
self.assertIn("user-invocable: true", review)
self.assertIn("disable-model-invocation: true", review)
self.assertIn("dyro-board/SKILL.md", review)
self.assertIn("Follow `dyro-board` exactly", review)
self.assertIn("do not invent a second protocol", review.lower())
self.assertIn("baseline + seat lifecycle", review)
self.assertIn("Refuse those delivery actions", review)
self.assertNotIn("--yes", review)
self.assertNotIn("board", USER_INTEGRATION_CHOICES)
self.assertIn("review-board", USER_INTEGRATION_CHOICES)
self.assertIn("board", INTEGRATION_CHOICES)

def test_board_record_protocol_requires_baseline_and_seat_lifecycle(
self,
) -> None:
board = (manager._asset_root("board") / "SKILL.md").read_text(
encoding="utf-8"
)
review = (manager._asset_root("review-board") / "SKILL.md").read_text(
encoding="utf-8"
)
collapsed = re.sub(r"\s+", " ", board)
record = _record_protocol_section(board)
plain = _plain_record(board)
self.assertIn("## Record protocol", board)
self.assertIn("<repo> <ref> <sha> <date>", board)
self.assertIn("production line", collapsed)
self.assertIn("origin/release", board)
self.assertIn("Must not default to `master`/`main`", collapsed)
self.assertIn("existing review-directory convention", plain)
self.assertIn("docs/reviews/YYYY-MM-DD-<topic>-adversarial-board.md", board)
self.assertIn("prior-board directory wins", plain)
self.assertIn("signed section", collapsed)
self.assertIn("same-batch seat opinions", collapsed)
self.assertIn("re-check every seat claim", collapsed)
self.assertIn("未复核·转述", board)
self.assertIn("须人工核", board)
self.assertTrue(_has_prior_closeout(plain), msg=plain)
self.assertIn("已检索·无先前会审", record)
self.assertFalse(
_has_prior_closeout(plain.replace("0b.", "1b.", 1)),
msg="dropping 0b must fail the close-out pin",
)
self.assertFalse(
_has_prior_closeout(
plain.replace("已闭环", "", 1).replace("未闭环", "", 1)
),
msg="deleting 已闭环 / 未闭环 must fail the close-out pin",
)
self.assertIn("ListAgents", board)
self.assertIn("TaskOutput", board)
self.assertTrue(_has_empty_api_polarity(plain), msg=plain)
self.assertNotIn("is enough to declare a seat dead", plain)
self.assertFalse(
_has_empty_api_polarity(
plain.replace(
"is not enough to declare a seat dead",
"is enough to declare a seat dead",
1,
)
),
msg="flipping empty-API polarity to 'is enough' must fail",
)
self.assertIn("wait window", collapsed)
self.assertIn("10 minutes", collapsed)
self.assertIn("first dispatch", plain)
self.assertIn("must not publish arbitration", collapsed)
self.assertIn("逾期未交", board)
self.assertIn("revised record", plain)
self.assertIn("the full command", collapsed)
self.assertIn("raw output", collapsed)
self.assertTrue(_has_test_verdict_polarity(plain), msg=plain)
self.assertNotIn("must assert pass", plain)
self.assertFalse(
_has_test_verdict_polarity(
plain.replace("must not assert pass", "must assert pass", 1)
),
msg="inverting 'must not assert pass' must fail",
)
self.assertFalse(
_has_test_verdict_polarity(
plain.replace("pipeline/redirect hid the exit code", "", 1)
),
msg="deleting pipeline/redirect hid-exit-code must fail",
)
self.assertIn("未执行", board)
self.assertIn("P0/P1/P2", board)
self.assertIn("Go/No-Go", board)
self.assertIn("not a Proof", collapsed)
self.assertIn("task review` PASS", board)
self.assertIn("Do not merge, push, signoff", collapsed)
self.assertNotIn("--yes", board)
self.assertIn("user-invocable: false", board)
self.assertNotIn("/dyro-board", board)
self.assertNotIn("$dyro-board", board)
self.assertIn("Follow `dyro-board` exactly", review)
self.assertIn("do not invent a second protocol", review.lower())
self.assertNotIn("## Record protocol", review)
self.assertNotIn("逾期未交", review)
self.assertNotIn("<repo> <ref> <sha> <date>", review)
self.assertNotIn("已检索·无先前会审", review)
discipline = Path(__file__).resolve().parents[1].joinpath(
"docs", "agent-orchestration-discipline.md"
).read_text(encoding="utf-8")
self.assertIn("dyro-board/SKILL.md", discipline)
self.assertNotIn("完整模板见", discipline)
self.assertNotIn("1. 单一共享评审文件", discipline)

def test_review_board_install_also_installs_protocol(self) -> None:
installed = install_integration("review-board", yes=True)
self.assertEqual(installed.status.state, IntegrationState.CURRENT)
Expand Down
35 changes: 35 additions & 0 deletions tests/test_seats.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,31 @@ def test_packaged_first_batch_skills_auto_trigger_and_stay_bounded(self) -> None
self.assertNotIn("user-invocable: true", frontmatter)
self.assertNotIn("/dyro-board", content)
self.assertIn("/dyro-review-board", content)
self.assertIn("production line", content)
self.assertIn("Must not default to `master`/`main`", content)
collapsed = re.sub(r"\s+", " ", re.sub(r"[*_]", "", content))
self.assertIn("existing review-directory convention", collapsed)
self.assertIn("0b.", content)
self.assertIn("same development line / same topic", content)
self.assertIn("已闭环", content)
self.assertIn("未闭环", content)
self.assertIn("已检索·无先前会审", content)
self.assertIn("is not enough to declare a seat dead", collapsed)
self.assertNotIn("is enough to declare a seat dead", collapsed)
self.assertIn("first dispatch", collapsed)
self.assertIn("prior-board directory wins", collapsed)
self.assertIn("re-check every seat claim", content)
self.assertIn("wait window", content)
self.assertIn("逾期未交", content)
self.assertIn("revised record", collapsed)
self.assertIn("the full command", content)
self.assertIn("raw output", content)
self.assertIn("pipeline/redirect hid the exit code", content)
self.assertIn("must not assert pass", content)
self.assertNotIn("must assert pass", collapsed)
self.assertIn("须人工核", content)
self.assertIn("未复核·转述", content)
self.assertIn("P0/P1/P2", content)
self.assertEqual(keys, expected_keys, msg=seat.skill_name)
self.assertIn(f"name: {seat.skill_name}", frontmatter)
lowered = content.lower()
Expand Down Expand Up @@ -160,6 +185,16 @@ def test_slash_skills_are_packaged_but_not_auto_load_seats(self) -> None:
self.assertIn("user-invocable: false", board)
self.assertNotIn("user-invocable: true", board)
self.assertNotIn("/dyro-board", board)
self.assertIn("Must not default to `master`/`main`", board)
self.assertIn("逾期未交", board)
collapsed = re.sub(r"\s+", " ", re.sub(r"[*_]", "", board))
self.assertIn("existing review-directory convention", collapsed)
self.assertIn("0b.", board)
self.assertIn("已闭环", board)
self.assertIn("未闭环", board)
self.assertIn("is not enough to declare a seat dead", collapsed)
self.assertIn("must not assert pass", board)
self.assertIn("pipeline/redirect hid the exit code", board)

def test_wheel_package_data_lists_every_first_batch_skill(self) -> None:
metadata = Path(__file__).resolve().parents[1].joinpath("pyproject.toml")
Expand Down