Skip to content

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

Closed
tstapler wants to merge 2064 commits into
mainfrom
backlog/stapler-squad-terminal-resize-loop-fix
Closed

fix(web): dead-band terminal resize loop, RPC dedup, and WebGL fallback#191
tstapler wants to merge 2064 commits into
mainfrom
backlog/stapler-squad-terminal-resize-loop-fix

Conversation

@tstapler

Copy link
Copy Markdown
Collaborator

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 (see below) 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.

github-actions Bot and others added 30 commits July 1, 2026 23:05
…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>
github-actions Bot and others added 6 commits July 25, 2026 14:55
…on (#184)

* fix(ui): restore checkpoint/fork UI lost in benchmarks merge regression

The benchmarks PR (#17, 9479a1e) carried an older SessionCard.tsx and
silently overwrote 213 lines of checkpoint/fork UI introduced by the
session-resumption PR (#18, 38d40fd) merged 4 hours earlier.

Restored three files from 38d40fd:
- SessionCard.tsx: CheckpointProto import, 3 props, 10 useState vars,
  handlers (create/submit/cancel/fork), checkpoint + fork dialogs,
  📍 Checkpoint and 🍴 Fork action buttons
- useSessionService.ts: createCheckpoint, listCheckpoints, forkSession
  hooks + CheckpointProto import + interface declarations
- page.tsx: destructure + pass checkpoint/fork props to SessionCard

TypeScript compiles clean (tsc --noEmit).

* feat(checkpoints): wire HistoryLinker startup, shutdown capture, and pre-switch checkpoint

Task A - HistoryLinker startup pipeline:
- Add NewHistoryLinkerFromRealInspector() production constructor with fsnotify
  watcher on ~/.claude/projects/ for fast JSONL detection
- Add HistoryLinker.Instances() snapshot getter for live session set
- Wire HistoryLinker.Start() in server.go; wire Add/RemoveInstance into
  ExternalDiscovery callbacks so discovered sessions are tracked

Task B - Shutdown capture (cold restore CWD):
- Add GetPaneCurrentPath() to TmuxSession (tmux display-message #{pane_current_path})
- Add CaptureCurrentState() to Instance with guards (not started, paused, dead tmux)
- Register shutdown hook in server.go: captures pane CWDs then SaveInstances with
  4-second deadline to keep within SIGTERM kill window
- Shutdown hook uses HistoryLinker.Instances() (live set) instead of startup snapshot
  so externally-discovered sessions are also captured

Task C - Auto-checkpoint before workspace switch:
- Create "pre-switch: <target>" checkpoint in SwitchWorkspace before switching;
  non-fatal if it fails so the switch always proceeds

Tests (8 new):
- GetPaneCurrentPath: trim behavior + error wrapping
- CaptureCurrentState: not-started, paused, dead-tmux guard branches
- NewHistoryLinkerFromRealInspector: smoke test (non-nil constructor)
- HistoryLinker.Instances(): snapshot correctness + independence from internal state

Code quality fixes:
- defer stateMutex.Unlock() in CaptureCurrentState
- os.UserHomeDir() error now logged instead of silently producing /.claude/projects

* fix(web): add missing checkpoint props to SessionListProps

SessionList was passing onCreateCheckpoint, onListCheckpoints, and
onForkFromCheckpoint to SessionCard but these were absent from the
SessionListProps interface and function signature, causing a TypeScript
compile error in CI.

* test: add missing happy-path and integration tests for checkpoints

- TestInstance_CaptureCurrentState_UpdatesWorkingDir: verifies WorkingDir
  is populated from the tmux pane path on graceful shutdown (Story 1.2.1)
- TestHistoryLinker_LinksUUIDToSession: verifies the scan loop propagates
  a conversation UUID to an instance via correlateSession (Story 1.1.4)
- TestHistoryLinker_IdempotentRelink: verifies correlateSession returns
  early when HasClaudeSession() is already true
- Add 5 missing CSS module classes for the fork-from-checkpoint dialog
  (.forkEmptyMessage, .forkCheckpointList, .forkCheckpointItem,
   .forkCheckpointLabel, .forkGitSha)

* docs(terminal-visibility-resync): SDD phases 1-4 (requirements, research, plan, validation)

Requirements derived from backlog item 7728f6df-268a-4578-9066-c300ff69269b,
grounded against the actual codebase rather than the (stale) issue text --
the referenced StateApplicator/useTerminalSnapshot primitives don't exist;
the real fix wires up requestFullResync/markResyncComplete/markPaneResponseReceived
(already implemented in useTerminalFlowControl.ts but never exposed/called)
and fixes useDebouncedCallback's useState-backed timer bug.

Plan went through two rounds of architecture + adversarial review (both
BLOCKED -> CONCERNS after fixes: Promise<void> return types, session-switch
cleanup, connect()-in-flight guard, hook extraction to useVisibilityResync.ts)
plus a Product/UX/Engineering triad review (UX gap fixed by adding the
2s-intermediate-banner story the plan's own Pattern Decisions table had
already promised but never implemented).

No source code changed yet -- planning artifacts only.

* feat(terminal): expose resync primitives from useTerminalStream

Add requestFullResync, markResyncComplete, and markPaneResponseReceived
to TerminalStreamResult so TerminalOutput can trigger/track a full pane
resync without new prop plumbing. Also widen connect/disconnect's
declared return types from () => void to Promise<void>-returning,
matching their existing async implementations — a pre-existing type
bug that Phase 2's stall watchdog needs corrected to type-check its
disconnect().then(() => connect()) chain.

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

* fix(hooks): useDebouncedCallback root-cause double-fire bug

Store the debounce timer id in a useRef instead of useState so two
calls landing in the same JS tick (e.g. visibilitychange + focus
firing back-to-back) don't read a stale timer id from a batched
setState update and let both timers fire. Wrap the returned callback
in useCallback keyed on [callback, delay] so it's referentially
stable across re-renders, making it safe to use in dependency arrays
and as an event listener reference.

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

* test(terminal): AC7 integration proof for full-resync repaint via TerminalStreamManager.write

Proves, against a real (unmocked) @xterm/xterm Terminal and real
TerminalStreamManager, that writing the server's clearAndHome-prefixed
resync payload produces a genuine full repaint over stale/corrupted prior
content, verified by reading back the actual rendered buffer rather than
asserting on a mock call count.

Adds a cross-reference comment on connectrpc_websocket.go's
ansiSnapshotPrefix constant pointing at the new test's duplicated
clearAndHome fixture, since the two have no compile-time link.

* feat(terminal): visibility/focus resync via useVisibilityResync hook

Implements Epic 2.1 of terminal-visibility-resync: a standalone
useVisibilityResync hook that debounces document visibilitychange/window
focus events (300ms) and, while connected, triggers exactly one
requestFullResync(true) per debounce window to repaint a render-desynced
xterm buffer after a backgrounded tab regains focus (AC1). While
disconnected (and not mid-handshake), it falls back to a direct connect()
+ setShowReconnectButton(true) (AC4). A 4000ms stall watchdog force-clears
pending resync state and runs disconnect().then(connect) if no output
arrives (AC5), and a 2000ms intermediate timer surfaces the existing
reconnecting-banner UI for slow resyncs. The hook never touches focus
(AC3) and tears down session-scoped pending state on sessionId change,
including a guard against a debounced call that fires mid-flight against
a session it was no longer scheduled for.

Wired into TerminalOutput.tsx via a small hook call plus a
notifyResyncOutputReceived() call in handleOutput (routed through a
ref-mirror since the callback isn't available until after
useTerminalStream/useVisibilityResync are both called, avoiding a TDZ
reference-before-declaration issue in the plan's literal sketch).
XtermTerminal.tsx, useTerminalStream.ts's NEXT_PUBLIC_RECONNECT_V2
listener, and handleManualReconnect's body are all unmodified (AC6,
verified via scoped git diff).

Also updates the mocked useTerminalStream() return objects across six
existing TerminalOutput test files to include the three
already-committed-but-previously-unmocked fields
(requestFullResync/markResyncComplete/markPaneResponseReceived), which
were undefined and caused the new hook's session-cleanup effect to throw
on unmount.

19 new tests in useVisibilityResync.test.ts cover AC1, AC3, AC4, AC5, the
Story 2.1.8 banner behavior, and the Story 2.1.6 session-switch cleanup
(including the mid-debounce-switch guard).

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

* refactor(terminal): address sdd:6-verify Layer 1/2 findings for visibility resync

Move notifyResyncOutputReceivedRef sync into a useEffect instead of the
render body (both the idiom and architecture review agents independently
flagged the mid-render ref write as inconsistent with this file's own
ref-mirror convention). Remove the redundant unmount-only cleanup effect
in useVisibilityResync, superseded by the sessionId-keyed effect that
already performs a strict superset of the same cleanup.

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

* chore: restore backlog scaffolding files accidentally deleted by prior commit

The prior refactor commit's `git add <2 files>` picked up 10 unrelated
pre-existing staged deletions (.backlog-context.md,
.claude/commands/backlog/*.md) that were already in the index before this
session touched anything. Restoring them to their pre-commit tracked
content; unrelated to the terminal-visibility-resync feature.

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

* chore: revert erroneous restoration of stale cross-item backlog scaffolding

The prior "restore" commit brought back done-0/1/2.md, fail-0/1/2.md,
review.md, status.md, and .backlog-context.md at their old committed
content, which referenced a different, unrelated backlog item
(9292b871-ab19-4c7b-8df5-7e7ce90895f2) rather than the item this branch
is actually for (7728f6df-268a-4578-9066-c300ff69269b, confirmed via
every get_backlog_item/report_progress call this session). Re-removing
them restores the pre-session state where the harness had already staged
their deletion in favor of the current item's regenerated (untracked)
versions.

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

* chore: add current-item backlog command scaffolding (done/fail 3-7, ship)

Harness-generated command files for backlog item 7728f6df, matching its
current 8-criteria checklist (done-0..2/fail-0..2/help/review/status were
already tracked from the correct-item regeneration in a prior commit).

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

* fix(ci): extract full tmux release tarball tree, not just configure

configure.ac declares AC_CONFIG_AUX_DIR(etc), so ./configure needs
etc/install-sh (and other etc/ aux scripts) from the tmux release
tarball. The script only extracted the top-level configure file,
so configure failed with "cannot find install-sh, install.sh, or
shtool in etc" — breaking the Test CI job on every PR (confirmed
failing on main since at least 2026-07-07, unrelated to this branch).

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

* fix(test): raise CI timeout for hook-URL integration tests to 30s

TestServer_should_WriteRealPortIntoSessionHooksAndMCPURL_When_StartedWithPortZeroThenSessionCreated
and TestServer_should_WriteUnchangedHookURL_When_StartedOnExplicitPort pass in
under 1s locally, but were flaking on the "Test" CI job (tmux session startup +
async hook injection missing the old 15s deadline under -race + parallel-suite
load). Bumping both to 30s to absorb that contention; unrelated to this PR's
terminal visibility-resync change.

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

* revert: restore backlog scaffolding files swept into prior commit unintentionally

These 11 files (.claude/commands/backlog/{done,fail}-3..7.md, ship.md) were
already staged for deletion in the working tree before this session started
and got included in the previous commit alongside the actual test fix. They
are unrelated to this PR's scope — restoring to avoid touching harness
scaffolding this session didn't intend to change.

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

* fix(session): route ForceStatus through actor mailbox to fix data race

go test -race caught SessionService.CreateSession's async goroutine
calling Instance.ForceStatus() (which took i.mu directly) racing with
runActor's own unguarded buildSnapshot rebuild after each actor command
(session/actor.go). The actor model relies on single-goroutine
confinement instead of i.mu, so a direct-lock writer from outside the
actor is invisible to it.

Route ForceStatus through the existing sendCtx actor-command helper so
the write is serialized with the actor's command loop when the
instance is actor-owned, matching the pattern already used by Epic 4+
mutators. Falls back to running in place when there's no LiveInstance
(bare *Instance in tests).

Fixes CI failure in TestServer_should_WriteRealPortIntoSessionHooksAndMCPURL_When_StartedWithPortZeroThenSessionCreated
blocking PR #184 — root cause identified via git log --all --grep=race,
which found this exact fix (bdd621e) already authored and merged into
an unrelated, still-open cross-repo sync PR (#169) that hasn't reached
this branch yet. Cherry-picked rather than re-derived.

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

* test(terminal): AC7 wire-to-repaint integration proof via useTerminalStream

Closes the gap TerminalStreamManager.resync.test.ts left open: that test
calls manager.write() directly and never proves a resync payload reaches
TerminalStreamManager via the real wire-message pipeline. This test drives
the real (non-mocked) useTerminalStream message loop with a scripted
CurrentPaneRequest-style resync `output` message, feeding a real
TerminalStreamManager backed by a real @xterm/xterm Terminal — the same
onOutput -> manager.write() wiring TerminalOutput.tsx uses in production.

AC7's wording names a "StateApplicator" class with resetSequence()/
applyState() methods that was never built (confirmed via grep) — the
implementation instead reuses the pre-existing resize-resync path
(requestFullResync -> CurrentPaneRequest -> clearAndHome-prefixed output ->
TerminalStreamManager.write()). Documents that mapping in the test file so
future review isn't blocked on a class name mismatch.

* revert: restore backlog scaffolding files unintentionally swept into AC7 test commit

Following files were staged as deletions from before this session started
(pre-existing index state, unrelated to the AC7 integration test change) and
got bundled into the previous commit by an overly broad `git add`. Same
unintentional-sweep pattern already reverted once in this repo's history
(9936987); restoring rather than repeating it.

* chore: commit current-item backlog scaffolding snapshot

Harness-generated per-session files (.backlog-context.md, .claude/commands/backlog/*)
required by request_review's clean-worktree check; content is just command
scaffolding and a snapshot of this backlog item's current state.

* fix(terminal): address code-review findings on visibility resync

Three issues found by /code:review's parallel testing/code-quality/
architecture pass over PR #184:

- TerminalOutput.tsx handleOutput only signalled a pending resync's
  completion on the direct-write path; output queued during a concurrent
  RESIZING state never notified useVisibilityResync, so a resync whose
  response happened to land mid-resize looked stalled and triggered the
  4s watchdog's spurious disconnect+reconnect. Move the notify call
  before the RESIZING early return so it fires unconditionally. Added a
  regression test (TerminalOutputBug.test.tsx) that fails on the old
  code and passes on the fix.

- useTerminalStream.ts's pre-existing Story 3.1.3 visibilitychange
  listener (gated behind NEXT_PUBLIC_RECONNECT_V2, off by default) and
  useVisibilityResync's new disconnected-branch fallback both call
  connect() independently on the same event with different debounce
  windows; connect() only guarded against "already connected," not
  "already connecting," so both could race and orphan the first
  in-flight stream. Added an isConnectingRef guard.

- session/instance_state.go's ForceStatus (from an unrelated, already-
  bundled data-race fix) routed through sendCtx with context.Background()
  — an error-recovery path with no timeout would hang its caller
  indefinitely if the actor mailbox were ever wedged. Added a 5s timeout
  with an error log on expiry.

Deferred (documented, not blocking): dual ownership of
showReconnectBanner between TerminalOutput.tsx's isConnected-driven
effect and useVisibilityResync's own timer is a real design smell but
requires a larger return-value refactor across an already-tested API;
a regression test for the ForceStatus actor-mailbox race itself; an
unrelated pre-existing checksum gap in scripts/build-tmux.sh's tarball
fallback path.

* chore: untrack backlog scaffolding files, add to .gitignore

.backlog-context.md and .claude/commands/backlog/* are session-scoped
files the backlog harness injects into a worktree to drive an agent
session — not source files. They ended up committed to this branch by
mistake across several earlier sessions. The personal fork (tstapler/
stapler-squad) already gitignores both paths; origin was missing the
same entries.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@tstapler tstapler added the patch label Jul 25, 2026
tstapler and others added 6 commits July 25, 2026 16:42
* docs: add requirements.md for GitHub issue/PR link surfacing

Reset this branch onto origin/main first (it was ~1989 commits stale
and missing the backlog feature entirely). Requirements grounded in
the actual current session/backlog_* and server/services/backlog_service.go
code per the item's 10 acceptance criteria.

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

* docs: add phase-2 research for GitHub issue/PR link surfacing

Stack, features, architecture, pitfalls, and build-vs-buy research
for adding ExternalURL end-to-end (ent schema, plugin mapping, prompt
rendering, sync backfill). Key findings: stdlib-only (no new dep),
ent auto-migration via existing --feature sql/upsert generate command,
fact line belongs inside the prompt's trusted-data boundary while the
closingKeywordFor instruction line stays outside it, and SyncOne's
anyField gate needs an explicit fix so it doesn't skip the unconditional
ExternalURL backfill when title/description/priority are all user-modified.

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

* docs: add implementation plan, ADR-001, and reviews for GitHub issue link

Plan covers 6 phases / 7 epics / 13 stories / 32 tasks with a concrete
Given-When-Then example per acceptance criterion. ADR-001 records two
non-obvious decisions: SyncOne's anyField must be set independently of
UserModifiedFields gating for the ExternalURL backfill, and the fact
line renders inside BuildSessionInitialPrompt's inert-data boundary
while the closingKeywordFor instruction line stays outside it.

Architecture and adversarial review both caught the same real bug pre-
implementation: the instruction-line format string would have rendered
"Fixes: <url>" (extra colon) instead of AC3's literal "Fixes <url>" /
"Related: <url>" asymmetric punctuation. Fixed in plan.md and both
reviews confirmed RESOLVED after a repair-loop pass.

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

* docs: validation plan, pre-mortem, and a real defect fix from sdd:4-validate

Validation.md maps all 9 ACs to concrete tests and closes 3 test-coverage
gaps the plan's task list hadn't named explicitly (a migration NULL-safety
test, a Create/Get round-trip test, and an AttachSessionToItem integration
test — the last one closes exactly the kind of silent two-literal drift
this whole feature exists to fix).

Pre-mortem's P1 finding was a real, confirmed defect, not just an
unverified assumption: fetched GitHub's own docs and confirmed the
closing-keyword parser only recognizes "#N"/"owner/repo#N", never a bare
URL. The original design (Fixes <full-url>) would have silently failed
to auto-close anything. Added a githubShortRefFor helper (ADR-001
Decision 3) to derive and render the correct short reference instead,
and propagated the fix through plan.md's tasks/tests and validation.md.

Cross-artifact consistency check also caught a stale requirements.md
line claiming the AC6 backfill "bypasses" BacklogItemUpdate, when the
actual (correct) design routes through it and only bypasses the
UserModifiedFields gate — fixed.

Product Triad Review: all three legs (Product/UX/Engineering) ready,
zero blockers.

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

* feat(backlog): surface linked GitHub issue/PR to agent prompts

Imported backlog items now carry the originating GitHub issue/PR URL
end to end, so an agent working the item can reference it in its PR
and GitHub can auto-close/cross-reference on merge.

- ent schema/BacklogItemData/BacklogItemUpdate gain ExternalURL
  (session/ent/schema/backlog_item.go, session/repository.go),
  populated by both GitHubIssuesPlugin and GitHubPRsPlugin
  (capped at 500 chars, matching the existing Title/Description
  truncation pattern).
- SyncOne backfills ExternalURL on pre-existing rows unconditionally,
  bypassing UserModifiedFields local-wins, with anyField set
  independently so the backfill fires even when title/description/
  priority are all user-locked (ADR-001 Decision 1). Only backfills
  items still returned by the plugin's state=open Fetch — a
  documented, tested limitation, not a bug.
- BuildSessionInitialPrompt renders a "Linked GitHub Issue/PR: <url>"
  fact line inside the prompt's inert-data boundary, and a
  deterministic closing-keyword instruction line outside it
  (ADR-001 Decision 2), gated on ExternalURL being non-empty so the
  prompt is byte-identical to today when it's absent.
- githubShortRefFor derives the actual "owner/repo#N" reference
  GitHub's closing-keyword parser recognizes (confirmed via GitHub's
  own docs during sdd:4-validate — a bare full URL is NOT a
  recognized closing-keyword form, only #N/owner/repo#N are).
  Fixes a defect the original design would have shipped silently
  (ADR-001 Decision 3).
- Both hand-built ent.BacklogItem{} literals in
  server/services/backlog_service.go (SpawnSessionFromItem,
  AttachSessionToItem) now include ExternalURL.

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

* fix(backlog): harden githubShortRefFor and sanitize ExternalURL in prompt

sdd:6-verify Layer 1/2 review findings (0 blockers, applying the two
worthwhile SUGGEST items inline):

- githubShortRefFor now strips query string/fragment/trailing-slash
  noise from the parsed issue/PR number, so a URL like
  ".../issues/42?tab=comments" renders "acme/widget#42" instead of a
  malformed "acme/widget#42?tab=comments" that GitHub wouldn't
  recognize. Added cross-reference comments between closingKeywordFor
  and githubShortRefFor noting they use different URL-shape strictness
  by design (both real producers are github.com URLs, so they can't
  disagree today) rather than restructuring tested pure functions this
  late for a currently-unreachable case.
- item.ExternalURL is now run through sanitizeField before rendering
  in both the fact line and instruction line, matching the existing
  convention for every other externally-sourced field (Description,
  Notes, LastCommitMessage) rather than being the one exception.

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

---------

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

Copy link
Copy Markdown

✅ Registry Validation

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

Building backend scanner...
Scanning backend features...
Wrote 116 feature files to /tmp/tmp.4HnPheeUtl/backend
Wrote 14 feature files to /tmp/tmp.4HnPheeUtl/backend
Wrote 27 feature files to /tmp/tmp.4HnPheeUtl/backend
Wrote 6 feature files to /tmp/tmp.4HnPheeUtl/backend
Wrote 10 feature files to /tmp/tmp.4HnPheeUtl/backend

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

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 6/161 features have testIds (3.7%)

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

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

Go Benchmarks (Tier 1)

benchmarks/go/tier1-baseline.txt:97: missing iteration count
benchmarks/go/tier1-baseline.txt:197: missing iteration count
tier1-bench.txt:97: missing iteration count
tier1-bench.txt:197: missing iteration count
goos: linux
goarch: amd64
pkg: github.com/tstapler/stapler-squad/session
cpu: AMD EPYC 7763 64-Core Processor                
                                            │ tier1-bench.txt │
                                            │     sec/op      │
CircularBufferWrite_4KB-4                         84.10n ± 2%
CircularBufferWrite_4KB_Allocs-4                  86.70n ± 3%
CircularBufferGetRecent_4KB-4                     469.2n ± 3%
CircularBufferGetAll-4                            3.651µ ± 2%
GetTimeSinceLastMeaningfulOutput_HotPath-4        65.71n ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4       33.11n ± 1%
geomean                                           173.4n

                                            │ tier1-bench.txt │
                                            │      B/op       │
CircularBufferWrite_4KB-4                        0.000 ± 0%
CircularBufferWrite_4KB_Allocs-4                 0.000 ± 0%
CircularBufferGetRecent_4KB-4                  4.000Ki ± 0%
CircularBufferGetAll-4                         40.00Ki ± 0%
GetTimeSinceLastMeaningfulOutput_HotPath-4       0.000 ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4      0.000 ± 0%
geomean                                                     ¹
¹ summaries must be >0 to compute geomean

                                            │ tier1-bench.txt │
                                            │    allocs/op    │
CircularBufferWrite_4KB-4                        0.000 ± 0%
CircularBufferWrite_4KB_Allocs-4                 0.000 ± 0%
CircularBufferGetRecent_4KB-4                    1.000 ± 0%
CircularBufferGetAll-4                           1.000 ± 0%
GetTimeSinceLastMeaningfulOutput_HotPath-4       0.000 ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4      0.000 ± 0%
geomean                                                     ¹
¹ summaries must be >0 to compute geomean

                              │ tier1-bench.txt │
                              │       B/s       │
CircularBufferWrite_4KB-4          45.36Gi ± 2%
CircularBufferGetRecent_4KB-4      8.130Gi ± 3%
geomean                            19.20Gi

cpu: AMD EPYC 9V74 80-Core Processor                
                                            │ benchmarks/go/tier1-baseline.txt │
                                            │              sec/op              │
CircularBufferWrite_4KB-4                                          80.55n ± 1%
CircularBufferWrite_4KB_Allocs-4                                   80.74n ± 1%
CircularBufferGetRecent_4KB-4                                      551.9n ± 4%
CircularBufferGetAll-4                                             3.894µ ± 5%
GetTimeSinceLastMeaningfulOutput_HotPath-4                         70.26n ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4                        34.53n ± 0%
geomean                                                            179.9n

                                            │ benchmarks/go/tier1-baseline.txt │
                                            │               B/op               │
CircularBufferWrite_4KB-4                                         0.000 ± 0%
CircularBufferWrite_4KB_Allocs-4                                  0.000 ± 0%
CircularBufferGetRecent_4KB-4                                   4.000Ki ± 0%
CircularBufferGetAll-4                                          40.00Ki ± 0%
GetTimeSinceLastMeaningfulOutput_HotPath-4                        0.000 ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4                       0.000 ± 0%
geomean                                                                      ¹
¹ summaries must be >0 to compute geomean

                                            │ benchmarks/go/tier1-baseline.txt │
                                            │            allocs/op             │
CircularBufferWrite_4KB-4                                         0.000 ± 0%
CircularBufferWrite_4KB_Allocs-4                                  0.000 ± 0%
CircularBufferGetRecent_4KB-4                                     1.000 ± 0%
CircularBufferGetAll-4                                            1.000 ± 0%
GetTimeSinceLastMeaningfulOutput_HotPath-4                        0.000 ± 0%
GetTimeSinceLastMeaningfulOutput_ColdPath-4                       0.000 ± 0%
geomean                                                                      ¹
¹ summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │
                              │               B/s                │
CircularBufferWrite_4KB-4                           47.35Gi ± 1%
CircularBufferGetRecent_4KB-4                       6.911Gi ± 4%
geomean                                             18.09Gi

pkg: github.com/tstapler/stapler-squad/session/detection/ratelimit
cpu: AMD EPYC 7763 64-Core Processor                
                              │ tier1-bench.txt │
                              │     sec/op      │
StripANSI_PlainText-4               6.867n ± 0%
StripANSI_WithEscapes-4             772.2n ± 0%
ProcessOutput_InactiveState-4       6.336n ± 0%
geomean                             32.27n

                              │ tier1-bench.txt │
                              │      B/op       │
StripANSI_PlainText-4              0.000 ± 0%
StripANSI_WithEscapes-4            136.0 ± 0%
ProcessOutput_InactiveState-4      0.000 ± 0%
geomean                                       ¹
¹ summaries must be >0 to compute geomean

                              │ tier1-bench.txt │
                              │    allocs/op    │
StripANSI_PlainText-4              0.000 ± 0%
StripANSI_WithEscapes-4            5.000 ± 0%
ProcessOutput_InactiveState-4      0.000 ± 0%
geomean                                       ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                              │ benchmarks/go/tier1-baseline.txt │
                              │              sec/op              │
StripANSI_PlainText-4                                7.048n ± 2%
StripANSI_WithEscapes-4                              652.1n ± 0%
ProcessOutput_InactiveState-4                        6.641n ± 1%
geomean                                              31.25n

                              │ benchmarks/go/tier1-baseline.txt │
                              │               B/op               │
StripANSI_PlainText-4                               0.000 ± 0%
StripANSI_WithEscapes-4                             136.0 ± 0%
ProcessOutput_InactiveState-4                       0.000 ± 0%
geomean                                                        ¹
¹ summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │
                              │            allocs/op             │
StripANSI_PlainText-4                               0.000 ± 0%
StripANSI_WithEscapes-4                             5.000 ± 0%
ProcessOutput_InactiveState-4                       0.000 ± 0%
geomean                                                        ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/queue
cpu: AMD EPYC 7763 64-Core Processor                
                              │ tier1-bench.txt │
                              │     sec/op      │
ReviewQueue_ConcurrentReads-4       93.46n ± 4%
ReviewQueue_Add-4                   508.0n ± 0%
geomean                             217.9n

                              │ tier1-bench.txt │
                              │      B/op       │
ReviewQueue_ConcurrentReads-4      0.000 ± 0%
ReviewQueue_Add-4                  640.0 ± 0%
geomean                                       ¹
¹ summaries must be >0 to compute geomean

                              │ tier1-bench.txt │
                              │    allocs/op    │
ReviewQueue_ConcurrentReads-4      0.000 ± 0%
ReviewQueue_Add-4                  4.000 ± 0%
geomean                                       ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                              │ benchmarks/go/tier1-baseline.txt │
                              │              sec/op              │
ReviewQueue_ConcurrentReads-4                       82.24n ± 11%
ReviewQueue_Add-4                                   511.8n ±  0%
geomean                                             205.1n

                              │ benchmarks/go/tier1-baseline.txt │
                              │               B/op               │
ReviewQueue_ConcurrentReads-4                       0.000 ± 0%
ReviewQueue_Add-4                                   640.0 ± 0%
geomean                                                        ¹
¹ summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │
                              │            allocs/op             │
ReviewQueue_ConcurrentReads-4                       0.000 ± 0%
ReviewQueue_Add-4                                   4.000 ± 0%
geomean                                                        ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/scrollback
cpu: AMD EPYC 7763 64-Core Processor                
                                      │ tier1-bench.txt │
                                      │     sec/op      │
CircularBuffer_ConcurrentReadWrite-4        3.943µ ± 2%
CircularBuffer_BurstAppend-4                102.1µ ± 2%
CircularBuffer_GetLastN_LargeBuffer-4       19.80µ ± 1%
CircularBuffer_GetRange_Sequential-4        10.85µ ± 1%
CircularBufferAppend-4                      100.1n ± 3%
CircularBufferGetLastN-4                    2.459µ ± 8%
CircularBufferConcurrentAppend-4            125.1n ± 0%
geomean                                     3.085µ

                                      │ tier1-bench.txt │
                                      │      B/op       │
CircularBuffer_ConcurrentReadWrite-4       6.062Ki ± 0%
CircularBuffer_BurstAppend-4               62.50Ki ± 0%
CircularBuffer_GetLastN_LargeBuffer-4      56.00Ki ± 0%
CircularBuffer_GetRange_Sequential-4       28.00Ki ± 0%
CircularBufferAppend-4                       24.00 ± 0%
CircularBufferGetLastN-4                   6.000Ki ± 0%
CircularBufferConcurrentAppend-4             32.00 ± 0%
geomean                                    3.077Ki

                                      │ tier1-bench.txt │
                                      │    allocs/op    │
CircularBuffer_ConcurrentReadWrite-4         2.000 ± 0%
CircularBuffer_BurstAppend-4                1.000k ± 0%
CircularBuffer_GetLastN_LargeBuffer-4        1.000 ± 0%
CircularBuffer_GetRange_Sequential-4         1.000 ± 0%
CircularBufferAppend-4                       1.000 ± 0%
CircularBufferGetLastN-4                     1.000 ± 0%
CircularBufferConcurrentAppend-4             1.000 ± 0%
geomean                                      2.962

                             │ tier1-bench.txt │
                             │       B/s       │
CircularBuffer_BurstAppend-4      597.9Mi ± 3%

cpu: AMD EPYC 9V74 80-Core Processor                
                                      │ benchmarks/go/tier1-baseline.txt │
                                      │              sec/op              │
CircularBuffer_ConcurrentReadWrite-4                         3.477µ ± 1%
CircularBuffer_BurstAppend-4                                 107.0µ ± 0%
CircularBuffer_GetLastN_LargeBuffer-4                        19.74µ ± 1%
CircularBuffer_GetRange_Sequential-4                         10.64µ ± 3%
CircularBufferAppend-4                                       103.2n ± 1%
CircularBufferGetLastN-4                                     2.373µ ± 1%
CircularBufferConcurrentAppend-4                             137.8n ± 0%
geomean                                                      3.081µ

                                      │ benchmarks/go/tier1-baseline.txt │
                                      │               B/op               │
CircularBuffer_ConcurrentReadWrite-4                        6.062Ki ± 0%
CircularBuffer_BurstAppend-4                                62.50Ki ± 0%
CircularBuffer_GetLastN_LargeBuffer-4                       56.00Ki ± 0%
CircularBuffer_GetRange_Sequential-4                        28.00Ki ± 0%
CircularBufferAppend-4                                        24.00 ± 0%
CircularBufferGetLastN-4                                    6.000Ki ± 0%
CircularBufferConcurrentAppend-4                              32.00 ± 0%
geomean                                                     3.077Ki

                                      │ benchmarks/go/tier1-baseline.txt │
                                      │            allocs/op             │
CircularBuffer_ConcurrentReadWrite-4                          2.000 ± 0%
CircularBuffer_BurstAppend-4                                 1.000k ± 0%
CircularBuffer_GetLastN_LargeBuffer-4                         1.000 ± 0%
CircularBuffer_GetRange_Sequential-4                          1.000 ± 0%
CircularBufferAppend-4                                        1.000 ± 0%
CircularBufferGetLastN-4                                      1.000 ± 0%
CircularBufferConcurrentAppend-4                              1.000 ± 0%
geomean                                                       2.962

                             │ benchmarks/go/tier1-baseline.txt │
                             │               B/s                │
CircularBuffer_BurstAppend-4                       570.3Mi ± 5%

pkg: github.com/tstapler/stapler-squad/session/tmux
cpu: AMD EPYC 7763 64-Core Processor                
                             │ tier1-bench.txt │
                             │     sec/op      │
StripANSICodes_PlainText-4         6.878n ± 0%
StripANSICodes_WithEscapes-4       684.4n ± 1%
IsBanner_PlainText-4               519.0n ± 2%
geomean                            134.7n

                             │ tier1-bench.txt │
                             │      B/op       │
StripANSICodes_PlainText-4        0.000 ± 0%
StripANSICodes_WithEscapes-4      56.00 ± 0%
IsBanner_PlainText-4              0.000 ± 0%
geomean                                      ¹
¹ summaries must be >0 to compute geomean

                             │ tier1-bench.txt │
                             │    allocs/op    │
StripANSICodes_PlainText-4        0.000 ± 0%
StripANSICodes_WithEscapes-4      4.000 ± 0%
IsBanner_PlainText-4              0.000 ± 0%
geomean                                      ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                             │ benchmarks/go/tier1-baseline.txt │
                             │              sec/op              │
StripANSICodes_PlainText-4                          6.808n ± 4%
StripANSICodes_WithEscapes-4                        616.0n ± 1%
IsBanner_PlainText-4                                469.9n ± 0%
geomean                                             125.4n

                             │ benchmarks/go/tier1-baseline.txt │
                             │               B/op               │
StripANSICodes_PlainText-4                         0.000 ± 0%
StripANSICodes_WithEscapes-4                       56.00 ± 0%
IsBanner_PlainText-4                               0.000 ± 0%
geomean                                                       ¹
¹ summaries must be >0 to compute geomean

                             │ benchmarks/go/tier1-baseline.txt │
                             │            allocs/op             │
StripANSICodes_PlainText-4                         0.000 ± 0%
StripANSICodes_WithEscapes-4                       4.000 ± 0%
IsBanner_PlainText-4                               0.000 ± 0%
geomean                                                       ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/tokens
cpu: AMD EPYC 7763 64-Core Processor                
                                   │ tier1-bench.txt │
                                   │     sec/op      │
TokenParser_ProcessUserEntry-4           5.168m ± 2%
DetectCommandsInText/NoSlash-4           7.487n ± 0%
DetectCommandsInText/WithCommand-4       1.654µ ± 1%
geomean                                  3.999µ

                                   │ tier1-bench.txt │
                                   │      B/op       │
TokenParser_ProcessUserEntry-4        11.02Mi ± 0%
DetectCommandsInText/NoSlash-4          0.000 ± 0%
DetectCommandsInText/WithCommand-4      433.0 ± 0%
geomean                                            ¹
¹ summaries must be >0 to compute geomean

                                   │ tier1-bench.txt │
                                   │    allocs/op    │
TokenParser_ProcessUserEntry-4          34.00 ± 0%
DetectCommandsInText/NoSlash-4          0.000 ± 0%
DetectCommandsInText/WithCommand-4      6.000 ± 0%
geomean                                            ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                                   │ benchmarks/go/tier1-baseline.txt │
                                   │              sec/op              │
TokenParser_ProcessUserEntry-4                            5.470m ± 1%
DetectCommandsInText/NoSlash-4                            6.357n ± 5%
DetectCommandsInText/WithCommand-4                        1.516µ ± 2%
geomean                                                   3.749µ

                                   │ benchmarks/go/tier1-baseline.txt │
                                   │               B/op               │
TokenParser_ProcessUserEntry-4                         11.02Mi ± 0%
DetectCommandsInText/NoSlash-4                           0.000 ± 0%
DetectCommandsInText/WithCommand-4                       433.0 ± 0%
geomean                                                             ¹
¹ summaries must be >0 to compute geomean

                                   │ benchmarks/go/tier1-baseline.txt │
                                   │            allocs/op             │
TokenParser_ProcessUserEntry-4                           34.00 ± 0%
DetectCommandsInText/NoSlash-4                           0.000 ± 0%
DetectCommandsInText/WithCommand-4                       6.000 ± 0%
geomean                                                             ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/unfinished
cpu: AMD EPYC 7763 64-Core Processor                
                               │ tier1-bench.txt │
                               │     sec/op      │
DiffShortstat/GitVCSReader-4         3.259m ± 1%
DiffShortstat/GoGitVCSReader-4       76.86n ± 1%
DiffShortstatCached-4                75.44n ± 0%
geomean                              2.663µ

                               │ tier1-bench.txt │
                               │      B/op       │
DiffShortstat/GitVCSReader-4      62.57Ki ± 0%
DiffShortstat/GoGitVCSReader-4      0.000 ± 0%
DiffShortstatCached-4               0.000 ± 0%
geomean                                        ¹
¹ summaries must be >0 to compute geomean

                               │ tier1-bench.txt │
                               │    allocs/op    │
DiffShortstat/GitVCSReader-4        360.0 ± 0%
DiffShortstat/GoGitVCSReader-4      0.000 ± 0%
DiffShortstatCached-4               0.000 ± 0%
geomean                                        ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                               │ benchmarks/go/tier1-baseline.txt │
                               │              sec/op              │
DiffShortstat/GitVCSReader-4                          3.483m ± 2%
DiffShortstat/GoGitVCSReader-4                        80.92n ± 0%
DiffShortstatCached-4                                 80.94n ± 0%
geomean                                               2.836µ

                               │ benchmarks/go/tier1-baseline.txt │
                               │               B/op               │
DiffShortstat/GitVCSReader-4                       62.58Ki ± 0%
DiffShortstat/GoGitVCSReader-4                       0.000 ± 0%
DiffShortstatCached-4                                0.000 ± 0%
geomean                                                         ¹
¹ summaries must be >0 to compute geomean

                               │ benchmarks/go/tier1-baseline.txt │
                               │            allocs/op             │
DiffShortstat/GitVCSReader-4                         360.0 ± 0%
DiffShortstat/GoGitVCSReader-4                       0.000 ± 0%
DiffShortstatCached-4                                0.000 ± 0%
geomean                                                         ¹
¹ summaries must be >0 to compute geomean

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

Frontend Terminal Throughput

terminal-throughput-mean: 15 KB/s ▲ +9.9% (baseline: 14 KB/s)
terminal-throughput-p50: 16 KB/s ▼ -1.2% (baseline: 16 KB/s)

@github-actions

Copy link
Copy Markdown

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

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

E2E RPC Latency

list-sessions-ttfb-mean: 7ms (▲ slower +17.1%; baseline: 6ms)
list-sessions-total-mean: 11ms (▲ slower +16.4%; baseline: 9ms)

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

🎬 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.

tstapler and others added 3 commits July 25, 2026 17:32
… 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
@github-actions

Copy link
Copy Markdown

✅ Registry Validation

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

Building backend scanner...
Scanning backend features...
Wrote 116 feature files to /tmp/tmp.cMZw4TsGIi/backend
Wrote 14 feature files to /tmp/tmp.cMZw4TsGIi/backend
Wrote 27 feature files to /tmp/tmp.cMZw4TsGIi/backend
Wrote 6 feature files to /tmp/tmp.cMZw4TsGIi/backend
Wrote 10 feature files to /tmp/tmp.cMZw4TsGIi/backend

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

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 6/161 features have testIds (3.7%)

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

tstapler added a commit that referenced this pull request Jul 26, 2026
…#191)

Backlog work sessions never got archived, even after their item reached
done/archived — the session archival mechanism (ArchivedAt, ArchiveSession
RPC, ListSessions exclusion) was built exclusively for the Workflow feature
and never wired to backlog work sessions. Live data showed 101/130 sessions
(78%) tagged backlog:work mapping to only 18 items, with unarchived sessions
piled up to 13 deep on a single item and dating back over 3 months.

Root cause: wireAutoArchiveCallback/maybeAutoArchive only fire when
inst.WorkflowID != "", which SpawnSessionFromItem never sets.

Fixes, reusing the existing ArchivedAt mechanism end to end:
- Archive an item's work sessions when it transitions to done/archived
  (TransitionBacklogItemStatus), alongside the existing worktree cleanup.
- Archive the superseded prior-round work session on rework respawn
  (SpawnSessionFromItem's reopen path) — the main fix for items that bounce
  through many rework rounds while still open.
- Add an archive_terminal_sessions safety-net detector to ReconcileStuck
  (60s tick) that retroactively archives work sessions for any item already
  done/archived that the transition hook missed — covers the several other
  internal call sites that transition straight to done (SubmitManualReview,
  ReconcilePRPending, etc.) plus the pre-existing terminal items in live
  data, without needing a separate one-time backfill script.
- Wire IncludeArchived into the general session list: a "Show Archived"
  toggle in SessionList re-fetches with includeArchived and the list hides
  archived sessions by default.

New SessionArchiver (session package) / extended SessionStopper
(server/services) interfaces both delegate to SessionService's single
ArchiveSessionByUUID, which reuses ArchiveSession's CAS logic
(SetArchivedAtIfNil) so it's a safe no-op to call redundantly from the
sweep every tick.


Claude-Session: https://claude.ai/code/session_01BxNAMeGteuzNyN46Q4zAn1

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
tstapler added a commit that referenced this pull request Jul 26, 2026
…m leak in default view (#194)

Backlog items could already be manually transitioned done -> archived, but
nothing ever did it automatically, so completed work piled up in "done"
forever (the item-level counterpart to PR #191's session-archival fix).

Part 1: add an auto_archive_done detector to the existing 60s ReconcileStuck
sweep (session/backlog_lifecycle.go) that transitions items from "done" to
"archived" 3 days (maxDoneAge, a fixed constant) after their most recent
done-transition. Age is measured from the BacklogStatusEvent history
(FindDoneItemsOlderThan in ent_repository_backlog.go) rather than
UpdatedAt, since UpdatedAt resets on unrelated edits like progress notes.
Registered immediately before the archive_terminal_sessions detector so a
freshly archived item's work sessions get swept in the same tick.

Part 2: fix a confirmed filtering gap where the one boolean the backlog
page used to show done items by default (includeTerminal) also silently
showed archived items, with no way to hide them. Split
session.BacklogItemFilter's combined ExcludeTerminal into independent
ExcludeDone/ExcludeArchived flags, added a matching include_archived proto
field, and added a "Show Archived" toggle to the backlog list page
(mirrors PR #191's session-list pattern) that defaults to off.

Verified end to end with an integration test proving an item auto-archived
by the Part 1 sweep disappears from the Part 2 default-filtered view.


Claude-Session: https://claude.ai/code/session_01BxNAMeGteuzNyN46Q4zAn1

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@tstapler

Copy link
Copy Markdown
Collaborator Author

Closing as part of winding down this fork. This work is being redone fresh against tstapler/stapler-squad (the linked issue has been migrated to tstapler/stapler-squad#255).

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants