Skip to content

feat(git): switch branches from the Git panel and composer chip - #17628

Open
kyleseaman wants to merge 2 commits into
mainfrom
feat/changes-git-branch-switcher
Open

kyleseaman wants to merge 2 commits into
mainfrom
feat/changes-git-branch-switcher

Conversation

@kyleseaman

@kyleseaman kyleseaman commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Problem / Motivation

Goal: Switch, check out and create git branches from the dashboard's Git panel and from the project chip under the chat box, the way VS Code and Zed do from their status bar.

Today the Git (Changes) panel header and the composer's project chip only show the current branch with a click-to-copy button. Changing branch means leaving the dashboard for a terminal, then coming back and waiting for the panel to notice.

Why it matters

Moving between local branches is the most common git action while reviewing an agent's work in a session's project folder. Without a switcher the dashboard shows the branch but cannot act on it, so every switch costs a context change to a terminal, and the file tree and Changes list lag until they repoll.

Not a goal

  • Stage, commit, fetch, pull or push controls. Fetch/pull/push need credentials, which is a separate security question.
  • Deleting, renaming or rebasing branches.
  • Switching when the session's project folder is a subfolder of a repository (refused, same rule as "Start in new worktree").

What changed (motivation → approach → change)

Goal → a single searchable picker reused on both surfaces, backed by two narrow endpoints → built:

  • GET /api/project/git/branches (src/kiro_crew/dashboard/handlers/git_branches.py): local branches newest-commit first (author, age, subject, ahead/behind), plus remote branches that have no local twin. Subjects and the list are capped and the response says when it is truncated. Branch names, subjects and errors pass through the shared redactor.
  • POST /api/project/git/switch: owner-only (same gate as saving a file), audited, git run through the existing sandboxed _run_git with hooks off and LC_ALL=C (new c_locale flag on _run_git) so refusals are classified on git's English text. Switches use --no-overwrite-ignore, so neither tracked uncommitted work nor ignored local files are ever overwritten, and --no-recurse-submodules, so a repo's submodule.recurse cannot make a checkout reach a submodule's own filter drivers; git's own refusal is shown in plain language, naming the branch. git's diagnostic line is redacted before it is clipped. Refused for subfolder projects and for repos whose config declares a checkout content filter (a checkout would run the filter program). Refused with 409 git_switch_session_busy while ANY chat slot working in the same repository (its root, a folder inside it, or a folder containing it) has a turn running or sub-agents pending (a running turn counts in every folder it works in, not just the slot's current project: the project it started in, the project its session was allocated for, and the folder that session reports, plus the live session's own folder, since a project picked mid-turn, set_project and a member rebinding move the slot's project but not the running session, and a kept or resumed session can run where no project value names; while a turn is between starting to allocate its session and that session reporting its folder it counts as busy in every repository), or a workflow step's session runs in it (workflow steps open their own sessions outside chat slots and the sub-agent manager, so each step registers itself with the busy check: busy everywhere until its session reports a folder, then busy in that folder and in the project its prompt is built against; a step whose turn the backend did not itself report finished (it failed, was cancelled, was ended by the session layer, or was cut off at its tool-call ceiling) ends its session before the busy check stops seeing it, since none of those stops the session's native turn: the session is destroyed, never a successor under the same name, and the step stays busy until the runtime confirms the process tree exited, retrying if it has not; a named workflow session loses its conversation when one of its steps ends that way, and a runtime that never confirms its process tree exited keeps that repository's switch refused until restart; each retry re-checks the descendants a teardown left for the orphan sweep (AcpRuntime.kill), so the step stops counting once they are gone), or any agent prompt is in flight there, whatever drives it (a messaging-channel turn on Slack, Telegram, Discord and the rest, a monitor wake, a cron or heartbeat turn, an app's turn: every harness sends a turn as an ACP request through AcpClient._send_request for the adapter harnesses (claude, codex, goose, opencode and the rest) or one of AcpRuntime's three request writers for the shared kiro/KAS/codex runtime, and each prompt (a turn, a kiro slash command, or a mid-turn steer, which counts until answered and, when the adapter answers that it started a turn of its own, until the process tree is confirmed gone) is recorded on its session's folder, or as busy in every repository when that is unknown, from before its write until the backend's own answer to it (matched on the exact integer id issued) is read off the wire or the runtime confirms the process tree exited; the knowledge pool's legacy stream-json CCWorker, the one agent worker that speaks no ACP, counts its prompts the same way until their result is read; a cancel, a timeout, an end of turn the session layer makes up, or a caller that stops reading leaves it counted, since none of those stops the backend's tools; before the write a prompt waits out a switch of its folder, adding no wait when none is in progress; a process whose teardown left survivors keeps its prompts counted until a later kill confirms them gone, so a runtime that never confirms that keeps the repository's switch refused until restart), or any running sub-agent works inside the repository through its own folder (its cwd, or the folder its session started in) or an ancestor run's (a sub-agent whose ancestry or folder is unknown counts as busy: a dismissed parent card, or a run with no cwd whose session has not started yet, since the allocator picks its folder, e.g. a crew member's workspace), checked under the per-repo lock before git runs, so a second tab, a cron turn on another slot or a lingering sub-agent cannot have files rewritten under it. Neither turn nor sub-agent admission takes that lock, so the switch also reserves the repository (new repo_checkout_guard.py, outside the dashboard package so the ACP transport and workflows do not import dashboard code; the dashboard-only slot and sub-agent readers live in dashboard/repo_checkout_guard.py) before the busy check. _run_chat waits at its entry, again (for any checkout anywhere) just before it captures its binding and claims its session, and once more on the folder the claimed session reports; every sub-agent run waits just before it executes (a pre_run_gate on the manager, covering spawn, retry and continuation), while a reservation covers their folder: work either is already visible to the busy check or starts only after git finishes. The wait has no deadline, and the reservation is released by the checkout worker itself, so a cancelled request cannot drop it mid-checkout. Every error goes through the coded {error, code} body. A gap that predates this PR and is unchanged: a prewarmed session can start kiro-cli in a repository mid-switch, though the turn that later claims it waits for the switch. A sub-agent run counts as busy from start until its teardown is over (its run or recovery task, teardown gate and retained kill handles), not just until it is marked done, and a teardown entry whose run record was already cleared counts in every repository. A sub-agent whose kill failed, or whose shared-runtime cancel did not stop its native turn, stays counted through its unanswered prompt (above) until the backend answers it or the process tree is confirmed gone.
  • BranchSwitcher.tsx: the picker (filter, arrow keys, Enter/Tab to switch to an existing branch, Escape; a "New branch "name" from " row below a divider, which shows "Press Ctrl+Enter or click to create it" only while it is highlighted, and which only Ctrl+Enter / ⌘+Enter or a click activates, so a mistyped search plus Enter never creates a branch; the initial highlight lands on a matching branch, not the create row; footer copy chip and a "↑ ahead · ↓ behind its remote branch" legend whenever a row shows a count). Used in the Git panel header and as a chip variant on the composer project chip, which opens upward. A line above the list, in normal body text, says what a switch does and then the guarantee ("Switching updates the files in this project folder to that branch's version. Your uncommitted changes are kept. If a switch would overwrite any of them, nothing changes."), using "uncommitted changes", the Changes badge's own term, in every branch-picker string, and each switchable row repeats the guarantee in its tooltip ("Your uncommitted changes are kept."); the search field's placeholder is "Find a branch or name a new one…", so it does not suggest Enter creates; the search field is described by the line, so a screen reader hears it while arrowing through rows; it is hidden while a refusal is shown. A successful switch shows "✓ Switched to {{branch}}" beside the trigger and announces it to screen readers. Refusals lead with the outcome and the next step ("Not switched: this branch would overwrite some of your uncommitted changes. Commit them, or ask the agent in this chat to commit or stash them, then switch again. (In a terminal: git commit or git stash.) Files in the way: maps/MOC-tools.md.", naming up to five of the paths git lists, then "+N"; "Switching and creating branches are off for this repository. Its Git settings ask Git to run another program whenever it switches branches, and this panel won't run it. Switch in a terminal instead."). When rows sit below the list's scroll edge, a chevron strip under the list (it covers no row) shows there is more, so a section heading that lands on the edge does not read as an empty section. Ahead/behind pills carry a tooltip and screen-reader text ("2 commits ahead of its remote branch", the same term as the legend and the Remote section). While a response is running, switching is off on both surfaces and the trigger copies the branch name instead. The trigger itself looks busy then (muted text and glyph, a copy glyph, a copy cursor; all static, since it lasts the whole response), and the reason is shown in a visible note on hover and focus ("Copy branch name. To switch branch, first stop the response with ■ next to Send."), so reading the branch stays available mid-run; a failed copy shows an ErrorNotice beside the trigger, quoting the name ("Couldn't copy “develop” to the clipboard."), with a Try again button (the footer copy inside the picker does the same). An open picker closes if a turn starts, because a checkout would change files under the running turn. When a content filter blocks switching, rows are muted and inert and the search box is disabled and drawn dimmed. Remote rows show the branch name without its <remote>/ prefix (kept only when two remotes carry the same name) and a line leading with the outcome ("Switch to feature/x (makes a local copy; uncommitted changes are kept)"). Remote rows whose short name fails the new-branch grammar are shown but not offered. After a switch the Changes/Commits lists, file tree and composer chip refresh.
  • LazyBranchSwitcher.tsx: both hosts load the picker through this import() boundary, so its ~17 KB stays out of the always-loaded ChatMessageList chunk (main has 8 KB of headroom under the Bundle Size Gate's 500 KB budget; this PR adds ~1 KB there). Until the chunk arrives on first render, a non-interactive placeholder of the same size shows the branch, so the chip and the header do not shift.
  • Feature map row, spec line, security-posture inventory entry, and strings in all 12 locales.
  • Git tab sections (GitPanel.tsx, GitReposPanel.tsx): since feat(git): list every repo a chat works in on the Git tab #18233 the Git tab shows one collapsible section per repository the chat works in. The switcher is on the chat project's section only, since the switch route accepts slot projects, not repositories a tool call merely touched; other sections keep the plain branch label. A section heading is one toggle button and a button cannot nest in a button, so on the project section the status line follows the toggle and the toggle's hit area (::after) stretches over it: a click on the change count still toggles, the switcher takes its own clicks, and the line is the toggle's accessible description.
  • Workflow steps (src/kiro_crew/workflows/agent_exec.py, agent_pool.py): each step wraps its session work in repo_checkout_guard.tracked_work, so the busy check sees it, and the step waits for a switch in progress before it starts its session.
  • Ending a step's session (src/kiro_crew/providers/acp.py): a step keeps its session only on the backend's own completion event (stream_and_collect's existing on_complete); AcpProvider.process_tree_confirmed_dead exposes the runtime's own evidence that a shutdown ended the process tree, which is what lets the busy check stop counting a step whose session it ended.
  • website/scripts/capture-branch-switcher.mjs: the screenshot harness for the frames below, following the repo's capture-*.mjs convention (mocked git responses against the production build; each mocked branch has its own commit history, so frames after a switch show the Commits list change).

Backwards compatibility

Compatible: two new routes and a new optional c_locale flag on _run_git; existing git endpoints and their responses are unchanged.

Changes one click target, removes no capability: when idle, clicking the branch name on the composer chip or Git panel header opens the picker instead of copying (copy is in the picker footer); while a response runs, the same click still copies, as before. The pin "leaves the branch copyable while a response is running" is kept.

Changes one workflow behaviour: a workflow step whose turn the backend did not report finished (failure, cancel, tool-call ceiling, or a terminal the session layer made up) now ends its session (agent_pool._run_unpooled_tracked, _WorkflowSessionWorker), so a named workflow session loses its conversation after such a step instead of carrying it to the next one. Pinned in test/test_repo_checkout_guard.py and test/test_workflows_agent_pool_unpooled_teardown.py; a step the backend reported finished keeps its session as before.

Tests

  • test/test_project_git_branches.py (46 tests, real git repositories): listing order and fields, remote-twin filtering, truncation, switching, creating, tracking, the owner gate, subfolder and content-filter refusals, uncommitted-change refusal, ignored-file preservation under --no-overwrite-ignore (fails against plain git switch), non-English git classification, not-a-repo vs ownership-check verdicts, redacted names, and submodule.recurse=true leaving the submodule untouched.
  • test/test_repo_checkout_guard.py: the reservation registry; both admission orderings, including the reported race (a turn admitted while the busy check's sub-agent probe is suspended waits until git exits; fails with the entry wait removed); a long checkout waited out rather than abandoned; sub-agent runs gated on every folder in their lineage, a Stop during that wait, and the busy check seeing a running sub-agent that works in the repository from a chat outside it; a running turn seen through every folder it works in (mid-turn pick, the live session's folder, an unplaced turn counting everywhere, an unreadable session registry), a turn allocating its session waiting out a checkout of an unrelated repository, and a claimed session's reported folder recorded and waited on; a workflow step tracked on each of its session paths (cold, pooled worker start, reset and turn, named session), unplaced while it claims and placed where its session runs and where its prompt reads; a step's session kept only after a backend-reported end of turn and destroyed, while the step is still busy, after a failure, a cancel, the ceiling, or any terminal the session layer made up, on each session path, never a successor under the same name; the step kept busy while a sibling's turn holds the session, while the destroy raises, and while a destroy that returned leaves the process tree standing; a session another teardown removed shut down on retry; a cancelled retry keeping the record; and a second cancel finishing the teardown rather than restarting it.
  • test/test_project_git_branches_coverage.py (115 tests, no git, no sandbox): the real-git file skips on CI shards without a namespace sandbox, so this file fakes the git seams and covers every refusal, parse, switch and exception path (100% lines measured on its own), including a token straddling the 300-character diagnostic cut, the repository reserved before the busy check, a second switch refused while one holds it, and a cancelled request keeping the reservation until git exits.
  • website/src/test/BranchSwitcher.test.tsx, LazyBranchSwitcher.test.tsx, GitPanel.test.tsx, GitReposPanel.test.tsx, ChatInput.projectBranch.test.tsx: the switcher on the project section only and outside the section toggle, the lazy boundary (placeholder, hand-over, props pass-through, no static import from either host), picker behaviour, keyboard navigation (Enter/Tab never create; Ctrl+Enter, ⌘+Enter and click do), initial highlight, create row (its base branch or commit, and a key hint shown only while it is highlighted), ahead/behind legend, disabled search when filter-blocked, error notices naming the branch, copy-while-running on both surfaces, the busy-looking trigger, the scroll-edge cue, muted filter-blocked rows, refresh after switch.
  • test/test_prompt_checkout_seam.py (54 tests): every prompt counted at both ACP transports from before its write until its exact answer is read (a cancelled caller, a later turn reading it, an error answer), a prompt with no folder busy everywhere, a prompt waiting out a switch of its folder before the write, a switch reserved during the off-loop folder match waited out, no suspension when no switch is in progress, all three runtime request writers and the client's, steers (a startedNewTurn answer latched against any duplicate until the process tree is confirmed gone), KAS checkpoint restores, exact integer ids only (1.9, "1", true end nothing), a respawned process never answering its predecessor, teardowns ending prompts only on a confirmed tree exit, and the knowledge pool's legacy worker. Each protection was disabled in turn and a test failed (27 of 27).
  • test/test_json_object_body_guard.py registers the switch body's cap decision; ApiClient.refactor.surface.test.ts pins the two new client methods.

Local, on this head: black, isort, flake8 and mypy on every changed Python file; tsc, eslint and i18n:check; 25 vitest files (723 tests); 27,266 backend tests across the guard, the ACP transports and runtime, sessions, KAS, harness parity, chat turns, workflows, sub-agents, channels, the knowledge pool, steering and the rule/ratchet scans, whose 20 failures reproduce on clean origin/main on this host (19 deterministic, plus test_session_tree_push, which also fails 1 run in 6 there); the comment-history and line-cap checks. test:electron could not run locally. Full CI is the authority.

Manual verification

Driven in the production build with mocked git responses (not a live gateway): open/filter/switch, dirty-tree refusal, filter-blocked state, composer chip open upward and after-switch refresh, dark and light themes.

Screenshots / video

Git panel picker, open and filtered:

Git panel branch picker open

Composer chip opening the same picker upward:

Composer chip branch picker

Uncommitted changes would be overwritten, so git refuses and the picker says so:

Dirty tree refusal

More states

Header closed
Filtered with create row
Create row highlighted, showing how it creates (Ctrl+Enter or a click; plain Enter does not)
After switch
Content filter blocks switching
Light theme
Composer chip closed
Composer chip after switch
Composer chip light theme
Composer chip hover
Response running: chip copies instead of switching
Response running: after copy
Ahead/behind pill hovered (headless capture draws no native tooltip; the title is pinned in tests)
Dirty-tree refusal, light theme
Response running: copy failed
Empty list: the query names no branch and is not a valid new name
Another session in this repository is still running, so the switch is refused

Related Issues

N/A — requested directly by a maintainer.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

Contribution License Agreement

Contributions are accepted under the project's Apache-2.0 license.

@kyleseaman
kyleseaman requested a review from a team as a code owner October 7, 2026 07:15
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Sensitive Change Review

Sensitive files changed:

  • src/kiro_crew/security_posture.py

  • No reasoning template for 84c2088 yet.

How to clear this check:

  1. The PR author posts the template below as a PR comment, with
    every field filled (N/A needs a reason). Keep the
    Head commit line: it must name the current head SHA.
  2. The check turns green within a minute, with no push.
  3. A new push resets this check: post it again for the new head.

Only the author's comment counts this way. A reviewer can instead
paste the filled template into an "Approve" review on this head.
Merging still needs a reviewer's approval.

## Sensitive change review
- Head commit: 84c2088b36753ab4373fc2e529e726cd0f13e0cc
- What rule changed:
- Before -> after (what was allowed/blocked, now):
- Worst case if wrong (breaks commands / leaks secret / locks user out):
- Evidence checked (which sandbox result proves it):
- No regression:
  - Tools still work (which tools/commands were run, result):
  - Backward compatible (old config / old data / old callers still work, how checked):
  - Existing tests (which suites passed on this head):
- How to undo:

@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Design Review (Opus 5.5) — 🟡 CONCERNS

Design-level review of 84c2088b36753ab4373fc2e529e726cd0f13e0cc — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

A dashboard branch picker forces a busy-registry into every ACP prompt write, sub-agent run and workflow step, and it changes workflow behaviour even for users who never switch branches.

Watch

  • Workflows change even when no switch happens. agent_pool.py now passes stop=lambda retry: end_managed_session(...) for every step, and the description says "a named workflow session loses its conversation when one of its steps ends that way". So any failed, cancelled or ceiling-cut step now destroys its session, in every install, whether or not a switch is ever requested. A session= chain that used to survive one bad step now restarts with no context. docs/system-specs/modules/workflows.md is not in the diff, so this change is not recorded there. The description is cut off before ## Backwards compatibility, so I couldn't check whether this reader is listed; that alone holds this at CONCERNS.
    Clears when: session teardown only happens while a checkout reservation is pending or in flight, OR the workflows spec and the Backwards-compatibility section record the session-loss change, with a test.
  • A leaked record can disable the feature until restart. The transport tracks prompts in client_transport.py, runtime.py and llm_pool.py, which ties every harness's prompt lifecycle to this feature. The description admits "a runtime that never confirms its process tree exited keeps that repository's switch refused until restart". Any missed release on a rarely-used path silently breaks switching, and the only remedy is restarting the gateway.
    Clears when: the stuck record shows up in the 409 body (which work, which pid) and can be cleared in the product, or the PR shows why the narrower option below was rejected.

Suggestions

  • Narrower design: refuse only on the busy state the dashboard already owns (chat slots and sub-agents), keep --no-overwrite-ignore, and warn about other agents in the repo instead of tracking them. That is the same risk a terminal git switch carries today, with no changes to the transport, runtimes or workflows.

[DESIGN-REVIEWED] 84c2088

Description read

[DESCRIPTION-READ] b20855b7c8836b371d39799317af45d6c3aa03f1f94e73250cb28bf917967f14

That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Opus 5.5) — 🔴 BLOCK

Premise-level review of 84c2088b36753ab4373fc2e529e726cd0f13e0cc — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

First-Principles-Verdict: BLOCK

product-shape change without accepted RFC: a workflow step that fails or is cancelled now ends its session, so a named workflow session loses its conversation, branch switcher used or not.

Not justified as shipped

  • Item 6 — rides along, product shape: the description says "a named workflow session loses its conversation when one of its steps ends that way". This goes against the rule in docs/system-specs/modules/workflows.md:1174, which this PR leaves as it is: "Named workflow sessions retain their provider and conversation … on success, exception and cancellation. A later call on that same name reuses the retained history."
    Subtraction: delete the stop=… end_managed_session(…) callbacks and _end_session_for_checkout in workflows/agent_exec.py and workflows/agent_pool.py. The transport-level prompt_sent/prompt_answered tracking this PR adds in acp/client_transport.py already keeps the repository busy until the backend answers the abandoned prompt.
    Clears when: the teardown is deleted, or the decision is recorded (see Blockers).

What this change ships

Inventory (10 items) — 9 justified

Intent: switch, check out and create git branches from the dashboard without opening a terminal. ADDITION.

  1. The Git panel header's branch name becomes a searchable branch picker — justified
  2. Clicking the composer chip's branch segment opens the picker; it copies only while a response runs — justified
  3. A remote-only branch can be checked out as a tracking branch, or a branch created at HEAD — justified
  4. A switch is refused while any agent work runs in that repository — justified
  5. Agent turns, prompts and sub-agents wait while their folder is mid-switch — justified
  6. A failed or cancelled workflow step ends its session; named sessions lose their history — rides along
  7. A repository with a checkout filter lists its branches but refuses to switch — justified
  8. Branch names, subjects and errors are redacted; a redacted name can't be switched to — justified
  9. _run_git gains a c_locale flag — justified
  10. A screenshot harness script for the picker — justified

Blockers

  • product-shape change without accepted RFC: workflow named sessions lose their conversation when a step does not finish. This changes what the workflow engine does by default for every user, and the documented retention rule quoted above still says the opposite. The base RFC list has no non-draft RFC covering it; rfc-global-workflow-library.md is about something else.
    Clears when: an RFC covering the end-session-on-unfinished-step behaviour is merged non-draft on the base, followed by a push or rebase; or a maintainer posts /ai-review override first-principles 84c2088b36753ab4373fc2e529e726cd0f13e0cc: <decision>.

[FIRST-PRINCIPLES-REVIEWED] 84c2088

Description read

[DESCRIPTION-READ] 25d0f0a9a5f14d42091fca674822cfc6563e523d9879325f59456b60923d56d8

That sha256 names the PR description this verdict read. Recompute it with .github/scripts/pr-description-capture.sh; if it differs, the description changed after this verdict was formed and any finding drawn from the description is unproven.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

UX Review (Opus 5.5) — 🟡 CONCERNS

UX-level review of 84c2088b36753ab4373fc2e529e726cd0f13e0cc — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: CONCERNS

Branch rows, the main switch target, were read correctly but only as "a guess", with the reader willing to click "only a little". Several notices name things the user cannot find.

Watch

  • Branch rows are a guess. The rows read like plain list items, and safe_switch_hint is a three-sentence grey paragraph above them. The reader said clicking a row "switches to it -> a guess -> only a little", and of the hint, "I only half understand it". This hits every first switch. Fix: cut the hint to one line, e.g. "Switching keeps your uncommitted changes", and show a switch affordance on the focused or hovered row.
  • The busy error doesn't say which session. error_session_busy says "another session working in this repository is still running", but the guard already knows which slot or sub-agent is busy. The reader "can't see which session it means… it isn't marked as busy", so all they can do is wait without knowing for what. Fix: name the session or workflow, and link to it.
  • The busy tooltip points to a "Send" button that doesn't exist. copy_branch_while_running says "■ next to Send", but the reader saw "no button labelled Send, only an arrow that has become a red square". This hits every attempt to switch during a turn. Fix: "stop the response first (■ in the message box)".
  • The chevron silently becomes a copy icon while a turn runs. The reader would "only notice by hovering", so a click they expected to open the list copies the name instead. Fix: keep the chevron and disable it, with the reason shown, rather than changing what the click does.

Suggestions

  • invalid_new_name "Not a valid branch name": say what is wrong, e.g. "Branch names can't contain '..', spaces or ~^:?*[".
  • error_dirty: put "ask the agent in this chat to commit them" first, and keep the git commands as the fallback.

[UX-REVIEWED] 84c2088

Description read

[DESCRIPTION-READ] 8f8b9f03a6bea5e71c3e5b7a257ad5de46f405c55fd68047d2b9bbacd05bddf6

That sha256 names everything this verdict read: the PR description, plus the 23 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

GPT 6.1 Review — 🔴 changes requested (blocking)

GPT 6.1 found at least one blocking issue that must be resolved before merging 84c2088b36753ab4373fc2e529e726cd0f13e0cc. 1 of 1 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands.

This comment is updated in place on each push.

BLOCKING -- src/kiro_crew/dashboard/handlers/git_branches.py:821 -- Cancelled checkout requests can complete without SEL auditing

await asyncio.wait([worker])
Slow checkout outlasting gateway shutdown grace -> handler cancellation -> worker completes, then cancellation is re-raised before the outcome audit; middleware’s except Exception also misses CancelledError, leaving the mutation unaudited.
Anchor: backend-security-controls
Fix: Record the worker’s outcome independently of request cancellation, before propagating cancellation.
[BLOCK-MERGE] 84c2088
[GPT-REVIEWED] 84c2088

Adjudication (Opus 5.5) — is blocking on each finding proportionate?

F1 (fenced): I upheld this one. The scenario is uncommon but nothing about it is contradictory, and once it happens nothing brings the audit record back.

  • What it takes to hit it: the dashboard owner starts a branch switch (owner check at git_branches.py:736), and the gateway shuts down while the checkout is still running. The handler's cancel path at git_branches.py:818-822 waits for the worker and then re-raises the cancel, which skips the outcome audit at git_branches.py:849. Neither slowloris.py:219 nor any other file under src/kiro_crew opts into cancelling a handler when the client disconnects, so shutdown is the only trigger. A restart during a slow checkout of a large repo is an ordinary thing to happen.

  • Recovery: none. No audit record is written before the switch, and the cancel skips the one written after it, so a completed checkout leaves no SEL record.

  • Fix cost: small. Record the worker's outcome in that cancel branch before re-raising.

    [ADJUDICATION] 84c2088 total=0 uphold=0 downgrade=0
    [GPT-ADJUDICATED] 84c2088

    [ADJUDICATION-FENCED] 84c2088 fenced=1 flagged=0
    UPHOLD-FENCED F1 src/kiro_crew/dashboard/handlers/git_branches.py:821 -- A gateway shutdown during a slow owner-initiated checkout is rare but plausible, it leaves a completed repo change with no SEL record and no way to recover one, and the fix is a single audit call.
    [GPT-ADJUDICATED-FENCED] 84c2088

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 84c2088b36753ab4373fc2e529e726cd0f13e0cc: <one-sentence reason>

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Opus 5.5 Review — ✅ no blocking findings

Reviewed 84c2088b36753ab4373fc2e529e726cd0f13e0cc — this comment is updated in place on each push.

Review details

Branch switching can stay refused until the gateway restarts after a single knowledge-pool timeout.

FINDING — src/kiro_crew/knowledge/llm_pool.py:904 — prompt_sent(instance, prompt_id, folder) records the CCWorker's folder, which is the gateway's own working directory (self._spawn_cwd = os.getcwd(); the EC2 unit sets WorkingDirectory=$RUN_HOME). On a timeout, a cancel or a dead reader, the else: await self.shutdown() branch kills and reaps the CLI, but nothing ever clears that record. After that, tracked_work_folders → folders_overlap matches every repository under the gateway's cwd (_covering_root matches when the folder contains the root), so every branch switch there returns 409 git_switch_session_busy until the gateway restarts. A knowledge prompt that succeeds still marks those repositories busy while it runs → Fix: call repo_checkout_guard.prompt_process_gone(instance) once shutdown()'s kill_and_reap has returned, and don't record the gateway cwd as a project folder for a worker that never works on a project.

[OPUS-REVIEWED] 84c2088

Verdict parsed from the review's SHA-scoped output markers for commit 84c2088b36753ab4373fc2e529e726cd0f13e0cc.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 84c2088b36753ab4373fc2e529e726cd0f13e0cc: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Oct 7, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from a48259a to 20485b4 Compare October 7, 2026 16:01
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 7, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from 20485b4 to d31fff7 Compare October 7, 2026 16:48
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Oct 7, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from d31fff7 to a69e356 Compare October 7, 2026 18:53
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 7, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from a69e356 to ae6d5fc Compare October 8, 2026 00:21
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Oct 8, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from 6bdc91c to 5fd2b16 Compare October 12, 2026 17:30
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Oct 12, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from 5fd2b16 to a7be5ba Compare October 12, 2026 17:36
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Oct 12, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from a7be5ba to 8343a00 Compare October 13, 2026 15:06
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Oct 13, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from 8343a00 to ce4ef66 Compare October 13, 2026 20:11
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Oct 13, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from ce4ef66 to ec33897 Compare October 13, 2026 20:48
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Oct 13, 2026
@kyleseaman
kyleseaman force-pushed the feat/changes-git-branch-switcher branch from ec33897 to 5e1fe8c Compare October 14, 2026 12:45
@github-actions github-actions Bot removed the readiness: action required A blocking check or review needs attention label Oct 14, 2026
@iamwhatever

Copy link
Copy Markdown
Contributor

Holding at 26aa2bd. This needs a human maintainer and author replies before it can be approved.

  1. Policy: the PR adds a network write route that runs git checkout (
    async def api_project_git_switch(request: web.Request) -> web.Response:
    ), a process-killing teardown that destroys a failed workflow step's session (
    async def end_managed_session(
    sessions: Any, key: str, provider: Any, *, escalate: bool = False
    ) -> bool:
    """End ``provider``'s session for good and report whether its process is gone.
    Destroying the session ends its process, which is the only way to stop a
    native turn its cancelled task left running: cancelling the task closes the
    stream but tells the backend nothing until the session's next prompt. The
    destroy is fenced on the provider, re-checked under the registry lock, so a
    session someone else has since put under ``key`` is never destroyed; a
    session a turn holds (a sibling step on a shared named session) is skipped
    and retried. When ``key`` does not hold ``provider`` -- another
    teardown removed it, perhaps failing part-way -- a retry (``escalate``)
    shuts the provider down itself.
    Done only on the runtime's confirmation that the process tree exited
    (:func:`process_tree_gone`).
    """
    if process_tree_gone(provider):
    return True
    if sessions.get_provider(key) is provider:
    generation = sessions.session_generation(key)
    await sessions.destroy_if(key, generation, lambda: sessions.get_provider(key) is provider)
    elif escalate:
    await provider.shutdown()
    return process_tree_gone(provider)
    async def _attempt_stop(work: TrackedWork, *, retry: bool) -> bool:
    """One try at ``work.stop``: whether the session's process is now gone.
    A failure is logged by exception type only, and only on the first try:
    session error text can carry conversation content, and a teardown that keeps
    failing is retried every few seconds.
    """
    if work.stop is None:
    return True
    try:
    return await work.stop(retry) is True
    except Exception as exc: # noqa: BLE001 - retried; the record stays meanwhile
    if not retry:
    logger.warning(
    "%s: session teardown failed (%s); retrying", work.waiter, type(exc).__name__
    )
    return False
    async def _retry_stop(work: TrackedWork) -> None:
    """Keep trying ``work.stop`` until the session's process is gone, then drop
    the record.
    Until then the busy check keeps refusing: dropping the record on a teardown
    that did not finish would let a switch run under a process that may still
    be running the turn's tools, so one that never finishes fails closed. A
    retry that is itself cancelled (the gateway shutting down) leaves the
    record.
    """
    warned = False
    while True:
    await asyncio.sleep(TEARDOWN_RETRY_SECS)
    if await _attempt_stop(work, retry=True):
    _tracked.pop(id(work), None)
    return
    if not warned:
    logger.warning("%s: session process not confirmed gone; still busy", work.waiter)
    ), and a redaction posture row in security_posture.py. These are reserved for human review.
  2. Red check: Sensitive Change Review is failing because the reasoning template for this head has not been posted.
  3. Unanswered AI findings: Design, First Principles and UX are all CONCERNS with no disposition. The main item is the workflow behaviour change, where a named workflow session loses its conversation on a failed step. Both lanes suggest refusing the switch while the session lives, rather than killing it. The two Opus advisories are also open: _retry_stop at repo_checkout_guard.py:295 can retry forever if a child escapes the process group, and _repo_has_running_work misses Slack turns on a linked dashboard session. Please fix or answer each one.

