Skip to content

feat(desktop): scope agent activity and relay model control - #6993

Open
Jari81 wants to merge 76 commits into
block:mainfrom
Jari81:windows-integration
Open

Jari81 wants to merge 76 commits into
block:mainfrom
Jari81:windows-integration

Conversation

@Jari81

@Jari81 Jari81 commented Aug 28, 2026 •

Copy link
Copy Markdown

Summary

This PR publishes the current cumulative windows-integration lineage. The new slice implements Kanban tasks t_1c0a152b, t_10383b9c, and t_5702bffb on top of the already-built Windows integration commits.

  • scope live observer activity and persisted context usage to the active conversationRoot / threadRoot
  • preserve the documented channel fallback for rootless legacy frames and channel scope for DMs
  • render thread-specific working indicators and context rings without sibling-thread leakage
  • request relay model catalogs with request correlation and subscribe-before-send ordering
  • add a bounded relay-agent model picker to the activity bar
  • scope model switching by channelId + conversationRoot; distinguish unsupported and ambiguous in-flight outcomes
  • live-switch acknowledgements use one atomically evaluated multi-target control with per-request correlation and allowlist only the real Rust success statuses (sent, switched, turn_ending); unrelated running conversations, malformed targets, replayed frames, mismatched requests, and unknown statuses fail closed
  • scope turn termination and activity headlines by conversation root while preserving the documented rootless legacy fallback
  • reset relay model-picker state across agent/channel/thread changes and make session-config capture subscriptions safe for co-mounted bridges
  • keep the cumulative branch inside repository file-size limits through focused helper/module extraction

The branch also contains the previously verified Windows integration lineage already pushed through 4d8002f11; this PR does not pretend those 24 cumulative commits appeared by magic.

Verification

  • targeted regression suites: 155 passed, including atomic multi-target switching, request correlation, replay deduplication, sibling-thread termination/headline isolation, picker remount, and multi-subscriber lifecycle coverage
  • sabotage checks: scoping, sibling-result isolation, atomic target evaluation, request correlation, replay handling, and real/unknown switch-status handling fail under restored old behavior
  • full Desktop test suite: 5,085 passed, 0 failed
  • buzz-acp tests: 852 passed, 0 failed
  • pnpm typecheck: passed
  • pnpm check: passed
  • pnpm build: passed
  • Tauri cargo check --workspace: passed
  • Tauri lib tests: 2,449 passed, 0 failed, 15 ignored
  • cargo fmt --all -- --check: passed
  • git diff --check: passed
  • added-line security scan: no findings
  • independent Claude cross-model review: final PASS deferred by Jari's explicit publish override after the reviewed multi-channel blocker was fixed test-first; any later findings become follow-up issues

Risk and compatibility

  • legacy observer frames without a conversation root intentionally remain channel-visible
  • DMs remain channel-scoped
  • relay model catalogs are bounded and failures degrade to an unavailable/timeout presentation
  • Jari explicitly deferred the final independent Claude PASS; later findings will be tracked as follow-up issues
  • upstream DCO is red because two older commits already present on the cumulative Windows line (594842150976, 7a61f7a5017a) lack trailers; the new candidate commit is signed off and history was deliberately not rewritten during delivery
  • no migration or destructive storage operation
  • no merge or production deployment is performed by this PR

Kanban

  • t_1c0a152b
  • t_10383b9c
  • t_5702bffb

Robi Hermes and others added 26 commits August 16, 2026 21:30
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Co-authored-by: Brad Groux <bradgroux@hotmail.com>
Signed-off-by: Brad Groux <bradgroux@hotmail.com>
…read

On Windows the notification plugin's injected shim stamps `denied` at
startup without ever consulting the backend, whose `permission_state()`
returns `Granted` unconditionally on desktop. Its bootstrap short-circuits
on the `__TEMPLATE_windows__` literal and compares its own freshly
initialised `'default'` against `'granted'`.

`getDesktopNotificationPermissionState()` trusted that value, so the
mount-time read reported `denied` and the settings effect turned
`desktopEnabled` off and persisted it on every launch -- which is why
fixing only the enable path leaves a Windows user re-enabling alerts
after each restart.

Repair the value at the read instead of reporting it. Inside the Windows
Tauri app a `denied` reading is never a real denial, and
`requestPermission()` is the only call that reaches the backend and
rewrites the shim's cached value. It is self-limiting: the next read
short-circuits as `granted` and never requests again.

Repairing at the read rather than at the toggle also covers
`sendDesktopNotification()`, which gates delivery on `=== "granted"` and
would otherwise drop notifications at boot.

`denied` stays terminal on macOS, on Linux and on ordinary Windows web
pages, and a denial that survives the request is still reported as
`denied`.

Refs block#2445

Signed-off-by: VSCteam <144167621+Joxyko@users.noreply.github.com>
PR block#2483's e2e addition wasn't run through this fork's biome config;
pnpm check --write reflows the long JSON.stringify argument. No
behavior change.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
BUZZ-DESKTOP-003. Two related rendering gaps on the fork:

1. In DM channels the composer activity bar stayed empty even while
   observer frames arrived (sidebar working badge proved it).
   getChannelAgentSessionAgents filtered relay agents by the declared
   channel_ids of their kind:10100 profile, and DM channels never
   appear there, so every relay agent fell out of activityAgents in
   its own DM. For DMs the channel participantPubkeys are now the
   authoritative admission signal.

2. Thread panels fed threadComposerBotTypingPubkeys only from bot
   typing entries (kind 20002), never from the observer store, so
   threads rendered "is typing" at best. Merge the channel-level
   working set (observer-derived turns + typing) so threads show the
   same activity bar as the main composer. Observer frames carry
   channel scope only, which is the finest resolution available.

Both changes are additive desktop-only filters; relay and harness are
untouched. Unit tests: 4975/4975 pass (9 new), typecheck clean.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Pure function building block for the planned Agent Sessions panel
(session tree grouped across agents). Not wired into any UI yet.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
BUZZ-DESKTOP-003 phase 1. Built on the notification-fix branch so the
next canary installer carries both.

- Agent Sessions panel behind preview flag agentSessions: sidebar
  button, /agent-sessions route, session tree (deriveAgentSessions,
  cherry-picked from Claude-Host WIP 9869cf8) + existing transcript
  components, per-session model/context%/cost header, stuck heuristic
  (open turn idle >15 min).
- DM context ring in the composer toolbar: fill = latest archived
  kind:44200 turn.inputTokens for the channel / model context window;
  color steps at 60%/85% to surface degradation before the window
  fills.
- Data layer reads decrypted kind:44200 payloads via the existing
  owner_p archive path — no Rust changes. modelContextLimits.json
  ships ~60 known windows (Claude/GPT/Alibaba token-plan Qwen/
  Gemini/DeepSeek/Llama/Mistral) with prefix matching and the
  200k->1M observed-max heuristic.
- Empirically verified 2026-08-20: Claude-Host/Claude-Review/Robi
  publish kind:44200; Claude-DEV had zero all-time (harness-side
  detection issue, fixed separately as BUZZ-ACP-017).

Tests: 5003/5003 unit (18 new), tsc clean, biome clean.
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
…pick bc51952)

The sessions-panel build (feat/agent-sessions-panel) branched from
fix/windows-notification-permissions, which predates the integration
branch carrying PR block#5881 (membership-first mentions). The new canary
client therefore fell back to directory channel_ids-only eligibility:
agents added to a new channel were members but not mentionable.

Cherry-picks bc51952 (authorize external member mentions) onto the
sessions-panel branch: live channel membership now admits relay agents
into the mention picker even when their kind:10100 profile is stale;
response policy and allowlist remain enforced, send-boundary
revalidation still fails closed.

Regression 2026-08-21: Claude-Host added to 2 new channels -> member
in both, absent from @-mention picker until profile re-publish. Fix
removes the profile-dependency entirely (per LOCAL_MODIFICATIONS
BUZZ-DESKTOP-002).

Tests: 39/39 focused unit tests green, tsc --noEmit clean.
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
…ry-pick 8ca8852)

