Skip to content

Commit ba41398

Browse files
committed
Merge remote-tracking branch 'origin/main' into investigate-buzz-cli-handoff
* origin/main: feat(desktop): refine context-aware Projects collaboration (#6396) fix(desktop): distinguish duplicate agent devices (#6337) feat(desktop): close Buzz window with Cmd+W (#6314) refactor(prompt): simplify Buzz agent guidance (#6340) feat(desktop): make Projects workspaces selectable (#6368) Add Buzz-native collaboration benchmarks (#6264) Polish mobile timeline and emoji interactions (#6297) feat(desktop): make the Projects overview follow the selected section (#6335) refactor(desktop): coordinate TTS playback (#6341) fix(desktop): show complete repository trees (#5102) Add appearance preference previews (#6193) fix(desktop): restore emoji recents (#6263) chore: serialize mobile pre-push checks (#6322) Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
2 parents 53476a2 + 3c228b1 commit ba41398

236 files changed

Lines changed: 17653 additions & 4138 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/benchmark-harbor.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ on:
55
branches: [main]
66
paths:
77
- "benchmarks/harbor-buzz-orchestra/**"
8+
- "benchmarks/buzz-dataset/**"
89
- ".github/workflows/benchmark-harbor.yml"
910
pull_request:
1011
paths:
1112
- "benchmarks/harbor-buzz-orchestra/**"
13+
- "benchmarks/buzz-dataset/**"
1214
- ".github/workflows/benchmark-harbor.yml"
1315

1416
permissions:
@@ -31,6 +33,9 @@ jobs:
3133
python -m pip install --disable-pip-version-check -e ".[dev]"
3234
pytest -q
3335
ruff check .
36+
# The task verifiers live in the sibling benchmarks/buzz-dataset, so
37+
# they need the harness config passed explicitly to stay linted.
38+
ruff check --config pyproject.toml ../buzz-dataset
3439
- name: Test provisioner
3540
working-directory: benchmarks/harbor-buzz-orchestra/testbed
3641
run: |

Justfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,31 @@ benchmark *ARGS:
996996
uv run --project benchmarks/harbor-buzz-orchestra/testbed \
997997
benchmarks/harbor-buzz-orchestra/scripts/benchmark.py {{ARGS}}
998998
999+
# Run the benchmark adapter + testbed gate exactly as CI does (pytest + ruff, pinned ruff from pyproject)
1000+
benchmark-check:
1001+
#!/usr/bin/env bash
1002+
set -euo pipefail
1003+
cd "{{justfile_directory()}}/benchmarks/harbor-buzz-orchestra"
1004+
# CI installs the dev extra with pip, so pyproject — not uv.lock — decides
1005+
# which ruff lints. Read the pin from there so this recipe cannot drift
1006+
# from the workflow (a floating specifier once meant CI failed on RUF100
1007+
# while the locked local ruff passed).
1008+
ruff_pin="$(grep -oE 'ruff==[0-9.]+' pyproject.toml | head -1 | cut -d= -f3)"
1009+
for project in . testbed; do
1010+
(
1011+
cd "$project"
1012+
echo "── harbor-buzz-orchestra/$project (ruff $ruff_pin)"
1013+
uv run --frozen pytest -q
1014+
uvx "ruff@$ruff_pin" check .
1015+
uvx "ruff@$ruff_pin" format --check .
1016+
)
1017+
done
1018+
# The task verifiers live in the sibling benchmarks/buzz-dataset, so they
1019+
# need the harness config passed explicitly to stay linted.
1020+
echo "── buzz-dataset (ruff $ruff_pin)"
1021+
uvx "ruff@$ruff_pin" check --config pyproject.toml ../buzz-dataset
1022+
uvx "ruff@$ruff_pin" format --check --config pyproject.toml ../buzz-dataset
1023+
9991024
# Stop the benchmark Docker stack (state and channels are kept)
10001025
benchmark-down:
10011026
docker compose --project-name buzz-benchmark down

benchmarks/buzz-dataset/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# buzz-dataset
2+
3+
Harbor tasks that score **Buzz product behavior**, not just task correctness.
4+
Each task poses an ordinary-looking question; what is graded is how the agent
5+
answers it through Buzz — where the reply lands, who it notifies, what it was
6+
willing to read.
7+
8+
| Task | Behavior under test |
9+
| --- | --- |
10+
| [`reply-to-thread`](reply-to-thread) | Answers in the user's thread instead of as a new top-level message |
11+
| [`user-mention`](user-mention) | Hands the turn back with an event-level `p`-tag mention of the requesting human |
12+
| [`read-named-path-outside-workspace`](read-named-path-outside-workspace) | Reads a path the user named explicitly instead of refusing it as out of bounds |
13+
| [`create-channel-invite-users`](create-channel-invite-users) | Creates a channel with the exact shape, TTL, and membership asked for |
14+
15+
For `reply-to-thread` and `user-mention` the graded behavior is **deliberately
16+
absent from `instruction.md`** — it has to come from `buzz-acp`'s production
17+
base prompt. Read a task's own `README.md` before editing its instruction or
18+
verifier.
19+
20+
## Running
21+
22+
These tasks need the [`harbor-buzz-orchestra`](../harbor-buzz-orchestra)
23+
harness, which launches the real `buzz-acp``buzz-agent``buzz-dev-mcp`
24+
stack inside the task container and exports the relay snapshot each verifier
25+
grades. Plain `harbor run` against this directory will not work, and neither
26+
will `harbor run -a oracle` (no `solution/solve.sh` is shipped — the Oracle
27+
agent replaces the Buzz agent, so no relay trial is provisioned).
28+
29+
From the repo root:
30+
31+
```bash
32+
just benchmark \
33+
--path benchmarks/buzz-dataset/reply-to-thread \
34+
--attempts 1 \
35+
--manifest benchmarks/harbor-buzz-orchestra/manifests/buzz-native-solo-luna.yaml \
36+
--endpoint-config benchmarks/harbor-buzz-orchestra/testbed/endpoints/openai-live.json \
37+
--n-concurrent 1
38+
```
39+
40+
Pass `--path benchmarks/buzz-dataset` to run the whole suite. The default
41+
condition is one solo agent on `gpt-5.6-luna` at `thinking_effort: medium`,
42+
which needs `OPENAI_COMPAT_API_KEY`; see
43+
[the harness README](../harbor-buzz-orchestra/README.md#buzz-native-tasks) for
44+
the alternative Sonnet condition and the evidence-snapshot contract.
45+
46+
The verifiers are covered by fixture tests that live with the harness, in
47+
`../harbor-buzz-orchestra/tests/`.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# create-channel-invite-users
2+
3+
## What the agent does
4+
5+
Creates a temporary private stream channel named `fix-pr-1234` with a one-hour
6+
lifetime and invites an exact subset of a seeded directory: three named users as
7+
members and two named bots with the `bot` role
8+
([instruction.md](instruction.md)).
9+
10+
Unlike the other tasks in this suite, the graded behavior **is** stated in the
11+
instruction. What makes it hard is precision at scale: the provisioner seeds 50
12+
users (`benchmark-user-01``50`) and 10 bots (`benchmark-bot-01``10`), so the
13+
agent has to resolve five specific names out of sixty look-alikes and invite
14+
nobody else.
15+
16+
## Environment
17+
18+
`python:3.12-slim-bookworm`, no extra packages: the agent never runs in this
19+
container's shell. `BuzzOrchestraAgent` launches the real `buzz-acp` /
20+
`buzz-agent` stack against a dedicated relay, and the agent does all its work
21+
through `buzz channels create` / `channels invite`. Agent timeout 300s.
22+
23+
Directory identities are derived deterministically from the owner key
24+
(`BuzzTrialProvisioner._stable_credential`) without persisting any secret, and
25+
`_seed_directory` skips profiles already published — so reruns are idempotent
26+
and pubkeys are stable across trials.
27+
28+
## Verifier
29+
30+
Reads the post-agent `/logs/artifacts/buzz-evidence.json` snapshot. The
31+
snapshot's `observed_channels` come from the production CLI
32+
(`channels search --exact --include-archived` plus `channels members`), so the
33+
verifier grades the same view a user would see. Every dimension is
34+
programmatic; `reward` is the conjunction of all of them.
35+
36+
| Dimension | Type | Measures |
37+
| --- | --- | --- |
38+
| `evidence_complete` | programmatic | Snapshot is v1, names this task, and carries all 60 directory rows (50 users + 10 bots), the 5 resolvable targets, and exactly one orchestrator. Harness health, not agent skill — a 0 here means the provisioner or relay is suspect |
39+
| `channel_created` | programmatic | Exactly one channel named `fix-pr-1234` exists |
40+
| `channel_shape` | programmatic | `channel_type = stream`, `visibility = private`, not archived |
41+
| `temporary_channel` | programmatic | `ttl_seconds == 3600` — "for one hour", read from the kind:39000 `ttl` tag surfaced by `channels search` |
42+
| `exact_membership` | programmatic | Member pubkeys are exactly the owner plus the 5 targets — no extras, no duplicates |
43+
| `expected_roles` | programmatic | The 3 users hold `member`, the 2 bots hold `bot`, the creator holds `owner` |
44+
45+
## Layout
46+
47+
```
48+
create-channel-invite-users/
49+
├── instruction.md # Prompt posted to the agent as the trial user
50+
├── task.toml # Metadata, timeouts, 1 CPU / 1 GiB environment
51+
├── environment/Dockerfile # Bare python image; the relay stack is uploaded
52+
└── tests/
53+
├── test.sh # Runs verify.py against the evidence snapshot
54+
└── verify.py # Deterministic scorer (see table above)
55+
```
56+
57+
To change the target set, edit `task_fixtures.TARGET_USERS` / `TARGET_BOTS`,
58+
`instruction.md`, and the matching constants at the top of `tests/verify.py`
59+
all three must agree, and `evidence_complete` will fail loudly if the directory
60+
size drifts from 60.
61+
62+
## Running
63+
64+
```bash
65+
just benchmark \
66+
--path benchmarks/buzz-dataset/create-channel-invite-users \
67+
--attempts 1 \
68+
--manifest benchmarks/harbor-buzz-orchestra/manifests/buzz-native-solo-luna.yaml \
69+
--endpoint-config benchmarks/harbor-buzz-orchestra/testbed/endpoints/openai-live.json \
70+
--n-concurrent 1
71+
```
72+
73+
`harbor run -a oracle` does **not** work here, and no `solution/solve.sh` is
74+
shipped: the Oracle agent replaces `BuzzOrchestraAgent`, so no relay trial is
75+
provisioned and no evidence snapshot is exported. The verifier is covered
76+
instead by fixture tests in
77+
`../harbor-buzz-orchestra/tests/test_create_channel_invite_users_verifier.py`.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM python:3.12-slim-bookworm
2+
3+
WORKDIR /app
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Create a temporary private stream channel named `fix-pr-1234` for one hour.
2+
3+
Invite these users as members:
4+
5+
- `benchmark-user-07`
6+
- `benchmark-user-19`
7+
- `benchmark-user-42`
8+
9+
Invite these bots with the bot role:
10+
11+
- `benchmark-bot-03`
12+
- `benchmark-bot-08`
13+
14+
Do not invite any other users or bots. When finished, reply briefly with what
15+
you created.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
schema_version = "1.3"
2+
3+
[task]
4+
name = "buzz-native/create-channel-invite-users"
5+
description = "Create a temporary PR channel with an exact subset of users and bots."
6+
authors = [{ name = "Buzz" }]
7+
keywords = ["buzz-native", "channels", "membership", "cli"]
8+
9+
[metadata]
10+
difficulty = "medium"
11+
category = "collaboration"
12+
tags = ["channels", "membership", "cli"]
13+
14+
[agent]
15+
timeout_sec = 300.0
16+
17+
[verifier]
18+
timeout_sec = 30.0
19+
20+
[environment]
21+
network_mode = "public"
22+
cpus = 1
23+
memory_mb = 1024
24+
storage_mb = 1024
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
python3 /tests/verify.py \
5+
--evidence /logs/artifacts/buzz-evidence.json \
6+
--reward /logs/verifier/reward.json \
7+
--details /logs/verifier/details.json
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env python3
2+
"""Deterministic verifier for exact Buzz channel creation and membership."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import json
8+
from pathlib import Path
9+
from typing import Any
10+
11+
CHANNEL_NAME = "fix-pr-1234"
12+
TARGET_USERS = {"benchmark-user-07", "benchmark-user-19", "benchmark-user-42"}
13+
TARGET_BOTS = {"benchmark-bot-03", "benchmark-bot-08"}
14+
15+
16+
def _zero_metrics() -> dict[str, float]:
17+
return {
18+
"reward": 0.0,
19+
"channel_created": 0.0,
20+
"channel_shape": 0.0,
21+
"temporary_channel": 0.0,
22+
"exact_membership": 0.0,
23+
"expected_roles": 0.0,
24+
"evidence_complete": 0.0,
25+
}
26+
27+
28+
def score_evidence(evidence: object) -> tuple[dict[str, float], dict[str, Any]]:
29+
if not isinstance(evidence, dict):
30+
return _zero_metrics(), {"error": "evidence root is not an object"}
31+
32+
directory_rows = [
33+
row for row in evidence.get("directory", []) if isinstance(row, dict)
34+
]
35+
directory = {
36+
row.get("name"): row
37+
for row in directory_rows
38+
if isinstance(row.get("name"), str)
39+
}
40+
channels = [
41+
channel
42+
for channel in evidence.get("observed_channels", [])
43+
if isinstance(channel, dict) and channel.get("name") == CHANNEL_NAME
44+
]
45+
channel = channels[0] if len(channels) == 1 else None
46+
identities = (
47+
evidence.get("identities")
48+
if isinstance(evidence.get("identities"), dict)
49+
else {}
50+
)
51+
orchestrators = [
52+
row
53+
for row in identities.values()
54+
if isinstance(row, dict) and row.get("role") == "orchestrator"
55+
]
56+
owner_pubkey = orchestrators[0].get("pubkey") if len(orchestrators) == 1 else None
57+
58+
expected_names = TARGET_USERS | TARGET_BOTS
59+
expected_targets = {
60+
directory[name]["pubkey"]: "bot" if name in TARGET_BOTS else "member"
61+
for name in expected_names
62+
if name in directory and isinstance(directory[name].get("pubkey"), str)
63+
}
64+
expected_members = (
65+
{owner_pubkey: "owner", **expected_targets}
66+
if isinstance(owner_pubkey, str)
67+
else expected_targets
68+
)
69+
member_rows = (
70+
[row for row in channel.get("members", []) if isinstance(row, dict)]
71+
if channel is not None
72+
else []
73+
)
74+
actual_members = {
75+
row.get("pubkey"): row.get("role")
76+
for row in member_rows
77+
if isinstance(row.get("pubkey"), str)
78+
}
79+
80+
evidence_complete = float(
81+
evidence.get("schema_version") == 1
82+
and evidence.get("task_name") == "create-channel-invite-users"
83+
and len(directory_rows) == 60
84+
and len(directory) == 60
85+
and sum(row.get("role") == "user" for row in directory_rows) == 50
86+
and sum(row.get("role") == "bot" for row in directory_rows) == 10
87+
and len(expected_targets) == 5
88+
and len(orchestrators) == 1
89+
)
90+
channel_created = float(channel is not None)
91+
channel_shape = float(
92+
channel is not None
93+
and channel.get("channel_type") == "stream"
94+
and channel.get("visibility") == "private"
95+
and channel.get("archived") is False
96+
)
97+
temporary_channel = float(
98+
channel is not None and channel.get("ttl_seconds") == 3600
99+
)
100+
exact_membership = float(
101+
len(member_rows) == len(actual_members)
102+
and set(actual_members) == set(expected_members)
103+
)
104+
expected_roles = float(actual_members == expected_members)
105+
reward = float(
106+
all(
107+
metric == 1.0
108+
for metric in (
109+
evidence_complete,
110+
channel_created,
111+
channel_shape,
112+
temporary_channel,
113+
exact_membership,
114+
expected_roles,
115+
)
116+
)
117+
)
118+
metrics = {
119+
"reward": reward,
120+
"channel_created": channel_created,
121+
"channel_shape": channel_shape,
122+
"temporary_channel": temporary_channel,
123+
"exact_membership": exact_membership,
124+
"expected_roles": expected_roles,
125+
"evidence_complete": evidence_complete,
126+
}
127+
details = {
128+
"matching_channel_count": len(channels),
129+
"channel_id": channel.get("channel_id") if channel is not None else None,
130+
"expected_members": expected_members,
131+
"actual_members": actual_members,
132+
}
133+
return metrics, details
134+
135+
136+
def main() -> int:
137+
parser = argparse.ArgumentParser()
138+
parser.add_argument("--evidence", type=Path, required=True)
139+
parser.add_argument("--reward", type=Path, required=True)
140+
parser.add_argument("--details", type=Path, required=True)
141+
args = parser.parse_args()
142+
143+
try:
144+
evidence = json.loads(args.evidence.read_text(encoding="utf-8"))
145+
metrics, details = score_evidence(evidence)
146+
except (OSError, json.JSONDecodeError) as error:
147+
metrics, details = _zero_metrics(), {"error": str(error)}
148+
149+
args.reward.write_text(json.dumps(metrics, sort_keys=True) + "\n", encoding="utf-8")
150+
args.details.write_text(
151+
json.dumps(details, indent=2, sort_keys=True) + "\n", encoding="utf-8"
152+
)
153+
return 0
154+
155+
156+
if __name__ == "__main__":
157+
raise SystemExit(main())

0 commit comments

Comments
 (0)