Integration head f4379884 brings in the close, focus, and repaint lanes.
Only TerminalSubstrate.test.mjs conflicted: Dawn's repaint block and this
lane's tabFixture/chord block both append at the former EOF, with an empty
merge base between them. Resolved additively -- both blocks kept, repaint
first. Verified no top-level name collisions between the two blocks or
against the shared prefix, and the resolved file deletes nothing relative
to the integration parent.
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Buzz Term had no keyboard path to its tab actions: `encodeTerminalKey`
rejects every metaKey event and the substrate handled only the Cmd+J
handoff, so `onNewSession`/`onCloseSession`/`onSelectSession` existed as
props with nothing bound to them.
Add a capture-phase chord layer alongside the Cmd+J listener, gated on
`enabled && owner === "terminal"`: Cmd+T spawns, Cmd+W closes the active
tab, Shift+Cmd+Left/Right step between tabs. Matching lives in
`matchTabChord`/`stepSession` as pure functions, mirroring the existing
`matchBackForwardChord` split.
Cmd+W additionally needed the native layer. Buzz never called
`Builder::menu()`, so Tauri auto-installed `Menu::default()`, whose File
and Window submenus each carry a `close_window` item bound to Cmd+W --
and macOS resolves a menu key equivalent before the webview sees any key
event, so no JS listener could ever claim it. That accelerator was also
already wrong on its own terms: `CloseRequested` on the main window is
intercepted and turned into hide-to-tray, so Cmd+W hid the whole app
rather than closing anything, duplicating Cmd+H. `app_menu` now builds
the standard menu minus both `close_window` items, keeping Tauri's
well-known Window/Help submenu ids so `init_app_menu` still hands them
to AppKit.
Control is deliberately excluded as a chord modifier on macOS: Ctrl-W is
werase and Ctrl-T transposes, and consuming them in capture phase would
starve the PTY.
Tests pin each chord through the real window listener, plus the guards a
matcher-only test would miss: chords inert in Buzz mode, Ctrl-W still
reaching the PTY, and no repeat close on an already-closing tab. Six
mutants (dispatch removed, owner gate dropped, closing guard dropped,
Control accepted, closing-skip dropped, direction swapped) each fail at
least one test; the direction pair needs three sessions, since with two
tabs previous and next resolve to the same id.
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Switching tabs left the outgoing session's text on screen. Only the cursor
line moved, because `apply()` re-dirties it.
`TerminalGrid` drains its dirty set in `paint()`, not in `apply()` (the sole
`#dirty.clear()` is the last statement of `paint`). So an inactive session
accumulates dirty lines and repaints fine on the first switch to it -- the
failure needs a round trip. Once a grid has been painted and then deactivated,
it holds rows with an empty dirty set, and the paint effect swapped `gridRef`
without marking anything: `paint()` drew zero lines and the canvas kept the
previous session's pixels.
`markAllDirty()` alone is not sufficient. Switching to a session that has
delivered no frame yet has no grid at all, so `markAllDirty()` and `paint()`
are both no-ops on the optional chain, and only the background fill can erase
the outgoing session. The fill is therefore not grid-guarded, and it covers the
full canvas bounds rather than `columns * cellWidth`, which leaves a gutter.
Both refs are read-then-written after the effect's early returns, so a pass
that bails on a missing canvas or context keeps the switch pending instead of
swallowing it.
Four tests, each pinned by a mutant that only it kills: the round trip, the
no-frame case, a negative test that a frame on an unchanged session does not
force a full repaint (guarding the damage model against a permanently-true
`sessionChanged`), and the bailed-pass case. The suite's `getContext` stub
discarded every argument, which makes any assertion about painting vacuous;
it now records draw calls keyed by canvas element so the banner canvas cannot
be mistaken for the grid.
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
* origin/dawn/tui-scrollback:
refactor(terminal): give the scroll sign boundary its own module
fix(terminal): pin the DOM->engine sign at the boundary it lives on
feat(terminal): reach the scrollback the engine was already keeping
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
`pnpm check:file-sizes` fails on the sign tests: `terminal_runtime.rs`
reaches 1144 lines against a 1000 cap. The ratchet bases against the
merge-base with `origin/main`, where this file does not exist yet, so it
scores as a new file under the flat cap rather than ratcheting from the
986 it sits at on the branch. A real gate, not an artifact -- and Max
will meet the same wall integrating this.
Moved rather than trimmed, because the thing that grew is worth its own
file: the conversion is two characters of code and a hundred lines of
tests and reasoning about which way is back, and that reasoning reads
better away from unrelated session plumbing. `terminal_runtime` goes
back to 982 lines and the new module is 179.
A submodule of `terminal_runtime` rather than a top-level one: `lib.rs`
is at 997 of its own 1000, so a `mod` line there would spend a third of
the remaining headroom on a file nothing else needs to reach.
No behaviour change. All four sign tests run under the new path, and the
mutant matrix is re-run at this SHA to confirm the move orphaned none of
them.
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Wren caught that the one negation was executed by everything and
asserted by nothing. The frontend test proved a wheel delta reaches IPC
as `lines: -2`; the engine tests proved `scroll(+2)` goes back into
history. Neither ran the conversion between them, so deleting `-lines`
from `terminal_scroll` left both suites green and reversed the terminal
on a real trackpad. Reproduced before fixing: that mutant SURVIVED the
full `buzz-terminal` and `buzz-desktop --lib` suites.
The negation becomes `engine_scroll_lines`, a named function rather than
a `-` in an argument list, because a conversion with no name has nowhere
to hang an assertion. Three tests: the arithmetic with the direction in
the test name, an end-to-end pass that drives a real `SharedTerminal`
through the helper and asserts on the text that lands on screen -- the
unit alone would still pass if the engine read the sign the other way --
and the `i32::MIN` case.
`saturating_neg` rather than unary negation, also Wren's: `lines`
arrives over IPC, and `-i32::MIN` panics in debug and wraps back to
`i32::MIN` in release. The release case is the dangerous one, silently
scrolling the wrong way on a crafted command. Saturating keeps the
direction the caller asked for and lets the engine clamp.
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
paintTerminalBanner cleared the overlay canvas and drew glyphs, with no
background fill, and .buzz-terminal-welcome sets no CSS background. On a
real 111x46 viewport the banner covers about 38% of cells, so the grid
canvas underneath showed through the gaps. That was invisible while the
shell prompt dismissed the banner within milliseconds of spawn; now that
the splash survives until the terminal is revealed, the shell's startup
output composites into the wordmark.
Fill the overlay with palette.background before the glyph loop. Same
theme source the grid uses for its own cell background, so the splash
reads as an opaque screen instead of a translucent layer.
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
The grid retains 10k lines and nothing could move the viewport off the
live edge: the substrate accumulated trackpad deltas into whole cells
and handed them to `onScroll={() => {}}`, and there was no command on
the other side to call.
Three layers, one new idea each.
`buzz-terminal` gains `scroll`/`scroll_to_bottom`, both returning
whether the viewport *moved*. Capture becomes offset-aware by one
subtraction -- screen row r reads grid line r - display_offset, the
identity at the live edge -- and the cursor plane converts from the
grid's active-area row to the screen row the renderer paints, reporting
itself invisible once scrolling pushes it off the bottom rather than
painting a caret on an unrelated line of history.
`terminal_scroll` is the only place the two sign conventions meet. The
wire carries the DOM's sign and the negation lives there, because the
spec has to be written against `deltaY` rather than against finger
direction: macOS natural scrolling flips what the OS reports, so "swipe
up goes back" is correct for one preference setting and backwards for
the other. Against `deltaY` the rule is stable and matches the page
hosting the terminal -- the gesture that scrolls a page toward the top
of the document scrolls the terminal into history.
Republication goes through `snapshot`, reusing the resize path's
property that it does not consume damage. That is the whole defence
against the silent case: the renderer's per-row hashes describe the
screen it last saw, scrolling changes every row without changing a
cell, and a scroll that ate the full-damage flag would leave those
hashes free to suppress a row that really did change.
Snap-to-bottom lives in `terminal_input` rather than the renderer.
Output alone never returns the viewport -- the grid deliberately pins a
scrolled-back reader and piles new lines above -- so it has to be
explicit, and putting it at the PTY write covers pasted text and
encoded keys for free.
Scope is viewport scrolling: no selection, no search.
Registering the command puts `desktop/src-tauri/src/lib.rs` one line over
the size ratchet, which sits exactly at its 1000-line limit. The line
comes back from deleting `#[cfg(not(buzz_updater_enabled))] let builder =
builder;` -- an identity rebinding that does nothing on either arm of the
cfg, verified by compiling both with a control that proves the enabled
arm is really being exercised -- rather than from deleting blank lines.
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
The welcome banner was dismissed by the first visible output from the
active PTY, but TerminalBootstrap spawns the shell when AppShell mounts
and a real shell prints its prompt immediately. The banner was therefore
always gone before the user first opened the terminal.
Dismiss on the first keystroke sent to the PTY, or on visible output that
arrives after the terminal has been revealed at least once. Keystroke
alone would leave the banner painted over live scrollback when a
background job prints into a revealed-but-idle terminal.
Also stop comparing a mounted DOM node against null in the welcome test
helper: building that AssertionError message inspects the element, and
the React fiber keys make the walk take ~2 minutes. A failure in these
rows now reports in about a second.
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
## What
Two changes, both fallout/follow-up from #4289 landing:
### 1. Fix the Security job failing on main (lockfile-only)
Eight RUSTSEC advisories published today against the nostr stack turned
`cargo-deny check` advisories red on main ([failing
run](https://github.com/block/buzz/actions/runs/30761611723/job/91533106673)).
Not introduced by #4289 — the advisories landed upstream and any push to
main today would have tripped them.
- **RUSTSEC-2026-0225..0230** → `nostr` 0.44.6 → **0.44.7** (Debug
output exposing NIP-46/NIP-60 credentials; wallet parsers accepting
unauthenticated events; NIP-44/NIP-04/NIP-98 resource exhaustion; NIP-50
empty-filter panic)
- **RUSTSEC-2026-0231..0232** → `nostr-relay-pool` 0.44.2 (root) /
0.44.1 (tauri) → **0.44.3** (auth-challenge memory exhaustion;
processing of unverified relay events)
Both workspace lockfiles bumped (`Cargo.lock`,
`desktop/src-tauri/Cargo.lock`). No manifest changes.
### 2. Default the desktop GUI's sprig image to the published
`ghcr.io/block/buzz-sprig`
The first main-push after #4289 published the image publicly (package
created 18:44Z, visibility `public`). The `config_schema()`'s `image`
property now carries a `default`:
```
ghcr.io/block/buzz-sprig:sha-6530b58@sha256:17facfc7608d8ddb33bc056c9aaba1098f4ef6abe5655702fbfd7584d1f74d76
```
**Why tag+digest, not tag:** the backend deliberately rejects tag-only
references — the pod runs with the agent's nsec and tags are mutable
pointers (`image.rs` §Image). The tag+digest form keeps the
human-traceable `sha-6530b58` while the digest does the pinning;
`image::parse` already normalizes it to the tagless canonical form, so
create-intent fingerprints are identical to the bare-digest spelling.
The digest is the **multi-arch manifest-list digest** (amd64+arm64),
resolved via `docker buildx imagetools inspect`.
**This is a UI prefill, not a baked fallback:** `image` stays in the
schema's `required` list, an empty value still fails closed with a named
field, and the desktop submits the value explicitly in `provider_config`
(the `WhereToRunSection` probe seeds `providerConfig` from schema
defaults) — so deploy fingerprints never depend on compiled-in provider
state, and the spec's §K8s pod-reconciliation concern about
baked-default divergence is not engaged. Module prose that said "no
published image exists yet" is updated to match reality.
No desktop code changes needed: the form already prefills from
`properties[*].default` and submits seeded defaults.
## Testing
- `cargo-deny check` at head: **advisories ok, bans ok, licenses ok,
sources ok** (was: advisories FAILED)
- `cargo test -p buzz-backend-kubernetes`: **158 passed** (154 lib + 4
wire), including new `schema_default_image_round_trips_through_parse`
pinning the constant + its normalization, and the wire `info` test now
asserting the default is present in the provider's real stdout response
- Live provider probe: `{"op":"info"}` against the built binary returns
the default in `config_schema.properties.image.default` with `required`
unchanged (`["namespace","image"]`)
- Full workspace test suite via pre-push hook: green (earlier direct
`cargo test --workspace` run: sole failure was
`api::mesh_demo::demo_join_forwarded_arm_round_trips_echo`, the
documented pre-existing main flake — unrelated, fails on base)
- Image existence verified against GHCR: `docker buildx imagetools
inspect ghcr.io/block/buzz-sprig:sha-6530b58` resolves to the pinned
manifest-list digest with linux/amd64 + linux/arm64 manifests
---------
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
## Problem
Two related gaps in global back/forward navigation. Fixes#3775.
1. The keyboard shortcuts almost never fire in real use — users fall
back to clicking the toolbar chevrons and assume the shortcuts don't
exist.
2. On macOS, mouse back/forward buttons (X1/X2) and horizontal swipe
gestures do nothing, although they navigate in every browser and in
Slack.
**Duplicate check:** searched open PRs and issues — none found beyond
#3775 (filed alongside this fix). #3078 / #3377 are
next/previous-*channel* navigation, a different feature.
## Root causes
**Keyboard:** `useBackForwardControls`'s keydown handler bailed whenever
the event target was editable — but `useComposerAutofocus` deliberately
focuses the message composer (a ProseMirror contenteditable) on mount
and on every channel switch. In steady state focus almost always lives
in the composer, so the chords were silently swallowed. Invisible to CI
because `navigation.spec.ts` only ever clicked the `global-back` /
`global-forward` buttons, never pressed the keys.
**Mouse/swipe:** on macOS, WKWebView never delivers X1/X2 button events
or swipe gestures to the page (Safari handles them natively in the app
layer, not in page JS), and Buzz had no native handler.
## Fix
### Keyboard chords (web layer)
Match the existing platform chord regardless of the event target and
drop the editable-target guard:
- `⌘[` / `⌘]` have no text-editing semantics in macOS text fields, and
the TipTap/StarterKit editor config binds no `Mod-[` / `Mod-]` shortcuts
(checked `useRichTextEditor.ts` — list indentation is Tab/Shift-Tab).
- `preventDefault()` keeps the chord out of the editor — asserted in the
e2e test.
This matches browsers and Slack, where back/forward chords work while a
text field is focused. Chord matching is extracted into a pure helper,
`app/navigation/backForwardChords.ts`, so it can be unit tested;
behavior (bindings, modifier exclusivity, `code`-based matching for
non-US layouts) is unchanged.
### macOS mouse buttons and swipe gestures (native layer)
An NSEvent local monitor in `mouse_nav.rs` catches what the webview
can't see and emits a `mouse-nav` Tauri event to the main window
(`emit_to`, so navigation stays scoped if multi-window ever lands) that
the frontend acts on. Two AppKit event shapes map to navigation:
- `otherMouseUp` with button 3/4 — mice whose X1/X2 buttons arrive as
plain button events. These are swallowed after emitting so nothing
downstream double-handles them.
- `swipe` with a horizontal delta — AppKit's page-swipe gesture
(`swipeWithEvent:`): `deltaX > 0` back, `deltaX < 0` forward. Sent by
mouse drivers that synthesize a page-swipe gesture for the back/forward
buttons instead of button-3/4 events (the hardware this was verified
on). Stock Apple trackpad and Magic Mouse swipes arrive as phased
scroll-wheel events instead, which this PR does not handle — that path
(`ScrollWheel` + `trackSwipeEventWithOptions:`, which also needs
scroll-edge detection) is deferred to a follow-up. Swipes are passed
through (swallowing mid-gesture events could confuse AppKit gesture
tracking).
The swipe path was verified end to end on hardware whose back/forward
buttons emit only swipe gestures, never button-3/4 events — an
instrumented event monitor confirmed the events arrive as
`NSEventType::Swipe` with `deltaX ±1`, and navigation worked after
mapping them.
## Tests
- **13 unit tests** for the web-side chord matcher
(`backForwardChords.test.mjs`): supported chords, modifier exclusivity,
`code` fallback, and preservation of line-editing shortcuts.
- **6 Rust unit tests** for the native mapping helpers (`mouse_nav.rs`):
button 3/4 directions, other buttons ignored, swipe delta sign →
direction, zero-delta (gesture-begin) ignored.
- **e2e regression case** in `navigation.spec.ts`: presses the platform
chord *while the composer is focused* — the missing coverage. Verified
it fails against the pre-fix implementation and passes with the fix.
- Full desktop unit suite: 3832/3832 pass. Full Rust suite (`cargo
test`, buzz-desktop): 1888 passed / 0 failed. `pnpm typecheck`, `biome
check`, `pnpm check`, `cargo fmt --check`, `cargo clippy`: clean (no new
warnings).
- Full Playwright e2e: 958 passed; 6 failures are relay-infrastructure
tests (live relay seeding / relay state seam) that fail identically
without this change — `navigation.spec.ts` is fully green.
## Manual test
1. Open a channel, then another (composer autofocuses on each switch).
2. `⌘[` — returns to the previous channel; `⌘]` — forward again. Typing
`[` / `]` in the composer inserts normally.
3. Mouse back/forward buttons navigate the same way, from anywhere in
the window (verified on macOS on hardware using both event shapes).
## Update — 2026-07-31
Removed the redundant DOM mouse-button handler after verifying it was
unnecessary. The native macOS path remains unchanged and was revalidated
manually.
---------
Signed-off-by: npub1yvnq5equak5errqpku8stskushny9wsvt0fc2ywcpwt79yslwaqswe7tse <23260a641ceda9918c01b70f05c2dc85e642ba0c5bd38511d80b97e2921f7741@buzz.block.builderlab.xyz>
Signed-off-by: Matheus Iser <matheusiser@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1yvnq5equak5errqpku8stskushny9wsvt0fc2ywcpwt79yslwaqswe7tse <23260a641ceda9918c01b70f05c2dc85e642ba0c5bd38511d80b97e2921f7741@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
# Kubernetes backend plugin (crates/buzz-backend-kubernetes) + desktop
deploy path
Implements docs/remote-agents.md (merged @ 28ae6cd21) as ONE PR: the
provider
binary, the desktop changes that make it work, the harness inactivity
reaper,
the Sprig image, and the conformance/live-test suites.
Channel: buzz-remote-agents (29414326-dba7-402d-b384-b1b34d63a2e6),
thread c42b70ef.
## What's here (by lane)
- **crates/buzz-backend-kubernetes** (Dawn): stdin/stdout JSON provider,
info +
deploy; pure classify.rs (one match arm per spec state-machine row);
reconcile/GC with ownership-marker gate + same-clock orphan check;
per-attempt
immutable Secrets; three-tier env with clear-then-write authoritative
tier.
- **Desktop** (Mari): KD3 launch block from resolved descriptor, KD5
pre-secret
negotiation gate (resolve-once → stage-and-digest → info → protocol gate
→
deploy), KD1 Windows extension strip, bundling (externalBin + Justfile +
release/canary workflows + stub loops), tauri.windows.conf.json platform
override (Decision B: no Windows artifact).
- **buzz-acp** (Max): KD4 BUZZ_ACP_EXIT_AFTER_INACTIVITY reaper
(pool-independent;
reset only at accepted dispatch; in-flight turn/heartbeat defers, never
resets);
BUZZ_ACP_EXIT_AFTER_INACTIVITY + BUZZ_ACP_NO_PRESENCE reserved. KD8 fix.
- **Image + tests** (Perci): Dockerfile.sprig (digest-pinned bases, exec
buzz-acp
PID 1, relay-scoped credential config), image contract script, provider
conformance suites (golden wire fixtures shared with desktop tests),
live-local
runbook (namespace-scoped, shared-cluster safe).
- **Docs** (Sami, first commit): citation re-pin c1bca1b56 → 28ae6cd21
(44/49
were already byte-exact; 3 offsets fixed) + I3 presence-bound correction
(below).
## Named spec deviations (deliberate, each with rationale)
1. **No baked default image yet.** ghcr.io/block/buzz-sprig is
unpublished
(verified: anonymous pull 403 vs control 200). Omitted `image` returns
an
in-band field-required error instead of a default.
2. **Image override STRICTER than spec §Image:** digest-only
(`name@sha256:<64hex>`); ALL tags rejected; `name:tag@digest`
normalized.
With no baked default the override is the only path, so tag-acceptance
would
make mutability the v1 norm. Strictness is reversible; a moved tag under
an
nsec is not. Baked digest default + tag re-acceptance = follow-up with
image
publish.
3. **imagePullSecrets not in schema (v1).** Explicit user images may
rely on
namespace-preprovisioned pull credentials — the substrate boundary.
Field
added only if the publish decision proves it necessary. 9-field budget
intact.
4. **Decision A closed: writable empty workspace.** Nest projection =
named
follow-up; no image-side scaffolding.
5. **Decision D overridden by Tyler (event b55398d8):** provider ships
bundled
with the desktop like buzz-acp/buzz-agent; spec §Distribution's separate
release workflow deleted for v1.
6. **I3/vision presence bound corrected 90s → 180s.** PRESENCE_TTL_SECS
moved in
#3783 during this spec's base→merge window; the number was inherited,
not
chosen. Spec :206/:216/:928 + inline quote + VISION_REMOTE_AGENTS.md:59
corrected. ← Tyler: the vision is your document; this edit is flagged
for
your explicit eyes.
7. **Spec citations are pinned to 28ae6cd21** (main at spec merge) and
resolve
there, not at this PR's head — this PR's own lanes move
crates/buzz-acp/src/lib.rs by ~100 lines (19 citations across
KD4/KD6/KD7/
§Stop/§Launch data). Known Defects rows fixed BY this PR retire on
merge;
the section documents main as of the pin.
8. **KD7 grace tension declared:** pod terminationGracePeriodSeconds=60
vs
KD7's measured ~87s shutdown tail at parallelism 10 (~197s at cap 32).
KD7 is ruled out of scope, so L1-3's "enough grace for full graceful
shutdown" is NOT met at default config — deliberate, resolved by the KD7
follow-up, not silently.
## Question for Tyler
Will ghcr.io/block/buzz-sprig publish PUBLIC? If private-by-policy,
§Image needs
an imagePullSecrets story before the baked-default follow-up can land.
## Out of scope (named follow-ups)
KD6 exit-code contract + KD7 shutdown budget (gate OnFailure), OnFailure
restart
policy, Windows provider binary, PVCs/nest projection, mesh
deployability,
sprig image publish workflow + baked multi-arch digest default.
## Reproduce locally (four traps that cost us real time)
**1. Git hooks inherit the invoking shell's PATH — pin the shell, not
just your
verification commands.** `rust-toolchain.toml` pins `1.95.0`, but the
rustup shim
that honors that pin lives in `~/.cargo/bin`. If Homebrew's cargo is
earlier on
PATH, `cargo` in this repo is 1.89.0, which cannot build the workspace
at all:
```
$ /opt/homebrew/bin/cargo check -p buzz-db
error: rustc 1.89.0 is not supported by the following packages:
sqlx@0.9.0 requires rustc 1.94.0
... # exit 101
```
Verifying with `PATH="$HOME/.cargo/bin:$PATH" cargo test` does *not*
protect the
push: lefthook's `pre-push` → `just test-unit` re-resolves `cargo` from
the
shell's own PATH, so a green local run is followed by a hook failure on
a crate
you never touched. Export the PATH for the whole shell, not per-command.
This
bit twice.
**2. Line-scope your mutations, or the mutation edits its own
detector.** When
mutation-testing the respond-to guard, a whole-file `sed` on the mode
literal
touches 5 sites — the guard *and* the fixtures/assertions that test it.
The
mutation and its detector move together and the suite stays green, which
reads
as "this code is dead" when it actually means "you deleted the
experiment":
```
# WRONG — 5 sites, guard and tests mutate together
$ sed -i '' 's/"allowlist"/"allowlist-DISABLED"/g' src/env.rs
test result: ok. 145 passed; 0 failed # false survivor
# RIGHT — 1 site, anchored to the guard's own definition line
$ sed -i '' '/^const RESPOND_TO_ALLOWLIST/s/"allowlist"/"allowlist-DISABLED"/' src/env.rs
failures:
env::tests::allowlist_mode_with_an_empty_list_is_refused
env::tests::an_allowlist_entry_that_is_not_64_hex_is_refused
test result: FAILED. 143 passed; 2 failed # real kill
```
Restore by copying a pristine file back and confirming `git diff --stat`
is
empty, not by re-running an inverse `sed`.
**3. A completeness guard is not a correctness guard.** The shared wire
fixture
`tests/fixtures/provider-wire/deploy-full-launch.request.json` passed
every test
we had while containing four classes of invented data (wrong
`respond_to`
encoding, an env key no emitter writes, allowlist entries that fail the
harness's own 64-hex rule, a `launch.env` key from no descriptor layer).
The
provider's tests could not have caught this: its types are deliberately
indifferent to these values (`Option<String>`, `Vec<String>`, arbitrary
map), so
"the provider parses it" was never evidence that the desktop emits it.
The fix
was not a stronger provider assertion but a rule about provenance —
"recorded"
means executed-and-transcribed, and the desktop's whole-object equality
test is
the only enforcement that can exist. See the fixture README.
**4. Every drift this arc was a value that agreed with itself.** Five
invented
values were found, and not one was caught by an assertion failing — each
was
caught by someone asking where a value came from. A named constant
referenced
symbolically on both the fixture and assertion side. A `sed` that
mutated its
own detector. Six probe rows that all died at the same unrelated error.
A
descriptor struct literal compared against a fixture built from that
literal
(`launch.args: ["run","--session"]`, which the resolver actually returns
as
`["acp"]`). The general defense is not more assertions but provenance: a
stub is
a control that varies nothing, and the more faithful it looks the better
it
hides. Ask what executed, not what passed.
*Fixture-test determinism caveat (post-verification, Quinn + Dawn).* The
desktop's whole-object fixture test calls the real resolver, which
consults a
process-global harness registry whose own docs require
`registry_test_lock`
for any test touching it. The fixture test holds no lock and is
nonetheless
deterministic — but by containment, not by ordering. Measured, not
derived:
planting a definition with `id: "goose"` directly into the registry
(bypassing
the loader) changes the resolved descriptor from `args: ["acp"]` to
`args: ["--poisoned"]`, so `resolve_effective_harness_descriptor`
**does**
reach the registry for this id — it does not short-circuit on the
builtin
table first. Two controls discriminate: an empty registry and a registry
poisoned under a *different* id both return `["acp"]`. What actually
protects
the test is that the registry has exactly one writer
(`update_loaded_harness_registry`, reached only via
`warm_harness_registry_from_dir`) — but that writer concatenates **two**
sources of unequal strength (`custom_harnesses.rs:319-326`). Custom
files
pass through `load_custom_harnesses`, whose `check_id_collision` rejects
the
reserved builtin id `goose` case-insensitively at the loader — and that
leg
is tested (`load_applies_id_collision_check` writes a real `goose.json`
and
asserts the loader drops it). Preset definitions
(`preset_harness_definitions`, `presets.rs:177-193`) are a bare `.map`
over
`PRESET_HARNESSES` with **no collision check** — exhaustive call-site
enumeration at `60007fda4` finds four production `check_id_collision`
sites,
none on the preset path. That leg holds only because `goose` is not in
the
preset table today (intersection of TIER1 and preset ids is empty) —
executed, not just read: adding a preset with `id: "goose"`,
`args: ["--poisoned"]` and warming via the normal preset-only path
(`warm_harness_registry_from_dir(None)`, no custom dir, no direct
writer)
flips the fixture's emitted `launch.args` from `["acp"]` to
`["--poisoned"]`
at `60007fda4`, command/env/policy_env unchanged. So: no test in the
suite
can put a `goose` entry in the registry
via the custom path, and no preset currently carries one, so no
interleaving
can perturb this fixture — containment with one checked leg and one
coincidental one. A future fixture built on a **non-builtin** runtime id
has
no containment at all — it would be order-dependent against whatever
registry-writing test ran last and must take the lock.
*Late instance, found while reviewing the mode guard.* The guard
exact-matches
`respond_to` untrimmed and case-sensitively, which is only correct if
clap's
`ValueEnum` derive is case-sensitive. `config.rs` gives two answers: the
derive
at `:448-453` carries no `ignore_case`, while the crate's own tests call
`RespondTo::from_str(s, true)` — `ignore_case = true`. Reading the
source
supports either. Measured on the built binary instead: `owner-only`
starts,
`OWNER-ONLY` / `Owner-Only` / `ALLOWLIST` / `NOBODY` all exit rc=2
`invalid
value`. Case-sensitive at the CLI, so the guard is right — and right for
a
reason the source does not state. The `from_str(_, true)` tests exercise
a
different surface and are not evidence about the CLI.
*Corollary, and the sharper half.* When a test helper **reimplements**
production instead of calling it, the helper is a fork — and a fork can
be
right while production is wrong, or wrong in the same way, and the suite
reports green either way. Both `BUZZ_ACP_ALLOWED_*` gates are forked
like this:
production compares **strings** while the helpers compare **post-parse
enums**
(`config.rs:2623`) or re-derive the split
(`buzz-cli/.../channels.rs:1296`).
Production and the helper each carry their *own* copy of the empty-entry
filter
(`:1025` and `:1300`), so fixing one says nothing about the other.
Measured on
`buzz-cli`, restoring byte-exact between runs:
| tree | result |
|---|---|
| baseline | 274 passed |
| drop the empty-filter in **production** only (the real fix) | **274
passed** — no signal |
| drop it in the **test helper** only | **273 passed, 1 failed**
(`channels.rs:1338`) |
Two independent defects, stacked, and worse together than either alone:
production can be fixed with no test ever noticing, *and* the helper
cannot be
corrected without a false alarm demanding the bug back. The root cause
is one
bit of type information — `check_allowed_channel_add_policy(allowed_raw:
&str,
..)` cannot represent "unset", while production reads `env::var(..) ->
Result`,
where unset and `""` are different states. A helper whose parameter type
can't
represent all of production's input states isn't testing production's
states —
it's testing a subset it silently chose. Same family as the
struct-literal
descriptor and the fixture drift: the test and the thing it tests
agreeing with
each other, rather than the test measuring the thing. Neither defect is
in this
PR's diff (`git diff --name-only 28ae6cd21 <head> -- crates/buzz-cli` is
empty); both are now filed as NIP-34 issues on this repo: the fail-open
+
fork-helper defect at issue event `0524a4113f2d97fd…` and the respond-to
self-lock at `e32837498969b5e7…` (filed 2026-08-02 after Quinn measured
that
no prior filing existed — zero hits on GitHub `block/buzz` open *or*
closed
and zero on the relay's kind:1621 issues, against working positive
controls). The prescription was itself
mutation-tested before being written down: repairing the fork's
signature
(`Option<&str>` + assertion → `None`) still let the reintroduced
production
bug ship 274-green — an expressive fork is still a fork; it never
executes
production. So the `buzz-cli` fix has **three parts and one explicit
keep**:
drop the production filter; **delete** the helper and point its tests at
the
real `cmd_set_add_policy` (which self-discriminates by error variant —
`Usage` = refused, `Network(BadScheme)` = passed the gate — no relay
needed);
serialize the env-var tests behind one
**`tokio::sync::Mutex::const_new`**
lock taken with `.lock().await`, including the pre-existing `:1362`
integration test (the fork was silently buying test isolation — without
the
lock, parallel runs flake nondeterministically; a `std::sync::Mutex`
held
across `.await` trips `clippy::await_holding_lock` under `-D warnings`);
and
**keep** the then-dead `!allowed.is_empty()` clause with a comment
saying
why. It is unreachable-false (`split(',')` never yields an empty vec),
but it
is the only thing that keeps the reintroduced production bug detectable
—
mutation-tested: on a tree that deletes the clause, reintroducing the
empty-filter bug survives 275/0, because `""`/`","`/`" "` refuse either
way
and the filter goes semantically inert. Dead code can be load-bearing
for
tests: "provably unreachable" is an argument about behavior, never about
coverage. When a helper forks production, the fix has to delete the
fork:
any change that leaves two implementations standing can only ever be
verified against the one the tests call. *Final shape:* the keep and the
broad lock are both artifacts of the fork surviving in some form. The
extraction variant (Dawn, mutation-tested at `60007fda4`) removes the
tension: extract one `check_channel_add_policy_allowed(Option<&str>,
&str)`
that **production calls**, with the `Option` placed at the env boundary
where the `Result<String, VarError>` bit actually lives. 5/6 mutants
killed; the empty-filter survivor is proven **equivalent** (exhaustive
6174-pair check, 0 divergences, with a diverging negative control;
independently re-derived by a second generator — different tokens and
shape — 0 divergences on admitted policies, 500 on a non-admitted
control),
not a coverage hole — on a one-implementation tree there is no fork left
to
witness, so no dead clause needs keeping. One scope line on that
equivalence: it is **caller-conditional**, a property of the only
current
caller, not of the gate function — `cmd_set_add_policy`'s own match at
`:1027-1034` admits only three policies before the gate runs; a second
caller reaching the gate with arbitrary strings resurrects m1 as a real
hole. The lock does not disappear, it
narrows (Dawn's own correction, caught by Mari): lock exactly the tests
that mutate the process env — three-plus-one on a fork tree, two on the
extraction tree — behind one `tokio::sync::Mutex`, and the lock is part
of
the assertion, not hygiene: with it deleted, the gate test fails 8/8
runs
deterministically by receiving `Network(BadScheme)` where it expects
`Usage` — the unset test's `remove_var` clobbers the other's `set_var`,
and
**the gate test passes straight through the gate**, a false negative on
the
exact authz assertion the test exists to make. State it as an outcome:
these two tests must not observe each other's env writes. 276/0 stable
across 5 parallel runs, clippy `-D warnings` clean; independently
verified
(patch applied to a second worktree: result blob `d67e584be` matches the
patch index, full mutant matrix reproduces row for row). One new row no
earlier
prescription covered: collapsing unset into `Some("")` fails **closed**
—
an unconfigured deployment refuses every policy — killed by the unset
test.
Patch: `OUTBOX/BUZZ_CLI_ADD_POLICY_GATE_EXTRACT_FIX.patch`. The filed
issue
(`0524a411…`) carries the fork-shape prescription; whoever picks it up
should prefer the extraction shape, drop the dead-clause keep with it,
and
keep part 3 outcome-shaped: serialize whichever tests mutate the env.
## Verification (final HEAD `60007fda4`)
- Full touched-package suites at each integration merge (log in plan
file).
At candidate parent `00e5b5fe9`: buzz-backend-kubernetes 154,
buzz-acp 673, desktop tauri 2100+3, pnpm 3908, workspace clippy/fmt/tsc
all clean. The only delta to `60007fda4` is one character in
`scripts/test-k8s-sprig-image-live.sh` (heredoc escape so the readlink
probe evaluates pod-side, not host-side at render); `crates/` tree hash
is byte-identical at both SHAs, so the Rust receipts attach by tree
identity. buzz-backend-kubernetes suite re-run in-shell at
`HEAD == 60007fda4`: 154 passed.
- Adversarial one-HEAD gate (Sami): guard matrix 12/12, predicate
mutants
7/7, doomed-invocation finding closed end-to-end; tree-hash carry to
`60007fda4` confirmed (crates/buzz-backend-kubernetes blob unchanged).
- Live-local pass per TESTING.md + skill-buzz-testing (Perci, at
`60007fda4`): explicit `docker-desktop` context, digest-qualified image
imported into node containerd `k8s.io` namespace, pull policy `Never`;
pod printed `DIGEST_ABI_OK`, `resolved_spec` and `image_id` both the
exact requested digest, script exit 0. Dedicated per-run namespace,
ownership labels on every object, scoped cleanup verified empty after.
- Implementation review (Wren) at `60007fda4`: 9.6 minimalness /
9.4 elegance / 9.3 correctness, no blocker.
- `origin/eva/k8s-backend` == `60007fda4` (ls-remote verified; SHA
identity is byte identity).
---------
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Tal here, human. Trying to help. This bug bugged me...
## Summary
A repository's first branch becomes its symbolic `HEAD`, and Git's
bare-repository default rejects deleting that branch even when another
branch survives.
This change:
- sets `receive.denyDeleteCurrent=ignore` only for the ephemeral `git
receive-pack` process
- preserves the existing server-side `core.hooksPath` override and
authorization hook
- lets the existing CAS publication logic select a surviving branch as
the next manifest `HEAD`
- adds regression coverage using a real stateless `git receive-pack`
request and a manifest HEAD-selection test
This lets users replace an accidental default branch without deleting
the object-storage manifest pointer.
### Related issue
Fixes#3572
### Testing
- `cargo test -p buzz-relay api::git::` (128 passed, 5 ignored)
- `just ci`
- live E2E roundtrip against a release relay with PostgreSQL, Redis, and
MinIO:
- created a repository through signed Nostr events
- verified authorized pushes and rejected unauthorized clone/push
- pushed a surviving `master` branch
- deleted the active `main` branch over authenticated Smart HTTP
- freshly cloned the repository and verified `master` became HEAD,
`origin/main` was absent, and repository content remained intact
Signed-off-by: Tal Weiss <major.tal@gmail.com>
Two CI failures in buzz-terminal, both platform-shaped.
lifecycle_tests::reader_drains_through_termination_and_reap took 10.008s
on Linux and passed on macOS. The test never dropped `pair.slave`, so the
master could not reach EOF: a PTY master stays readable while any process
holds the slave open, and the test itself was one. Darwin ends the read
when the session leader exits, which hid the retained slave on the
platform the test was written on. The runtime already drops the slave
(terminal_runtime.rs:441); the test now mirrors it. 10.009s -> 5.24ms.
The timing assertion is also narrowed to the instant `stop` is called, so
it measures child termination rather than reader teardown -- the stall was
misreporting itself as a wedged child. Narrowing alone would make the test
blind to a wedged reader, so `join`'s silent deadline abandon becomes an
assert: with a wedged-reader mutant and the narrowed clock, the old
`return` passed green in 10.07s.
`--all-targets` compiles `#[cfg(test)]` modules, so a Windows check built
env_fence_tests and lifecycle_tests, which drive real PTYs, `libc::kill`,
and unix permission bits (E0432/E0433/E0425). Both modules are now
`#[cfg(all(test, unix))]`. context_tests is pure string logic and stays
portable.
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
* commit 'e1eaf88b0d7c9b2f9922ffea6a2cbd47682e746d':
docs(terminal): cite the discriminating tail payload
docs(terminal): say why tail_full cannot fire, not just that nothing calls it
test(terminal): require the runtime to pump deferred work
docs(terminal): say that the tail-depth signals have no consumer yet
docs(terminal): repoint the links the slice_bytes deletion broke
test(terminal): reject stop before child reap
refactor(terminal): delete the slice-sizing function nothing calls
test(terminal): make the decrease and RIS arms assert what they claim
fix(terminal): retain the scrollback debt a shrink does not immediately repay
fix(terminal): repair three defects the gate found in the work-bounded seam
fix(terminal): raw-drain while child exits
feat(terminal): bound the lock hold by weighted work, not by bytes
test(terminal): close the review gaps in the cluster and snapshot contracts
feat(terminal): give an attaching subscriber the screen as it stands
feat(terminal): give the renderer each cluster's true column
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
The comment cited 1 MiB of RIS as evidence the tail high-water is zero. At
1 MiB the *deleted-pump* arm never reaches TAIL_CAP either, so the number
proved nothing: both worlds report "never full" and the measurement agrees
with whatever it was pointed at. 4 MiB is also short -- a 16 KiB read of RIS
defers 16382 bytes, not 16384, so it stops 512 bytes under the cap. 8 MiB is
where the counterfactual fires.
Also disambiguates the read count. 257 is reads *completed*; the 256 in the
review thread was a zero-based loop index, and a comment that doesn't say
which invites the next probe to disagree with it by one.
Found by Sami, in wording he had written and I had shipped -- I hit the same
defect on my own probe an hour earlier, fixed my run, and copied his number
across without noticing it had the flaw I'd just corrected.
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>