Second half of the documented BUZZ-DESKTOP-002 contract
(LOCAL_MODIFICATIONS.md: patches/buzz-desktop-pr5883-pr5881.patch) was
missing from the sessions-panel branch — only block#5881 (membership-first
mentions) had been restored. This cherry-picks upstream PR block#5883 so
external kind:10100 relay agents are visible and inspectable again in
the Agents view.

Cherry-pick applied clean on feat/agent-sessions-panel.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Kills stuck/dead ACP sessions from the sessions panel: publishes the
harness '!rotate' owner command (kind 9, content exactly '!rotate',
agent p-tag mention) into the session's channel. buzz-acp consumes it
owner-side — in-flight turns are cancelled and the channel session
invalidated; idle channel sessions are dropped immediately, so the next
mention starts a fresh session.

Motivation: session corpses (e.g. a crashed turn leaving a 'stuck?'
row in the tree) had no cleanup path from the UI; today only manual
CLI '!rotate' messages or a service restart clear them.

- RotateSessionButton in the session header, confirm dialog, toasts
- isRotateSessionAuthorized lib gate: enabled only for identities in
  the agent's respond-to allowlist (harness stays owner-authoritative
  either way); unlisted agents stay ungated, harness rejects non-owner
- 8 unit tests green, tsc clean

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
issues get/list now include status-transition events (kind 1630-1633)
with content, making `buzz issues status --content` readable — the
commentary was accepted by the relay but had no CLI read path.

repos delete removes one's own repo announcement via a head-based
kind:5 tombstone, mirroring projects delete (submit + re-query verify).
The relay rejects events whose created_at drifts more than ±15 minutes
from server time. Both projects delete and the new repos delete built
the tombstone at head.created_at+1, which fails for any head older than
~14 minutes. Use max(head+1, wall-clock) instead.
The Windows client refused any download above 50 MiB while the relay
accepts generic-file uploads up to 100 MiB (buzz-media
default_max_file_bytes). A 51 MiB installer uploaded cleanly and was then
undownloadable by the same client: "file too large (51 MiB, max 50 MiB)".

Raise MAX_DOWNLOAD_BYTES to 104_857_600 so the two caps match, and split
the clipboard decompression-bomb guard onto its own constant. That guard
bounds the decoded RGBA buffer rather than the transferred bytes, so it
must not grow just because the transfer cap did; it stays at 50 MiB.

Buzz-Issue: 000717741d1590ca9112465b1fbcdce79fc8d4df6e5ec12a22c4557e442a32d8
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
The desktop could read NIP-34 issue status but never write it: pull
requests have lifecycle buttons, issues had a static badge, and the only
way to move an issue was `buzz issues status` on the CLI.

The status badge in the issue meta rail becomes a picker that publishes a
status event (1630 Backlog / 1633 Triage / 1631 Done / 1632 Closed). The
event carries the repo coordinate in an `a` tag, which is load-bearing:
fetchProjectIssues reads status events with an `#a` filter, so an event
without it is invisible to the desktop and the issue silently falls back
to Backlog.

"In Progress" and "In Review" stay label heuristics with no status event
behind them, so they can be the current state but are never offered as a
choice.

The picker follows the same trust rule the parser applies
(allowedActorsForRoot): only the issue author and the repo owner. When the
repo owner is a managed agent, sign_project_issue_status signs on its
behalf — the mirror of sign_project_pull_request_status.

Also records statusCreatedAt on the parsed issue so a second change inside
the same whole-second Nostr timestamp still outranks the one it replaces.

Buzz-Issue: ed272cd3c0c168edf4270f28382e29c12587c2a201013ba6ca3a8882e16292c5
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
The status picker and the status read path trusted only the issue author
and the repo owner. Handlers (assignees) who actually work a ticket could
not move it.

- canChangeProjectIssueStatus now accepts anyone assigned to the issue
- latestStatusForIssue honors status events published by assignees, so a
  handler's status change is not silently discarded on re-read
- eventToProjectIssue resolves assignment state before status so the
  assignee set is known when the status event is checked
- assignments keep their existing authority rules; strangers are still
  ignored

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Each session row in the Agent Sessions panel now carries two icon buttons:

- Reset (RotateCcw): sends !rotate to the session's channel, owner-gated
  exactly like the former transcript-header button, which this replaces
- Kill (Trash2): removes the session from the list on this device

Kill is a display-level removal — observer frames are immutable relay
data. Killed session ids persist per identity in localStorage; the agent
counter and empty state follow the visible set, and killing the selected
session clears the transcript view. Reset keeps its confirm dialog.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Drag-drop/paste uploads failed with 'raw upload requires a byte body' for
any file type (e.g. PDF). Tauri's webview IPC prefers a custom-protocol
fetch that carries raw bytes, but after a single failure of that transport
(e.g. AV/VPN interception of ipc.localhost) it permanently falls back to
postMessage, which JSON-serializes the byte payload into a number array.
The renderer sends the same call; only the transport changed.

upload_media_bytes_raw now recovers the bytes from that JSON number-array
shape instead of rejecting it, with strict validation (integers 0-255 only).
The raw fast path is untouched while it works. Adds unit tests covering
recovery, out-of-range numbers, negatives, and non-array bodies.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
- filter aggregate issues by repository and show full active groups
- prioritize review-ready workflow evidence over historic approval labels
- reset channel issue detail navigation and expose a visible back action
- place Channel Issues before Buzz Terminal in the channel header
- cover repository filtering, status precedence, navigation, and action order

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Render the channel issue panel header above the shared channel chrome so Back to issues and Close remain visible. Add a regression assertion for the z-index contract.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
The desktop can already switch a running agent's model over the relay
(`switch_model`), but it can only learn which models exist from
`session_config_captured`, which is emitted on session creation. An agent
that has not run a turn since spawn therefore has no catalog on the
desktop side, and a picker fed only by that frame comes up empty.

Add a read-only `list_models` control frame that answers from the
catalog buzz-acp already caches on the agent, flattened across both ACP
dialects so the desktop sees one shape and every id it is offered is an
id the switch path can resolve. `providers` narrows the answer to given
provider prefixes.

The catalog is read process-wide rather than per channel, matching the
assumption `switch_idle_agent_model` already relies on when it validates
a pick against every worker. Statuses reuse that path's vocabulary
(`catalog_unavailable`, `no_active_turn`) so the desktop learns one set
for both frames.

The response caps its own list. This is not defensive padding: measured
against kilo 7.4.11, the model catalog is 698 entries / ~60 KB against a
65,535-byte observer budget, and the generic oversize path cannot help,
because it elides string leaves with a 3 KB retain floor while model ids
are 20-50 bytes. An oversized catalog is therefore not trimmed but
replaced wholesale by the "payload too large" stub. Truncating with an
explicit flag is the difference between a short list and no list.

No session is created and no agent state changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
BUZZ-ACP-019 gave every thread in a group channel its own ACP session but
left `switch_model` keyed on the channel. Two defects followed.

First, blast radius. The idle path called `invalidate_channel`, which drops
the session, turn counter and delivery ledger of *every* conversation in the
channel. That was harmless while a channel had exactly one session; after
the split it means switching the model from one thread silently resets every
sibling thread's context. The switch now invalidates only the conversation
that asked for it. Siblings keep their sessions and pick the new model up
when their own session is next created.

Second, the busy path was removed. `ControlSignal::SwitchModel` and its
oneshot delivery were replaced by a fail-closed `in_flight_ambiguous`, which
fires whenever *any* turn in the channel is running — including the caller's
own. That disables the switch in exactly the situation it is meant for:
escaping a rate-limited provider mid-turn. The signal is restored and
delivered per conversation via `signal_in_flight_conversation`, so the turn
is cancelled, the model lands, and the batch is requeued under it. Refusal
is now narrowed to the case that genuinely cannot be made safe: a worker
checked out by some *other* conversation, which is unreachable from the pool
and would keep serving the old model.

The override stays agent-global by design — `desired_model` lives on the
worker process, not the conversation — so `set_model_on_held_slots` replaces
`switch_idle_agent_model`: it sets every held slot and leaves invalidation,
a policy decision, to the caller.

