Skip to content

feat(projects): add agent and CLI project-home support - #6590

Open
thomaspblock wants to merge 27 commits into
mainfrom
projects-channel-first-pt1-agent-cli
Open

feat(projects): add agent and CLI project-home support#6590
thomaspblock wants to merge 27 commits into
mainfrom
projects-channel-first-pt1-agent-cli

Conversation

@thomaspblock

Copy link
Copy Markdown
Contributor

Summary

  • inject bounded project-home identity and repository context into managed agent sessions
  • add project-aware CLI flows for creating projects, repositories, issues, and related channels
  • match project homes through the existing relay query surface, then filter channel metadata client-side

This is Part 1 of the channel-first Projects stack. Part 2 contains project creation and model foundations.

Testing

  • cargo fmt --all -- --check
  • cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings
  • cargo test -p buzz-cli -p buzz-acp — 1,184 tests passed, 1 doc test ignored
  • full pre-push gate passed

Post-Deploy Monitoring & Validation

  • validate project-home agent context and project-aware CLI commands against a staging relay
  • healthy signals: project context matches the active channel, explicit repo coordinates remain stable, and normal channels receive no project block
  • failure signals: cross-channel project context, duplicate project creation, or commands targeting an unrelated repository; mitigate by reverting this PR

Give agents bounded project-home context and project-aware CLI operations while keeping channel matching client-filtered through the existing relay query surface.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
@thomaspblock
thomaspblock requested a review from a team as a code owner August 23, 2026 01:02
@thomaspblock
thomaspblock marked this pull request as draft August 23, 2026 03:53

@thomaspblock thomaspblock left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cassandra adversarial/security review — needs work

The red Unit Tests job is not caused by this diff: it fails linking untouched buzz-voice with could not find native static library 'sherpa-onnx-c-api'. That check should be retried rather than patched in this projects PR.

I found two source-level blockers independently while tracing the new project-home resolution.

P1 — Any relay writer can hijack a channel's agent project context and redirect channel-scoped issues (confidence 100)

Evidence

  • crates/buzz-acp/src/prompt_project.rs:23-25: !event_is_unlisted(event) && event_has_tag_value(event, "buzz-channel", channel_id)
  • crates/buzz-acp/src/prompt_project.rs:27-33: the matching events are ordered only by created_at, then the first parseable event wins.
  • crates/buzz-cli/src/commands/project_channel.rs:27-31: let project = pick_oldest_listed(&projects); followed by if let Some(member) = first_member_repo(event) { return Ok(member); }
  • docs/nips/NIP-MP.md:139: `buzz-channel` on a project is **metadata only**.
  • docs/nips/NIP-MP.md:188: The relay MUST NOT check whether the signer owns, maintains, or has any relationship to a member repository.

Trigger scenario

  1. An attacker who knows a project channel UUID publishes a listed kind:30621 carrying that buzz-channel and an a tag for the attacker's repository. This is protocol-valid and requires no authority over the channel.
  2. The attacker gives it an earlier accepted timestamp than the legitimate project (or simply publishes before project creation).
  3. ACP selects that event as the channel's project home and promotes its name/owner/repository into generated [Context] instructions.
  4. buzz issues create --channel <victim-channel> independently makes the same oldest-event choice and returns the attacker's first member coordinate without checking that the project signer controls the channel or that the member repo is actually bound to it.
  5. A normal “create a task in this project” request is therefore signed against an unrelated attacker-chosen repository.

This crosses an integrity boundary: unauthenticated project metadata is being treated as authoritative routing configuration. Resolve the project from an authenticated channel-owned binding/type, or require a verifiable relationship between the selected project signer and channel authority. At minimum, channel-scoped repo resolution must verify the selected 30617 is bound to the requested channel and reject ambiguous projects rather than choosing oldest.

P1 — Global slug squatting lets any signer block another user's project creation (confidence 100)

Evidence

  • crates/buzz-cli/src/commands/projects.rs:373-379: other_listed_project(&fetch_projects_by_dtag(client, slug).await?, &caller_pubkey) causes a conflict when any other pubkey has the slug.
  • docs/nips/NIP-MP.md:134: Only the signer can replace their (pubkey, 30621, d) coordinate.
  • docs/nips/NIP-MP.md:194: newest created_at wins per (pubkey, 30621, d), and one pubkey can never overwrite another's coordinate.

Trigger scenario

An attacker publishes listed projects for common slugs (app, website, a known upcoming product name). Every later buzz projects create <slug> by every other identity is rejected locally, even though the protocol intentionally namespaces projects by signer. The suggested error action (“Add a repository to that project instead”) cannot work because editing is signer-only. Do not impose relay-wide uniqueness on an owner-namespaced coordinate; duplicate-card prevention needs an authority-scoped rule.

Additional adversarial risk retained in this PR comment

crates/buzz-cli/src/commands/project_channel.rs:178-185 adds the selected foreign project owner as a maintainers tag on an implicitly created caller-owned repository. Under docs/nips/NIP-MP.md:215-217, that tag is sufficient claim authority for the foreign signer. I did not live-test Desktop's resulting fold, but this should be removed or explicitly justified before merge; untrusted project metadata must not grant provenance/claim authority over a newly created repo.

Coverage: full 12-file diff read; traced ACP project lookup → generated context, CLI channel lookup → issue creation, implicit repo creation, project collision checks, NIP-MP authority and claim semantics. I did not mutate the branch or run a live hostile relay reproduction.

Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>

@thomaspblock thomaspblock left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cassandra re-review of 7a9af2ac — one routing blocker remains

The original two P1 findings are fixed in the authoritative-selection path: foreign channel/project claims no longer route ACP or CLI, ambiguity fails closed, cross-signer slug/channel squatting is removed, and implicit repo creation no longer grants foreign maintainers authority.

P1 — Existing same-id repository bypasses the new channel-binding check (confidence 100)

Evidence

  • crates/buzz-cli/src/commands/project_channel.rs:181-188:
    if let Some(existing) =
        crate::commands::repos::fetch_own_repo_announcement(client, &repo_id).await?
    {
        let _ = try_add_own_repo_to_channel_project(client, channel, &repo_id).await;
        return Ok(ChannelProjectRepo {
            repo_owner: existing.pubkey.to_hex(),
            repo_id,
        });
    }
  • The new binding check exists in repo_from_announcement at lines 94-104, but this fallback does not call it.

Trigger scenario

  1. The caller already owns repo 30617:<caller>:app, bound to channel A (or unbound).
  2. They own a repository-empty project home with slug app in channel B.
  3. buzz issues create --channel B finds no authoritative project/member and no caller-owned repo bound to B, then reaches ensure_default_repo.
  4. fetch_own_repo_announcement("app") returns the channel-A repository. The code attaches it to the channel-B project and returns it without checking or rebinding its buzz-channel.
  5. The issue is silently created against channel A's unrelated repository. Subsequent calls repeat the same misrouting, while ACP correctly refuses to recognize that member as authoritative for B.

The fallback must apply the same first-buzz-channel equality invariant before returning. If an existing same-id repo is bound elsewhere, fail with an actionable conflict or choose a non-colliding id; do not attach or route to it.

Advisory — maintainer authorization reads only the first value (confidence 75)

Evidence

  • crates/buzz-cli/src/commands/project_channel.rs:88-91:
    || repo.tags.iter().any(|tag| {
        matches!(tag.as_slice(), [name, value, ..]
            if name == "maintainers" && value.eq_ignore_ascii_case(&signer))
    })
  • crates/buzz-acp/src/prompt_project.rs:93-101 likewise returns only tag.get(1) for each maintainers tag.
  • VISION_PROJECTS.md:27 and NIP-34 model maintainers as a multi-value tag; Desktop deliberately reads all values (desktop/src/features/projects/projectModels.ts:283-285).

A valid ['maintainers', first, project_signer] repository authorizes the signer in Desktop but is rejected by both new routing implementations. Iterate all values after the tag name so ACP, CLI, and Desktop share one authority rule.

Re-review coverage: exact fix diff a6c5f1db..7a9af2ac; traced authoritative selection, ambiguity, project creation collisions, implicit repo fallback, and maintainer parsing. Report-only; no branch mutation.

Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>

@thomaspblock thomaspblock left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cassandra final security/authority re-review — findings cleared at 7bbed3f1

No remaining security or adversarial findings in the incremental fix.

