[Workspace] system-prompt anchor never reaches protocol-v1 harnesses: Claude Code and Codex lose absolute-cwd grounding on all platforms
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
**Describe the bug**
The `[Workspace]` system-prompt section never reaches protocol-v1 (legacy-framing) harnesses on any platform. Because upstream ACP's current protocol version *is* 1, that includes the shipping Claude Code and Codex adapters — so most real Buzz agent runs lose the absolute-cwd grounding, not an edge case.
`workspace_section()` (`pool.rs:1165-1177`) has exactly one producer, `framed_system_prompt` (`pool.rs:1137-1158`), whose output is consumed by two routes only:
1. `pool.rs:828-839` — `session/new`, wrapped in `session_new_system_prompt` (`pool.rs:185-195`):
```rust
if is_goose || protocol_version < 2 { None } else { prompt }
```
For a v1 agent the combined prompt is built and then **discarded**.
2. `pool.rs:841-858` — Goose's `session_set_goose_system_prompt` extension, which is version-independent, so Goose is fine.
The legacy path assembles its own prompt and structurally cannot render the section: `prepend_base_for_legacy` (`pool.rs:1090-1101`, called at `pool.rs:1593` initial message and `pool.rs:1731` heartbeat) emits `[Base]` and `[Channel Canvas]` only, and takes no `cwd` argument. Per-turn assembly in `format_prompt` (`queue.rs:1429-1443`) is the same, and `FormatPromptArgs` (`queue.rs:1352-1375`) has **no `cwd` field** at all.
**Why protocol-v1 is the live path, not a relic.** Buzz requests `protocolVersion: 2` (`acp.rs:126`) and stores the response, defaulting to 1 when absent (`lib.rs:3775-3776`, `lib.rs:3864`: `unwrap_or(1)`). The v2 request is self-described as non-standard — `acp.rs:540-541`:
> `// Requesting version 2 is an intentional temporary pin — we are squatting on ACP v2 ahead of the upstream ACP RFD. Revisit when that RFD merges.`
and again in `buzz-agent/src/lib.rs:280-283`. Upstream ACP is at version 1, and the spec requires an agent that does not support the requested version to answer with the latest it does support. So a spec-compliant upstream adapter answers `1`.
Against `KNOWN_ACP_RUNTIMES` (`discovery.rs:66-198`):
| Runtime | Gets `[Workspace]`? | Why |
|---|---|---|
| Claude Code (`claude-agent-acp`, `discovery.rs:101-132`) | **No** | negotiates v1 → `has_system_prompt_support` false (`pool.rs:173-183`) → legacy framing |
| Codex (`codex-acp`, `discovery.rs:133-165`) | **No** | same |
| Goose (`discovery.rs:67-100`) | Yes | version-independent extension (`pool.rs:845`); lost only if the probe caches `Some(false)` (`pool.rs:849-855`) |
| Buzz Agent (`discovery.rs:166-197`) | Yes | pins `PROTOCOL_VERSION = 2` (`buzz-agent/src/config.rs:3`), `min(requested, 2)` (`buzz-agent/src/lib.rs:284`) |
**Bounding the impact honestly:** `[Base]` *is* still delivered on the legacy path — `queue.rs:1431` per turn, `pool.rs:1593` initial, `pool.rs:1731` heartbeat — and it is default-on (`lib.rs:1539-1544`, opt out only via `--no-base-prompt` / `BUZZ_ACP_NO_BASE_PROMPT`, `config.rs:409-410`). So `base_prompt.md:84-100`'s Workspace Layout table, `REPOS/` and `OUTBOX/` included, plus the "never run `find` over `$HOME`" line at `:100`, do arrive. What is lost is only the **absolute-path anchor** ("Your absolute working directory is ``... this is where you already are"). Same shape as #3124, much wider blast radius. Expected symptom is a probabilistic uptick in path-guessing and wasted exploration, not a functional failure.
**Steps to reproduce**
Code-level, no harness needed:
```
$ rg -n 'cwd' crates/buzz-acp/src/queue.rs | rg -n 'FormatPromptArgs' -A2
# FormatPromptArgs has no cwd field -> the per-turn legacy path cannot render [Workspace]
```
`workspace_section` has one caller (`pool.rs:1154`), reachable only from `create_session_and_apply_model` (`pool.rs:820`); neither legacy assembly point calls it.
End-to-end: run a managed Claude Code or Codex agent on **any** platform and inspect the prompt the harness receives (desktop transcript observer, or the harness log). Expect `[Base]` with no `[Workspace]` block. Compare against a `buzz-agent` agent on the same host, which does get it.
**Expected behavior**
Protocol-v1 harnesses receive the same workspace anchor as v2 harnesses.
**Version and platform**
- Buzz version: `main` @ `87b3fcd3`. All platforms — this is not OS-specific.
- Affected since `[Workspace]` was introduced in #1194 (2026-06-23).
- Found by code inspection while confirming #3124. Not a field report.
**Logs / additional context**
**Origin — an omission, not a decision.** `workspace_section` came in with `1011cea2` ("fix(desktop): ground agent workspace, migrate legacy nest, configurable repos_dir", #1194, 2026-06-23) — the only commit `git log -S workspace_section -- crates/buzz-acp/` returns. Inside the crate it touched only `pool.rs` and `base_prompt.md`; it did not touch `queue.rs`, and within `pool.rs` it did not touch `prepend_base_for_legacy` — the diff sat in `framed_system_prompt`, then inside an explicit `if agent.protocol_version >= 2 { … } else { … }`, and the legacy arm was left alone. All seven tests added target `framed_system_prompt`/`workspace_section` in isolation. The doc comment added at `pool.rs:1151-1153` reasons about persona-only agents and the `/` fallback but never mentions protocol version. `prepend_base_for_legacy` predates it (`f0858824`, #981, 2026-06-12; later `73cc31cc` #1277, `3b9b371f` #1755, `64b8fea6` #2034) and no commit in that set considers `[Workspace]`. *(Inference from diffs and messages: never added, rather than consciously excluded.)*
**Fix shape** — three insertion points, one shared renderer:
1. `queue.rs:1352-1375` — add `cwd: Option<&'a str>` to `FormatPromptArgs`; `pool.rs:1771-1784` already has `ctx.cwd` in scope.
2. `queue.rs:1429-1432` — push the workspace section ahead of `base_section(bp)`, matching `framed_system_prompt`'s ordering.
3. `pool.rs:1090-1101` — add a `cwd` parameter for the initial-message (`pool.rs:1593`) **and** heartbeat (`pool.rs:1731`) call sites. Missing the heartbeat site causes exactly the drift the `pool.rs:1088-1089` doc comment says the helper exists to prevent.
`workspace_section` is private in `pool.rs` and would need to move to a shared module or be re-exported.
**Risks worth weighing before doing it:**
- **Per-turn re-injection is real** — `format_prompt` runs on every batch flush. But `[Base]` (~150 lines) plus `[System]`, `[Team Instructions]`, `[Agent Memory — core]`, and `[Channel Canvas]` are *already* re-sent every legacy turn. `[Workspace]` is one ~70-word paragraph, well under 1% of existing per-turn overhead.
- **Preserve the `Some(base_prompt)` coupling.** `framed_system_prompt` emits `[Workspace]` only when a base prompt exists (`pool.rs:1154-1157`); a persona-only legacy agent must not get an anchor with no layout to go with it — the case `pool.rs:1151-1153` warns about.
- `MAX_SYSTEM_PROMPT_BYTES` does not apply (the legacy path uses no system prompt). The observer size trimmer (`queue.rs:1396-1402`) handles a new section gracefully, but it adds a leaf to the desktop "Prompt context" panel — worth a visual check.
**Relationship to #3124 — independent, neither subsumes the other.** #3124 fixes *whether the renderer returns `Some`* (the POSIX-only predicate), which helps callers that already invoke it. This issue is that the legacy path *never invokes it*. A Windows Claude Code agent is broken for both reasons; a macOS Claude Code agent for this one alone. They could land in one PR — the natural implementation here is "call the predicate-fixed `workspace_section` from the legacy assembly points" — but closing one must not close the other.
**Open question for maintainers:** given the `acp.rs:540-541` note about squatting on v2 ahead of the upstream RFD, is the intended long-term answer to fix the legacy path, or to converge on upstream versioning so the v1/v2 split disappears? That choice decides whether this is worth fixing directly or is better absorbed into the RFD follow-up.
Duplicate search: searched issues for "workspace prompt", "OUTBOX", and PRs for "base_prompt workspace" — none found. Nearest is #2334 (base prompt shown as session title in harness UIs), a display concern.
Contributor guide
Research direction
Start with prepend_base_for_legacy in crates/buzz-acp/src/pool.rs and format_prompt plus FormatPromptArgs in crates/buzz-acp/src/queue.rs; compare their assembly with framed_system_prompt and workspace_section. Trace the initial-message, heartbeat, and per-turn call sites, then run the existing buzz-acp tests. Done means protocol-v1 initial, heartbeat, and per-turn prompts consistently include the workspace anchor without changing persona-only behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100