I did not read every line of the 7,400-line diff, so this is not a full code review.

— crew-pr-reviewer (r111)

Clicking the branch name in the Git panel header, or the branch segment
of the project chip under the chat box, opens a searchable picker that
switches local branches, checks out remote-only branches as tracking
branches, or creates a branch from the current commit.

GET /api/project/git/branches lists branches; POST /api/project/git/switch
is owner-gated, runs git sandboxed with hooks off, never discards work,
and refuses for subfolder projects and repos declaring content filters.
- Refuse a switch with 409 git_switch_session_busy while any chat slot
  working in the same repository has a turn running or sub-agents
  pending; the picker's own guard only sees its own chat.
- Pass --no-recurse-submodules on every git switch so a submodule's own
  filter drivers are never run.
- Redact git's diagnostic line before clipping it.
- Mark remote rows switchable only when the short name passes the
  follow-up branch grammar the track path enforces.
- Drop the sha, upstream, upstreamGone and previous fields no client
  reads; the same-branch no-op reads HEAD with symbolic-ref.
- Replace _run_git's free-form env_overrides with a c_locale flag.
- Add sandbox-free tests covering the handler where real git cannot run,
  patched through _floor_monkeypatch and portable to Windows paths.
- Keep the branch copyable while a response runs, with a visible reason
  and an ErrorNotice when the copy fails.
- Picker says what a switch does, confirms a switch, hides the safety
  line beside a refusal, leads refusals with the outcome, mutes rows
  when switching is blocked, and explains ahead/behind counts.
@inariku

inariku commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Hi @kyleseaman, a few of us want branch switching in the dashboard (#22670), so I picked this up in #23102: this PR rebased onto current main, with the CI reds fixed and the review findings addressed. The biggest change is dropping the workflow session teardown that First Principles blocked on, since the prompt tracking at the transport already covers that case. You're credited as co-author. If you'd rather keep this one, say so and I'll close mine and hand the changes over.

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

merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants