Skip to content
Open
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
32 changes: 32 additions & 0 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,38 @@ Forum event kinds:

> **Note:** Without `--no-mention-filter` (or `require_mention = false`), the default `subscribe=mentions` mode filters events that don't @mention the agent — forum posts will be invisible.

## Chat targeting (Slack-parity)

Mentions may include inline options that the harness injects as a `[Run Options]` prompt section:

| Option | Example | Effect |
|--------|---------|--------|
| `repo=` | `repo=web` | Prefer nest checkout `REPOS/web` |
| `env=` / `environment=` | `env=Platform` | Named multi-repo environment |
| `branch=` | `branch=dev` | Prefer base branch |
| `model=` | `model=gpt-5` | Prefer model when the runtime supports it |
| `autopr=` | `autopr=false` | Prefer / disallow auto `buzz pr open` |
| `channel=` | `channel=#eng` | Post updates to another channel (membership required) |

Quoted values with spaces are supported (`env="Platform Staging"`). Light natural language also works: `use the Platform environment`, `use the "Platform Staging" environment`, and `in web` (single-token repo name only).

### Nest environments file

Optional `{cwd}/.buzz/environments.toml`:

```toml
[[environment]]
name = "Platform"
repos = ["web", "api"]
default_branch = "main" # optional
```

Repo names are directories under nest `REPOS/`. When `env=` matches, preferred paths are `{cwd}/REPOS/{name}` for each listed repo that exists.

### Rule defaults

Subscription rules (`--subscribe config`) may set optional `target_repo` / `target_env`. Explicit inline options in the mention text always win over rule defaults.

## How It Works

1. **Startup** — Spawns N agent subprocesses (default 1), sends ACP `initialize` to each, connects to the relay with NIP-42 auth.
Expand Down
5 changes: 5 additions & 0 deletions crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ Your `core` memory is auto-injected into your context every turn — it holds id

These are guidelines, not a fixed procedure — apply judgment to the task in front of you.

- **Plan before start.** On multi-step or coding work, your **first** channel message is a short plan (goal, steps, repos/paths in scope, risks). Then wait for redirect or post that you are starting and execute.
- **Honor `[Run Options]` when present.** Prefer the listed repo/env checkouts, branch, model, autopr, and output channel. `autopr=false` means do not auto-open a PR; `channel=` means post updates there when membership allows.
- **Owner session controls.** The owner can `@mention` you with `!cancel` (stop the current turn) or `!rotate` (fresh ACP session). These are harness commands — acknowledge only if useful; do not invent equivalents.
- **Handoff.** When you open a PR, use `buzz pr open --channel <uuid>`. Completion messages include the PR URL and the absolute worktree path so a human can Open in Cursor Desktop.
- **Status.** The harness shows 👀 (seen) and 💬 (working). You post **milestones only** (picked up with plan, blocked + need input, PR up, done). Optional ✅/❌ on done/fail is harness-side.
- **Work in the open.** Your tool calls and reasoning are invisible to humans — narrate as you go in brief messages, and never go dark between "picked up" and "done." If you didn't post it, it didn't happen.
- **Be candid.** Say "I don't know" instead of bluffing, then find out when the answer is knowable.
- **Understand before changing.** Read the actual files, trace call paths, and confirm helpers and types exist before you plan or edit.
Expand Down
2 changes: 2 additions & 0 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,8 @@ mod tests {
prompt_tag: None,
compiled_filter: None,
consecutive_timeouts: Arc::new(AtomicU32::new(0)),
target_repo: None,
target_env: None,
}
}

Expand Down
20 changes: 20 additions & 0 deletions crates/buzz-acp/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ pub struct SubscriptionRule {
/// disabled and `match_event` returns `None` (fail-closed).
#[serde(skip)]
pub consecutive_timeouts: Arc<AtomicU32>,
/// Optional default repo name for Slack-parity targeting (`REPOS/{name}`).
/// Overridden by explicit `repo=` / NL cues in the mention text.
#[serde(default)]
pub target_repo: Option<String>,
/// Optional default nest environment name (see `.buzz/environments.toml`).
/// Overridden by explicit `env=` / NL cues in the mention text.
#[serde(default)]
pub target_env: Option<String>,
}

impl Default for SubscriptionRule {
Expand All @@ -124,6 +132,8 @@ impl Default for SubscriptionRule {
prompt_tag: None,
compiled_filter: None,
consecutive_timeouts: Arc::new(AtomicU32::new(0)),
target_repo: None,
target_env: None,
}
}
}
Expand All @@ -141,6 +151,8 @@ impl Clone for SubscriptionRule {
// Share the same counter across clones so all copies of a rule
// agree on the timeout state.
consecutive_timeouts: self.consecutive_timeouts.clone(),
target_repo: self.target_repo.clone(),
target_env: self.target_env.clone(),
}
}
}
Expand All @@ -153,6 +165,10 @@ pub struct MatchedRule {
pub rule_index: usize,
/// Prompt tag to use (rule's `prompt_tag` or its `name`).
pub prompt_tag: String,
/// Rule-level default repo (if any).
pub target_repo: Option<String>,
/// Rule-level default environment (if any).
pub target_env: Option<String>,
}

