Skip to content

fix(web): dead-band terminal resize loop, RPC dedup, and WebGL fallback - #272

Merged
tstapler merged 2067 commits into
mainfrom
backlog/stapler-squad-terminal-resize-loop-fix
Jul 28, 2026
Merged

fix(web): dead-band terminal resize loop, RPC dedup, and WebGL fallback#272
tstapler merged 2067 commits into
mainfrom
backlog/stapler-squad-terminal-resize-loop-fix

Conversation

@tstapler

Copy link
Copy Markdown
Owner

Summary

  • The tiled/multi-pane terminal view could enter an infinite fit() <-> ResizeObserver feedback loop: a WebGL cell-width measurement mismatch (actual vs. expected px/col) meant FitAddon.proposeDimensions() never reached a fixpoint, so every resize observation re-triggered another sub-pixel resize, flooding the server with TerminalResize RPCs and pegging CPU.
  • This fixes the loop at its source (dead-band the observer, dedup the RPC, and correct/fall back on the WebGL mismatch) rather than papering over symptoms.

What Changed

  • Dead-band ResizeObserver (XtermTerminal.tsx): only schedules fit() when proposeDimensions() reports integer cols/rows that differ from the currently-applied size AND repeat on two consecutive ticks — closes both the sub-cell-jitter loop and boundary-flapping near an exact cell-width boundary.
  • RPC value-dedup (useTerminalFlowControl.ts): resize() skips sending TerminalResize (and the follow-up currentPaneRequest) when the incoming (cols, rows) equals the last pair actually sent — independent of the existing 200ms time throttle.
  • Explicit force-bypass at the two call sites that must skip dedup (reconnect-resync, manual "Resize" button) via a literal force: true third argument, with regression tests asserting that literal argument at each call site.
  • WebGL mismatch tracker + one-directional Canvas fallback: sustained actual-vs-expected px/col mismatch beyond a defined tolerance trips a one-way fallback from the WebGL renderer to the Canvas renderer (using Number.isFinite guards against proposeDimensions() returning Infinity), so fit() has a stable fixpoint even when the WebGL glyph metrics disagree with the DOM measurement.
  • New/updated Jest coverage in XtermTerminal.test.tsx, TerminalOutput.test.tsx, and useTerminalFlowControl.test.ts (32 tests) simulating sub-cell jitter, boundary-flapping, WebGL mismatch escalation, and both force-bypass call sites — each asserting fit()/RPC/dispose fire at most the expected number of times, not once per observed frame.
  • Full SDD planning artifacts under project_plans/terminal-resize-fit-loop/ (requirements, research, architecture/adversarial review, plan, validation, pre-mortem).

Test plan

  • npx jest XtermTerminal TerminalOutput useTerminalFlowControl — 3 suites, 32/32 passing
  • Manual verification against an isolated stapler-squad instance with 3 concurrently-mounted terminal sessions (closest available proxy in this codebase to a literal tiled-pane layout — no same-page multi-terminal-mount view exists here):
    • Simulated tab background (10s hidden) -> resume on all 3 concurrently: zero [XtermTerminal] resize/fit log lines fired in an 8s watch window.
    • Single window/viewport resize on all 3: exactly 4 log lines each (Container resized... -> Terminal dimensions BEFORE fit... -> Sending resize to server: 172x37 -> Sampler confirmed resize, fit applied: 172 cols x 37 rows), then zero further lines over the next 8s.
    • Typed input in each of the 3 echoed back immediately, no perceptible lag.
    • Manual "Resize" button: exactly 1 RPC line per click (force-bypass path works end-to-end, not just in unit tests).
    • Forced a WebGL -> Canvas fallback trip via the debug hook: fired cleanly, and the terminal continued rendering correct glyphs/colors/cursor and accepting input afterward.

Originally opened as TylerStaplerAtFanatics/stapler-squad#191; moved here per request.

github-actions Bot and others added 30 commits July 1, 2026 23:53
…n is always visible

Button was pushed off-screen on narrow viewports due to flex-row layout with
flexGrow:1 on the text. Switch to column direction so the button always renders
below the error message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…detection

Show GitHubBadge inline in SessionRow (row/list view) so PR status is
visible without switching to card view. Previously the badge only
rendered in SessionCard (card view).

Switch getCurrentBranchName from subprocess (git rev-parse) to go-git
direct file read — no subprocess overhead. Add exported
GetCurrentBranchName wrapper and CurrentBranch() method on Instance
that falls back to live git read for directory sessions (Branch field
is always empty for non-worktree sessions). Add UpdatePRStatus() helper
for atomic in-memory PR status updates from PRStatusPoller.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: repair broken release pipeline and build-from-source path

Every GoReleaser release since v1.9.0 has failed with "found 3 builds
with the ID 'stapler-squad'" because none of the three build entries
in .goreleaser.yaml declared an explicit id, so GoReleaser assigned
them all the same default. This is why brew install pulls the ancient
1.9.0 build (Formula/stapler-squad.rb hasn't updated since) and why
install.sh's release-asset download has had nothing to fetch for
every tag from v1.20.1 through v1.32.0. Give each build block an
explicit unique id.

Also fixes two things blocking the build-from-source path:
- config/executor.go: lookPathOnlyExecutor.Command used a raw
  exec.Command instead of safeexec.CommandContext, tripping the
  norawexec custom lint rule and failing `make build` outright.
- Makefile: `go build` never set the version ldflag, so both
  `make build` and plain `go build .` reported the stale hardcoded
  "1.1.2" regardless of what was actually built. Derive VERSION from
  `git describe` and pass it via -ldflags, matching what GoReleaser
  already does for tagged releases.

Verified locally: `make build` now succeeds end-to-end and
`./stapler-squad version` reports the real git-described version.
`goreleaser check` and a full `goreleaser release --snapshot --clean`
(with the GITHUB_* env vars CI provides) both succeed, including
Homebrew formula generation.

Fixes #143

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: isolate TestGetConfigDir from ambient STAPLER_SQUAD_* env vars

GetConfigDir() checks STAPLER_SQUAD_TEST_DIR and STAPLER_SQUAD_INSTANCE
before falling through to test-mode auto-detection. When the test
process inherits either from its environment (e.g. running inside a
stapler-squad-managed session), the "uses test mode isolation for
tests" subtest short-circuits on the ambient value instead of
exercising auto-detection, and fails. Clear both for the duration of
the subtest and restore them afterward.

Verified with `go test ./config/... -run TestGetConfigDir -count=3`
and a full `go test ./config/... -count=1`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: sanitize VERSION and wire it into build-embedded too

Code review on this branch surfaced two real gaps in the version-ldflag
fix:

1. Security: git tag names may legally contain shell metacharacters
   (backtick, $()). Make's $(VERSION) substitution is pure text
   substitution done before the shell parses the recipe line, so those
   characters land as live shell syntax inside the double-quoted
   `-ldflags` argument — anyone who can get a maliciously-tagged ref
   fetched into a checkout gets command execution on `make build` /
   `make install-service`. Strip VERSION to a safe charset before it
   ever reaches the shell.

   (Checked whether the analogous `VERSION=$(git describe ...)` in
   .github/workflows/build.yml has the same problem: it doesn't. That's
   a bash variable expansion of an already-computed string, not a
   macro substitution before the shell parses the command — bash does
   not re-evaluate `$()`/backticks embedded in an expanded variable's
   value. Verified empirically. Left that file alone.)

2. Completeness: `build-embedded` (the tmux-bundled single-binary
   target used by `make build-tmux` -> `make build-embedded`) builds
   the same stapler-squad binary as the primary `stapler-squad` target
   but wasn't wired to the new LDFLAGS, so it would have kept shipping
   the exact stale "1.1.2" version string issue #143 complains about.

Verified: `make build` still succeeds and reports a correct, sanitized
version. `make -n build-embedded` confirms the ldflags now appear in
that target's go build invocation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* ci: add goreleaser check as a regression guard for .goreleaser.yaml

The build-ID collision this PR fixes broke every release for 15+
months with zero visibility: the only place it ever surfaced was a
failed Action run on a tag push (release.yml only runs `goreleaser
release` on `push: tags: v*`), which nobody was watching closely
enough to catch. Add a small, fast, dedicated workflow that runs
`goreleaser check` on every change to .goreleaser.yaml, so a config
mistake like this one fails a PR check immediately instead of silently
breaking every subsequent release.

`goreleaser check` also fails non-zero for known-but-accepted
deprecation warnings, not just genuine invalidity, so a naive `args:
check` step would have gone red on day one against this repo's
existing config (it still uses the classic `brews` publisher, which
GoReleaser wants migrated to `homebrew_casks` — a real behavioral
change for end users, not a syntax rename: casks use different install
semantics, code-signing/Gatekeeper expectations, and app-bundle
lifecycle hooks that don't apply to a plain CLI binary, and would very
likely break the `brew install` command this repo's README documents.
That migration needs its own careful, tested PR, not a blind swap
bundled into an install-bug fix). Fixed the two safe, pure-syntax
deprecations in the same commit (`archives.format`/
`format_overrides.format` -> `formats`, now a list — verified via a
full snapshot build that archive naming/extension per-OS is
unchanged) and left `brews` alone. The new workflow's check step
distinguishes "configuration is invalid" (hard fail) from "valid, but
uses deprecated properties" (pass, tracked separately) by output
content rather than exit code, so it stays a real regression guard
instead of either being permanently red on accepted debt or silently
disabled.

Verified locally:
- `goreleaser check` on the current config: valid, only the accepted
  `brews` deprecation remains.
- Simulated the exact original bug (duplicate build ids) against a
  scratch copy of the config: the same check logic correctly reports
  "configuration is invalid" and would fail CI.
- Full `goreleaser release --snapshot --clean` still succeeds
  end-to-end after the formats-list migration, archive names/
  extensions unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: sync registry validation with github_user.proto and add missing feature files

CI's Registry Validation check was failing on this PR (unrelated to the
actual fix, but blocking it from going green): `tools/scanner/validate-registry.sh`
never scans `proto/session/v1/github_user.proto`, even though the
Makefile's `registry-generate-backend` target does. Both were last
touched independently, and the validation script's hardcoded proto
list was never updated when github_user.proto's RPCs (added in
3be7e09, well before this branch existed) were registered. The
result: `docs/registry/features/backend/*.json` never had entries for
ListGitHubAccounts/PollGitHubDeviceAuth/RevokeGitHubToken/
StartGitHubDeviceAuth, and the validation script would report them as
"Removed RPCs" (154 committed vs. 147 generated, 4.55% divergence)
forever, regardless of whether the per-feature files existed — the
scanner it runs simply never looks at that proto file.

- Added the missing `github_user.proto` scan step to
  validate-registry.sh, matching the Makefile.
- Ran `make registry-generate` to create the 4 missing per-feature
  JSON files these RPCs were always supposed to have.