Verified:

  • crates/buzz-cli/src/commands/project_channel.rs:197-205 now calls require_repo_channel_binding before reusing or attaching a same-slug existing repository, so a repository bound to channel A cannot route a channel-B issue.
  • require_repo_channel_binding uses the first buzz-channel value, matching the relay's fail-closed binding semantics, and rejects both mismatched and absent bindings.
  • ACP's multi_tag_values and CLI's tag.as_slice()[1..] now inspect every pubkey value in every maintainers tag, matching NIP-34/Desktop semantics.
  • Regressions cover the mismatched existing binding and authorization by a later maintainer value.
  • The prior fixes remain intact: project-home selection requires a channel-bound live member repository plus signer authority; ambiguity fails closed; cross-signer slug/channel squatting is absent; implicit creation does not grant foreign maintainer authority.

Verdict for my security/authority lane: merge-ready at exact head 7bbed3f127f25559fc301044842ee6582b2fdc9a. CI and independent correctness review are outside this verdict and were still in progress when checked.

@thomaspblock
thomaspblock marked this pull request as ready for review August 23, 2026 12:02
@thomaspblock
thomaspblock enabled auto-merge (squash) August 23, 2026 21:48
thomaspblock and others added 3 commits August 23, 2026 17:48
## Summary
- create explicit NIP-MP projects with a home channel and default
repository
- preserve standalone repository folding, project deletion, and
deterministic repository selection
- restore Template, Team, visibility, and agent settings in the project
creation flow

This is Part 2 of the channel-first Projects stack, following #6590. It
is independently based on `main`; Part 3 adds the project-home channel
surface.

## Testing
- focused project collection, creation, channel, and model tests: 38/38
passed
- Desktop unit suite: 5,415/5,415 passed
- TypeScript, Biome, and differential file-size checks passed
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- create listed and unlisted projects with and without templates in the
first staging Desktop session
- healthy signals: one home channel, one default repository, stable
project coordinates, and no duplicate legacy card
- failure signals: partial project creation, duplicate projects, missing
default repository, or stale sidebar entries; mitigate by reverting this
PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
## Summary
- classify and render project-home channels through the shared channel
glyph and lifecycle helpers
- let the normal channel pane host a project idle auxiliary surface and
focus drawer
- align channel management, headers, member bars, and empty-channel
actions with project channel semantics

This is Part 3 of the channel-first Projects stack, based on #6591. Part
4 adds the project-home navigation and context experience.

## Testing
- focused channel lifecycle, pane helper, and project-home channel
tests: 7/7 passed
- Desktop unit suite: 5,422/5,422 passed
- E2E-mode Desktop build passed
- TypeScript, Biome, and differential file-size checks passed
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- open normal, temporary, private, and project-home channels in the
first staging Desktop session
- healthy signals: normal channels retain their existing composer/thread
behavior and project homes use the project glyph and auxiliary slot
- failure signals: missing composer, incorrect channel kind, stuck focus
drawer, or project chrome on a normal channel; mitigate by reverting
this PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
## Summary
- render an explicit project's home channel through the normal channel
timeline and composer
- add a resizable project context rail with codebase, channel, people,
and workspace navigation
- keep project agent conversations bounded to the project home and
preserve repository/detail routes

This is Part 4 of the channel-first Projects stack, based on #6594. The
final part contains overview and workspace completion polish.

## Testing
- focused project conversation, route, summary, workspace-sheet, and
related-channel tests: 39/39 passed
- Desktop unit suite: 5,439/5,439 passed
- E2E-mode Desktop build passed
- TypeScript, Biome, and differential file-size checks passed
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- open project homes from project and channel entry points in the first
staging Desktop session
- healthy signals: one channel timeline/composer, stable repository
context, bounded project agent history, and reversible workspace sheets
- failure signals: duplicate channel surfaces, stale repository
selection, unrelated DM history, or sheets replacing the channel route;
mitigate by reverting this PR

Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Comment on lines +251 to +256
fn truncate_repo_name(name: &str) -> String {
if name.len() <= 128 {
return name.to_string();
}
name.chars().take(128).collect()
}

@matt2e matt2e Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard measures bytes but the truncation takes chars, while build_repo_announcement rejects names over 128 bytes. A multibyte project name over 128 bytes still exceeds the byte limit after chars().take(128), so default-repo creation errors instead of truncating (e.g. a 100-CJK-character name). Same pattern in projects.rs ensure_default_create_repo, which has no byte check at all — truncate on a byte budget at a char boundary, as the prompt-side truncation does.
🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2e0fe69: both default-repository paths now share UTF-8-safe truncation on a 128-byte budget, with a CJK regression test.

Comment on lines +178 to +191
const workspaceSheet =
workspaceSheetOpen && workspaceSheetTab && workspaceRepository ? (
<ProjectHomeWorkspaceSheet
key={`${workspaceSheetTab}:${workspaceRepository.id}`}
identityPubkey={identityQuery.data?.pubkey}
onOpenCommit={handleOpenCommit}
onRepositoryAdded={handleFilesAdded}
onSelectRepository={setWorkspaceRepositoryId}
project={project}
projects={projects}
repository={workspaceRepository}
tab={workspaceSheetTab}
/>
) : null;

@matt2e matt2e Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

workspaceSheet is a fresh JSX element every render and flows into the memoized ChannelPane as idleAuxiliaryPanel, so while the sheet is open any parent render (query cache updates, local state) defeats React.memo(ChannelPane) and re-renders the whole message timeline behind the drawer — the exact unstable-prop gotcha the repo docs call out. Its inputs are all stable callbacks/ids, so wrapping the construction in React.useMemo restores the memo boundary.
🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2e0fe69: the conditional workspace sheet element is memoized with its complete dependency set, preserving the downstream ChannelPane memo boundary.

Comment on lines +158 to +170
if (homeChannel) {
const alreadyMember = homeChannel.memberPubkeys.some(
(pubkey) =>
normalizePubkey(pubkey) === normalizePubkey(selectedAgent.pubkey),
);
if (!alreadyMember) {
await addChannelMembers({
channelId: homeChannel.id,
pubkeys: [selectedAgent.pubkey],
role: "bot",
});
}
}

@matt2e matt2e Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bot member-add is gated on homeChannel being set, not on the message actually targeting it. restoreProjectsAgentConversation can restore a 1:1 DM while homeChannelId is set, and submitProjectAgentMessage then sends to the DM — in that case this block silently adds the agent as a bot member of the project home channel as a side effect of a DM follow-up. Guard on the resolved target, e.g. only add when !conversation || conversation.channel.id === homeChannel.id.
🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2e0fe69: bot membership is now added only when the resolved existing conversation is the project home channel (or no conversation exists yet).

matt2e
matt2e previously approved these changes Aug 24, 2026
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>

@thomaspblock thomaspblock left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cassandra security re-review of 2e0fe6999 — one tenant-scope blocker remains

Matt's three reported defects are correctly fixed: UTF-8 names now truncate to a 128-byte prefix at a character boundary in both callers, the CJK regression passes, the workspace-sheet element has a complete useMemo dependency set, and an existing DM no longer triggers project-home membership.

P1 — Project-home membership is not bound to the captured relay/signer scope (confidence 75)

Evidence

  • desktop/src/features/projects/ui/ProjectAgentChatPanel.tsx:167-171:
    await addChannelMembers({
      channelId: homeChannel.id,
      pubkeys: [selectedAgent.pubkey],
      role: "bot",
    });
  • The immediately following agent start/open/send path passes the captured relayScope and signer at ProjectAgentChatPanel.tsx:183-201, but this membership mutation passes neither.
  • desktop/src/shared/api/types.ts:88-92 exposes no expected relay/signer fields on AddChannelMembersInput.
  • desktop/src-tauri/src/commands/channels.rs:533-559 accepts only channel/pubkeys/role and calls unscoped submit_event(builder, &state).
  • desktop/src-tauri/src/relay/submit.rs:71-77 resolves the currently active relay and signing keys when called.

Trigger scenario

  1. The panel captures project home channel A, relay A, and signer A.
  2. The user submits while a community or identity switch races the Tauri membership command (or the switch occurs after this unscoped await starts).
  3. add_channel_members resolves the then-active workspace and signs/publishes the captured channel UUID there; if that UUID exists in relay B, the bot membership is mutated in the wrong tenant. Even without a collision, the wrong-relay failure occurs outside the later fail-closed path.
  4. submitProjectAgentMessage then checks expectedRelayUrl / expectedSignerPubkey and fails closed, leaving membership as a partial side effect even though no project message was sent.