Also carries the conversation root on kind:44200 turn metrics. The payload
had `channelId` but no thread identity, so after the session split every
thread of a channel folds into one number and a per-thread context readout
is impossible. `threadRoot` is additive and omitted for DMs and heartbeats,
which have one conversation per channel anyway.

Tests: 840 buzz-acp + 9 lifecycle + 253 buzz-core, clippy --all-targets
-D warnings clean. The sibling-survival guard was verified to fail against
the previous `invalidate_channel` behaviour before being committed green.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Scope observer activity and context metrics to conversations, add relay model catalog controls and a bounded picker, and make live model switching atomic across all targeted turns.

Co-authored-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
@Jari81
Jari81 requested a review from a team as a code owner August 28, 2026 16:40
@github-actions

github-actions Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 326e2301cb4b1edcb8a72d01ac4b19a83545365f...41dad30382230365c13f336b26ef8a68abb8f9aa.
A new review must complete for this exact range. When manual authorization
is required, a user with write access must comment exactly
@buzz-security-review 41dad30382230365c13f336b26ef8a68abb8f9aa to authorize a new review.
Any previous review applies only to its recorded range.

Parse project-scoped review authority, keep raw verdicts pending until an exact coordinator confirmation, and expose accepted or rejected outcomes in the Issue panel.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
cd src-tauri
cargo update --workspace

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
Robi Hermes and others added 29 commits September 10, 2026 22:19
Adds the write side of NIP-BW issue tracking on top of P4C's read
bridge: a single Tauri command (submit_project_bw_record) that signs
and readback-confirms issue-state/issue-update/triage-action records
through the already-fixture-tested buzz-sdk Publication boundary, so
role, causality, policy-binding and cutover checks are never
reimplemented client-side. Issue creation signs a 1621 root plus a
deliberate BW-enroll transition and surfaces (never hides) a partial
failure where the root exists but enrollment did not land. Fixed
bug/feature/operations templates require at least one acceptance
criterion before submit. Text/criteria edits and the four triage
actions chain off the exact current per-issue head and refuse to
submit into an already-forked chain, surfacing a visible conflict
instead of a silent overwrite. Fixed views group issues by
new/needs-clarification/backlog/in-progress/implemented, with visible
messaging for not-yet-enrolled, pending and conflicted history.

Kept the existing uncommitted draft's design and tests: it already
correctly excluded the legacy `p` tag from BW roots, delegated all
authorization to Core, and computed conflict/chain-head state from the
projection rather than guessing. Added: fixture-driven Rust tests that
exercise this command's own tag-assembly/record-building path against
the pinned NIP-BW corpus (role rejection, pre-cutover rejection,
successful enroll/update/accept) — proving a direct, scripted call
bypassing any UI is refused by Core, not by a hidden button; a mocked
`submit_project_bw_record`/configurable `get_project_bw` in the e2e
bridge; and a Playwright suite covering create+reload, missing
criteria, a double-submit race, a Core-refused triage action, a
concurrent text-edit fork, and a root that exists but was never
enrolled.

Pre-existing, unrelated to this change (verified via git stash against
unmodified HEAD before excluding): two failing desktop unit tests
(projectWorkItems review-authority status, the ProjectIssuesPanel
workflow-status-reason contract test), one flaky/failing Playwright
test (issue status from the detail rail), 3 pre-existing biome format
errors in issueStatus.ts/projectIssues.mjs/projectIssues.test.mjs, and
a file-size ratchet violation already present in lib.rs (+1 more line
from the new command registration) and three untouched files
(ChannelPane.tsx, ChannelScreen.tsx, UserProfilePanel.tsx).

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
…elations (P4E)

Completes the desktop BW write surface P4D deliberately stopped short of:
assignment over the *existing* kind:1 wire (no new grammar), the
backlog->ready->in-development->implemented handoff, parent/child/
blocks/duplicate-of relations, and read-side display of all of it.

- crates/buzz-core/src/bw: projection now exposes issue_state/
  issue_state_id (the current issue-state head's own accepted body,
  verbatim) and leaf (executable-leaf eligibility). is_executable_leaf
  is factored out of the freeze-time membership check so the read-side
  projection and the freeze gate apply the identical parent/duplicate/
  blocker rule.
- desktop/src-tauri: new submit_project_bw_assignment command signs/
  submits an assignment or unassignment candidate over the existing
  issueAssignments.ts kind:1 grammar, delegating role/causality entirely
  to Publication::prepare. submit_project_bw_record gains the
  issue-relation record type and, for the implemented transition,
  resolves the real current commit of the bound stream via `git
  ls-remote` against the repository's own owner-signed clone URL and
  overwrites any caller-supplied commit/remote_readback before Core ever
  sees the candidate (NIP-BW.md: "the signed claim alone proves no
  remote fact"). sign_project_issue_status now refuses outright for any
  issue BW's own projection already shows a state for, closing the last
  path that could mutate a BW-enrolled issue through the legacy
  1630-1633 lifecycle.
- desktop/src/features/projects: bwWrite.ts adds
  submitBwAssignment/submitBwReadyTransition/
  submitBwInDevelopmentTransition/submitBwImplementedTransition/
  submitBwRelation, each computing only the previous/prior pointer from
  the already-fetched snapshot and refusing up front on a visible fork.
  bwProjection.ts adds bwAssignmentHead (mirrors bwChainHead's fork
  handling for the kind:1 assignment chain) and feeds the resolved
  writer into the existing assignee facepile instead of a parallel BW-
  unaware display. BwIssueActions.tsx adds the assignment section, the
  three transition actions, read-only implemented details and the
  relations list/add/remove UI, gated by issue.bw.state exactly like
  the existing triage actions.

Verified before commit: cargo test -p buzz-core/-sdk/-cli (267 passed),
cargo fmt --all --check, cargo clippy -p buzz-core/-sdk/-cli --all-targets
-D warnings, desktop-tauri-{fmt-check,clippy,check,test} (2474 passed),
pnpm --dir desktop {typecheck,test}, scripts/{check,test}-nip-bw-fixtures.py
(fixture SHA-256 b489fb18... unchanged), and a scoped Playwright run of
every touched surface (project-bw-issues, the new project-bw-assignment,
project-issue-comments, project-inbox, project-pr-review,
project-commit-detail, projects-v3-screenshots). pnpm test's 2
pre-existing failures and pnpm check's 3 pre-existing format findings,
plus 4 pre-existing Playwright failures (3 deterministic, 1 load-
dependent), all reproduce identically on unmodified HEAD (verified via
git stash) and are documented baseline issues from P4C/P4D, not
introduced here.

Not pushed yet: this touches signer/causality-adjacent code
(assignment authority, the git-readback evidence substitution), so it
waits for the Opus review the P4E task card requires for that surface
before publishing to the canonical stream.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
…eview

An independent Opus review of 72fd3ec (bound to that exact commit,
per the P4E task card's Assignment/Signer review requirement) found one
P1 and confirmed several other findings. This addresses the P1 and the
two cheapest, clearly-correct P2/P3 fixes; the remaining P2 findings are
reported separately as scope/product questions rather than silently
redesigned.

- P1 (fixed): `resolve_stream_head_blocking` took the first line of
  `git ls-remote <url> refs/heads/<stream>` output. ls-remote matches a
  pattern against the *tail* of a ref, anchored at a `/` boundary, so a
  ref pushed as `refs/heads/x/refs/heads/<stream>` also matches and can
  sort before the real branch. Anyone with push access to the repository
  — the assigned writer, by construction — could push such a shadow ref
  and have it accepted as the "externally observed" commit for their own
  implemented claim, defeating the exact guarantee this transition
  exists to provide. Now only a line whose own ref is byte-for-byte
  `refs/heads/<stream>` is accepted. Added a regression test that
  reproduces the shadow-ref push and confirms the resolver still picks
  the real head.
- P3 (fixed): `unsupported_operations_are_refused_before_any_network_call`
  in project_bw_assignment.rs was vacuous — it asserted a struct field
  equalled the value just assigned to it, never exercising the actual
  rejection. Extracted `validate_assignment_operation` as its own
  function and tested that directly.
- P2 (fixed): a forked/refused kind:1 assignment or unassignment
  candidate was invisible to the desktop UI — `bw_projection.rs` only
  ever surfaced a per-issue notice for 46100 records, and a conflicted
  event of either kind never reaches `accept` so it never appears in
  `records` either. Generalized notice attribution
  (`bw_projection.rs::notice_issue`) to also cover kind:1 assignment/
  unassignment candidates via their `e` tag, matching how 46100 records
  already attribute via their `issue` tag. Split the existing
  "competing assignment heads" Playwright test (which asserted a
  `records` shape the real backend cannot produce — the review correctly
  called this a false positive) into an explicitly-labelled defensive-UI
  test plus a new test proving the actual, now-reachable notice signal
  renders visibly.

Not changed here, reported to Jari as follow-up/product-decision items:
`sign_project_issue_status`'s BW-enrollment gate fails open on transient
`get_project_bw` errors and isn't bound to a specific issue/repo pair
(low practical impact per review); and `implemented` issue-state detail
(commit/tests/remote_readback) does not survive a reload today because
`project_bw.rs::load`'s `Evidence` is still the empty P4C placeholder —
consistent with P4C's documented "evidence collection is a later phase"
boundary, but worth an explicit decision before P4F's first real
issue-to-implemented run.

Verified: cargo test -p buzz-core/-sdk/-cli (267, including the full
1804-step pinned corpus via bw_desktop_bridge.rs), desktop-tauri test
(2475, +1 from the new regression test), fmt+clippy clean on both, pnpm
typecheck clean, and both BW Playwright specs (18/18) rebuilt and
passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Tauri binds command args by parameter name; both commands take a single
`input: SubmitBw*Input` struct param, so the wire shape must be
`{ input: {...} }`. Every caller sent the fields flat, so any real BW
write (enroll, triage, ready, in-development, implemented, assignment,
relation, text-update) failed in the built app with "missing required
key input" while the mocked e2e bridge — which never exercises Tauri's
real arg binding — stayed green. Update the two affected specs to
match the corrected wire shape.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
The desktop UI always submitted triage-action with delegate:false, so a
human holding a valid triage_delegation in the activated role policy
(issue, action, signer, expires_at) still failed with
bw:reject:role:unauthorized on click — Core and the Tauri write path
already handled delegate:true correctly, only the UI never set it.

BwTriageActions now derives delegate per action from the already-loaded
BW snapshot via a new bwMatchingTriageDelegation helper that mirrors
Core's own delegation match, using the current signer from the existing
identity query. No Core/backend change; RESERVED_CALLER_TAGS and
assemble_bw_tags are untouched.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
The BW issue panel's assignment section required typing a full 64-hex
delegate pubkey. Add a MentionAutocomplete-based picker (candidates
from the relay agent directory and the repo-bound channel's members,
resolved through the existing profile lookup) so a writer can be found
and assigned by display name/avatar; raw hex remains reachable via a
toggle for identities with no name to search by. The submitted
assignment payload is unchanged either way. The assignment header now
shows name/avatar/truncated pubkey instead of the naked hex, with the
full pubkey still available via the title attribute.

Split BwAssignmentSection out of BwIssueActions.tsx (now over the
desktop file-size ratchet) into its own file, sharing the small
invalidate/error helpers via bwIssueActionsShared.ts.

Left the "Stream (branch)" field as free text: the issue asked for a
dropdown of the five platform keys (windows/android/ios/macos/acp),
but the field's actual accepted value is a git branch name (e.g.
"windows-integration", per the existing e2e coverage and shape.rs's
"stream" validator) — collapsing it into the platform-key set would
submit a wire-incompatible value. Per the source issue's own note,
splitting this off as a separate finding rather than blocking the
picker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
…e text

Point 4 of the picker issue (cb6885ba) is amended twice on the source
issue: Jari replaces the fixed five-platform-key list with a dynamic
value help sourced from the issue repository's actual branches (small
repos may have just one); Robi's follow-up corrects the single-branch
case to a read-only pre-filled field rather than an open dropdown.