/// Maximum expression length accepted by `evaluate_filter`.
Expand Down Expand Up @@ -453,6 +469,8 @@ pub async fn match_event(
return Some(MatchedRule {
rule_index: index,
prompt_tag,
target_repo: rule.target_repo.clone(),
target_env: rule.target_env.clone(),
});
}

Expand Down Expand Up @@ -505,6 +523,8 @@ mod tests {
prompt_tag: prompt_tag.map(|s| s.into()),
compiled_filter: None,
consecutive_timeouts: Arc::new(AtomicU32::new(0)),
target_repo: None,
target_env: None,
}
}

Expand Down
29 changes: 27 additions & 2 deletions crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod config;
mod engram_fetch;
mod filter;
mod observer;
mod options;
mod pool;
mod pool_lifecycle;
mod queue;
Expand Down Expand Up @@ -1453,6 +1454,8 @@ async fn tokio_main() -> Result<()> {
compiled_filter: None,
consecutive_timeouts: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
prompt_tag: Some("@mention".into()),
target_repo: None,
target_env: None,
}]
}
SubscribeMode::All => {
Expand All @@ -1465,6 +1468,8 @@ async fn tokio_main() -> Result<()> {
compiled_filter: None,
consecutive_timeouts: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)),
prompt_tag: Some("all".into()),
target_repo: None,
target_env: None,
}]
}
SubscribeMode::Config => {
Expand Down Expand Up @@ -2173,8 +2178,8 @@ async fn tokio_main() -> Result<()> {
}

let matched = filter::match_event(&buzz_event.event, buzz_event.channel_id, &rules, &pubkey_hex).await;
let prompt_tag = match matched {
Some(m) => m.prompt_tag,
let (prompt_tag, target_repo, target_env) = match matched {
Some(m) => (m.prompt_tag, m.target_repo, m.target_env),
None => {
tracing::debug!(channel_id = %buzz_event.channel_id, kind = buzz_event.event.kind.as_u16(), "event matched no rule — dropping");
continue;
Expand All @@ -2201,6 +2206,8 @@ async fn tokio_main() -> Result<()> {
event: buzz_event.event,
received_at: std::time::Instant::now(),
prompt_tag,
target_repo,
target_env,
});
// 👀 — immediate "seen" reaction, only if the event
// was actually queued (not dropped by DedupMode::Drop).
Expand Down Expand Up @@ -2828,6 +2835,8 @@ fn try_native_steer(
event,
prompt_tag: prompt_tag.clone(),
received_at: std::time::Instant::now(),
target_repo: None,
target_env: None,
};
let event_block = queue::format_event_block(channel_id, None, &be, None);
let body = format!("{header}\n\n[Buzz event: {prompt_tag}]\n{event_block}\n\n{closing}");
Expand Down Expand Up @@ -5517,6 +5526,8 @@ mod error_outcome_emission_tests {
event,
prompt_tag: "test".into(),
received_at: std::time::Instant::now(),
target_repo: None,
target_env: None,
}],
cancelled_events: vec![],
cancel_reason: None,
Expand Down Expand Up @@ -5623,6 +5634,8 @@ mod error_outcome_emission_tests {
event,
prompt_tag: "test".into(),
received_at: std::time::Instant::now(),
target_repo: None,
target_env: None,
}],
cancelled_events: vec![],
cancel_reason: None,
Expand Down Expand Up @@ -5741,6 +5754,8 @@ mod error_outcome_emission_tests {
.unwrap(),
prompt_tag: "test".into(),
received_at: std::time::Instant::now(),
target_repo: None,
target_env: None,
}],
cancelled_events: vec![],
cancel_reason: None,
Expand Down Expand Up @@ -5834,6 +5849,8 @@ mod error_outcome_emission_tests {
.unwrap(),
prompt_tag: "test".into(),
received_at: std::time::Instant::now(),
target_repo: None,
target_env: None,
}],
cancelled_events: vec![],
cancel_reason: None,
Expand Down Expand Up @@ -5912,6 +5929,8 @@ mod error_outcome_emission_tests {
event: original_event.clone(),
prompt_tag: "test".into(),
received_at: std::time::Instant::now(),
target_repo: None,
target_env: None,
}],
cancelled_events: vec![],
cancel_reason: Some(CancelReason::Steer),
Expand Down Expand Up @@ -5941,6 +5960,8 @@ mod error_outcome_emission_tests {
event: new_event.clone(),
received_at: std::time::Instant::now(),
prompt_tag: "test".into(),
target_repo: None,
target_env: None,
});
let config = test_config();
let mut heartbeat_in_flight = false;
Expand Down Expand Up @@ -6233,6 +6254,8 @@ mod error_outcome_emission_tests {
event,
prompt_tag: "test".into(),
received_at: std::time::Instant::now(),
target_repo: None,
target_env: None,
}],
cancelled_events: vec![],
cancel_reason: None,
Expand Down Expand Up @@ -6318,6 +6341,8 @@ mod error_outcome_emission_tests {
event,
prompt_tag: "test".into(),
received_at: std::time::Instant::now(),
target_repo: None,
target_env: None,
}],
cancelled_events: vec![],
cancel_reason: None,
Expand Down
Loading