This contradicts the nearby invariant that “every relay side effect” is scope-bound. Extend the membership command/API with expected relay and signer parameters and perform the same assert/captured-target submission used by the message path, or move the membership operation into a scoped orchestration boundary. The channel-target guard fixes Matt's DM case but not this tenant race.

Verification: reviewed exact incremental diff 7d6c4abce..2e0fe6999; git diff --check passed; independently ran the new CJK test at exact head (1 passed). Report-only; no branch mutation.

Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
@thomaspblock

Copy link
Copy Markdown
Contributor Author

Cassandra security/adversarial re-review — adff0acff7f5fbe61795aad6e3a5670e1300b45d

Verdict: merge-ready from my lane. No actionable findings.

I reviewed the full 2e0fe6999..adff0acff diff before tracing the surrounding Projects submit path, Tauri command boundary, relay scope helpers, explicit-key submission helper, workspace state snapshots, and mock bridge. The remaining tenant-scope blocker is closed:

  • ProjectAgentChatPanel.tsx:168-175 passes the callback-captured relay and signer scopes into project-home membership.
  • channels.rs:546-552 resolves one relay base and one signable key snapshot, then validates both captured scopes before mutation.
  • channels.rs:572 submits with submit_event_at_with_keys(builder, &state, &relay_base, &signing_keys), so neither relay nor signer is re-read after validation.
  • e2eBridge.ts:7419-7426 applies both checks after the injected delay, matching the race shape rather than checking too early.

Adversarial scenarios checked: switch before relay resolution; identity swap between relay and key reads; switch after validation; malformed/empty optional scopes; multi-member partial failure; restored-DM guard interaction; membership failure before message send; and mismatch behavior in the mock bridge. The fixed snapshot either fails closed before publication or publishes only with the captured relay/key pair.

Independent verification at exact HEAD:

  • Desktop full test suite: 5,451 passed.
  • Tauri relay::scope::tests: 11 passed.
  • git diff --check: clean.

Residual/testing gap (recorded here durably): I did not exercise a live relay-backed community switch during an in-flight membership request. The production call path and deterministic delayed bridge cover the relevant ordering, and I do not consider this merge-blocking.

@thomaspblock
thomaspblock requested review from jedwards27 and removed request for jedwards27 August 24, 2026 13:34

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — request changes

Reviewed base e23632941331502c0330e51d407e667bea26ef57 through exact head adff0acff7f5fbe61795aad6e3a5670e1300b45d against VISION.md, VISION_PROJECTS.md, TESTING.md, NIP-MP authority semantics, the ACP resolver/cache, CLI project/repository routing, relay query execution, and the changed Desktop project journeys.

P1 — reject a same-slug repository bound outside the new project home

crates/buzz-cli/src/commands/projects.rs:372-380 adds the coordinate returned by ensure_default_create_repo to the project and publishes it. But ensure_default_create_repo at projects.rs:665-670 returns any caller-owned same-ID repository without checking its buzz-channel. This omits the binding invariant already enforced for issue-time reuse at crates/buzz-cli/src/commands/project_channel.rs:171-182,197-205.

If the caller already owns repo app bound to channel A (or with no binding), buzz projects create app --channel B reports success and publishes a channel-B project containing the channel-A repo. ACP correctly refuses to treat that member as authoritative for B, and later channel-scoped issue routing conflicts rather than targeting the advertised project. Apply the same binding check before reuse (or fail before project publication), with a regression for mismatched and absent bindings.

P1 — do not permanently cache project absence or mutable project metadata

crates/buzz-acp/src/pool.rs:598-611 caches Option<PromptProjectInfo> indefinitely, including None; fetch_project_home_for_channel explicitly treats empty as final at pool.rs:2986-2989. There is no TTL, relevant-event invalidation, or session-boundary refresh.

If ACP resolves channel C before its project/repository publication completes, it caches None. Creating the project later cannot add the Project block to any later turn/session in that process until restart. Positive entries likewise retain obsolete project names/default repositories. Use bounded freshness or invalidate on relevant project/repository events, and regress None → project resolution without restarting ACP.

P1 — do not treat a truncated global query page as authoritative absence

The project-home paths issue one-shot 1,000-row queries: ACP at crates/buzz-acp/src/pool.rs:2993-3003, CLI projects at crates/buzz-cli/src/commands/projects.rs:74-86, and CLI repositories at crates/buzz-cli/src/commands/project_channel.rs:160-168. The relay clamps the SQL query to 1,000 at crates/buzz-relay/src/handlers/req.rs:957-960, while non-single-letter custom-tag matching occurs only after that limited read at crates/buzz-relay/src/api/bridge.rs:1308-1315; the SQL tag pushdown at req.rs:1001-1044 covers #p/#d, not #buzz-channel.

Once more than 1,000 newer visible heads exist, an older authoritative project or repository can be excluded by unrelated global rows. ACP then resolves (and permanently caches) no project; CLI channel routing can say the channel is not a project home or take fallback behavior. BuzzClient already exposes composite-cursor pagination at crates/buzz-cli/src/client.rs:683-729. Page to a defined exhaustive/bounded result with explicit truncation failure, or add indexed relay-side support; a full page cannot prove absence. Add coverage that places the authoritative head beyond page one.

Validation and residual risk

  • Clean exact-head cargo test -p buzz-cli -p buzz-acp passed (809 + 9 + 374 tests; one doc test ignored); clippy for both packages passed with -D warnings.
  • Desktop unit suite passed 5,451/5,451; Desktop check/typecheck passed; five targeted create/open/retry/lost-ack/sidebar project journeys passed.
  • Keyboard Enter/Space and aria-pressed behavior, a 900×720 viewport at 24px root text, control visibility, and horizontal overflow were probed successfully in the browser artifact. No additional source-level product/accessibility blocker was found.
  • All applicable exact-head GitHub checks are green. Those checks do not exercise the three failure shapes above.
  • Residual product risk: no exact-head native Tauri/WebView journey or native receipt was available for the materially changed navigation/layout, so native focus, OS input, and shell resizing remain unproven.
  • The 1,001-head starvation case was established from the client/relay control flow, not reproduced against a seeded live relay.

@thomaspblock

Copy link
Copy Markdown
Contributor Author

Gauge correctness/testing verification of Jude's three P1s — all confirmed at exact head adff0acff7f5fbe61795aad6e3a5670e1300b45d

Independent source verification, formed before reading other responders. All three mechanisms are real; none is speculative. For each, the answer to "which test would fail if this were wrong?" is currently none — that is itself the coverage finding.

1. ensure_default_create_repo reuses a same-slug repo with no binding check — confirmed, confidence 100

crates/buzz-cli/src/commands/projects.rs:665-670:

    let repo_id = repo_id_from_project_slug(slug)?;
    if fetch_own_repo_announcement(client, &repo_id)
        .await?
        .is_some()
    {
        return Ok(repo_id);
    }

The issue-time path already enforces the invariant this skips — crates/buzz-cli/src/commands/project_channel.rs:197-200:

    if let Some(existing) =
        crate::commands::repos::fetch_own_repo_announcement(client, &repo_id).await?
    {
        require_repo_channel_binding(&existing, channel)?;

So buzz projects create app --channel B publishes a channel-B project whose member repo is bound to channel A (or unbound), and the divergence surfaces later as an ACP/CLI routing conflict rather than at creation time. Required regression (must fail on today's code): create-with-existing-repo where the repo's first buzz-channel (a) mismatches → Conflict before project publication; (b) is absent → Conflict; (c) matches → reuse succeeds.

2. Permanent caching of None and of mutable project metadata — confirmed, confidence 100

crates/buzz-acp/src/pool.rs:598-610 (lookup_project) returns any cached entry — including a cached None (cache.get(&channel_id).cloned() yields Some(None)return cached;) — and inserts fetched.clone() unconditionally with no TTL. The projects map (pool.rs:547) is a separate Arc<RwLock<HashMap>> with no invalidation path: invalidate_channel / invalidate_channel_sessions operate on session state only and never touch it. The fetch helper documents absence as final: pool.rs:2988 — "Empty results are not retried: most channels are not project homes." Consequence: resolve channel before project creation → Project block unavailable until process restart; renames/default-repo changes similarly frozen. Required regression: None resolved → project published → subsequent turn (same process) carries the Project block.

3. Truncated 1,000-row page treated as authoritative absence — confirmed, confidence 100

  • ACP fetch_project_home_for_channel, pool.rs:2993-3003: "kinds": [KIND_PROJECT], "limit": 1000no tag filter at all; channel matching is client-side, so the page is all listed projects relay-wide.
  • CLI fetch_projects_for_channel, crates/buzz-cli/src/commands/projects.rs:74-86: same shape, client-side project_tags_match_channel post-filter.
  • CLI fetch_channel_repos, project_channel.rs:161-166: uses #buzz-channel, but the relay pushes only #p/#d into SQL (crates/buzz-relay/src/handlers/req.rs:1001-1044); custom-tag matching happens after the LIMIT-clamped read (req.rs:957-960, api/bridge.rs:1308-1315).

Past 1,000 newer visible heads, an older authoritative project/repo silently drops out; the caller concludes "not a project home" or takes default-repo fallback. Compounding interaction worth naming: a starvation-induced false absence from (3) is then cached permanently by (2) — a transient load condition becomes a sticky wrong answer for the process lifetime. Required regression: authoritative head seeded beyond page one → resolver either finds it (pagination; BuzzClient composite cursor exists at crates/buzz-cli/src/client.rs:683-729) or fails explicitly on truncation — never a silent absence.

Verified clean / scope

I re-derived each claim from the head sources quoted above; I did not re-run the suites (Jude's exact-head runs and the green check-runs at adff0ac stand). Negative claims here are scoped to the files and line ranges cited. Wrench owns the fixes; I will re-review the incremental diff when it lands, gating on the three regressions above failing-before/passing-after.

— Gauge (correctness/testing/reliability lane)

@thomaspblock

Copy link
Copy Markdown
Contributor Author

Cassandra security/adversarial response to Jude's review — exact head adff0acff7f5fbe61795aad6e3a5670e1300b45d

Verdict: all three requested changes are valid; the current head needs work. I formed this assessment from the full diff and surrounding call paths before reading the other creature reports.

  1. Same-slug repository reuse breaks the channel authority invariant — confidence 100.

    crates/buzz-cli/src/commands/projects.rs:665-670 currently says:

        if fetch_own_repo_announcement(client, &repo_id)
            .await?
            .is_some()
        {
            return Ok(repo_id);
        }

    This returns an existing repository without applying the require_repo_channel_binding check already used by issue-time routing. A repository bound to channel A, or with no binding, can therefore be published as channel B's default member and leave project creation reporting success while ACP later rejects the project home. The creation path must fail before project publication unless the first buzz-channel binding equals the requested channel.

  2. Permanent positive/negative ACP caching makes mutable authority metadata stale — confidence 100.

    crates/buzz-acp/src/pool.rs:604-610 currently says:

        if let Some(cached) = self
            .projects
            .read()
            .ok()
            .and_then(|cache| cache.get(&channel_id).cloned())
        {
            return cached;
        }

    Because the map stores Option<PromptProjectInfo>, this permanently returns cached absence as well as stale positive metadata. Resolve-before-create therefore suppresses the Project block until process restart; later repository/default changes are also invisible. Bounded freshness or relevant-event invalidation is required, including a None -> project regression without restart.

  3. A one-shot, post-filtered page cannot establish absence — confidence 100.

    crates/buzz-relay/src/api/bridge.rs:1308-1315 currently applies the complete filter only after the limited database read:

                for se in stored_events {
                    if !event_in_accessible_channel(&se, &accessible_channels) {
                        continue;
                    }
                    if !buzz_core::filter::filters_match(std::slice::from_ref(filter), &se) {
                        continue;
                    }

    #buzz-channel is not pushed into that SQL read at the reviewed head. More than 1,000 newer nonmatching heads can hide the older authoritative repository/project and produce false absence.

    Implementation constraint: ordinary response-length pagination does not fix the custom-tag case by itself. If a 500-row underlying DB page post-filters to zero matches, a client that treats page.len() < 500 as EOF stops immediately and never advances to the older candidate rows. The fix must either push the exact buzz-channel predicate into SQL before LIMIT, or expose an underlying-scan cursor/exhaustion signal independent of filtered response length. Any exhaustive scan must also have a defined bound and fail explicitly when that bound is exceeded; otherwise attacker-controlled relay history becomes unbounded prompt-path memory/time work.

I reviewed only exact pushed head adff0acff; the implementation work is still in progress and is not covered by this verdict. I will re-review the exact pushed revision, including bound behavior and the starvation regression, when it lands.

Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
@thomaspblock

Copy link
Copy Markdown
Contributor Author

Cassandra re-review — b4e1d8c13c795dc799f6d8dfaee1be7fb532114b

Verdict: merge-ready from the security/adversarial lane; no actionable findings.

F3 is closed. The incremental diff adds the missing repository-context transition at desktop/tests/e2e/project-conversation-load-failure.spec.ts:151 before selecting the repository workspace's Channels tab. I traced this against the equivalent bound-home navigation used by the sibling project specs and independently ran the exact smoke spec at this exact head: 1/1 passed. git diff --check 73d4e2aa..b4e1d8c1 also passed.

Coverage: full one-line incremental diff, surrounding navigation and failure/retry path, sibling bound-home project flows, exact smoke execution. No new security, correctness, or test-causality defect found. No durable residual arose from this delta.

GitHub reports the exact head MERGEABLE; the fresh CI run is still in progress, so terminal CI remains the only unverified gate. F4 remains unbound and is not promoted: the formerly reported messaging.spec.ts:2049 failure passed Wrench's rerun and this delta does not touch that path.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — REQUEST CHANGES

Reviewed: f6e6617a9dcc2308d5039f8afaab974b49fb9577..b4e1d8c13c795dc799f6d8dfaee1be7fb532114b (exact head b4e1d8c13c795dc799f6d8dfaee1be7fb532114b)

Risk: high — project discovery decides ACP authority/context and CLI routing under community-wide relay cardinality. The new-head delta repairs one Desktop E2E navigation step but leaves both prior production defects unchanged.

P1 — expired cached absence becomes ordinary-channel context after refresh failure

ChannelInfoResolver::resolve treats Ok(None) as proven non-project context (crates/buzz-acp/src/pool.rs:604-617). After the 30-second TTL expires, lookup_project returns every stale cache value when refresh fails (pool.rs:624-644). If the stale value is None, an earlier legitimate miss followed by project creation and a relay timeout/error therefore emits ordinary-channel context precisely when project authority is indeterminate.

The checked-in failure regression covers only an empty cache (pool.rs:8565-8614); the expired-negative test at pool.rs:8500-8563 covers successful refresh. A review-only causal mutation seeded expired CachedProjectInfo { value: None } before the malformed-response refresh. cargo test -p buzz-acp failed_project_lookup_through_resolve_cannot_become_ordinary_context -- --nocapture then failed at the intended fail-closed assertion (exit 101), establishing the uncovered branch. Source was restored and the checkout was clean.

Author action: on refresh error, never return stale None as authoritative absence. Propagate ProjectLookupError so resolve() fails closed. If stale Some(project) is intentionally retained for availability, test that policy separately. Add the causal expired-negative → failed-refresh regression through resolve(), and mutation-prove removing the guard fails it.

Verification owner: author for fix/regression; :bot: Jude’s code review agent for exact-new-head mutation and full buzz-acp rerun.

P1 — CLI applies its 10,000-event bound before channel scoping

fetch_projects_for_channel queries community-global kind 30621, calls query_all_bounded(..., 10_000), and only afterward filters buzz-channel client-side (crates/buzz-cli/src/commands/projects.rs:82-96). More than 10,000 unrelated project heads can therefore exhaust or hide valid target-channel discovery. Relay-side #buzz-channel filtering already exists, and ACP uses it; this CLI path omits it.

A review-only production-call-site test captured the exact request from fetch_projects_for_channel and required #buzz-channel. It failed with body [{"kinds":[30621],"limit":500}] at this head. The test was removed and the tree restored clean. Existing project_channel_matching_ignores_unrelated_claims coverage proves only post-fetch matching, not relay query scope.

Author action: add "#buzz-channel": [channel] to the relay filter before query_all_bounded, retaining client filtering as defense in depth. Add a production-call-site regression proving more than 10,000 unrelated events cannot consume the bound, and mutation-prove deleting the filter fails it.

Verification owner: author for fix/regression; :bot: Jude’s code review agent for exact-new-head mutation and full buzz-cli rerun.

Exact-head evidence and residual risk

  • Both independent review lanes reproduced their assigned defect against exact head; neither carried prior clearance forward.
  • Full cargo test -p buzz-cli: 377/377 passed. Full cargo test -p buzz-acp: 812 library + 9 lifecycle tests passed. cargo check -p buzz-cli -p buzz-acp and base-to-head git diff --check passed. These green suites omit the two causal failure shapes above.
  • 73d4e2aa..b4e1d8c1 changes only desktop/tests/e2e/project-conversation-load-failure.spec.ts:151, selecting the repository before Channels. Exact-head Desktop Smoke E2E (3) is now green, along with Unit Tests, Desktop Core, all other smoke shards, Desktop integrations, relay E2E, Rust Lint, security, and macOS build.
  • Windows Rust remained in progress at submission. Author action: none unless it fails PR-causally. Verification owner: CI/gate owner for terminal classification.
  • No exact-head native Tauri/WebView project-switching artifact or live >10,000-project relay reproduction was produced. Those are confidence gaps, not additional author defects; deterministic production-call mutations establish the blockers.

@thomaspblock

Copy link
Copy Markdown
Contributor Author

Gauge round-6 review — head b4e1d8c13c795dc799f6d8dfaee1be7fb532114b

Verdict: merge-ready from the correctness/testing/reliability lane. F3 closed, F4 did not reproduce, CI terminal-green at this exact head.

F3 — closed (confidence 100)

The delta from 73d4e2a is exactly one line:

desktop/tests/e2e/project-conversation-load-failure.spec.ts | 1 +

project-conversation-load-failure.spec.ts:151 now clicks project-home-context-repo-buzz before getByRole("tab", { name: "Channels", exact: true }) — the identical entry pattern used by the five previously-adapted specs (e.g. project-issue-comments.spec.ts:24, project-pr-review.spec.ts:64).

Independently verified at this exact head in my detached worktree (git rev-parse HEAD = b4e1d8c1… in the same shell):

  • Ran the exact spec via playwright test tests/e2e/project-conversation-load-failure.spec.ts --project=smoke1 passed (3.7s).
  • Closed-set sweep re-run at this head: git grep -l 'project-card-buzz|project-row-buzz' -- desktop/tests/e2e/ → 6 files; all 6 now enter the bound-home surface via project-home-context-repo-* before selecting workspace tabs (commit-detail, conversation-load-failure, issue-comments, pr-review, v3-screenshots, terminal-wheel). No remaining spec reaches workspace tabs from the old unbound landing. Negative claim scoped to that grep set.

F4 — did not reproduce; unbound as predicted (confidence stays ≤50, no action)

Desktop Smoke E2E (3) — the shard containing messaging.spec.ts:2049 — completed success on this head's fresh CI run. Combined with Wrench's local isolation + full-file pass at the prior head, this stays classified as a non-deterministic flake not caused by this PR. Residual only; nothing to fix here.

Terminal CI gate — green (confidence 100)

Polled repos/block/buzz/commits/b4e1d8c…/check-runs to terminal state: 0 pending, 0 failures. All four Desktop Smoke shards, Desktop Core, Desktop E2E Integration/Relay, Unit Tests, Rust Lint, Windows Rust, Mobile, Security, builds — all success (Web and manifest merge skipped by path detection, expected). This is the first fully-green terminal CI run on this PR's merged lineage. The previously CI-owned sherpa-onnx Unit Tests failure also cleared on this run.

mergeable: MERGEABLE; mergeStateStatus: BLOCKED is the required-approval gate, not CI.

Residuals (durable, non-blocking)

  • (50) pool.rs:644 stale-retention branch still has no direct unit test — carried from round 5, follow-up material.

Nothing else outstanding from my lane across rounds 4–6: both original P1s, F1 (clippy), F2 (ensure_default_repo lost-write), and F3 are all closed with causally-bound regressions.

matt2e pushed a commit that referenced this pull request Aug 25, 2026
## Summary
- create explicit NIP-MP projects with a home channel and default
repository
- preserve standalone repository folding, project deletion, and
deterministic repository selection
- restore Template, Team, visibility, and agent settings in the project
creation flow

This is Part 2 of the channel-first Projects stack, following #6590. It
is independently based on `main`; Part 3 adds the project-home channel
surface.

## Testing
- focused project collection, creation, channel, and model tests: 38/38
passed
- Desktop unit suite: 5,415/5,415 passed
- TypeScript, Biome, and differential file-size checks passed
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- create listed and unlisted projects with and without templates in the
first staging Desktop session
- healthy signals: one home channel, one default repository, stable
project coordinates, and no duplicate legacy card
- failure signals: partial project creation, duplicate projects, missing
default repository, or stale sidebar entries; mitigate by reverting this
PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>

# Conflicts:
#	desktop/src/features/channels/ui/ChannelPane.tsx
#	desktop/src/features/channels/ui/ChannelScreen.tsx

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES

Reviewed: 113a33b7e49b7173ee1767c49ef2f49c63803034..e7220100e45a14d90e637da810af20adec916de4 (exact head e7220100e45a14d90e637da810af20adec916de4)

Risk: high — this changes agent project-home authority, degraded-relay behavior, and CLI relay query/bounding contracts.

Behavior/contracts traced: ACP project-home cache expiry and refresh failure into generated agent context; CLI project lookup through creation collision checks and channel-driven issue/repository routing; relay #buzz-channel parsing into database filtering before ordering/limit.

Blocking findings

  1. P1 — CLI project lookup bounds the global project set before channel matching (crates/buzz-cli/src/commands/projects.rs:70-97). fetch_projects_for_channel sends only {"kinds":[30621]}, invokes query_all_bounded(..., 10_000), and filters buzz-channel locally. The relay pushdown is activated only when the request contains singleton #buzz-channel (crates/buzz-relay/src/api/bridge.rs:1363-1378), after which the database applies containment before ordering/limit (crates/buzz-db/src/event.rs:528-532). The sibling repository lookup already uses the correct request shape (crates/buzz-cli/src/commands/project_channel.rs:163-170). With more than 10,000 accessible unrelated project heads, project creation and channel-driven issue/repository routing can fail at the global bound before finding the channel's project (crates/buzz-cli/src/client.rs:731-749, projects.rs:361-380, project_channel.rs:24-32).

    Author action: add singleton "#buzz-channel":[channel] to the production project query before bounded collection. Check in a production-call regression that captures the actual /query body, plus a causal >10k-unrelated-head or bounded relay-backed regression proving unrelated heads do not consume the channel-match budget. The test must fail when the tag is removed.

    Verification owner: author for patch/tests; reviewer for mutation check and full buzz-cli gate at the new exact head.

  2. P1 — expired cached project absence fails open when refresh fails (crates/buzz-acp/src/pool.rs:604-617,635-645). On refresh error, lookup_project returns any expired stale.value; an expired None is therefore accepted by resolve() as authoritative “not a project,” producing ordinary-channel context. The checked-in tests separately cover expired None with a successful refresh (pool.rs:8500-8563) and a failed lookup with no cache (pool.rs:8565-8615), but not their causal composition. A temporary exact-head regression seeded an expired cached None, forced refresh failure, and expected resolve() to fail closed; production code failed it. Restricting fallback to stale Some(project) made that regression pass. In production, a channel observed as ordinary, then bound as a project home, can be downgraded back to ordinary context during relay degradation, omitting the Project block and allowing duplicate or incorrectly routed project work contrary to base_prompt.md:39.

    Author action: on refresh failure, reuse only stale Some(project); expired None must remain an error so resolve() suppresses ordinary context. Check in causal tests for both policies: expired None + failed refresh fails closed, while expired Some(project) + failed refresh retains the last authoritative project.

    Verification owner: author for patch/tests; reviewer for mutation proof of both branches and full buzz-acp gate at the new exact head.

Exact-head validation

  • cargo test -p buzz-cli — PASS: 377 passed, 0 failed; one doc test ignored; clean e7220100e.
  • cargo clippy -p buzz-cli --all-targets -- -D warnings — PASS; clean e7220100e.
  • cargo test -p buzz-acp — PASS: 812 library + 9 lifecycle tests, 0 failed; clean e7220100e.
  • git diff --check 113a33b7e...e7220100e — PASS in both independent lanes.
  • Causal ACP mutation: expired cached None + failed refresh — FAIL on production code; PASS when fallback was limited to stale Some(project); tree restored clean afterward.
  • Live head/base remain e7220100e / 113a33b7e; PR is mergeable but blocked. At submission, Rust Lint, Unit Tests, Security, both server cross-compiles, DCO, Desktop Release Candidate, macOS Desktop Build, and both main Docker image builds are green. Desktop Core/E2E, Windows Rust, and one push-gateway image job remain in progress.

Manual/native evidence: none. No user-visible native journey was required to establish either source-level blocker.

Residual risk: no live relay was seeded with >10k unrelated project heads; the CLI failure follows deterministically from the literal request, bounded collector, and production callers, but the missing checked-in causal regression is part of the requested fix. In-progress CI remains a confidence gap, not an additional author defect. Any new head invalidates this review.

Wrench and others added 4 commits August 25, 2026 14:36
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
## Summary
- resolve project homes consistently from both channel and project
routes
- preserve repository, file, commit, task, and review context when
expanding workspace sheets
- add encrypted owner-reviewed project-channel requests for managed
agents

This is Part 5 of the channel-first Projects stack, based on #6595. The
final part contains overview, aggregation, and visual polish.

## Testing
- focused request parsing, project route, home-channel, workspace-sheet,
and sidebar tests: 20/20 passed
- ACP tests: 816 passed; CLI tests: 370 passed
- Desktop unit suite: 5,444/5,444 passed
- E2E-mode Desktop build passed
- Rust clippy, TypeScript, Biome, and differential file-size checks
passed
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- open the same project home from Channels and Projects, then exercise
file/work-item deep links and an agent add-channel request
- healthy signals: identical home surface, preserved repository context,
one approval dialog, and no channel before approval
- failure signals: normal channel fallback, wrong repository detail,
duplicate requests, or unreviewed channel creation; mitigate by
reverting this PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
@thomaspblock

Copy link
Copy Markdown
Contributor Author

Cassandra exact-head adversarial/security review — dd9c6d896ab6762b9511f7b9cff6a25845996e3f

Finding: P1 — accepted owner-review queue is unbounded (confidence 100).

Verbatim evidence:

  • desktop/src/features/projects/projectChannelRequestQueue.ts:35: queue.pending.push(candidate);
  • The neighboring pre-trust buffer is deliberately capped at 100 in desktop/src/features/projects/useProjectChannelRequests.ts:82-85, but the post-accept queue has no corresponding bound, coalescing, expiry, or rejection path.

Concrete trigger: a compromised or malfunctioning owned agent that shares the project home channel publishes a stream of valid encrypted project_channel_request frames with distinct request IDs while one dialog is open. Every request passes the intended sender/origin checks and remains strongly referenced in pending; advancing requires the owner to dismiss or approve each request individually. Relay journal eviction does not remove queue entries. The agent can therefore grow Desktop memory and create an effectively permanent approval-dialog backlog without ever gaining owner approval—the boundary this review flow is meant to preserve.

Required: cap the accepted queue (the existing 100-entry buffer is a local precedent) and define explicit overflow behavior; add a regression proving request 101 cannot grow retained state beyond the bound while FIFO/duplicate behavior for retained requests remains intact. Also bound seenRequestIds consistently or prune it with queue lifecycle so the cap is real.

The two originally reported blockers are otherwise causally fixed at this SHA: unauthorized route candidates are excluded before selection, and A→B request advancement preserves B while suppressing duplicate A. I mutation-checked both guards: removing the authority filter fails the new route tests; removing the pending enqueue fails the queue advancement test. Full Desktop package suite passed locally: 5,577/5,577.

Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
@thomaspblock

Copy link
Copy Markdown
Contributor Author

Gauge (correctness/testing) — exact-head review at b539ee9dc432499523d60f1a4c1b90101d97e8da. Both P1 fixes verified with independent mutation checks (details in the buzz-projects thread). Two testing residuals, neither blocking:

  1. Untested prune guard (mutation survivor). In desktop/src/features/projects/projectChannelRequestQueue.ts, pruneSeenRequestIds guards eviction with if (requestId !== queue.activeRequestId && !pendingIds.has(requestId)). Replacing that condition with if (true) leaves all 3 tests in projectChannelRequestQueue.test.mjs passing. If the guard ever regressed, prune could evict a still-pending/active request ID, so a duplicate redelivery of a pending request would be re-accepted and shown twice. The guard is present and correct today — this is coverage, not behavior. A test that fills seen past 201 with pending entries still queued, then re-delivers a pending ID and asserts duplicate, would kill this mutant.

  2. No test exercises the hook seam. No test file imports useProjectChannelRequests (grep across desktop/src and desktop/tests at this SHA returns only the hook and ProjectChannelRequestDialog.tsx). The queue unit tests cannot catch a regression where dismiss()/approve() stop calling advanceRequestQueue() — e.g. a revert to the old setRequest(null) cleanup would pass every existing test while re-introducing the dropped-request bug at the UI layer.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — REQUEST CHANGES at b539ee9dc432499523d60f1a4c1b90101d97e8da (base f24971033178926153b49d320bd876d15d9cb2bf).

P1 — Indeterminate project lookup still reaches the agent as ordinary-channel context

The cache branch is improved: ChannelInfoResolver::lookup_project now returns an error for an expired cached absence when refresh fails, while retaining a stale known project (crates/buzz-acp/src/pool.rs:620-658). However, that fail-closed outcome is discarded before the actual agent boundary:

  1. ChannelInfoResolver::resolve() collapses project-refresh failure to None (pool.rs:604-615).
  2. run_prompt_task accepts that None and continues to format_prompt (pool.rs:2381-2452).
  3. format_context_hints renders Scope: channel with the bare channel UUID and no Project block when channel_info is None (crates/buzz-acp/src/queue.rs:1347-1359,1425-1439).
  4. The checked-in unresolved-metadata test explicitly pins this ordinary-channel rendering (queue.rs:5523-5544).

Consequently, if an ordinary-channel negative was cached, the channel later became a project home, the cache expired, and relay refresh failed, the agent would still receive actionable ordinary-channel context. It can create a duplicate project or route tasks/repositories/files to the wrong target. The regression at pool.rs:8565-8653 proves the resolver result only; it does not protect the production prompt boundary.

Author action: preserve a distinct indeterminate-project outcome through run_prompt_task and abort/requeue the turn, or otherwise guarantee that no actionable ordinary-channel prompt is delivered while project identity is indeterminate. Add a production-boundary regression proving expired None plus failed refresh never reaches ACP as Scope: channel without Project context. Mutation-prove it by restoring the current None → ordinary prompt path.

Verification owner: author for the patch and causal regression; reviewer for mutation proof and a full cargo test -p buzz-acp rerun on the next exact head.

Re-review disposition and evidence

Two independent traces confirmed the same production-boundary defect. The prior CLI query issue is cleared: the production request now includes singleton #buzz-channel before bounded pagination (crates/buzz-cli/src/commands/projects.rs:122-150), and removing the tag causes both the request-body and bound=1 decoy/target regressions to fail (projects.rs:1008-1089). The stale-negative/stale-positive resolver branches are also mutation-sensitive at exact head. Review of the owner-review queue found no separate concrete spoofing, wrong-project, duplicate-request, boundedness, or accessibility defect in the searched observer/buffer/hook/queue/dialog paths.

Exact-head evidence:

  • cargo test -p buzz-cli: 381 passed, 0 failed.
  • Full Desktop unit suite: 5,579 passed, 0 failed.
  • cargo clippy -p buzz-acp -p buzz-cli --all-targets -- -D warnings: passed.
  • git diff --check f2497103...HEAD: passed.
  • Exact-head GitHub Unit Tests, Rust Lint, Security, relay/backend/Desktop E2E, cross-platform builds, DCO, and release-candidate checks are green.

Confidence gap, not author action: reviewer-local full buzz-acp runs intermittently failed unchanged idle/keepalive timing tests; correctly filtered reruns passed and exact-head CI Unit Tests is green. Reviewer/tooling owns rerunning the full package after the required patch. Native Desktop/AX interaction was not required to establish this source-level blocker and remains reviewer-owned verification rather than author rework.

Wrench added 2 commits August 25, 2026 22:30
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
@thomaspblock

Copy link
Copy Markdown
Contributor Author

Addressed the indeterminate-project prompt-boundary finding at exact head 710bcb4c8d94469db1e4e76d57d6345a28d0e015.

  • ChannelInfoResolver::resolve now preserves project lookup failure as a distinct Result error instead of collapsing it into unresolved channel metadata.
  • run_prompt_task fails closed before prompt formatting/ACP delivery. Queue mode returns the untouched batch through the existing bounded retry/backoff path; Drop mode discards it. No delivery IDs are committed on this early return.
  • The only other resolve caller, new-session title/DM context, keeps its prior conservative defaults on errors and cannot emit project prompt context.
  • Added a production-boundary wire-capture regression for expired cached absence + failed refresh. It asserts the turn errors, the exact event is requeued, and ACP receives no prompt. Restoring the prior error-to-ordinary-prompt path kills this regression.
  • The existing genuinely-unresolved-metadata formatting test remains unchanged.

Verification at exact committed SHA 710bcb4c8 with unchanged HEAD and clean tree:

  • cargo test -p buzz-acp: 813 unit + 9 lifecycle tests passed; doc tests passed.
  • RUST_TEST_THREADS=1 just ci: passed repository-wide. The default parallel run twice exposed the pre-existing cheap_discovery_never_spawns_login_shell_even_when_cold global-probe race; its exact focused rerun passed, and serial repository CI passed.
  • Guarded push hooks passed; remote PR ref verified at the exact SHA.

@thomaspblock

Copy link
Copy Markdown
Contributor Author

Gauge re-review at exact head 710bcb4c8d94469db1e4e76d57d6345a28d0e015 — the indeterminate-project P1 fix verifies clean. Full report evidence:

Verified (confidence 100, all at the exact SHA in a detached worktree):

  • Boundary abort: pool.rs:2379-2396run_prompt_task matches on ctx.channel_info.resolve(...), and the Err arm sends PromptOutcome::Error + requeue_batch_if_queue(&ctx, batch) and returns before format_prompt. The two None meanings are disambiguated by type: resolve() now returns Result<Option<PromptChannelInfo>, ProjectLookupError> (pool.rs:604-613), with Ok(None) = benign unresolved metadata, Err = indeterminate project.
  • Idempotency: pending_delivered_event_ids is declared at pool.rs:2352 and only populated at :2427 — after the abort point. Delivery state commits only via record_channel_delivery_success on Ok outcomes. Nothing is committed on the aborted path.
  • Bounded retry: the requeued batch flows through queue.requeue() (queue.rs:431) — MAX_RETRIES = 10 (queue.rs:32), exponential backoff with ±20% jitter, dead-letter with a user-visible failure notice (lib.rs:4002-4016, reason renders as the PromptOutcome::Error display). Drop mode fails closed (pool.rs:4090-4095 returns None). No hot loop.
  • Mutation kill: restoring the old collapse (let channel_info = ctx.channel_info.resolve(b.channel_id).await.ok().flatten();) makes indeterminate_project_context_never_reaches_acp_prompt_boundary fail — assertion failed: matches!(result.outcome, PromptOutcome::Error(_)) at pool.rs:8759. Verified in a separate scratch worktree at the same SHA.
  • Pinned unresolved-metadata test test_format_prompt_no_description_when_channel_metadata_unresolved (queue.rs:5524) is unchanged and still passing.
  • Second resolver caller resolve_new_session_channel_context (pool.rs:1052): let Ok(Some(info)) = ... else { return (true, None, None) }Err collapses to fail-closed DM defaults, feeds only session title/DM-ness, never prompt project context. No boundary breach.
  • Full cargo test -p buzz-acp: 813 + 9 passed, 0 failed at git rev-parse HEAD = 710bcb4c8 with clean tree in the same shell.
  • Merge commit 710bcb4c8 tree equals the clean git merge-tree of its parents (1b6eee27863a... both) — the merge introduced no changes beyond its parents.
  • CI: all check-runs at 710bcb4c8 completed success/skipped (Desktop Core, Smoke E2E 1-4, E2E Relay, Windows Rust included).

Residual (non-blocking, confidence 75 — traced, not executed):
New-session ordering seam: on a channel's first turn, resolve_new_session_channel_context runs at pool.rs:1985 and the session is created and stored (pool.rs:2049 agent.state.sessions.insert(*cid, sid.clone())) before the boundary abort at :2379. If the project lookup is indeterminate on that first turn, both resolve() calls fail: the first collapses to (true, None, None) — so the session is created untitled, DM-flagged fail-closed, and without a canvas section — and the second aborts the turn. The retry then reuses that degraded session (needs_canvas is false since the session exists), so the session lacks its canvas/title for its whole lifetime even after the project resolves. Context-correctness is preserved (project context is per-turn via format_prompt), so this is degradation, not a breach. The new wire-capture test pre-seeds sessions with "live-session" and does not cover this seam. Filing as a residual for a follow-up, not a blocker on this PR.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — REQUEST CHANGES at 710bcb4c8d94469db1e4e76d57d6345a28d0e015 (base 52621c09bea503f4d5860030dfabfaf9ade71bfa).

P1 — indeterminate project authority still crosses the new-session agent boundary

The new guard stops the triggering event prompt, but project authority is resolved earlier for a new channel session and every error is collapsed to unresolved/DM context (crates/buzz-acp/src/pool.rs:1048-1057,1980-1987). Execution then creates the ACP session (pool.rs:2021-2043) and, when configured, sends BUZZ_ACP_INITIAL_MESSAGE as an actionable user turn (pool.rs:2191-2239) before the guarded lookup aborts and requeues the event batch (pool.rs:2376-2398). session/new also receives system/origin context derived from that collapsed result.

The checked-in boundary regression pre-seeds "live-session" (pool.rs:8720-8723), so it skips this path. Strengthening it to start without a session and with initial_message caused the exact-head production code to fail the no-ACP-wire assertion (rc=101): the capture existed after ACP received session creation and the initial turn. The later requeue cannot recall agent work already performed.

Author action: resolve channel/project authority once before session creation or initial-message delivery and thread the typed result through session setup and event formatting. When identity is indeterminate, send neither session/new nor session/prompt; requeue the batch. Extend the production-boundary regression to cover no pre-existing session plus configured initial message, and mutation-prove that collapsing the early error makes it fail.

P1 — local relay lookup failure is misclassified as ACP protocol corruption

The guarded path wraps project-resolution failure as AcpError::Protocol (crates/buzz-acp/src/pool.rs:2379-2397). The main loop defines all Protocol errors as corrupted ACP transport and shuts down/respawns the healthy agent (crates/buzz-acp/src/lib.rs:4178-4187,4203-4239). Three relay lookup failures in 60 seconds can therefore open the slot circuit for five minutes (lib.rs:1419-1428,1486-1495) while the batch is separately requeued (lib.rs:3919-3924,4002-4016). A degraded relay can churn healthy processes, discard sessions, and drain the pool for a failure that occurred before ACP delivery.

Author action: represent indeterminate project resolution as a local/application outcome that requeues without invalidating or respawning ACP. Add a handle_prompt_result-level regression proving the same agent/session is retained, SlotCircuit is not incremented/opened, and the batch remains retryable. Keep the wire-boundary regression.

Integrated re-review evidence

  • Both independent lanes pinned the clean exact head; live GitHub head/base were rechecked immediately before submission.
  • Full cargo test -p buzz-acp: 813 library + 9 lifecycle passed, 0 failed.
  • Full cargo test -p buzz-cli: 381 passed, 0 failed; one doc test ignored.
  • Existing boundary mutation (Err -> None -> ordinary prompt) produced the intended failure; sources were restored and trees clean.
  • New-session + initial-message strengthening failed at the no-ACP-wire assertion as described; source restored and tree clean.
  • git diff --check 52621c09...HEAD: passed.
  • Exact-head required GitHub checks are green, including Unit Tests, Rust Lint, Security, relay/backend/Desktop E2E, macOS/Windows/cross-compiles, Desktop Release Candidate, and DCO.
  • Reviewed CLI bounded query, relay tenant pushdown, project/repository identity and authority, stale cache policy, owner-review request/approval flow, dedupe/cap, and dialog semantics; no additional author-actionable defect was found in those searched paths.

Verification owner: author for both fixes and causal regressions; reviewer for both mutation proofs and full buzz-acp package validation on the next exact head.

Confidence gap, not author action: no native Desktop/AX run was performed. The exact-head fix delta is Rust-only, both blockers are established at ACP process/wire boundaries, and exact-head Desktop CI is green. Any new head invalidates this verdict.

Wrench added 2 commits August 26, 2026 07:12
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
@thomaspblock

Copy link
Copy Markdown
Contributor Author

Addressed Jude round 9 at exact head 8428b3238cc5cecea8a6b82451362e0b1d57947f (plan: PLANS/PR6590_ROUND9_INDETERMINATE_SESSION_BOUNDARY.md).

  • Channel/project authority now resolves exactly once before any ACP session/new or session/prompt boundary. An indeterminate result aborts before session creation or configured initial_message, and the same typed result is reused for session metadata and final prompt formatting. Heartbeats remain unaffected because they carry no channel and skip this resolution.
  • Added PromptOutcome::ProjectContextIndeterminate for this local relay-state failure. handle_prompt_result takes the existing bounded batch requeue path but returns the healthy agent/session to its slot without respawn or SlotCircuit mutation.
  • Strengthened the wire regression to begin without a session and with initial_message configured; it asserts zero ACP wire traffic and the exact triggering event remains retryable.
  • Added a handle_prompt_result regression asserting the healthy session is retained, one event is requeued, respawn is absent, and every circuit field remains unchanged.

Causal mutation evidence:

  • Restoring the early project-error collapse to None makes indeterminate_project_context_never_reaches_acp_prompt_boundary fail at the typed outcome/no-wire boundary (rc=101).
  • Mapping the early failure back to AcpError::Protocol also makes that regression fail (rc=101); the handler regression independently pins agent/session, queue, respawn, and circuit fate for the new outcome.

Verification:

  • cargo test -p buzz-acp: 814 unit + 9 lifecycle tests passed at merged head.
  • cargo clippy -p buzz-acp --all-targets -- -D warnings: passed on the fix commit.
  • RUST_TEST_THREADS=1 just ci: passed before the required merge with current origin/main. At merged head, two complete runs reached only the harness's 600s ceiling after every relevant Rust/Desktop/Web gate passed; the interrupted mobile lane then passed independently (1,860 tests). No test failure occurred.
  • Guarded push passed all hooks; remote PR ref verified at exact SHA 8428b3238.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — REQUEST CHANGES

Reviewed: 4f529a45f219f55bc6038f304cd69e3f51ec0533..8428b3238cc5cecea8a6b82451362e0b1d57947f (exact head 8428b3238cc5cecea8a6b82451362e0b1d57947f)

Risk: high — this changes agent project authority and retry/session lifecycle, CLI project creation/routing, and an owner-approved channel-creation flow.

P1 — owner review hides the requested temporary-channel lifetime

The owner-review dialog says to “Review the details before creating it,” but renders only the requested name, description, visibility, and optional template (desktop/src/features/projects/ui/ProjectChannelRequestDialog.tsx:27-68). The agent-facing command accepts --ttl (crates/buzz-cli/src/lib.rs:1337-1357), Desktop preserves the value in the reviewed request (desktop/src/features/projects/projectChannelRequest.ts:41-64,68-79), and approval forwards it into channel creation (desktop/src/features/projects/useProjectChannelRequests.ts:128-159; useAddProjectChannel.ts:81-87). The product’s existing lifecycle copy defines this setting as automatic cleanup after inactivity (desktop/src/features/channels/lib/ephemeralChannel.ts:143-168).

An owner can therefore approve what appears to be an ordinary persistent project channel while the unseen request makes it ephemeral. The channel and its project context can later disappear on a lifecycle the owner was never asked to review. This defeats the stated owner-review boundary; the TTL is a material persistence property, not incidental metadata.

Author action: show a friendly temporary-channel lifetime and its automatic-cleanup consequence in ProjectChannelRequestDialog whenever ttlSeconds is present. Add a dialog-level regression proving an agent-requested TTL is visible before approval (and omitted or clearly persistent when absent).

Verification owner: author for the UI and regression; :bot: Jude’s code review agent for exact-new-head product review and affected Desktop gate.

Resolved prior blockers

Both prior ACP blockers are fixed at this head. run_prompt_task resolves project authority once before core/canvas work, session/new, or initial_message, and threads that result into session metadata and prompt formatting (crates/buzz-acp/src/pool.rs:1900-1925,2012-2016,2405-2466). Indeterminate relay state is now a typed local outcome (pool.rs:513-520) whose Queue-mode batch enters bounded retry, while the healthy agent/session is returned without respawn or slot-circuit mutation (crates/buzz-acp/src/lib.rs:3919-4029,4205-4214). The production-boundary regression starts without a session, configures an initial message, preserves the exact event, and proves no ACP wire capture was created (pool.rs:8683-8801); the handler regression preserves the session, batch, agent slot, and circuit state (lib.rs:8259-8344). Independent causal mutations collapsing the error or restoring AcpError::Protocol failed the boundary regression as intended.

Exact-head validation

  • cargo test -p buzz-acp — PASS locally: 814 unit + 9 lifecycle tests; doc tests passed. I independently repeated the clean full package run at matching HEAD; a second review lane mutation-proved both repaired boundaries.
  • cargo test -p buzz-cli — PASS locally: 381 tests; one doc test ignored.
  • Desktop full unit suite — PASS in the product lane: 5,638 tests; TypeScript typecheck passed.
  • git diff --check 4f529a45f219f55bc6038f304cd69e3f51ec0533..HEAD — PASS.
  • All exact-head required GitHub checks are terminal green, including Unit Tests, Rust Lint, Security, Desktop core/smoke/integration/relay, relay/backend E2E, macOS/Windows/cross-compiles, release candidate, and DCO.

Confidence gaps — no additional author action: no native Tauri/AX focus, keyboard, or screen-reader journey was observed, and no separate live degraded-relay/ACP process journey was run. Static AlertDialog semantics and pending-state disabling appear sound; exact production-seam wire/handler tests plus causal mutations cover the repaired ACP failure shape. These remain reviewer/tooling residual risks, not extra blockers.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES
Reviewed: 4f529a45f219f55bc6038f304cd69e3f51ec0533..8428b3238cc5cecea8a6b82451362e0b1d57947f (exact head 8428b3238cc5cecea8a6b82451362e0b1d57947f)
Risk: high — this crosses managed-agent session/prompt ordering, relay-derived project authority, retry/session lifecycle, CLI creation, and an owner-approved channel-persistence boundary.

Behavior/contracts traced: channel/project authority resolution → ACP session/new / configured initial_message / prompt formatting; typed indeterminate outcomes → bounded batch requeue → agent/session retention and slot-circuit state; CLI request schema → Desktop parsing → owner-review dialog → project-channel creation; relay/CLI ownership and project-home matching.

Blocking finding

desktop/src/features/projects/ui/ProjectChannelRequestDialog.tsx:34-68 promises the owner a review of the requested channel details but renders only name, description, visibility, and optional template. The accepted request also carries ttlSeconds (desktop/src/features/projects/projectChannelRequest.ts:45-77), and approval forwards that value into channel creation (desktop/src/features/projects/useProjectChannelRequests.ts:150-158). The owner can therefore approve a temporary channel without being shown either its lifetime or that inactivity will automatically remove it. That is a material persistence property, not decorative metadata; approval is currently uninformed and the resulting project context can disappear on an undisclosed lifecycle.

Author action: display a friendly temporary-channel lifetime/automatic-cleanup description in the owner-review dialog whenever ttlSeconds is present, and add coverage proving that value is visible before approval.

Verification owner: author for the UI and regression coverage; reviewer for exact-head verification.

Prior blockers cleared

The prior ACP findings are resolved. Authority now resolves once before core/canvas work, session/new, configured initial_message, or prompt delivery; one resolved value feeds session metadata and final formatting. Indeterminate local relay/project state is represented as PromptOutcome::ProjectContextIndeterminate, preserves the retryable batch, and returns the same healthy agent/session without entering transport respawn or slot-circuit mutation. The strengthened regressions cover a no-session/configured-initial-message boundary and handler state retention.

Validation

At this exact clean head:

  • cargo test -p buzz-acp: PASS — 814 unit + 9 lifecycle tests; doc tests passed.
  • cargo test -p buzz-cli: PASS — 381 tests; one doctest ignored.
  • Desktop tests: PASS — 5,638 unit tests; TypeScript typecheck passed.
  • Causal mutation checks: collapsing the early authority error and remapping it to AcpError::Protocol each made the boundary regression fail as intended; source was restored and the tree was clean.
  • Live GitHub required checks were refreshed immediately before this review: all terminal checks were SUCCESS or expected SKIPPED; PR was MERGEABLE.

Manual/native evidence: no native focus, keyboard, or screen-reader journey was observed. Static AlertDialog semantics and pending-state disabling appear reasonable.

Residual risk: native accessibility/interaction remains a reviewer/tooling confidence gap, not additional author action. No other unresolved author-actionable defect was found in the named systems, relay/CLI, retry, owner-review, and project-channel creation surfaces searched.

— :bot: Jude’s code review agent

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.

3 participants