Verified: `./tools/scanner/validate-registry.sh` now reports
"Committed: 154  Generated: 154  Divergence: 0.0%" and exits 0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Tyler Stapler <tystapler@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…rsing crash) (#150)

* fix: shell-quote claude launch args to stop injection and flag-parsing crash

Backlog/triage spawned sessions died on launch: the prompt is interpolated
into a shell command (tmux launches programs through a shell), and Go's %q
produces double quotes, which do not suppress backtick/$(...)/$VAR
expansion. Backlog prompts are full of backtick-wrapped tokens
(`/backlog/done-N`, etc.), so the shell executed each as a command instead
of passing it to claude. Separately, backlog prompts begin with
"--- BACKLOG ITEM DATA ---", which claude's arg parser rejected as an
unrecognized flag once quoting was fixed.

Add shellQuote (POSIX single-quoting, the same style already used for
--mcp-config) and apply it to every claude flag value that gets
interpolated into the shell command: --append-system-prompt, --allowedTools,
--permission-mode, and the positional prompt. Insert a bare "--" before the
prompt so a leading "--" in the prompt text is treated as data, not flags.

Verified against the real claude CLI that both -- as an end-of-options
separator and --append-system-prompt-file are accepted, and confirmed via
a real shell execution that a $(...) payload in a backlog-shaped prompt no
longer executes.

Fixes #148

* fix: close remaining shell-injection gaps found by review

Multi-agent review of the shellQuote fix found the same vulnerability
class still present two call sites over:

- --resume value: claudeSessionID traces back to the client-supplied
  resume_id field on CreateSessionRequest with no format validation, and
  was still interpolated unquoted into the shell-executed launch command
  in the same function that was just patched.
- claudeMCPConfigFlag hand-rolled its own shell single-quoting (a literal
  '...' wrapper) instead of reusing shellQuote, leaving a second,
  untested implementation of the same job living next to the new one.
  Not currently exploitable (MCPServerURL/UUID aren't attacker-supplied
  today) but a latent gap in the same file that just added the primitive
  meant to prevent this.

Also add regression tests the review flagged as missing: --allowedTools
and --permission-mode had zero shell-safety coverage even though
shellQuote was applied to both, so a partial revert of just those two
lines would have passed the full suite silently. Reworked the two
existing Prompt/AppendSystemPrompt regression tests to assert against
hand-written expected literals instead of calling shellQuote() again,
so they don't just verify the function against itself. Added
only-single-quote, embedded-newline, and combined backtick+quote cases
to TestShellQuote's table.

Confirmed session/claude_command_builder.go's separate --resume path is
not affected: it validates the session ID against a strict UUID v4
regex before use, and is not wired into any production call site today.

---------

Co-authored-by: Tyler Stapler <tystapler@gmail.com>
…detection (#149)

* fix(analytics): escape analytics session_id mismatch and dead mangle detection

Escape event rows were tagged with the tmux session name instead of the
stable session UUID, so the web UI (which queries by stable UUID) never
found any data even though capture itself was working (185K+ rows in the
live DB). Mangle detection was fully implemented and unit-tested but never
wired into production — SetCorrelator was never called, emitEventWithStageAndSeq
always recorded Stage 1 observations instead of checking Stage 2 against them,
and the Stage 2 tap computed session_seq from the wrong buffer offset.

- Thread instance.GetStableID() into the escape parser via a new
  ResponseStream.SetStableSessionID, scoped narrowly so cc.sessionName's
  other use sites (PTY naming, persistence dirs, rate limiting) are untouched
- Wire MangleCorrelator per-parser with its eviction loop tied to stream
  lifetime; branch RecordStage1 vs CheckStage2 by stage instead of always
  recording
- Fix Stage 2 session_seq to use the coalesced frame's start offset, not its
  end offset, so it aligns with Stage 1's numbering
- Convert totalSequences/totalMangled to atomic.Int64 (both stages write
  through the same parser instance from different goroutines)
- Mirror escape analytics defaults into DefaultConfig() to match
  LoadConfigFromPath, per the existing "must mirror" comment

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(analytics): redesign mangle correlation to be offset-independent

Code review on PR #149 found the byte-offset arithmetic fix for Stage 2
correlation couldn't work regardless of the arithmetic: streamViaControlMode's
data comes from a separate tmux control-mode client, not the same producer as
Stage 1's raw PTY read, so the two sides have no shared byte-offset numbering.

Verified empirically (live tmux experiment with two simultaneous client
attachments) that the two streams carry identical content in the same order,
just offset by a constant that resets on each client's own connect/resize
redraw — a calibration problem, not a content mismatch. Redesigned
MangleCorrelator to correlate ordinally per (session, sequence type) instead
of by byte position, which is robust to that offset entirely.

Also addresses the review's MAJOR findings: sessionID is now
atomic.Pointer[string] instead of an unsynchronized plain string; the parser
setter is renamed SetStableSessionID to stop colliding with a tmux-name-keyed
SetSessionID called 4 lines away; the correlator eviction goroutine is now
tracked by ResponseStream's WaitGroup (so Stop() actually blocks on it) and
panic-recovered; and the production wiring line in ClaudeController.Start()
now has a test that would catch a regression back to the tmux name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore: restore .claude/scheduled_tasks.lock accidentally deleted in prior commit

Unrelated to this PR's changes — an environment-local lock file got staged as
deleted before this session started and was swept up by a non-path-scoped
git commit. Restoring it to match origin/main.

---------

Co-authored-by: Tyler Stapler <tystapler@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
… related bugs (#152)

* fix(backlog): resolve GitHub URLs in repo path, add first-visit tour

The Repository Path field silently accepted a GitHub URL and used it
verbatim as a filesystem path, producing garbage paths and silent
triage failures (stapler-squad#148's "Related" section). It also had
no guidance on what it expected, so users had no way to know a URL
wasn't a valid local path.

- BacklogService now resolves GitHub URLs/shorthand in repo_path to a
  local clone (same machinery CreateSession already uses for the
  Omnibar), or returns a clear validation error instead of storing
  garbage. Covers both CreateBacklogItem and the UpdateBacklogItem
  fix-up path.
- RepoPathInput gained an optional hint line and live GitHub-URL
  detection ("Will clone owner/repo to ~/.stapler-squad/repos/...").
- BacklogItemForm explains the two previously-unlabeled checkboxes
  and shows "Cloning repository…" while a fresh clone is in flight.
- New BacklogTourModal walks first-time visitors through the item
  lifecycle, the repo-path gotcha explicitly, and what the skip flags
  do; reopenable via a "?" button in the page header.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: shell-quote claude launch prompts, stop triage poll from losing edits

Two backlog-adjacent bugs Carl filed while debugging the repo-path
issue above:

- stapler-squad#148: backlog/triage session prompts were interpolated
  into the shell command with Go's %q (double quotes), so backtick-
  wrapped tokens and $(...) in the auto-generated prompt were executed
  by the shell, and a leading "--" was parsed as a claude CLI flag —
  spawned sessions died on launch. Now single-quoted (shellQuote, which
  suppresses all shell expansion) with a "--" separator before the
  prompt.
- stapler-squad#146: BacklogItemDetail's full-screen loading guard
  unmounted <BacklogItemForm> on every 5s triage-status poll, so any
  unsaved acceptance criteria typed during triage were silently
  discarded. The loader now only shows on the initial load, and the
  poll is suspended while the edit form is open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(e2e): install missing test deps, extend server-boot timeout

allure-playwright (declared in package.json) was missing from the
committed node_modules, breaking `npx playwright test` outright.
Installed it and its transitive deps.

Also bumped the test-server health-check timeout from 30s to 90s: a
cold test-mode boot (DB init + demo seeding) was observed taking
~30-45s before /health responds, right at the old cap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* revert(e2e): don't commit node_modules lock manifest without the packages

The previous commit updated .package-lock.json (npm's per-tree
manifest) after `npm install` pulled in allure-playwright and ~350
transitive deps, but those package directories are gitignored and
weren't force-added — committing just the manifest without the actual
files would claim the tree is in sync when it isn't.

tests/e2e/node_modules is vendored (git-tracked despite .gitignore),
so fully fixing the missing-dependency gap means force-adding ~thousands
of new files, which is out of scope for this PR. Leaving the
test-server.ts timeout bump from the previous commit in place since
that's independently correct; flagging the vendoring gap separately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: address code review findings (shell-quote gap, tour checkbox bug, path traversal)

Multi-dimension code review (Testing, Code Quality, Architecture, Security)
on PR #152 surfaced two CRITICALs, both cross-validated by 2-3 independent
reviewers, plus several MAJOR issues:

- CRITICAL: AllowedTools/PermissionMode in instance_tmux.go still used Go's
  %q instead of the new shellQuote — the exact same shell-injection class
  this PR fixes for AppendSystemPrompt/Prompt, just on two sibling fields
  populated directly from client RPC input.
- CRITICAL: BacklogTourModal's "Don't show this again" checkbox was a no-op
  — onClose (mapped to setTourComplete) unconditionally persisted
  onboarded=true regardless of the checkbox state. Fixed by changing the
  modal's callback contract to onComplete(persist: boolean), with a new
  hideTour() on the hook for the non-persisting path.
- MAJOR (security): GitHub owner/repo regexes in repo_path.go didn't reject
  "." / ".." segments, so a crafted repo_path could resolve the clone
  directory outside ~/.stapler-squad/repos/github.com/. Added an
  isTraversalSegment guard across all 4 parse branches.
- MAJOR: hardcoded 24px margin replaced with the vars.space token; extracted
  BacklogTourModal's reused modal-chrome styles out of OnboardingModal's own
  CSS module into a new shared components/ui/ModalTour.css.ts (OnboardingModal
  re-exports from it, so OnboardingModal.tsx needed no changes).
- MAJOR (testing): replaced two circular shellQuote()-derived test oracles
  with hardcoded literals, added a message-content assertion the Update-path
  resolver-error test was missing, and strengthened the poll-suspended-while-
  editing test to assert the actual unsaved acceptance criterion survives
  rather than just checking a fetch call count.

Deferred (documented, not blocking): DRY duplication between
backlog_service.go/session_service.go's GitHub resolution, a matching
hardcoded path format in the frontend hint text, and the pre-existing
synchronous-clone-in-RPC-handler pattern this PR extends to a second call
site (mirrors existing CreateSession behavior).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Tyler Stapler <tystapler@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
#155)

* fix: make web-build generate proto bindings on a fresh clone

`make web-build` builds `web-app/out` without depending on `proto-gen`,
so a clean checkout fails with "Module not found:
'@/gen/session/v1/session_pb'" because the TypeScript protobuf
bindings were never generated. `make build` was unaffected since it
lists `proto-gen` as a direct prerequisite of the top-level target.

Add `proto-gen` as a prerequisite of `web-app/out` so the TS bindings
exist before the Next.js build runs, regardless of which entry point
is used. `proto-gen` is a no-op when the bindings are already
up to date, so this doesn't slow down repeat builds.

Fixes #144 (Bug 1). Bug 2 (go-m1cpu SIGSEGV) is already resolved —
the repo depends on gopsutil/v4, which dropped the go-m1cpu cgo
dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test: add CI smoke test for standalone `make web-build`

The existing CI pipeline never exercises the Makefile's own dependency
graph: `.github/actions/prepare` hand-runs `buf generate` and
`pnpm run build` directly, bypassing `make` entirely. That's exactly
why the missing `proto-gen` prerequisite on `web-app/out` (previous
commit, fixes #144) went undetected - no CI job ever invoked
`make web-build` or `make build` as a fresh clone would.

Add a standalone job that checks out cleanly (no shared artifacts,
no manual buf/pnpm pre-steps) and runs `make web-build` directly,
then asserts the generated TS proto bindings exist. Verified this
job's steps fail against the pre-fix Makefile with the exact reported
error ("Module not found: '@/gen/session/v1/session_pb'") and pass
against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore: untrack stale generated proto files that were force-committed

gen/, web-app/src/gen/, and .proto-gen.stamp are already in .gitignore,
but 19 generated files under gen/proto/go/session/v1/ and
web-app/src/gen/session/v1/ were force-committed into git anyway
(going back through at least PR #60, #51, #54) and never cleaned up.

The tracked set was also incomplete/stale - e.g. session.pb.go and
session_pb.ts (generated from session.proto, the largest proto file)
were never committed at all, while sessionv1connect/session.connect.go
(which references types defined in session.pb.go) was. This is exactly
what produced the "undefined: v1.CreateSessionRequest" compile errors
and "Module not found '@/gen/session/v1/session_pb'" webpack errors
in #144 on any workflow that skipped `proto-gen` - the stale committed
files gave inconsistent partial signals instead of a clean "not
generated yet" failure.

`git rm --cached` only removes them from the index; the working-tree
copies (freshly regenerated by `make web-build` in the previous
commits) are untouched, and .gitignore now actually takes effect for
this tree going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Tyler Stapler <tystapler@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
github-actions Bot and others added 10 commits July 25, 2026 23:48
…-terminal-resize-loop-fix

# Conflicts:
#	server/dependencies.go
#	server/server.go
#	server/services/workspace_service.go
#	session/capture_test.go
#	session/history_linker.go
#	session/history_linker_test.go
#	session/instance.go
#	session/tmux/tmux.go
#	session/tmux/tmux_test.go
#	web-app/jest.config.js
#	web-app/jest.setup.js
#	web-app/package-lock.json
#	web-app/package.json
#	web-app/src/app/page.tsx
#	web-app/src/components/sessions/SessionCard.module.css
#	web-app/src/components/sessions/SessionCard.tsx
#	web-app/src/components/sessions/SessionList.tsx
#	web-app/src/components/sessions/TerminalOutput.tsx
#	web-app/src/components/sessions/XtermTerminal.tsx
#	web-app/src/components/sessions/__tests__/XtermTerminal.test.tsx
#	web-app/src/lib/hooks/useSessionService.ts
#	web-app/src/lib/hooks/useTerminalFlowControl.ts
#	web-app/src/lib/hooks/useTerminalStream.ts
Fixes surfaced by running the merged test suite after merging origin/main
into the terminal-resize-fit-loop branch (commit 65afd7b):

- XtermTerminal.tsx: remove the reintroduced synchronous initial onResize()
  call. Upstream deliberately deleted this (commit acb0750, "prevent
  premature resize from corrupting dimension cache") because it fired
  onResize(80,24) with xterm's construction-time defaults before fitAddon.fit()
  ever ran, corrupting TerminalOutput's dimension cache. Re-adding it during
  the merge reintroduced that exact regression, caught by the upstream
  regression suite XtermTerminalBug.test.tsx ("Bug 1").

- XtermTerminal.test.tsx: add a buffer stub to MockTerminal (origin's new
  updateScrollbar() reads buffer.active unconditionally), stub a minimal
  WebGL2RenderingContext global (jsdom has none, and the merged component now
  gates its WebGL load behind `typeof WebGL2RenderingContext !== 'undefined'`
  per xterm.js issue #2033), flush the dynamic import('@xterm/addon-webgl')
  microtask chain after mount, and recalibrate the resize-sampler test
  helpers' fake-timer advances for the flat 150ms debounce (replacing the old
  adaptive 10ms-for-first-3 debounce) instead of the old adaptive timing.

- TerminalOutput.test.tsx: add the supporting context/hook mocks
  (AnalyticsContext, ApprovalsContext, useHandedness, TerminalStreamManager,
  etc.) TerminalOutput now depends on, update the Fit button's aria-label and
  account for it living behind the (now-collapsible) toolbar toggle, and flush
  the XtermTerminal lazy-import Suspense boundary plus the new 250ms
  post-connection resize delay before asserting.

- BacklogItemForm.test.tsx: fix a pre-existing tsc error (unrelated to this
  merge, inherited from origin/main) by typing the onSubmit mock instead of
  letting it infer a zero-arg jest.Mock.

- pnpm-lock.yaml: regenerated after adding @xterm/addon-canvas back to
  package.json (needed for the Canvas-fallback feature; origin/main's
  dependency bump to xterm.js 6.x never had this addon since upstream lacks
  the WebGL->Canvas fallback feature).

All fixes verified with `go build ./...`, `go test ./...`, `npx tsc --noEmit`,
and the full `npx jest` suite (2840 passing; 3 pre-existing failures on
origin/main unrelated to this merge — SessionCard.click.test.tsx and
SessionCard.approval-suppression.test.tsx are missing an AnalyticsContext
mock that predates this branch, and BacklogEmptyState.test.tsx has an
unhandled promise rejection from mockRejectedValue, also pre-existing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSdgF9zfk4PcYuQE61ooHi
… in code review

- useTerminalFlowControl.ts: cancel any pending deferred resize timer
  unconditionally BEFORE the value-dedup early-return in resize(), so a
  bounce-back call (A -> B deferred -> back to A) can no longer dedup-return
  while leaving a stale deferred send for B still scheduled. Added a
  regression test reproducing the exact bounce-back scenario.
- XtermTerminal.tsx: guard the async @xterm/addon-webgl import with a
  `cancelled` flag set in the effect's unmount cleanup, preventing
  loadAddon() from running against an already-disposed terminal and leaking
  an orphaned WebGL context on a fast session-switch unmount.
- useTerminalFlowControl.test.ts: remove dead duplicate jest.mock('@bufbuild/protobuf', ...)
  merge-conflict leftover.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSdgF9zfk4PcYuQE61ooHi
triggerCanvasFallback() lacked the same cancelled-guard the async WebGL
loader uses, so a post-unmount onContextLoss could call loadAddon() on a
disposed terminal (caught but logged spuriously). The post-resize
currentPaneRequest timer in useTerminalFlowControl was also never tracked
or cleared on unmount, unlike its sibling pendingResizeTimerRef.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011HafE8LJ1P3qsX6TokT9Fc
…-terminal-resize-loop-fix

# Conflicts:
#	.claude/commands/perf/make-it-faster.md
#	.claude/docs/codesigning.md
#	.github/workflows/benchmark.yml
#	.github/workflows/build.yml
#	.github/workflows/demo-publish.yml
#	.github/workflows/release-please.yml
#	.github/workflows/release.yml
#	.github/workflows/ux-analysis.yml
#	.gitignore
#	.golangci.yml
#	.release-please-manifest.json
#	CHANGELOG.md
#	CLAUDE.md
#	Formula/stapler-squad.rb
#	Makefile
#	config/config.go
#	config/config_test.go
#	config/defaults_test.go
#	config/types.go
#	docs/api/features/unfinished-work.md
#	docs/demos/accessibility-Accessibilit-2de24-ndary-routes-are-accessible-chromium-dom.gif
#	docs/demos/accessibility-Accessibilit-2de24-ndary-routes-are-accessible.gif
#	docs/demos/accessibility-Accessibilit-8ae87-us-accessibility-violations-chromium-dom.gif
#	docs/demos/accessibility-Accessibilit-8ae87-us-accessibility-violations.gif
#	docs/demos/backlog-Backlog-Backlog-To-b1401-after-it-has-been-dismissed-chromium-dom.gif
#	docs/demos/backlog-Backlog-Backlog-To-b1401-after-it-has-been-dismissed.gif
#	docs/demos/backlog-Backlog-Backlog-To-e4158-g-it-persists-across-reload-chromium-dom.gif
#	docs/demos/backlog-Backlog-Backlog-To-e4158-g-it-persists-across-reload.gif
#	docs/demos/backlog-Backlog-Empty-Stat-1590e--button-reveals-inline-form-chromium-dom.gif
#	docs/demos/backlog-Backlog-Empty-Stat-1590e--button-reveals-inline-form.gif
#	docs/demos/backlog-Backlog-Empty-Stat-2f4c8--button-disabled-when-empty-chromium-dom.gif
#	docs/demos/backlog-Backlog-Empty-Stat-2f4c8--button-disabled-when-empty.gif
#	docs/demos/backlog-Backlog-Empty-Stat-82f48--Clicking-cancel-hides-form-chromium-dom.gif
#	docs/demos/backlog-Backlog-Empty-Stat-82f48--Clicking-cancel-hides-form.gif
#	docs/demos/backlog-Backlog-Empty-Stat-96beb-ycle-diagram-and-CTA-button-chromium-dom.gif
#	docs/demos/backlog-Backlog-Empty-Stat-96beb-ycle-diagram-and-CTA-button.gif
#	docs/demos/backlog-Backlog-Filter-Zer-700a3-lters-button-resets-filters-chromium-dom.gif
#	docs/demos/backlog-Backlog-Filter-Zer-700a3-lters-button-resets-filters.gif
#	docs/demos/backlog-Backlog-Filter-Zer-9d4df-filters-shows-empty-message-chromium-dom.gif
#	docs/demos/backlog-Backlog-Filter-Zer-9d4df-filters-shows-empty-message.gif
#	docs/demos/backlog-Backlog-Item-Creat-a24f6-t-item-via-empty-state-form-chromium-dom.gif
#	docs/demos/backlog-Backlog-Item-Creat-a24f6-t-item-via-empty-state-form.gif
#	docs/demos/backlog-Backlog-Item-Creat-e6bc5--the-list-after-empty-state-chromium-dom.gif
#	docs/demos/backlog-Backlog-Item-Creat-e6bc5--the-list-after-empty-state.gif
#	docs/demos/backlog-Backlog-Item-Creat-ee2ef-d-with-default-priority-P3--chromium-dom.gif
#	docs/demos/backlog-Backlog-Item-Creat-ee2ef-d-with-default-priority-P3-.gif
#	docs/demos/backlog-Backlog-Page-Navig-eb97f-s-accessible-and-functional-chromium-dom.gif
#	docs/demos/backlog-Backlog-Page-Navig-eb97f-s-accessible-and-functional.gif
#	docs/demos/backlog-Backlog-Page-Navig-f014a-age-loads-and-is-accessible-chromium-dom.gif
#	docs/demos/backlog-Backlog-Page-Navig-f014a-age-loads-and-is-accessible.gif
#	docs/demos/backlog-Backlog-Status-Tra-98946-ed-to-ready-via-detail-pane-chromium-dom.gif
#	docs/demos/backlog-Backlog-Status-Tra-98946-ed-to-ready-via-detail-pane.gif
#	docs/demos/backlog-Backlog-Status-Tra-a3f73-Item-button-appears-in-list-chromium-dom.gif
#	docs/demos/backlog-Backlog-Status-Tra-a3f73-Item-button-appears-in-list.gif
#	docs/demos/backlog-Backlog-Status-Tra-a8d60-ture-not-yet-exposed-in-UI--chromium-dom.gif
#	docs/demos/backlog-Backlog-Status-Tra-a8d60-ture-not-yet-exposed-in-UI-.gif
#	docs/demos/backlog-Backlog-Triage-e2e-000e4-bled-when-repoPath-is-empty-chromium-dom.gif
#	docs/demos/backlog-Backlog-Triage-e2e-000e4-bled-when-repoPath-is-empty.gif
#	docs/demos/backlog-sources-settings-b-1d1f3-urce-and-see-it-in-the-list.gif
#	docs/demos/backlog-sources-settings-b-2c08b-le-a-source-s-enabled-state-chromium-dom.gif
#	docs/demos/backlog-sources-settings-b-2c08b-le-a-source-s-enabled-state.gif
#	docs/demos/backlog-sources-settings-b-6f2de-ce-removes-it-from-the-list-chromium-dom.gif
#	docs/demos/backlog-sources-settings-b-6f2de-ce-removes-it-from-the-list.gif
#	docs/demos/browser-passthrough-browse-5468d-tate-when-cdp-not-connected-chromium-dom.gif
#	docs/demos/browser-passthrough-browse-5468d-tate-when-cdp-not-connected.gif
#	docs/demos/browser-passthrough-browse-fb705-wser-tab-when-cdp-available-chromium-dom.gif
#	docs/demos/browser-passthrough-browse-fb705-wser-tab-when-cdp-available.gif
#	docs/demos/bulk-select-bulk-select-bu-1b31d-sessions-show-paused-status-chromium-dom.gif
#	docs/demos/bulk-select-bulk-select-bu-1b31d-sessions-show-paused-status.gif
#	docs/demos/bulk-select-bulk-select-bu-468ba--sessions-removed-from-list-chromium-dom.gif
#	docs/demos/bulk-select-bulk-select-bu-468ba--sessions-removed-from-list.gif
#	docs/demos/bulk-select-bulk-select-es-e26ba-xes-hidden-and-toolbar-gone-chromium-dom.gif
#	docs/demos/bulk-select-bulk-select-es-e26ba-xes-hidden-and-toolbar-gone.gif
#	docs/demos/bulk-select-bulk-select-sh-fb302-row-3-rows-1-3-are-selected-chromium-dom.gif
#	docs/demos/bulk-select-bulk-select-sh-fb302-row-3-rows-1-3-are-selected.gif
#	docs/demos/bulk-select-bulk-select-un-f128e--in-toast-sessions-reappear-chromium-dom.gif
#	docs/demos/bulk-select-bulk-select-un-f128e--in-toast-sessions-reappear.gif
#	docs/demos/demo-Demo-Flow.gif
#	docs/demos/enter-detection-enter-dete-79093-hould-loadPageWithoutErrors-chromium-dom.gif
#	docs/demos/enter-detection-enter-dete-79093-hould-loadPageWithoutErrors.gif
#	docs/demos/history-search-History-Sea-95175-ory-search-UI-is-accessible-chromium-dom.gif
#	docs/demos/history-search-History-Sea-95175-ory-search-UI-is-accessible.gif
#	docs/demos/insights-insights-dashboar-36984-ingOrData-When-apiAvailable-chromium-dom.gif
#	docs/demos/mobile-navigation-mobile-n-efd32-sionList-When-sessionsExist-chromium-dom.gif
#	docs/demos/mobile-navigation-mobile-n-efd32-sionList-When-sessionsExist.gif
#	docs/demos/nav-navigation-nav-navigat-11cca-hen-session-param-is-in-URL-chromium-dom.gif
#	docs/demos/nav-navigation-nav-navigat-11cca-hen-session-param-is-in-URL.gif
#	docs/demos/nav-navigation-nav-navigat-399f4-s-back-from-unfinished-page-chromium-dom.gif
#	docs/demos/nav-navigation-nav-navigat-399f4-s-back-from-unfinished-page.gif
#	docs/demos/nav-navigation-nav-navigat-84fe4-avigates-from-sessions-page-chromium-dom.gif
#	docs/demos/nav-navigation-nav-navigat-84fe4-avigates-from-sessions-page.gif
#	docs/demos/nav-navigation-nav-navigat-98d3c-avigates-from-sessions-page-chromium-dom.gif
#	docs/demos/nav-navigation-nav-navigat-98d3c-avigates-from-sessions-page.gif
#	docs/demos/nav-navigation-nav-navigat-ed5f6-hen-session-param-is-in-URL-chromium-dom.gif
#	docs/demos/nav-navigation-nav-navigat-ed5f6-hen-session-param-is-in-URL.gif
#	docs/demos/onboarding-hook-install-on-a4529-n-the-final-onboarding-step-chromium-dom.gif
#	docs/demos/onboarding-hook-install-on-a4529-n-the-final-onboarding-step.gif
#	docs/demos/review-queue-Review-Queue--12c9a-tem-from-DOM-optimistic-UI--chromium-dom.gif
#	docs/demos/review-queue-Review-Queue--12c9a-tem-from-DOM-optimistic-UI-.gif
#	docs/demos/review-queue-Review-Queue--45ba3--present-after-page-renders-chromium-dom.gif
#	docs/demos/review-queue-Review-Queue--45ba3--present-after-page-renders.gif
#	docs/demos/review-queue-Review-Queue--bf4d4-eue-page-loads-successfully-chromium-dom.gif
#	docs/demos/review-queue-Review-Queue--bf4d4-eue-page-loads-successfully.gif
#	docs/demos/review-queue-Review-Queue--e61a4-ies-acknowledge-data-testid-chromium-dom.gif
#	docs/demos/review-queue-Review-Queue--e61a4-ies-acknowledge-data-testid.gif
#	docs/demos/review-queue-Session-Creat-7ee30-eation-wizard-has-all-steps-chromium-dom.gif
#	docs/demos/review-queue-Session-Creat-7ee30-eation-wizard-has-all-steps.gif
#	docs/demos/review-queue-Session-Creat-8345c--form-has-required-test-IDs-chromium-dom.gif
#	docs/demos/review-queue-Session-Creat-8345c--form-has-required-test-IDs.gif
#	docs/demos/rules-yaml-import-rules-ya-19648--state-has-explanatory-text-chromium-dom.gif
#	docs/demos/rules-yaml-import-rules-ya-19648--state-has-explanatory-text.gif
#	docs/demos/rules-yaml-import-rules-ya-3b89e-rt-duplicate-overwrite-mode-chromium-dom.gif
#	docs/demos/rules-yaml-import-rules-ya-3b89e-rt-duplicate-overwrite-mode.gif
#	docs/demos/rules-yaml-import-rules-ya-552d0-les-and-shows-preview-cards-chromium-dom.gif
#	docs/demos/rules-yaml-import-rules-ya-552d0-les-and-shows-preview-cards.gif
#	docs/demos/rules-yaml-import-rules-ya-62f98-ws-inline-validation-errors-chromium-dom.gif
#	docs/demos/rules-yaml-import-rules-ya-62f98-ws-inline-validation-errors.gif
#	docs/demos/rules-yaml-import-rules-ya-6a69d-d-rules-and-refreshes-table-chromium-dom.gif
#	docs/demos/rules-yaml-import-rules-ya-6a69d-d-rules-and-refreshes-table.gif
#	docs/demos/rules-yaml-import-rules-ya-7d21c--yaml-button-downloads-file-chromium-dom.gif
#	docs/demos/rules-yaml-import-rules-ya-7d21c--yaml-button-downloads-file.gif
#	docs/demos/rules-yaml-import-rules-ya-daed9--import-duplicate-skip-mode-chromium-dom.gif
#	docs/demos/rules-yaml-import-rules-ya-daed9--import-duplicate-skip-mode.gif
#	docs/demos/rules-yaml-import-rules-ya-fcaa2-port-modal-opens-and-closes-chromium-dom.gif
#	docs/demos/rules-yaml-import-rules-ya-fcaa2-port-modal-opens-and-closes.gif
#	docs/demos/session-create-directory-d-0e2cd-ory-session-type-in-payload-chromium-dom.gif
#	docs/demos/session-create-directory-d-0e2cd-ory-session-type-in-payload.gif
#	docs/demos/session-create-directory-d-11fc0--is-disabled-without-a-path-chromium-dom.gif
#	docs/demos/session-create-directory-d-11fc0--is-disabled-without-a-path.gif
#	docs/demos/session-create-directory-d-45cc2-ry-field-for-directory-mode-chromium-dom.gif
#	docs/demos/session-create-directory-d-45cc2-ry-field-for-directory-mode.gif
#	docs/demos/session-create-directory-d-7ec73-irectory-type-is-selectable-chromium-dom.gif
#	docs/demos/session-create-directory-d-7ec73-irectory-type-is-selectable.gif
#	docs/demos/session-create-directory-d-a9bd3--when-directory-is-selected-chromium-dom.gif
#	docs/demos/session-create-directory-d-a9bd3--when-directory-is-selected.gif
#	docs/demos/session-create-existing-wo-3a5aa-when-worktree-path-is-empty-chromium-dom.gif
#	docs/demos/session-create-existing-wo-3a5aa-when-worktree-path-is-empty.gif
#	docs/demos/session-create-existing-wo-6a4ba--for-existing-worktree-mode-chromium-dom.gif
#	docs/demos/session-create-existing-wo-6a4ba--for-existing-worktree-mode.gif
#	docs/demos/session-create-existing-wo-7131c-rktree-option-is-selectable-chromium-dom.gif
#	docs/demos/session-create-existing-wo-7131c-rktree-option-is-selectable.gif
#	docs/demos/session-create-existing-wo-79c0c-th-worktree-path-in-payload-chromium-dom.gif
#	docs/demos/session-create-existing-wo-79c0c-th-worktree-path-in-payload.gif
#	docs/demos/session-create-existing-wo-cde14-isting-worktree-is-selected-chromium-dom.gif
#	docs/demos/session-create-existing-wo-cde14-isting-worktree-is-selected.gif
#	docs/demos/session-create-existing-wo-dbe1a-isting-worktree-is-selected-chromium-dom.gif
#	docs/demos/session-create-existing-wo-dbe1a-isting-worktree-is-selected.gif
#	docs/demos/session-create-new-project-057be-irectory-hides-branch-field-chromium-dom.gif
#	docs/demos/session-create-new-project-057be-irectory-hides-branch-field.gif
#	docs/demos/session-create-new-project-1a3d0-parent-dir-and-project-name-chromium-dom.gif
#	docs/demos/session-create-new-project-1a3d0-parent-dir-and-project-name.gif
#	docs/demos/session-create-new-project-651a4-up-defaults-to-New-Worktree-chromium-dom.gif
#	docs/demos/session-create-new-project-651a4-up-defaults-to-New-Worktree.gif
#	docs/demos/session-create-new-project-789fd-and-project-name-are-filled-chromium-dom.gif
#	docs/demos/session-create-new-project-789fd-and-project-name-are-filled.gif
#	docs/demos/session-create-new-project-8f754-m-sends-correct-RPC-payload-chromium-dom.gif
#	docs/demos/session-create-new-project-8f754-m-sends-correct-RPC-payload.gif
#	docs/demos/session-create-new-project-90413-rectory-path-does-not-exist-chromium-dom.gif
#	docs/demos/session-create-new-project-90413-rectory-path-does-not-exist.gif
#	docs/demos/session-create-new-project-c4ade-it-without-creating-session-chromium-dom.gif
#	docs/demos/session-create-new-project-c4ade-it-without-creating-session.gif
#	docs/demos/session-create-new-project-d589d-s-visible-in-creation-panel-chromium-dom.gif
#	docs/demos/session-create-new-project-d589d-s-visible-in-creation-panel.gif
#	docs/demos/session-create-new-project-e22ba-hen-New-Project-is-selected-chromium-dom.gif
#	docs/demos/session-create-new-project-e22ba-hen-New-Project-is-selected.gif
#	docs/demos/session-create-new-project-ef382-dir-and-project-name-fields-chromium-dom.gif
#	docs/demos/session-create-new-project-ef382-dir-and-project-name-fields.gif
#	docs/demos/session-create-new-project-fb84d-t-with-createIfMissing-true-chromium-dom.gif
#	docs/demos/session-create-new-project-fb84d-t-with-createIfMissing-true.gif
#	docs/demos/session-create-new-worktre-0fc57-ee-is-the-default-selection-chromium-dom.gif
#	docs/demos/session-create-new-worktre-0fc57-ee-is-the-default-selection.gif
#	docs/demos/session-create-new-worktre-7f600-type-with-branch-in-payload-chromium-dom.gif
#	docs/demos/session-create-new-worktre-7f600-type-with-branch-in-payload.gif
#	docs/demos/session-create-new-worktre-9dd2f-ckbox-for-new-worktree-mode-chromium-dom.gif
#	docs/demos/session-create-new-worktre-9dd2f-ckbox-for-new-worktree-mode.gif
#	docs/demos/session-create-new-worktre-ce636-itle-as-branch-is-unchecked-chromium-dom.gif
#	docs/demos/session-create-new-worktre-ce636-itle-as-branch-is-unchecked.gif
#	docs/demos/session-create-new-worktre-ce772--title-as-branch-is-checked-chromium-dom.gif
#	docs/demos/session-create-new-worktre-ce772--title-as-branch-is-checked.gif
#	docs/demos/session-lifecycle-Session--10237-Session-status-filter-works-chromium-dom.gif
#	docs/demos/session-lifecycle-Session--10237-Session-status-filter-works.gif
#	docs/demos/session-lifecycle-Session--2a57f-ion-create-UI-is-accessible-chromium-dom.gif
#	docs/demos/session-lifecycle-Session--2a57f-ion-create-UI-is-accessible.gif
#	docs/demos/session-lifecycle-Session--7b493-ssion-management-page-loads-chromium-dom.gif
#	docs/demos/session-lifecycle-Session--7b493-ssion-management-page-loads.gif
#	docs/demos/session-lifecycle-Session--995da-paused-sessions-are-visible-chromium-dom.gif
#	docs/demos/session-lifecycle-Session--995da-paused-sessions-are-visible.gif
#	docs/demos/shell-tabs-shell-tabs-shel-39ca1-ellDialog-When-ctrlTPressed-chromium-dom.gif
#	docs/demos/shell-tabs-shell-tabs-shel-39ca1-ellDialog-When-ctrlTPressed.gif
#	docs/demos/shell-tabs-shell-tabs-shel-684a1-lTab-When-plusButtonClicked-chromium-dom.gif
#	docs/demos/shell-tabs-shell-tabs-shel-684a1-lTab-When-plusButtonClicked.gif
#	docs/demos/shell-tabs-shell-tabs-shel-70877-actionMenuSpawnShellClicked-chromium-dom.gif
#	docs/demos/shell-tabs-shell-tabs-shel-70877-actionMenuSpawnShellClicked.gif
#	docs/demos/shell-tabs-shell-tabs-shel-e3a96-ab-When-deleteButtonClicked-chromium-dom.gif
#	docs/demos/shell-tabs-shell-tabs-shel-e3a96-ab-When-deleteButtonClicked.gif
#	docs/demos/smoke-Smoke-Tests-home-page-loads-successfully-chromium-dom.gif
#	docs/demos/smoke-Smoke-Tests-home-page-loads-successfully.gif
#	docs/demos/smoke-Smoke-Tests-navigation-header-is-present-chromium-dom.gif
#	docs/demos/smoke-Smoke-Tests-navigation-header-is-present.gif
#	docs/demos/smoke-Smoke-Tests-review-queue-page-loads-successfully-chromium-dom.gif
#	docs/demos/smoke-Smoke-Tests-review-queue-page-loads-successfully.gif
#	docs/demos/terminal-mobile-overflow-m-50056--visible-on-mobile-viewport-chromium-dom.gif
#	docs/demos/terminal-mobile-overflow-m-50056--visible-on-mobile-viewport.gif
#	docs/demos/terminal-mobile-overflow-m-53f8a-le-without-opening-overflow-chromium-dom.gif
#	docs/demos/terminal-mobile-overflow-m-53f8a-le-without-opening-overflow.gif
#	docs/demos/terminal-mobile-overflow-m-64891--row-with-secondary-buttons-chromium-dom.gif
#	docs/demos/terminal-mobile-overflow-m-64891--row-with-secondary-buttons.gif
#	docs/demos/terminal-mobile-overflow-m-76599-Less-hides-the-overflow-row-chromium-dom.gif
#	docs/demos/terminal-mobile-overflow-m-76599-Less-hides-the-overflow-row.gif
#	docs/demos/terminal-resize-terminal-r-9b065-tays-connected-after-resize.gif
#	docs/demos/terminal-resize-terminal-r-e5784--after-resize-dom-renderer--chromium-dom.gif
#	docs/demos/theme-background-theme-bac-3d682-background-under-dark-theme-chromium-dom.gif
#	docs/demos/theme-background-theme-bac-3d682-background-under-dark-theme.gif
#	docs/demos/touch-targets-Touch-target-0c83a--keyboard-keys-are-≥44×44px-chromium-dom.gif
#	docs/demos/touch-targets-Touch-target-0c83a--keyboard-keys-are-≥44×44px.gif
#	docs/demos/touch-targets-Touch-target-31878-tton-is-≥44×44px-on-desktop-chromium-dom.gif
#	docs/demos/touch-targets-Touch-target-31878-tton-is-≥44×44px-on-desktop.gif
#	docs/demos/touch-targets-Touch-target-66543--session-button-is-≥44×44px-chromium-dom.gif
#	docs/demos/touch-targets-Touch-target-66543--session-button-is-≥44×44px.gif
#	docs/demos/touch-targets-Touch-target-b4371--actions-button-is-≥44×44px-chromium-dom.gif
#	docs/demos/touch-targets-Touch-target-b4371--actions-button-is-≥44×44px.gif
#	docs/demos/touch-targets-Touch-target-cbd53--toolbar-toggle-is-≥44×44px-chromium-dom.gif
#	docs/demos/touch-targets-Touch-target-cbd53--toolbar-toggle-is-≥44×44px.gif
#	docs/demos/touch-targets-Touch-target-e8172-om-nav-items-are-≥44px-tall-chromium-dom.gif
#	docs/demos/touch-targets-Touch-target-e8172-om-nav-items-are-≥44px-tall.gif
#	docs/demos/visual-regression-omnibar-open-chromium-dom.gif
#	docs/demos/visual-regression-omnibar-open.gif
#	docs/demos/visual-regression-session-list-empty-state-chromium-dom.gif
#	docs/demos/visual-regression-session-list-empty-state.gif
#	docs/demos/workspace-management-Works-debfe-e-information-is-accessible-chromium-dom.gif
#	docs/demos/workspace-management-Works-debfe-e-information-is-accessible.gif
#	docs/demos/workspace-management-Works-ee243-h---Review-queue-page-loads-chromium-dom.gif
#	docs/demos/workspace-management-Works-ee243-h---Review-queue-page-loads.gif
#	docs/registry/features/backend/ImportGitHubIssue.json
#	docs/registry/features/backend/ListGitHubIssues.json
#	docs/registry/features/backend/SearchGitHubRepos.json
#	docs/registry/features/backend/backlog/approve-plan.json
#	docs/registry/features/backend/backlog/archive-item.json
#	docs/registry/features/backend/backlog/attach-session.json
#	docs/registry/features/backend/backlog/cancel-triage.json
#	docs/registry/features/backend/backlog/create-item.json
#	docs/registry/features/backend/backlog/create-source.json
#	docs/registry/features/backend/backlog/delete-item.json
#	docs/registry/features/backend/backlog/delete-source.json
#	docs/registry/features/backend/backlog/get-item.json
#	docs/registry/features/backend/backlog/get-sync-history.json
#	docs/registry/features/backend/backlog/list-items.json
#	docs/registry/features/backend/backlog/list-sources.json
#	docs/registry/features/backend/backlog/override-verdict.json
#	docs/registry/features/backend/backlog/spawn-session.json
#	docs/registry/features/backend/backlog/suggest-next.json
#	docs/registry/features/backend/backlog/transition-status.json
#	docs/registry/features/backend/backlog/trigger-re-review.json
#	docs/registry/features/backend/backlog/trigger-sync.json
#	docs/registry/features/backend/backlog/trigger-triage.json
#	docs/registry/features/backend/backlog/update-item.json
#	docs/registry/features/backend/backlog/update-source.json
#	docs/registry/features/backend/session/run-one-shot.json
#	docs/registry/features/frontend/ui/backlog-board.json
#	docs/registry/features/frontend/ui/backlog-item-card.json
#	docs/registry/features/frontend/ui/backlog-item-detail.json
#	docs/registry/features/frontend/ui/backlog-item-form.json
#	docs/registry/features/frontend/ui/backlog-list-page.json
#	docs/tasks/completed/system-service-autostart.md
#	gen/proto/go/session/v1/backlog.pb.go
#	gen/proto/go/session/v1/session.pb.go
#	gen/proto/go/session/v1/sessionv1connect/backlog.connect.go
#	gen/proto/go/session/v1/types.pb.go
#	github/client.go
#	github/clone.go
#	github/etag_cache.go
#	github/http_client.go
#	github/keychain.go
#	github/repos.go
#	github/user_pr_cache.go
#	go.mod
#	go.sum
#	main.go
#	pkg/events/types.go
#	profiling/profiling.go
#	proto/session/v1/backlog.proto
#	proto/session/v1/session.proto
#	proto/session/v1/types.proto
#	scripts/build-tmux.sh
#	scripts/install-service.sh
#	scripts/setup-codesign.sh
#	server/adapters/instance_adapter.go
#	server/adapters/review_queue_adapter.go
#	server/adapters/review_queue_adapter_test.go
#	server/analytics/subscriber.go
#	server/analytics/subscriber_test.go
#	server/dependencies.go
#	server/dependencies_test.go
#	server/events/forward.go
#	server/features/backlog.go
#	server/mcp/server.go
#	server/mcp/server_integration_test.go
#	server/mcp/tools_backlog.go
#	server/mcp/tools_backlog_test.go
#	server/mcp/tools_github.go
#	server/mcp/tools_goal.go
#	server/mcp/tools_goal_test.go
#	server/mcp/tools_terminal.go
#	server/mcp/tools_terminal_test.go
#	server/push/subscriber.go
#	server/push/subscriber_test.go
#	server/review_queue_manager.go
#	server/review_queue_manager_test.go
#	server/server.go
#	server/server_integration_test.go
#	server/services/approval_handler.go
#	server/services/autonomous_orchestration_service.go
#	server/services/autonomous_orchestration_service_test.go
#	server/services/backlog_github_rpc_test.go
#	server/services/backlog_service.go
#	server/services/backlog_service_test.go
#	server/services/backlog_triage_harness_test.go
#	server/services/connectrpc_websocket.go
#	server/services/defaults_service.go
#	server/services/defaults_service_test.go
#	server/services/event_converter_test.go
#	server/services/feature_flag_service.go
#	server/services/file_service.go
#	server/services/file_service_test.go
#	server/services/github_user_service.go
#	server/services/hook_injector.go
#	server/services/hook_injector_test.go
#	server/services/hook_receivers.go
#	server/services/local_file_service.go
#	server/services/local_file_service_test.go
#	server/services/mcp_injector.go
#	server/services/mcp_injector_test.go
#	server/services/oneshot_test.go
#	server/services/path_completion_service.go
#	server/services/path_completion_service_test.go
#	server/services/push_service.go
#	server/services/search_service.go
#	server/services/session_service.go
#	server/services/session_service_create_test.go
#	server/services/session_service_shells.go
#	server/services/session_service_stream_terminal_test.go
#	server/services/session_service_test.go
#	server/services/terminal_service.go
#	server/services/unfinished_work_service.go
#	server/services/unfinished_work_test.go
#	server/services/workspace_service_test.go
#	session/actor.go
#	session/autonomous_driver.go
#	session/autonomous_driver_test.go
#	session/backlog.go
#	session/backlog_commands.go
#	session/backlog_commands_test.go
#	session/backlog_context.go
#	session/backlog_context_test.go
#	session/backlog_integration_test.go
#	session/backlog_lifecycle.go
#	session/backlog_lifecycle_test.go
#	session/backlog_plugin_github.go
#	session/backlog_plugin_github_prs.go
#	session/backlog_plugin_github_test.go
#	session/backlog_review.go
#	session/backlog_review_test.go
#	session/backlog_sync.go
#	session/backlog_sync_test.go
#	session/backlog_test.go
#	session/backlog_triage.go
#	session/backlog_triage_test.go
#	session/capture_test.go
#	session/circular_buffer.go
#	session/claude_command_builder.go
#	session/claude_controller.go
#	session/detection/approval.go
#	session/detection/detector_test.go
#	session/detection/pattern_set.go
#	session/detection/pattern_set_test.go
#	session/detection/proto_mapping.go
#	session/ent/backlogitem.go
#	session/ent/backlogitem/backlogitem.go
#	session/ent/backlogitem/where.go
#	session/ent/backlogitem_create.go
#	session/ent/backlogitem_query.go
#	session/ent/backlogitem_update.go
#	session/ent/backlogstatusevent.go
#	session/ent/backlogstatusevent/backlogstatusevent.go
#	session/ent/backlogstatusevent/where.go
#	session/ent/backlogstatusevent_create.go
#	session/ent/backlogstatusevent_update.go
#	session/ent/client.go
#	session/ent/ent.go
#	session/ent/hook/hook.go
#	session/ent/itemsession.go
#	session/ent/itemsession/itemsession.go
#	session/ent/itemsession/where.go
#	session/ent/itemsession_create.go
#	session/ent/itemsession_update.go
#	session/ent/migrate/schema.go
#	session/ent/mutation.go
#	session/ent/predicate/predicate.go
#	session/ent/runtime.go
#	session/ent/schema/backlog_item.go
#	session/ent/schema/backlog_status_event.go
#	session/ent/schema/item_session.go
#	session/ent/schema/session_goal.go
#	session/ent/sessiongoal.go
#	session/ent/sessiongoal/sessiongoal.go
#	session/ent/sessiongoal/where.go
#	session/ent/sessiongoal_create.go
#	session/ent/sessiongoal_update.go
#	session/ent/tx.go
#	session/ent_repository.go
#	session/ent_repository_backlog.go
#	session/ent_repository_backlog_test.go
#	session/external_tmux_streamer.go
#	session/git/util.go
#	session/git/worktree.go
#	session/git/worktree_creation_test.go
#	session/git/worktree_git.go
#	session/git/worktree_git_test.go
#	session/git/worktree_ops.go
#	session/git_worktree_manager.go
#	session/headless/caller.go
#	session/headless/client.go
#	session/headless/fake_runner.go
#	session/headless/features.go
#	session/headless/features_test.go
#	session/headless/integration_test.go
#	session/headless/pool_test.go
#	session/headless/runner.go
#	session/health.go
#	session/health_test.go
#	session/hibernation_sweeper_test.go
#	session/instance.go
#	session/instance_actor_setters.go
#	session/instance_approval.go
#	session/instance_checkpoint.go
#	session/instance_claude.go
#	session/instance_controller.go
#	session/instance_hibernate.go
#	session/instance_lifecycle_test.go
#	session/instance_serialization.go
#	session/instance_shells.go
#	session/instance_state.go
#	session/instance_terminal.go
#	session/instance_tmux.go
#	session/instance_tmux_test.go
#	session/instance_workspace.go
#	session/instance_workspace_test.go
#	session/instance_worktree.go
#	session/integration_test.go
#	session/memory/reader.go
#	session/mux/multiplexer.go
#	session/mux/testmain_test.go
#	session/orphan_sweep.go
#	session/orphan_sweep_test.go
#	session/pr_status_poller.go
#	session/pty_discovery.go
#	session/pty_discovery_test.go
#	session/repo_path.go
#	session/repository.go
#	session/review_queue_determiner.go
#	session/review_queue_determiner_test.go
#	session/review_queue_poller.go
#	session/review_queue_poller_test.go
#	session/review_state.go
#	session/session_driver.go
#	session/session_driver_test.go
#	session/startup_scanner_test.go
#	session/state_machine_test.go
#	session/status_mapping.go
#	session/status_mapping_test.go
#	session/storage.go
#	session/storage_backlog.go
#	session/storage_goal_test.go
#	session/storage_test.go
#	session/tmux/control_mode.go
#	session/tmux/server_registry.go
#	session/tmux/server_registry_integration_test.go
#	session/tmux/testmain_test.go
#	session/tmux/tmux.go
#	session/tmux/tmux_test.go
#	session/tmux_backend_test.go
#	session/tmux_process_manager.go
#	session/unfinished/gogit_vcs_reader.go
#	session/unfinished/gogit_vcs_reader_limits_test.go
#	session/unfinished/scanner.go
#	session/unfinished/scanner_test.go
#	session/worktree_pr_poller.go
#	telemetry/telemetry.go
#	tests/e2e/accessibility.spec.ts
#	tests/e2e/backlog-sources-settings.spec.ts
#	tests/e2e/backlog.spec.ts
#	tests/e2e/fixtures/clean-theme.json
#	tests/e2e/fixtures/cyberpunk77-theme.json
#	tests/e2e/fixtures/matrix-theme.json
#	tests/e2e/fixtures/wh40k-theme.json
#	tests/e2e/nav-navigation.spec.ts
#	tests/e2e/package-lock.json
#	tests/e2e/pages/BacklogPage.ts
#	tests/e2e/session-create-directory.spec.ts
#	tests/e2e/session-create-existing-worktree.spec.ts
#	tests/e2e/touch-targets.spec.ts
#	tests/e2e/unfinished-work.spec.ts
#	third_party/tmux~origin_main
#	tools/lint/cmd/linter/main.go
#	tools/scanner/backend/proto_scanner.go
#	web-app/.eslintrc.json
#	web-app/jest.config.js
#	web-app/jest.setup.js
#	web-app/lighthouserc.json
#	web-app/package-lock.json
#	web-app/package.json
#	web-app/pnpm-lock.yaml
#	web-app/src/__mocks__/styleMock.js
#	web-app/src/app/backlog/backlog.css.ts
#	web-app/src/app/backlog/board/page.tsx
#	web-app/src/app/backlog/page.tsx
#	web-app/src/app/config/ConfigPageContent.tsx
#	web-app/src/app/config/config.css.ts
#	web-app/src/app/files/page.tsx
#	web-app/src/app/insights/InsightsDashboard.tsx
#	web-app/src/app/insights/SessionDetailDrawer.css.ts
#	web-app/src/app/insights/SessionDetailDrawer.tsx
#	web-app/src/app/insights/SessionsTable.css.ts
#	web-app/src/app/insights/SessionsTable.tsx
#	web-app/src/app/page.tsx
#	web-app/src/app/rules/page.css.ts
#	web-app/src/app/rules/page.tsx
#	web-app/src/app/settings/backlog-sources/page.tsx
#	web-app/src/app/settings/features/page.tsx
#	web-app/src/app/settings/page.tsx
#	web-app/src/app/settings/settings.css.ts
#	web-app/src/app/unfinished/UnfinishedTab.css.ts
#	web-app/src/app/unfinished/UnfinishedTab.tsx
#	web-app/src/app/unfinished/page.tsx
#	web-app/src/components/backlog/BacklogBoard.css.ts
#	web-app/src/components/backlog/BacklogBoard.tsx
#	web-app/src/components/backlog/BacklogItemBadge.tsx
#	web-app/src/components/backlog/BacklogItemCard.css.ts
#	web-app/src/components/backlog/BacklogItemCard.tsx
#	web-app/src/components/backlog/BacklogItemDetail.css.ts
#	web-app/src/components/backlog/BacklogItemDetail.regression.test.tsx
#	web-app/src/components/backlog/BacklogItemDetail.tsx
#	web-app/src/components/backlog/BacklogItemForm.css.ts
#	web-app/src/components/backlog/BacklogItemForm.test.tsx
#	web-app/src/components/backlog/BacklogItemForm.tsx
#	web-app/src/components/backlog/BacklogItemPanel.css.ts
#	web-app/src/components/backlog/BacklogItemPanel.tsx
#	web-app/src/components/backlog/GateVerdictBox.css.ts
#	web-app/src/components/backlog/GateVerdictBox.test.tsx
#	web-app/src/components/backlog/GateVerdictBox.tsx
#	web-app/src/components/backlog/GitHubIssuePicker.css.ts
#	web-app/src/components/backlog/GitHubIssuePicker.tsx
#	web-app/src/components/backlog/SessionMonitor.css.ts
#	web-app/src/components/backlog/SessionMonitor.tsx
#	web-app/src/components/backlog/TriageReviewPanel.css.ts
#	web-app/src/components/backlog/TriageReviewPanel.test.tsx
#	web-app/src/components/backlog/TriageReviewPanel.tsx
#	web-app/src/components/files/LocalFileBrowser.css.ts
#	web-app/src/components/files/LocalFileBrowser.tsx
#	web-app/src/components/layout/DrawerNav.tsx
#	web-app/src/components/layout/Header.css.ts
#	web-app/src/components/layout/Header.tsx
#	web-app/src/components/layout/__tests__/DrawerNav.test.tsx
#	web-app/src/components/layout/__tests__/Header.test.tsx
#	web-app/src/components/pane/PaneHeader.tsx
#	web-app/src/components/pane/PaneSplitRenderer.tsx
#	web-app/src/components/rules/RulePreview.tsx
#	web-app/src/components/sessions/ApprovalAnalyticsPanel.css.ts
#	web-app/src/components/sessions/DetectionEventsPanel.tsx
#	web-app/src/components/sessions/FileContentViewer.css.ts
#	web-app/src/components/sessions/FileContentViewer.tsx
#	web-app/src/components/sessions/FileTree.css.ts
#	web-app/src/components/sessions/FileTree.tsx
#	web-app/src/components/sessions/FilesTab.css.ts
#	web-app/src/components/sessions/FilesTab.tsx
#	web-app/src/components/sessions/NewShellDialog.css.ts
#	web-app/src/components/sessions/NewShellDialog.tsx
#	web-app/src/components/sessions/Omnibar.tsx
#	web-app/src/components/sessions/OmnibarCreationPanel.tsx
#	web-app/src/components/sessions/QuickOpenPalette.css.ts
#	web-app/src/components/sessions/QuickOpenPalette.tsx
#	web-app/src/components/sessions/RecentFilesSection.css.ts
#	web-app/src/components/sessions/RecentFilesSection.tsx
#	web-app/src/components/sessions/ReviewQueuePanel.css.ts
#	web-app/src/components/sessions/ReviewQueuePanel.tsx
#	web-app/src/components/sessions/SessionActionsOverflow.tsx
#	web-app/src/components/sessions/SessionCard.tsx
#	web-app/src/components/sessions/SessionDetailView.tsx
#	web-app/src/components/sessions/SessionList.css.ts
#	web-app/src/components/sessions/SessionList.tsx
#	web-app/src/components/sessions/SessionRow.tsx
#	web-app/src/components/sessions/StatusBadge.tsx
#	web-app/src/components/sessions/SubStatusChip.tsx
#	web-app/src/components/sessions/TerminalOutput.tsx
#	web-app/src/components/sessions/VcsPanel.css.ts
#	web-app/src/components/sessions/VcsPanel.tsx
#	web-app/src/components/sessions/XtermTerminal.tsx
#	web-app/src/components/sessions/__tests__/FileTree.test.tsx
#	web-app/src/components/sessions/__tests__/NewShellDialog.test.tsx
#	web-app/src/components/sessions/__tests__/Omnibar.alias.test.tsx
#	web-app/src/components/sessions/__tests__/Omnibar.pathcompletion.test.tsx
#	web-app/src/components/sessions/__tests__/OmnibarCreationPanel.attach.test.tsx
#	web-app/src/components/sessions/__tests__/ReviewQueuePanel.test.tsx
#	web-app/src/components/sessions/__tests__/SessionActionsOverflow.test.tsx
#	web-app/src/components/sessions/__tests__/SessionCard.approval-suppression.test.tsx
#	web-app/src/components/sessions/__tests__/SessionCard.click.test.tsx
#	web-app/src/components/sessions/__tests__/SessionDetail.embedded.test.tsx
#	web-app/src/components/sessions/__tests__/StatusBadge.test.tsx
#	web-app/src/components/sessions/__tests__/TerminalOutput.enter-detection.test.tsx
#	web-app/src/components/sessions/__tests__/TerminalOutput.logstream.test.tsx
#	web-app/src/components/sessions/__tests__/TerminalOutput.reconnect.test.tsx
#	web-app/src/components/sessions/__tests__/TerminalOutput.toolbar-analytics.test.tsx
#	web-app/src/components/sessions/__tests__/TerminalOutput.upload.test.tsx
#	web-app/src/components/sessions/__tests__/TerminalOutputBug.test.tsx
#	web-app/src/components/sessions/__tests__/XtermTerminal.test.tsx
#	web-app/src/components/settings/GlobalDefaultsForm.css.ts
#	web-app/src/components/settings/GlobalDefaultsForm.tsx
#	web-app/src/components/shared/DiffRenderer.css.ts
#	web-app/src/components/shared/DiffRenderer.tsx
#	web-app/src/components/ui/NotificationPanel.css.ts
#	web-app/src/components/ui/RepoPathInput.tsx
#	web-app/src/components/unfinished/CommitPushModal.css.ts
#	web-app/src/components/unfinished/CommitPushModal.tsx
#	web-app/src/components/unfinished/UnfinishedItem.css.ts
#	web-app/src/components/unfinished/UnfinishedItemDetail.css.ts
#	web-app/src/components/unfinished/UnfinishedItemDetail.tsx
#	web-app/src/components/unfinished/WorktreeDiffModal.css.ts
#	web-app/src/components/workflows/WorkflowForm.tsx
#	web-app/src/gen/session/v1/backlog_pb.ts
#	web-app/src/gen/session/v1/session_pb.ts
#	web-app/src/gen/session/v1/types_pb.ts
#	web-app/src/lib/backlog/status.ts
#	web-app/src/lib/contexts/FeatureFlagsContext.tsx
#	web-app/src/lib/contexts/NotificationContext.tsx
#	web-app/src/lib/contexts/OmnibarContext.tsx
#	web-app/src/lib/contexts/SessionServiceContext.tsx
#	web-app/src/lib/features/features/unfinished-work.ts
#	web-app/src/lib/hooks/__tests__/useBrowserLogStream.test.ts
#	web-app/src/lib/hooks/__tests__/useSessionNotifications.test.ts
#	web-app/src/lib/hooks/__tests__/useSessionService.test.ts
#	web-app/src/lib/hooks/__tests__/useTerminalFlowControl.test.ts
#	web-app/src/lib/hooks/useBacklogService.ts
#	web-app/src/lib/hooks/useBrowserLogStream.ts
#	web-app/src/lib/hooks/useGitHubIssuePicker.ts
#	web-app/src/lib/hooks/useSessionNotifications.ts
#	web-app/src/lib/hooks/useSessionService.ts
#	web-app/src/lib/hooks/useTerminalFlowControl.ts
#	web-app/src/lib/hooks/useTerminalStream.ts
#	web-app/src/lib/hooks/useVcsStatus.ts
#	web-app/src/lib/hooks/useWorktreeSuggestions.ts
#	web-app/src/lib/nav-pages.ts
#	web-app/src/lib/omnibar/actions/dispatch.test.ts
#	web-app/src/lib/omnibar/actions/dispatch.ts
#	web-app/src/lib/omnibar/actions/types.ts
#	web-app/src/lib/omnibar/detector.test.ts
#	web-app/src/lib/omnibar/detectors/CommandDetector.ts
#	web-app/src/lib/omnibar/types.ts
#	web-app/src/lib/routes.ts
#	web-app/src/lib/store/__tests__/reviewQueueSlice.test.ts
#	web-app/src/lib/store/__tests__/sessionsSlice.test.ts
#	web-app/src/lib/store/store.ts
#	web-app/src/lib/utils/__tests__/deriveWorkingState.test.ts
#	web-app/src/lib/utils/deriveWorkingState.ts
#	web-app/src/lib/utils/issuePickerCache.ts
#	web-app/src/lib/utils/parseDiff.ts
#	web-app/src/styles/pane/paneHeader.css.ts
#	web-app/src/styles/theme-contract.css.ts
#	web-app/src/styles/theme.css.ts
#	web-app/tests/e2e/navigation.spec.ts
…nflict resolution

Resolving the origin/main merge (LFS history cutover left this branch far
behind main's history) required bulk-resolving hundreds of untouched-by-this-PR
conflicts to main's side. That correctly dropped stale duplicate content, but
also silently dropped a handful of pre-existing fixes/features that exist only
on this branch (inherited from an earlier, now-unreachable main state) with no
main-side equivalent:

- session/detection/proto_mapping.go: restore DetectedStatusToSubStatus,
  which proto_mapping_test.go (auto-merged, unaffected by the bulk resolve)
  already exercises.
- web-app/src/components/sessions/SubStatusChip.tsx: restore the
  forward-compatible "render nothing" default case for unrecognized
  SubStatus values instead of the throwing assertNever main's revision uses
  — proto enums are forward-compatible and one unrecognized wire value must
  not crash the sessions UI.
- TerminalOutput{,.reconnect,.enter-detection,.logstream,.toolbar-analytics,
  .upload}.test.tsx and TerminalOutputBug.test.tsx: add the
  requestFullResync/markResyncComplete/markPaneResponseReceived mock fields
  useVisibilityResync (only present on this branch, not main) calls
  unconditionally on unmount/session-id change, plus the XtermTerminal
  mock's resize() stub the pre-sizing effect now calls.

Found by running the full frontend/backend test suites after the merge
rather than trusting the conflict resolution alone.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Registry Validation

Registry Validation
===================

Building backend scanner...
Scanning backend features...
Wrote 117 feature files to /tmp/tmp.QXXVuu6qXk/backend
Wrote 15 feature files to /tmp/tmp.QXXVuu6qXk/backend
Wrote 45 feature files to /tmp/tmp.QXXVuu6qXk/backend
Wrote 7 feature files to /tmp/tmp.QXXVuu6qXk/backend
Wrote 11 feature files to /tmp/tmp.QXXVuu6qXk/backend

=== Backend Registry Diff ===
Committed: 179  Generated: 179  Divergence: 0.0%
⚠️  110 feature(s) missing // +api: marker (markerFound: false)

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 19/179 features have testIds (10.6%)

Divergence > 2% blocks merges. Coverage reporting is advisory only.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Go Benchmarks (Tier 1)

benchmarks/go/tier1-baseline.txt:98: missing iteration count
benchmarks/go/tier1-baseline.txt:198: missing iteration count
tier1-bench.txt:98: missing iteration count
tier1-bench.txt:198: missing iteration count
goos: linux
goarch: amd64
pkg: github.com/tstapler/stapler-squad/session
cpu: AMD EPYC 7763 64-Core Processor                
                                            │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt          │
                                            │              sec/op              │   sec/op     vs base              │
CircularBufferWrite_4KB-4                                          80.67n ± 2%   82.86n ± 1%  +2.71% (p=0.001 n=8)
CircularBufferWrite_4KB_Allocs-4                                   80.88n ± 0%   83.58n ± 0%  +3.34% (p=0.000 n=8)
CircularBufferGetRecent_4KB-4                                      532.5n ± 7%   501.3n ± 3%  -5.86% (p=0.007 n=7)
CircularBufferGetAll-4                                             3.944µ ± 2%   3.878µ ± 1%  -1.66% (p=0.028 n=8)
GetTimeSinceLastMeaningfulOutput_HotPath-4                         65.86n ± 1%   65.80n ± 0%       ~ (p=0.343 n=8)
GetTimeSinceLastMeaningfulOutput_ColdPath-4                        32.82n ± 1%   32.77n ± 0%       ~ (p=0.079 n=7)
geomean                                                            175.9n        175.3n       -0.33%

                                            │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt            │
                                            │               B/op               │     B/op      vs base                │
CircularBufferWrite_4KB-4                                         0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferWrite_4KB_Allocs-4                                  0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferGetRecent_4KB-4                                   4.000Ki ± 0%     4.000Ki ± 0%       ~ (p=1.000 n=7) ¹
CircularBufferGetAll-4                                          40.00Ki ± 0%     40.00Ki ± 0%       ~ (p=1.000 n=8) ¹
GetTimeSinceLastMeaningfulOutput_HotPath-4                        0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
GetTimeSinceLastMeaningfulOutput_ColdPath-4                       0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=7) ¹
geomean                                                                      ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                            │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                                            │            allocs/op             │ allocs/op   vs base                │
CircularBufferWrite_4KB-4                                         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferWrite_4KB_Allocs-4                                  0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferGetRecent_4KB-4                                     1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=7) ¹
CircularBufferGetAll-4                                            1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=8) ¹
GetTimeSinceLastMeaningfulOutput_HotPath-4                        0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
GetTimeSinceLastMeaningfulOutput_ColdPath-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=7) ¹
geomean                                                                      ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │               B/s                │     B/s       vs base              │
CircularBufferWrite_4KB-4                           47.29Gi ± 2%   46.04Gi ± 0%  -2.63% (p=0.001 n=8)
CircularBufferGetRecent_4KB-4                       7.164Gi ± 7%   7.609Gi ± 3%  +6.21% (p=0.007 n=7)
geomean                                             18.41Gi        18.72Gi       +1.69%

pkg: github.com/tstapler/stapler-squad/session/detection/ratelimit
                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt          │
                              │              sec/op              │   sec/op     vs base              │
StripANSI_PlainText-4                                6.882n ± 0%   6.867n ± 1%       ~ (p=0.185 n=8)
StripANSI_WithEscapes-4                              749.0n ± 0%   748.4n ± 0%       ~ (p=0.593 n=8)
ProcessOutput_InactiveState-4                        6.337n ± 1%   6.340n ± 1%       ~ (p=0.666 n=8)
geomean                                              31.97n        31.94n       -0.08%

                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │               B/op               │    B/op     vs base                │
StripANSI_PlainText-4                               0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
StripANSI_WithEscapes-4                             136.0 ± 0%     136.0 ± 0%       ~ (p=1.000 n=8) ¹
ProcessOutput_InactiveState-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                        ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │            allocs/op             │ allocs/op   vs base                │
StripANSI_PlainText-4                               0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
StripANSI_WithEscapes-4                             5.000 ± 0%     5.000 ± 0%       ~ (p=1.000 n=8) ¹
ProcessOutput_InactiveState-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                        ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/queue
                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt          │
                              │              sec/op              │   sec/op     vs base              │
ReviewQueue_ConcurrentReads-4                        85.34n ± 6%   84.79n ± 4%       ~ (p=0.130 n=8)
ReviewQueue_Add-4                                    511.9n ± 1%   504.2n ± 0%  -1.51% (p=0.001 n=8)
geomean                                              209.0n        206.8n       -1.08%

                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │               B/op               │    B/op     vs base                │
ReviewQueue_ConcurrentReads-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
ReviewQueue_Add-4                                   640.0 ± 0%     640.0 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                        ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │            allocs/op             │ allocs/op   vs base                │
ReviewQueue_ConcurrentReads-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
ReviewQueue_Add-4                                   4.000 ± 0%     4.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                        ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/scrollback
                                      │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                                      │              sec/op              │   sec/op     vs base               │
CircularBuffer_ConcurrentReadWrite-4                         3.977µ ± 2%   3.955µ ± 2%        ~ (p=0.169 n=8)
CircularBuffer_BurstAppend-4                                 102.7µ ± 1%   102.2µ ± 2%        ~ (p=0.195 n=8)
CircularBuffer_GetLastN_LargeBuffer-4                        20.34µ ± 0%   20.01µ ± 1%   -1.61% (p=0.010 n=8)
CircularBuffer_GetRange_Sequential-4                         11.04µ ± 2%   13.63µ ± 5%  +23.43% (p=0.000 n=8)
CircularBufferAppend-4                                       98.39n ± 1%   99.95n ± 1%   +1.58% (p=0.000 n=8)
CircularBufferGetLastN-4                                     2.260µ ± 2%   2.554µ ± 2%  +13.03% (p=0.000 n=8)
CircularBufferConcurrentAppend-4                             130.3n ± 0%   125.1n ± 0%   -3.99% (p=0.000 n=8)
geomean                                                      3.085µ        3.211µ        +4.09%

                                      │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt            │
                                      │               B/op               │     B/op      vs base                │
CircularBuffer_ConcurrentReadWrite-4                        6.062Ki ± 0%   6.062Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_BurstAppend-4                                62.50Ki ± 0%   62.50Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_GetLastN_LargeBuffer-4                       56.00Ki ± 0%   56.00Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_GetRange_Sequential-4                        28.00Ki ± 0%   28.00Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferAppend-4                                        24.00 ± 0%     24.00 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferGetLastN-4                                    6.000Ki ± 0%   6.000Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferConcurrentAppend-4                              32.00 ± 0%     32.00 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                     3.077Ki        3.077Ki       +0.00%
¹ all samples are equal

                                      │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt           │
                                      │            allocs/op             │  allocs/op   vs base                │
CircularBuffer_ConcurrentReadWrite-4                          2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_BurstAppend-4                                 1.000k ± 0%   1.000k ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_GetLastN_LargeBuffer-4                         1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_GetRange_Sequential-4                          1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferAppend-4                                        1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferGetLastN-4                                      1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferConcurrentAppend-4                              1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                       2.962         2.962       +0.00%
¹ all samples are equal

                             │ benchmarks/go/tier1-baseline.txt │        tier1-bench.txt        │
                             │               B/s                │     B/s       vs base         │
CircularBuffer_BurstAppend-4                       594.0Mi ± 0%   597.3Mi ± 3%  ~ (p=0.195 n=8)

pkg: github.com/tstapler/stapler-squad/session/tmux
                             │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt          │
                             │              sec/op              │   sec/op     vs base              │
StripANSICodes_PlainText-4                          6.927n ± 4%   6.885n ± 1%       ~ (p=0.224 n=8)
StripANSICodes_WithEscapes-4                        688.7n ± 1%   689.5n ± 0%       ~ (p=0.959 n=8)
IsBanner_PlainText-4                                478.5n ± 0%   477.9n ± 0%       ~ (p=0.738 n=8)
geomean                                             131.7n        131.4n       -0.20%

                             │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                             │               B/op               │    B/op     vs base                │
StripANSICodes_PlainText-4                         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
StripANSICodes_WithEscapes-4                       56.00 ± 0%     56.00 ± 0%       ~ (p=1.000 n=8) ¹
IsBanner_PlainText-4                               0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                       ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                             │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                             │            allocs/op             │ allocs/op   vs base                │
StripANSICodes_PlainText-4                         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
StripANSICodes_WithEscapes-4                       4.000 ± 0%     4.000 ± 0%       ~ (p=1.000 n=8) ¹
IsBanner_PlainText-4                               0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                       ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/tokens
                                   │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt          │
                                   │              sec/op              │   sec/op     vs base              │
TokenParser_ProcessUserEntry-4                            5.403m ± 2%   5.287m ± 1%  -2.15% (p=0.003 n=8)
DetectCommandsInText/NoSlash-4                            7.494n ± 0%   7.494n ± 0%       ~ (p=0.819 n=8)
DetectCommandsInText/WithCommand-4                        1.674µ ± 1%   1.664µ ± 1%       ~ (p=0.077 n=8)
geomean                                                   4.077µ        4.040µ       -0.91%

                                   │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt            │
                                   │               B/op               │     B/op      vs base                │
TokenParser_ProcessUserEntry-4                         11.02Mi ± 0%     11.02Mi ± 0%       ~ (p=0.097 n=8)
DetectCommandsInText/NoSlash-4                           0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
DetectCommandsInText/WithCommand-4                       433.0 ± 0%       433.0 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                             ²                 -0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                   │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                                   │            allocs/op             │ allocs/op   vs base                │
TokenParser_ProcessUserEntry-4                           34.00 ± 0%     34.00 ± 0%       ~ (p=1.000 n=8) ¹
DetectCommandsInText/NoSlash-4                           0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
DetectCommandsInText/WithCommand-4                       6.000 ± 0%     6.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/unfinished
                               │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt          │
                               │              sec/op              │   sec/op     vs base              │
DiffShortstat/GitVCSReader-4                          3.168m ± 1%   3.142m ± 1%  -0.80% (p=0.005 n=8)
DiffShortstat/GoGitVCSReader-4                        76.83n ± 1%   76.70n ± 0%       ~ (p=0.195 n=8)
DiffShortstatCached-4                                 76.06n ± 1%   75.58n ± 2%       ~ (p=0.195 n=8)
geomean                                               2.645µ        2.631µ       -0.54%

                               │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt            │
                               │               B/op               │     B/op      vs base                │
DiffShortstat/GitVCSReader-4                       62.57Ki ± 0%     62.57Ki ± 0%       ~ (p=0.170 n=8)
DiffShortstat/GoGitVCSReader-4                       0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
DiffShortstatCached-4                                0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                         ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                               │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                               │            allocs/op             │ allocs/op   vs base                │
DiffShortstat/GitVCSReader-4                         360.0 ± 0%     360.0 ± 0%       ~ (p=1.000 n=8) ¹
DiffShortstat/GoGitVCSReader-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
DiffShortstatCached-4                                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                         ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

E2E RPC Latency

list-sessions-ttfb-mean: 7ms (▲ slower +44.9%; baseline: 5ms)
list-sessions-total-mean: 8ms (▲ slower +1.6%; baseline: 8ms)

@github-actions

Copy link
Copy Markdown
Contributor

UX Analysis

Check Status Details
✅ Axe Core (WCAG 2.1 AA) success Critical/serious violations block merge
⚠️ Lighthouse Performance Score: unknown Warning if < 70 (non-blocking)
🤖 Claude UX Analysis Advisory See docs/qa/ for findings

Axe Core excludes terminal rendering areas (intentional design).
Lighthouse runs in desktop preset for this developer tool.

@github-actions

Copy link
Copy Markdown
Contributor

📊 Feature E2E Coverage

Feature coverage report unavailable

Run make e2e-report locally to view the full Allure report.

…e peer dep

@xterm/xterm was bumped to 6.0.0 in an unrelated PR after PR #272 added
@xterm/addon-canvas for the WebGL->Canvas fallback (ADR-001). Upstream
removed the canvas renderer from the xterm.js monorepo entirely as of
6.0.0 (xtermjs/xterm.js#5105), so addon-canvas@0.7.0's peer dependency
(^5.0.0) will never be updated -- pnpm install only warns, it doesn't
fail, so CI stayed green with no signal either way.

Verified ground truth instead of trusting the peer-dep warning: a real,
unmocked CanvasAddon activates, resizes, writes, and disposes cleanly
against a real, unmocked xterm 6 Terminal (jsdom has no real 2D canvas
context, so a minimal fake one stands in, same workaround already used
for @xterm/addon-serialize). Added a regression test proving this and
documented the finding in ADR-001's addendum and inline in
XtermTerminal.tsx, so a future xterm major bump gets caught instead of
silently shipping a dead fallback behind a green peer-dep warning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🎬 E2E Feature Demos

2 shard(s) recorded feature flows for this PR.

recordings shard 1
recordings shard 2

Demo preview opens directly in browser (single-file HTML). Raw WebM recordings in ZIP. Expires after 30 days.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Registry Validation

Registry Validation
===================

Building backend scanner...
Scanning backend features...
Wrote 117 feature files to /tmp/tmp.QLU8lqUtWE/backend
Wrote 15 feature files to /tmp/tmp.QLU8lqUtWE/backend
Wrote 45 feature files to /tmp/tmp.QLU8lqUtWE/backend
Wrote 7 feature files to /tmp/tmp.QLU8lqUtWE/backend
Wrote 11 feature files to /tmp/tmp.QLU8lqUtWE/backend

=== Backend Registry Diff ===
Committed: 179  Generated: 179  Divergence: 0.0%
⚠️  110 feature(s) missing // +api: marker (markerFound: false)

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 19/179 features have testIds (10.6%)

Divergence > 2% blocks merges. Coverage reporting is advisory only.

@github-actions

Copy link
Copy Markdown
Contributor

Frontend Terminal Throughput

terminal-throughput-mean: 14 KB/s ▲ +15.7% (baseline: 12 KB/s)
terminal-throughput-p50: 16 KB/s ▲ +2.0% (baseline: 15 KB/s)

@tstapler
tstapler marked this pull request as ready for review July 28, 2026 01:32
@tstapler
tstapler merged commit 9651edd into main Jul 28, 2026
21 of 24 checks passed
@tstapler
tstapler deleted the backlog/stapler-squad-terminal-resize-loop-fix branch July 28, 2026 01:32
tstapler added a commit that referenced this pull request Jul 28, 2026
…#272's merge

commit 591059a already removed these from git and gitignored benchmarks/
(baselines are persisted via GitHub Actions cache instead) — but PR #272's
branch predated that fix and still had them tracked, so merging it
resurrected all four as tracked files again. .gitignore doesn't stop a
merge from reintroducing already-tracked-on-one-side content, so this
needs its own removal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba
tstapler added a commit that referenced this pull request Aug 3, 2026
Fixes 5 confirmed MAJOR findings from the Gate 2 review pass:

1. TestRequestReview_ReportsDistinctMessage_WhenCASPreconditionFails
   called require.NoError from inside a spawned goroutine — t.FailNow()
   only unwinds that goroutine, not the test goroutine. Both racers now
   send {result, err} through the channel and every require/assert call
   happens in the main test goroutine. The same channel type is reused
   for the new report_duplicate CAS test (fix 3).

2. Added TestReportDuplicate_VerifyGitHubRefExists_DispatchesPRTypeToRealGetPR,
   the first test in the package that leaves verifyGitHubRef nil so the
   real verifyGitHubRefExists dispatch switch (PR/Issue/Commit -> GetPR/
   GetIssue/GetCommit) actually runs, pointed at an httptest.Server via
   githubpkg.GhBaseURL (mirrors github/repos_pr_test.go's pattern).

3. Added TestReportDuplicate_ReportsDistinctMessage_WhenCASPreconditionFails,
   mirroring the request_review CAS regression test for report_duplicate's
   identical errors.Is(transErr, session.ErrPreconditionFailed) branch.

4. Added TestReportDuplicate_DoesNotTreatPrefixRefAsIdempotentMatch,
   proving a shorter ref that is a literal string-prefix of an
   already-recorded longer ref (.../pull/27 vs .../pull/272) is not
   misclassified as the idempotent no-op retry.

5. requestReview's verification-notes persistence overwrote
   VerificationNotes via UpdateItemSessionVerificationNotes, silently
   erasing prior evidence (e.g. from an earlier report_duplicate call)
   on the same ItemSession. reportDuplicate already had this fixed with
   an append pattern; applied the same fix to requestReview and added
   TestRequestReview_AppendsToExistingVerificationNotes_RatherThanOverwriting
   as a new test, leaving the existing
   TestRequestReview_PersistsVerificationNotesOnWorkSession assertions
   unmodified per AC9/FR9.

All 5 pre-existing TestRequestReview_* tests still pass with their
original assertions intact. go build/vet/gofmt/golangci-lint clean on
server/mcp and github packages; go test ./server/mcp/... ./github/...
-race all green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxSH3xUu3fyFnprsybnRiN
tstapler added a commit that referenced this pull request Aug 3, 2026
…308)

* chore(sdd): DX research pass for jest-ci-wiring

No end-user-facing UX applies (pure CI/infra change) — lightweight DX
pass instead, comparing failure surfacing to golangci-lint annotations
vs plain-log precedent (ESLint/gofmt) and flagging --ci flag as a
zero-cost quick win.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): stack research for backlog-status-transitions

Documents the BacklogStatus enum pattern, mark3labs/mcp-go tool
registration/handler shape, and confirms the ent status column needs no
migration for a new terminal value.

* chore(sdd): architecture research for flaky-hook-url-tests

* chore(sdd): build-vs-buy research for detector-plugins

Evaluates TOML parsing (go-toml v2), regex engine (stdlib RE2), and
hot-reload (fsnotify) choices, plus config-aggregation frameworks
(Viper/koanf) and existing plugin-system prior art (Caddy/Vector/Grafana),
for the user-extensible agent detector plugin loader.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): pitfalls research for detector-plugins

Covers RE2/ReDoS guarantees and limits, fsnotify hot-reload gotchas
(temp-file-rename, debounce, inotify limits, macOS/Linux differences),
TOML/config pitfalls, the atomic-swap concurrency pattern to reuse from
worktree_git.go, and fail2ban/ESLint/VS Code prior art.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): UX research for token-cost-tracking

Documents existing table/sort patterns to mirror (SessionDetailDrawer's
Tools Breakdown, backlog page's aria-sort), the proto gaps blocking AC-1
(per-turn) and AC-6 (cache split), and concrete labeling/empty-state/
sort-order decisions for AC-1 through AC-3.

* chore(sdd): architecture research for token-cost-tracking

* chore(sdd): pitfalls research for token-cost-tracking gap closure

Research pass on what commonly breaks when adding to an already-live
analytics feature: the #280 silent-$0.00 precedent, TokenStore RWMutex
contention, jumping-list risk for async-sorted cost data, stale
registry schema.json vs real frontend entry shape, and the
backlogItemEventSender pattern needed before WatchInsights is testable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): feature/edge-case research for token-cost-tracking

Documents edge cases and unstated scope for the 6 gap-closing ACs: AC-1
requires a proto change (TurnTimeline isn't exposed over the wire, not just
unwired in the frontend); AC-2's SessionList.tsx has zero token data wired in
today (TokenBadge is unused dead code, contradicting requirements.md); AC-3
needs no backend change (ListSessionTokens sort_by already implemented,
SessionsTable.tsx's sort is just hardcoded); AC-4 needs the same
narrow-interface refactor WatchBacklogItems already established for testing
connect-go streaming RPCs; AC-5's 5 target files share one +feature marker
with an existing aggregated registry entry, creating a collision risk; AC-6
is free (client-side formula, data already on the wire).

* chore(sdd): implementation plan + ADR for flaky-hook-url-tests

Phase 3 planning artifacts: creative pass, domain glossary, pattern
decisions, task breakdown (3 epics / 4 stories / 9 tasks), and ADR-001
justifying -p 1 over an isolated second -race invocation for the CI
contention mitigation. Also commits the earlier requirements.md and
research/*.md that were still uncommitted from phases 1-2.

* chore(sdd): plan + ADR for subagent-spawn-tracking

Phase 3 architecture/task-breakdown plan (7 verified pipeline hops from
regex capture through proto to the SubStatusChip badge) plus ADR-001
documenting the statusCacheEntry cache-coherence requirement between
GetCurrentStatus and GetStatusAndIdleInfo. Also commits the requirements
and research docs from earlier phases that were still untracked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): implementation plan + ADRs for detector-plugins

Phase 3 planning artifacts: plan.md (3 phases, 6 epics, 14 stories,
41 tasks, 30-term glossary) plus four ADRs covering the go-toml/v2
dependency, the registry-level copy-on-write snapshot (vs. the existing
unused StatusDetector YAML loader), TOML schema v1, and the RE2 trust
boundary. Also picks up the architecture/features research files that
were still untracked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): implementation plan for token-cost-tracking

Phase 3 planning artifact for the token-cost-tracking gap-closure project:
domain glossary, pattern decisions (new-RPC vs bolt-on for AC-1, interface
extraction for AC-4's WatchInsights test, client-side derivation for AC-6),
risk control, and a 5-phase task breakdown sequenced by risk (AC-4/AC-5
low-risk first, AC-2's new SessionList data join last). Also commits the
requirements.md and research/{build-vs-buy,stack}.md artifacts from earlier
phases that were left uncommitted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): test validation plan for flaky-hook-url-tests

Maps each requirements.md scope item and plan.md task to concrete
verification: regression checks for the two affected integration
tests, a stress/flake-verification repro (-count=10 under artificial
CPU contention), the Task 1.2.4 non-fatal-teardown check, the Task
2.1.1 coverage-artifact/-race-scope check, and the Task 2.1.2
averaged (>=3-run) wall-clock measurement. No user-facing surface, so
UX Acceptance Tests section is N/A; no schema change, so Migration
Test is N/A.

* chore(sdd): UX design artifact for token-cost-tracking gap closure

Wireframes, interaction flows, error/empty states, and UX acceptance
criteria for the 4 user-facing surfaces in Phases 2-4 of the
implementation plan (SessionsTable click-to-sort, ModelBreakdownChart
cache-hit-rate label, SessionDetailDrawer per-turn table, SessionList
Sort: Cost option). Flags a concrete contrast/layout risk in reusing
TokenBadge.css.ts's badgeVariant.warning for outlier-turn highlighting
in a table-cell context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): validate + review artifacts for subagent-spawn-tracking

Adversarial/architecture review, UX design, validation plan, and
pre-mortem for the subagent-spawn-tracking feature; plan.md patched to
resolve 2 architecture-review CONCERNS, 1 adversarial-review safety
concern, 2 cross-artifact-consistency BLOCKERs (title-attribute
contradiction with ux.md, missing NaN/negative test coverage), and the
pre-mortem's P1 (corrected a false "no debounce precedent" claim).
Triad review (Product/UX/Engineering) verdict: READY TO BUILD.

* chore(sdd): validation/review artifacts + plan patches for flaky-hook-url-tests

Adds architecture review, adversarial review, and pre-mortem docs from SDD
Phase 3/4, and patches plan.md to resolve the cross-artifact consistency
blocker (missing runner-concurrency check task) and the pre-mortem P1 item
(testSocketOnce misdiagnosis prevention).

* chore(sdd): test validation plan for detector-plugins

Maps all 6 requirements.md acceptance criteria plus plan.md's
Phase 1-2 story-level Given/When/Then scenarios to concrete Go
test names, organized as unit vs. integration, so implementation
starts test-first.

* chore(sdd): adoption plan for ci-hookurl-race-flake

Research and planning surfaced that this backlog item duplicates the
already-planned, already-reviewed project_plans/flaky-hook-url-tests/
(same root cause, same two tests, ADR-001 + full task breakdown never
implemented). implementation/plan.md consolidates rather than
re-derives: it adopts that plan/ADR wholesale, reproduces the full
Epic/Story/Task breakdown here for self-containment, and re-verifies
every file:line reference against the current tree (all accurate, no
drift found).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): phase 4 validation, pre-mortem, and triad-review repair for detector-plugins

Phase 4 (sdd:4-validate): validation.md test suite, pre-mortem.md failure
analysis, and cross-artifact consistency check. The pre-mortem's P1 finding
(DetectForProgram/the plugin registry has zero production call sites, so a
loaded plugin would never change what a user's session actually shows) was
independently confirmed by adversarial-review.md's own Blocker and by direct
repo verification (idle.go's live detection path uses getDefaultPatterns(),
not the registry this plan builds). Resolved by adding Epic 2.4 to plan.md,
wiring ClaudeController.Start to resolve its detector via the new registry
snapshot instead of the always-generic default.

Also folds in a Hardening Addendum (compile-time budget, total-file-count
cap, rebuildSnapshot context cancellation, InitPlugins re-entrancy guard,
non-fatal seed-file-write) closing 5 previously-open adversarial-review
Concerns, and requirements.md amendments (Target User, Success Metric,
Risky Assumption sections; corrected plugin-directory and version-field
wording) from a 3-round product/engineering triad review.

* chore: untrack accidentally-swept file from detector-plugins commit

project_plans/backlog-status-transitions/research/architecture.md belongs
to an unrelated, concurrently in-progress backlog item and was swept into
the previous commit because it was already staged in the shared index at
commit time. Untracking (not deleting) it here so it returns to the
working tree for its own session to commit under its own message.

* chore(sdd): validation artifacts + review-driven patches for ci-hookurl-race-flake

Phase 4 (validate) for the flaky hook-URL/MCP-URL CI test backlog item:
validation.md, pre-mortem.md, architecture-review.md, adversarial-review.md.
Cross-artifact consistency review surfaced 3 blockers (AC #1's N/method never
decided, a citation to a "Success Metrics" section that didn't exist in this
project's own requirements.md, and a stale file path) — fixed by adding this
project's own Success Metrics section (N=20, distinct decision from the sibling
project) and correcting the path. Pre-mortem's 3 P1 items (unfolded review
concerns, un-rebutted build-vs-buy divergence, unenforced AC#4 evidence gate)
are folded directly into plan.md and research/build-vs-buy.md rather than left
as unread siblings for Phase 5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(sdd): build-vs-buy research for backlog-self-resolve

Evaluates GitHub verification (extend existing github/ + session.ParseGitHubURL,
no google/go-github), duplicate-marking helpers (none exist, build new), and
CAS precondition mechanism (reuse TransitionBacklogItemStatus as-is).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): requirements + research for backlog-self-resolve

Phase 1-2 planning artifacts for backlog item da58b867 (report_duplicate
MCP tool + request_review CAS generalization).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): implementation plan + ADRs for backlog-self-resolve

Phase 3 planning output for the report_duplicate MCP tool + request_review
CAS-precondition generalization (item da58b867). 11 epics / 26 stories /
51 tasks, one Given-When-Then example per FR1-FR10, and 4 ADRs covering
the GitHub-verification dispatcher/auth/error-classification design, the
TriggeredByAgent audit-attribution scope, report_duplicate's idempotency
rule, and the (flagged, owner-confirm-pending) decision not to extend
FR2's active-reviewer refusal to report_duplicate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): resolve ADR numbering collision with concurrent planning session

A second, independent SDD planning pass turned out to be running on this
same self-referential backlog item concurrently — its ADR-001/ADR-002
files appeared in decisions/ between this plan's mkdir and git add.
Renumber this plan's four ADRs to ADR-005-008 to stop clobbering the
other session's numbering, and flag the one substantive disagreement
found (github.GetPR HTTP addition vs. keeping the gh-CLI GetPRInfoCtx
path) as unresolved in both plan.md and ADR-005 for whoever picks up
implementation to reconcile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): reconcile ADR collision and fix verification-notes overwrite bug in backlog-self-resolve plan

Resolves a duplicate-execution artifact from planning: two independently-produced
ADRs disagreed on GitHub PR-verification (gh CLI vs HTTP). Adopted the HTTP-only
approach (ADR-002) per this repo's subshell-avoidance convention and the
auth-consistency risk pitfalls.md flagged; deleted the superseded draft and
renumbered ADR-001..005 sequentially. Also fixes a real bug a second research
pass surfaced: report_duplicate's verification-notes write must append rather
than overwrite, or it silently discards evidence from an earlier request_review
call on the same ItemSession.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): repair review blockers in backlog-self-resolve plan

Architecture review (1 blocker, 3 concerns) and adversarial review (1 blocker,
9 concerns, 5 minors) findings recorded and repaired:

- Fixed FR10's stuck-item detector citation: pr_pending_no_pr only covers
  items with NO PrNumber (wrong shape for this feature); the correct existing
  coverage is ReconcilePRPending's PRReadyUnmerged/PRNeedsFix family, which
  operates on pr_pending items that DO have a PR reference (this item's actual
  scenario). No new detector needed, just the right citation.
- Fixed verifyGitHubRefExists's Domain Glossary/GWT wording, which claimed a
  (bool, error) contract matching verifyPR when the actual designed signature
  is single-error-return.
- Rewrote Task 4.2.6a's garbled concurrency test to actually call both
  handlers, and corrected Story 4.2.6's acceptance criteria (the "exactly one
  status-event row" premise was false for two legitimately sequential
  transitions).
- Ratified ADR-003/ADR-004 to Accepted; tightened ADR-005's argument so it
  doesn't contradict Story 3.1.2's own independently-justified 4th refusal
  condition.
- (Already applied by a prior pass: hasActiveReviewSession export instead of
  a 4th duplicate copy — confirmed no import cycle; idempotency substring-
  match fix; verifyGitHubRef injectable test seam; corrected test-file
  citations to server/services/backlog_github_rpc_test.go's resetGhBaseURL.)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): fix residual review inconsistencies in backlog-self-resolve plan

Scoped re-review confirmed both prior blockers resolved. Cleaned up two
leftover contradictions the re-review flagged: Story 3.2.2's narrative line
still described verifyGitHubRefExists as verifyPR's (bool, error) shape, and
Task 3.1.2b still offered a non-functional plain-== alternative that can
never match the actual persisted notes format.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): test validation plan for backlog-self-resolve (report_duplicate)

Maps plan.md's Phase 4 test tasks (~22 functions across Epics 4.1-4.3) to
FR1-FR10, confirms 10/10 requirement coverage, and identifies 8 genuine
test gaps (G1-G8) found by cross-referencing architecture-review.md and
adversarial-review.md concerns that aren't yet reflected as Phase 4 tasks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): consolidate CAS validation and fix fail-open bugs in backlog-self-resolve plan

Accepted a concurrent fix pass addressing adversarial-review concerns:

- Collapse the whitelist-then-pin CAS fix (pitfalls.md #0) from 3 independently
  duplicated call sites into one validateSelfResolveSource chokepoint, so a
  future edit reverting to item.Status directly is a visible diff, not a
  silent regression.
- Fix FR2's active-reviewer guard: a ListItemSessions error was silently
  swallowed and fell through to allow the transition, defeating the guard's
  purpose. Now fails closed (ErrInternalError, retry wording).
- Fix FR5's messaging: a ListItemSessions error was silently treated as "no
  active reviewer," risking the exact false "reviewer notified" claim FR5
  exists to prevent. Now defaults to the conservative "next review pass"
  wording on error.
- Enforce "refused before any GitHub call" as a tested property (inject a
  t.Fatal-on-call verifier into every refusal-path test) rather than relying
  on code ordering alone.
- Add missing test coverage: duplicate_ref/reason length caps, and an
  explicit decision + test that cross-repo duplicate_ref values are allowed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): give FR8 a dedicated story in backlog-self-resolve plan

Cross-artifact consistency check found FR8 ("no schema changes") had no
story anywhere in the plan's 4 phases -- only a bare final-verification
task outside any Epic/Story, despite Epic 4.2's header claiming to cover
it. Promoted it into Epic 4.4 / Story 4.4.1 with an explicit GWT, and
corrected Epic 4.2's header to stop overclaiming FR8 coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): pre-mortem + P1 auth-mismatch fix for backlog-self-resolve

Pre-mortem found 1 P1: report_duplicate's HTTP-token GitHub auth
(GITHUB_TOKEN/keychain) and FR10's designated stuck-item safety net
ReconcilePRPending (gh CLI auth) are non-overlapping credential paths. A
session configured only via `gh auth login` would have report_duplicate
fail every call while its own safety net keeps functioning obliviously --
silently recreating the exact "stuck with no way to self-resolve" problem
this feature exists to fix.

Fix: classify a missing-token failure (existing ErrNotAuthenticated
sentinel) with a distinct, explicit non-retry message instead of folding
it into the generic transient-retry bucket, and surface the same guidance
in the tool description and ADR-002's Consequences. This doesn't eliminate
the auth-mechanism split (unifying it is out of scope) but makes the
failure legible instead of an infinite silent retry loop.

Also accepted a concurrent iteration-2 re-verification pass that closed
the last tracking gaps from the prior repair cycle (untracked fail-closed
tests promoted to real Phase 4 tasks, a refusal-test-range/success-test
contradiction fixed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): reconcile stale verdict headers ahead of readiness gate

adversarial-review.md's top-level Verdict still said BLOCKED after its own
appended iteration-2 section concluded CONCERNS (the blocker was resolved,
only the header wasn't updated). pre-mortem.md's one P1 item (auth-mechanism
split) is now resolved per the prior commit's fix -- checked off with a
resolution note. Readiness gate: 10/10 FRs covered in validation.md, no
TODO/TBD, all 5 referenced ADRs exist on disk, no open blockers in either
review, Migration Plan documented N/A (FR8/ADR-001), 0 open P1 items -- PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): fix stale F3 note in backlog-self-resolve pre-mortem

Two triad review lenses (PM, Engineering) independently flagged this note
as stale -- it claimed the ListItemSessions fail-open bugs were still
unfixed, but they were fixed in a same-day earlier commit. Left the
historical note in place with a resolution update rather than deleting it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* feat(session): add TriggeredByAgent audit constant

Epic 1.1 of backlog-self-resolve: agent-initiated backlog transitions
(request_review, report_duplicate) need to be audit-distinguishable
from TriggeredBySystem (reconciler) and TriggeredByUser (human).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* feat(github): add GetPR/GetCommit HTTP existence checks + sentinel errors

Implements Epic 1.2 of backlog-self-resolve (ADR-002): report_duplicate
needs to verify a duplicate_ref (PR/issue/commit URL) exists on GitHub
before mutating backlog state, with definitive-vs-transient error
classification.

- Add ErrGitHubRefNotFound (404) / ErrGitHubAccessDenied (401, or 403
  without a rate-limit signal) sentinels in github/repos.go.
- Retrofit GetIssue's existing 401/404/403 branches to wrap the new
  sentinels via fmt.Errorf("%w: ...", ...) — no behavior change for
  err != nil callers, just an additional errors.Is-checkable layer.
- Add github.GetPR (repos.go) and github.GetCommit (new commits.go),
  both HTTP-only via the existing newGHRequest/ghHTTPClient/getGHToken
  machinery (not the gh CLI subprocess GetPRInfoCtx uses), so all three
  ref-verification paths share one auth mechanism per ADR-002.
- Table-driven httptest.Server-backed tests for both new functions
  (github/commits_test.go, github/repos_pr_test.go) covering
  200/404/401/403-no-Retry-After/403-with-Retry-After/429, using an
  in-package resetGhBaseURL helper mirroring the pattern in
  server/services/backlog_github_rpc_test.go.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): fix double-review-spawn bug found in a fresh pre-mortem pass

A concurrent re-run pre-mortem verified directly against session/review_gate.go
that ADR-005's justification for not refusing report_duplicate when a reviewer
is already active -- "TriggerReviewForSession's own idempotency" -- was false:
ReviewGateRunner.Run has no active-reviewer dedup check anywhere and would
unconditionally spawn a second, concurrent review session for the same item.

Fixed with the narrowest correct scope: Task 3.3.3b's TriggerReviewForSession
call is now conditional on the same activeReview boolean Task 3.3.3a already
computes for message branching, instead of reversing ADR-005 (which would make
FR5's literal "still succeeds while a reviewer is active" text unreachable) or
patching the shared spawnReviewGate/Run infrastructure (broader blast radius,
affects every review-gate spawn path in the codebase, not just this feature).
report_duplicate still transitions the item and persists evidence either way;
only the redundant immediate spawn is skipped. Extended Task 4.2.5a's test to
assert the trigger is/isn't called in each branch.

Also accepted: a GHES-support gap (F2) explicitly triaged as an accepted P2 for
v1 (github.com only, not blocking); a trust-boundary disclosure line added to
the tool description (F5); and a re-derived validation.md superseding a draft
written against an intermediate plan.md revision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* feat(mcp): generalize request_review CAS precondition + active-reviewer guard

Epic 2.1 (FR1/FR9): add allowedSelfResolveSourceStatuses + the
validateSelfResolveSource chokepoint (server/mcp/tools_backlog.go) so
request_review's CAS precondition is pinned to the validated observed
status (in_progress or pr_pending), never a hardcoded constant or raw
item.Status. Populate BacklogItemPrecondition.Note, switch TriggeredBy
to TriggeredByAgent on both source paths (ADR-003), and give a CAS-race
loser a distinct non-retry message instead of the generic transition-
failed text.

Epic 2.2 (FR2): export HasActiveReviewSession from
server/services/backlog_service_triage.go (was unexported) and reuse it
in request_review's new pr_pending-only active-reviewer guard. Fails
closed on a ListItemSessions storage error (INTERNAL_ERROR, never a
silent pass-through) via a new listItemSessionsFn test seam on
backlogHandlers, mirroring the existing verifyPRMatchesBranch/
resolveSessionBranch shape — session.Storage.ListItemSessions has no
swappable-repository seam of its own (hard type-asserts to
*EntRepository), so this was needed to make the fail-closed path
testable without a real DB failure.

Epic 4.1: the 5 pre-existing TestRequestReview_* tests pass unmodified
(regression), plus 6 new tests covering the pr_pending success path,
whitelist rejection (table-driven), both active-reviewer-guard branches,
the fail-closed storage-error path, and the CAS-race-loser message
(genuine goroutine concurrency, stable across 20 runs under -race).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore(sdd): soften ReconcilePRPending "unconditional" claim, add success-text assertion

Architecture review's residual finding: ReconcilePRPending's IsPRMerged/GetPRStatus
calls can themselves fail (revoked token, GitHub outage) and continue past the tick
without marking anything stuck -- a pre-existing characteristic unrelated to this
feature, but "runs unconditionally" overstated it. Also extends Task 4.2.1b to
assert the affirmative "Reviewer notified" success text, closing a gap validation.md
flagged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* feat(mcp): add report_duplicate tool for backlog self-resolve (Phase 3)

Implements Epics 3.1-3.4 of the backlog-self-resolve plan: a new
report_duplicate MCP tool letting a work session route a backlog item to
review when it discovers the work duplicates an already-shipped GitHub
PR/issue/commit, plus Epic 4.2's full test suite (24 tests).

- Handler skeleton + FR6 refusal checks (SkipReviewGate, role, link,
  disallowed source status), reusing validateSelfResolveSource and
  services.HasActiveReviewSession rather than duplicating either chokepoint.
- GitHub verification dispatcher (verifyGitHubRefExists) behind an
  injectable verifyGitHubRef seam, with the 3-channel error split
  (ErrNotAuthenticated / ErrGitHubRefNotFound / ErrGitHubAccessDenied vs.
  plain transient) per ADR-002.
- CAS transition to review (never done/archived, ADR-001) with an
  append-not-overwrite VerificationNotes fix and ADR-004 idempotency
  (exact-retry no-op, differing second ref rejected).
- FR5 success messaging + the safety-critical fix from Task 3.3.3b: the
  review-gate trigger is now conditional on !activeReview (TriggerReviewForSession
  has no dedup check against an already-active reviewer, so calling it
  unconditionally would spawn a second, concurrent review session).
- MCP registration with explicit FR10 retry guidance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* test(mcp): fix stale in_progress->pr_pending comment in sequential-interaction test

Spec compliance sweep caught the mismatch: the fixture correctly seeds the
item at "review" (matching SetBacklogItemPRAndTransition's real precondition,
per the adjacent inline comment already explaining this), but the summary
and step comments still described the plan's original, inaccurate premise.
No behavior change -- comment only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* refactor(github): extract shared HTTP status classification helper

repos.go's SearchUserRepos/ListRepoIssues/GetIssue/GetPR and
commits.go's GetCommit each repeated an identical ~25-line
401/403/429/default classification block. Extracted
classifyGHResponse (github/http_client.go) parameterized by
notFoundMsg (empty for the two list/search endpoints, which have no
404 semantics) and a sentinels flag (false for
SearchUserRepos/ListRepoIssues to preserve their existing plain-error
behavior, true for GetIssue/GetPR/GetCommit to keep wrapping
ErrGitHubAccessDenied/ErrGitHubRefNotFound). Also swapped bare int
literals for http.Status* constants and fmt.Errorf (no verbs) for
errors.New (staticcheck S1028) in the consolidated logic.

Also, two unrelated nits flagged in the same review pass:
- tools_backlog.go: renamed checkGitHubRef -> verifyRef to match the
  file's field->shortened-method naming convention (verifyGitHubRef
  field, verifyPRMatchesBranch -> verifyPR, resolveSessionBranch ->
  sessionBranch).
- tools_backlog_test.go: renamed
  TestReportDuplicate_LoserGetsDistinctMessage_WhenRacingReportPRCreated,
  which is misleadingly named "Racing" despite testing sequential
  state-machine composition on one goroutine, to
  TestReportDuplicate_RejectsThirdCall_AfterSequentialReportPRCreatedThenReportDuplicate.

Verified SearchUserRepos/ListRepoIssues behavior is unchanged: no
existing tests cover them directly (confirmed via repo-wide grep), so
verification is by diff review — classifyGHResponse(resp, "", false)
produces byte-identical error strings to the removed inline blocks for
every status code path, and go build/vet/test -race stay green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* fix(mcp): address Gate 2 review findings on PR #308 backlog self-resolve

Fixes 5 confirmed MAJOR findings from the Gate 2 review pass:

1. TestRequestReview_ReportsDistinctMessage_WhenCASPreconditionFails
   called require.NoError from inside a spawned goroutine — t.FailNow()
   only unwinds that goroutine, not the test goroutine. Both racers now
   send {result, err} through the channel and every require/assert call
   happens in the main test goroutine. The same channel type is reused
   for the new report_duplicate CAS test (fix 3).

2. Added TestReportDuplicate_VerifyGitHubRefExists_DispatchesPRTypeToRealGetPR,
   the first test in the package that leaves verifyGitHubRef nil so the
   real verifyGitHubRefExists dispatch switch (PR/Issue/Commit -> GetPR/
   GetIssue/GetCommit) actually runs, pointed at an httptest.Server via
   githubpkg.GhBaseURL (mirrors github/repos_pr_test.go's pattern).

3. Added TestReportDuplicate_ReportsDistinctMessage_WhenCASPreconditionFails,
   mirroring the request_review CAS regression test for report_duplicate's
   identical errors.Is(transErr, session.ErrPreconditionFailed) branch.

4. Added TestReportDuplicate_DoesNotTreatPrefixRefAsIdempotentMatch,
   proving a shorter ref that is a literal string-prefix of an
   already-recorded longer ref (.../pull/27 vs .../pull/272) is not
   misclassified as the idempotent no-op retry.

5. requestReview's verification-notes persistence overwrote
   VerificationNotes via UpdateItemSessionVerificationNotes, silently
   erasing prior evidence (e.g. from an earlier report_duplicate call)
   on the same ItemSession. reportDuplicate already had this fixed with
   an append pattern; applied the same fix to requestReview and added
   TestRequestReview_AppendsToExistingVerificationNotes_RatherThanOverwriting
   as a new test, leaving the existing
   TestRequestReview_PersistsVerificationNotesOnWorkSession assertions
   unmodified per AC9/FR9.

All 5 pre-existing TestRequestReview_* tests still pass with their
original assertions intact. go build/vet/gofmt/golangci-lint clean on
server/mcp and github packages; go test ./server/mcp/... ./github/...
-race all green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxSH3xUu3fyFnprsybnRiN

* fix(github): address Copilot review comments on PR #308

Two real findings from the Copilot review, fixed:
- classifyGHResponse's notFoundMsg 404 branch didn't drain resp.Body,
  preventing HTTP connection reuse on keep-alive transports. Drain it,
  matching the pattern already used in the 403 rate-limit branches.
- resetGhBaseURL restored GhBaseURL to a hardcoded default instead of
  the captured prior value -- fragile if a future test changes GhBaseURL
  before calling this helper. Capture and restore the actual prior value.

Three other comments (claiming a missing encoding/json import x2, and
claiming server/mcp files are absent from the diff) were declined as
factually incorrect -- verified against the actual file contents, a
clean go build, and gh pr diff's file list. The review's own banner
noted it "was unable to run its full agentic suite" and only reviewed
67/87 changed files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDFVDKhxFquFRroHL7mDCW

* chore: trigger CI (no-op)

* fix(mcp): deterministic read-barrier for request_review CAS race test

Root cause: TestRequestReview_ReportsDistinctMessage_WhenCASPreconditionFails
synchronized only the *start* of its two racing goroutines via
startBarrier.Wait(). That doesn't guarantee both goroutines' fresh
GetBacklogItem reads (which feed validateSelfResolveSource before the
CAS write) complete before either write does. Locally this ordering
held 20/20 under -race, but on CI's runners (more cores, different
scheduling) one goroutine could finish its full read->whitelist->write
sequence before the other's first read even executed — so the "loser"
observed the post-write Status: "review" and failed the whitelist
check (ErrInvalidArgument) instead of racing the actual CAS write
(ErrPreconditionFailed / ErrInternalError), which is the behavior this
test exists to exercise. This is a test-harness determinism bug, not a
production bug: the CAS write itself is already proven race-safe by
TestTransitionBacklogItemStatus_should_letExactlyOneWinnerThrough_When_TwoWritersRaceConcurrently.

Fix: add an injectable getBacklogItemFn seam on backlogHandlers
(mirrors the existing listItemSessionsFn/itemSessionsFor pattern —
nil falls back to h.storage.GetBacklogItem, so all existing callers
are behavior-preserving) and wrap it in the test with a
sync.WaitGroup(2) read-barrier so both goroutines' reads must
complete (both observing "in_progress") before either can proceed to
its write. Verified deterministic with -race -count=50 (was flaky
under real CI scheduling); full server/mcp package remains green.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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