Reuses the existing useRepoStateQuery (kind:30618 repo state, the same
remote-branch source git ls-remote would report) already used by
CreatePullRequestDialog — no new Tauri command or backend surface.
Multi-branch repos get a dropdown; a single branch is pre-filled and
read-only; an empty/unloaded branch list blocks submit outright, never
falling back to free text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
NIP-BW's `ready` record requires a non-null `update` id naming the
issue's current text snapshot. An issue that has never had its own
`issue-update` record has no head to send, but `submitBwReadyTransition`
sent that null through anyway, failing Core's required `id` shape check
with the opaque `bw:reject:shape:schema` rejection instead of guiding
the user to save a text update first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
…date

project_bw.rs::load captures `now` before its own network round trips
(paginated queries across five kinds, plus recursive reference
resolution). Against a real relay that can take longer than Core's
zero-tolerance references:future window, so a candidate signed right
after that load — using the actual current time — could look like it
arrived from the future purely because of how long the load took,
refusing every write with the opaque bw:pending:references:future.

Both BW write commands now call Consumer::observe to refresh `now` to
the signing instant right after replay, before evaluating the
candidate — the same mechanism project_bw_write.rs already used for
the implemented transition's evidence refresh, now applied
unconditionally and before, not after, activation/preflight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
… backlog

BwReadyAction never seeded its stream selection from the issue's already
committed stream, so reopening an issue (or any re-render after an issue
switch) reset the dropdown to the first branch in the list regardless of
what was actually signed.

Separately, "Accept to backlog" only signed the triage-action record. Core's
own display projection already reports such an issue as "backlog" (a
convenience label in bw/projection.rs), but the issue-state chain itself
stays on "triage" until a second, Owner/coordinator-signed issue-state
record referencing that triage event lands (NIP-BW.md). Without it every
"ready" attempt was refused with bw:reject:causality:state-transition since
the real previous state was never "backlog". Chain that second record
automatically right after a successful accept so it is a single action for
the common owner/coordinator signer, instead of a separate manual step.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
submitBwAcceptToBacklog's auto-chain (9936df3) only fires on a fresh
"Accept to backlog" click. An issue already accepted before that fix
existed shows "Backlog" via Core's display-projection convenience label
but its real issue-state chain head never advanced past "triage" — and
the UI permanently hides the accept button once the label reads
"Backlog", so the missing backlog record could never be chained. Every
"ready" attempt on such an issue (e.g. ISS-064211C8) kept failing with
bw:reject:causality:state-transition.

submitBwReadyTransition now detects a real chain head still on "triage"
and signs the missing issue-state(backlog) record itself, using the
existing triage-action accept head, before signing ready — still a
single "Move to ready" click, no extra UI step.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
P5_6A (issue 35a572b21560bff16030db1428375f27f2e57857cad41ab3c6de10a964fc310a):
read-only Releases workspace tab over the existing get_project_bw Core
projection. Set status comes verbatim from projection.sets, verdicts from
projection.artifact_verdicts, the current handoff from projection.handoffs,
details from Core-accepted records, and pending/refused chains from
decisions/conflicts as traceable event references. No side-effecting
control; missing external evidence stays visibly pending; a provider run
success never upgrades anything to completed.

Desktop tests render Core-authored snapshots only: an env-gated export in
bw_desktop_bridge replays pinned NIP-BW.fixtures.json cases
(SHA-256 b489fb188b45e073e81783e8d591f24891a9c73bc56d8d2fc56a881606640e8a,
unchanged) through the real Core consumer into
bw-release-snapshots.json, fed through the existing transient BwSnapshot
E2E mock seam. Covers empty set, run pending/failure, digest mismatch,
non-durable download, late events, historically accepted issues with a new
unreviewed artifact, and conflicting verdicts.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Keep the signed BW pipeline unchanged. Extend the existing canary with a fork-only canonical SHA gate, cumulative ancestry, unsigned artifact receipt and CRLF regression. Authorized directly by Jari; no release, BW status or acceptance mutation.

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
…sites

Signed-off-by: Robi Hermes <robi.cholewa@gmail.com>
PY

- name: Upload unsigned installer and bound manifest
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants