Skip to content

chore: sync upstream-fanatics -> personal 2026-07-16 - #158

Closed
tstapler wants to merge 1 commit into
mainfrom
sync/work-to-personal-2026-07-16
Closed

chore: sync upstream-fanatics -> personal 2026-07-16#158
tstapler wants to merge 1 commit into
mainfrom
sync/work-to-personal-2026-07-16

Conversation

@tstapler

Copy link
Copy Markdown
Owner

Automated nightly sync. 1 commit from work fork.

Updates Formula/stapler-squad.rb Homebrew formula from v1.36.0 → v1.37.0 with new release URLs and SHA256 checksums for all platforms (darwin amd64/arm64, linux amd64/arm64).

Note: the two repos have diverged git histories (independent git trees), so this sync applies only the unique content from the work fork rather than a full git merge.


Generated by Claude Code

Brew formula update for stapler-squad version v1.37.0 from work fork.
@tstapler tstapler added the minor label Jul 16, 2026 — with Claude
@tstapler
tstapler force-pushed the sync/work-to-personal-2026-07-16 branch from 10592fd to c293558 Compare July 26, 2026 18:06
@tstapler

Copy link
Copy Markdown
Owner Author

Closing — stale (11 days old), superseded by #226's more current sync. Per direction: syncs are being discontinued.

@tstapler tstapler closed this Jul 27, 2026
tstapler added a commit that referenced this pull request Jul 28, 2026
…ck (#272)

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* fix(unfinished): stack GitHub auth banner vertically so Connect button 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>

* chore(demos): update E2E feature GIFs [skip ci]

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* feat(pr-status): show PR badge in row mode and use go-git for branch 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>

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* chore(demos): update E2E feature GIFs [skip ci]

* fix: repair broken release pipeline and build-from-source path (#147)

* 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
3be7e0902, 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>

* chore(main): release 1.33.0 (#145)

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* Brew formula update for stapler-squad version v1.33.0

* chore(demos): update E2E feature GIFs [skip ci]

* fix: backlog/triage sessions die on launch (shell injection + flag-parsing 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>

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* fix(analytics): escape analytics session_id mismatch and dead mangle 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>

* fix(backlog): GitHub URL repo-path support, first-visit tour, and two 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>

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* fix: web-build target doesn't generate proto bindings on a clean clone (#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>

* chore(main): release 1.33.1 (#153)

* chore(demos): update E2E feature GIFs [skip ci]

* Brew formula update for stapler-squad version v1.33.1

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update go tier2 baseline [skip ci]

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* fix: autonomous sessions rejected with "path is required" via omnibar (#157)

* fix: autonomous sessions rejected with "path is required" via omnibar

The omnibar sends autonomous sessions as SessionType=DIRECTORY with an
empty path, relying on the server to generate a scratch directory (same
pattern as one-off sessions). CreateSession's path-required guard and its
directory-generation logic only special-cased SESSION_TYPE_ONE_OFF, so
every autonomous session request was rejected with "path is required"
before any autonomous-specific logic ran.

Exempt AutonomousMode from the path guard and extend the one-off
directory-generation block to also fire when AutonomousMode is true and
no path was provided.

* fix: guard autonomous-mode sessions from clobbering an explicit path

Code review on PR #157 surfaced an asymmetry: the path-required guard
exempts AutonomousMode unconditionally, but the directory-generation
block only fires when resolvedPath == "". Add a regression test proving
an autonomous request with an explicit path keeps that path rather than
having it silently replaced by a generated scratch directory, and a
one-line comment explaining the guard's AutonomousMode clause.

---------

Co-authored-by: Tyler Stapler <tystapler@gmail.com>

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update go tier2 baseline [skip ci]

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* feat(backlog): add hard delete for backlog items

Adds DeleteBacklogItem RPC that permanently removes an item and all its
child records (ReviewVerdicts → ItemSessions → BacklogItem; status_events
cascade automatically). Previously only archive (soft-delete) existed.

Frontend gets a red Delete button in BacklogItemDetail, always visible
regardless of status, with a confirm dialog that closes the panel on success.

* chore: update serena project config

* chore(sdd): planning artifacts for perf-mutex-hotspots-2026-07

Adds full SDD planning artifacts for the GoGitVCSReader singleflight
thundering-herd fix: requirements, 5 research docs, implementation plan,
architecture review, adversarial review, pre-mortem, validation, and
consistency report.

Key decisions recorded:
- Separate singleflight.Group per method (diffStatSF, aheadBehindSF, hasUncommittedSF)
- entry.mu acquired with defer inside Do body — required for panic safety
- Named returns on Do closures so recover() can set the error return
- HasUncommitted inner-helper extraction mandatory (eliminates 8 explicit unlocks)
- CircularBuffer and IsDirty fixes already shipped; scope reduced to Fix 1 only

* feat(perf): add singleflight + hasUncommitted TTL cache to GoGitVCSReader

Wraps AheadBehind, DiffShortstat, and HasUncommitted slow paths in
per-method singleflight.Group to eliminate thundering-herd entry.mu
contention when 4 scanner workers hit the same repo simultaneously.
Adds hasUncommittedCache (30s TTL, mirroring diffStatCache) and
extracts hasUncommittedGoGitPhase helper to avoid deferred-unlock
deadlock on panic.

* feat(perf): invalidate IsDirty cache on session Pause and Resume

Adds InvalidateDirtyCache() to GitWorktreeManager and the GitManager
interface, then calls it on Pause (via defer) and after successful
transitionTo(Active) on Resume so the UI always reflects actual worktree
dirty state rather than a stale 15s-TTL cached value.

* test(perf): add singleflight concurrency and cache tests for GoGitVCSReader

Adds three white-box tests to session/unfinished/gogit_vcs_reader_limits_test.go
covering Epic 1.3 of perf-mutex-hotspots-2026-07: singleflight collapse of 4
parallel AheadBehind callers, panic-safe error return on bad path, and
HasUncommitted cache-hit fast path via pre-populated hasUncommittedCache.

* test(perf): rename PanicDoesNotCrashCaller to PanicRecovery per spec

* fix(perf): release entry.mu before OS stat walk in HasUncommitted; typed nil returns in Do bodies

- hasUncommittedGoGitPhase now returns []trackedFile instead of map[string]bool,
  releasing entry.mu (via defer) before any os.Lstat calls
- OS stat walk moved into HasUncommitted's singleflight.Do body after the lock
  is released; each early dirty=true return stores to hasUncommittedCache before
  returning to avoid recomputing within the 30s TTL
- trackedFile type promoted to package scope so it can cross the function boundary
- All return nil, err in HasUncommitted and AheadBehind Do closures replaced with
  typed zero values (false / abResult{}) to satisfy singleflight's any return
- HasUncommitted doc comment relocated to sit immediately above the function
- Removed misleading "lock still held here" comment from hasUncommittedGoGitPhase

* fix(perf): rename misleading panic test, add scope comment, move InvalidateDirtyCache post-transition

* refactor(perf): generic sfDo helper, defer tw.Close, fix silent walker error, map[string]struct{}

- Extract generic sfDo[T] package-level helper that wraps singleflight.Do
  with panic recovery, replacing identical boilerplate in AheadBehind,
  DiffShortstat, and HasUncommitted
- Wrap CommitMessages slow path in sfDo with commitMessagesSF field to
  deduplicate concurrent calls and use defer for lock release
- Replace explicit tw.Close() calls with defer tw.Close() in
  hasUncommittedGoGitPhase and diffShortstatUncached
- Propagate TreeWalker errors in diffShortstatUncached instead of
  silently breaking (was swallowed as a no-op)
- Change indexedMap and hasUntrackedFiles parameter from map[string]bool
  to map[string]struct{} for consistency with walkUntracked and to save memory
- Add non-re-entrancy comment above diffShortstatUncached call in DiffShortstat

* chore(sdd): update validation.md with Phase 4 and spec compliance findings

* fix(service): fall back to launchctl load when bootstrap fails on macOS

launchctl bootstrap can fail with I/O error (exit 5) on some macOS
versions even when the service is not loaded. Mirror the existing
bootout→unload fallback pattern for the start path.

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update go tier2 baseline [skip ci]

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* fix(terminal): correctly scan OSC/DCS escape sequences to stop render artifacts (#156)

* fix(terminal): correctly scan OSC/DCS escape sequences to stop render artifacts

stripANSIBytes and sanitizeUTF8Bytes treated any ASCII letter as the end
of an escape sequence. That's only true for CSI (ESC[...letter); OSC
(ESC]...BEL or ESC\) and DCS/PM/APC/SOS (ESC{P,^,_,X}...ESC\ or 0x9C)
terminate differently, and their payloads (window titles, hyperlink
URLs, shell-integration marks) almost always contain a letter before
the real terminator. Claude Code's newer renderer emits more of these
OSC sequences, so their payload tails were leaking through as literal
text in the web terminal and throwing off cursor-column math.

Add a shared scanEscapeSequence helper (mirrors the correct boundary
logic already used by pkg/analytics/escape_code_parser.go) and rewire
both duplicated stripANSIBytes definitions plus sanitizeUTF8Bytes to
consume whole sequences atomically instead of stopping at the first
letter.

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

* fix(terminal): widen CSI final-byte range to 0x40-0x7E, cap OSC/DCS scan size

Code review on PR #156 found that the new scanCSI only accepted A-Z/a-z
as CSI terminators, missing real ECMA-48-valid final bytes like '@'
(0x40, Insert Character) and '~' (0x7E, used by many real xterm
sequences e.g. function/navigation keys). Confirmed empirically:
stripANSIBytes("\x1b[5@Hello") leaked "@Hello" instead of "Hello" —
the exact bug class this PR exists to eliminate, just for a different
final byte. Widened to the full 0x40-0x7E range and aligned the
malformed-CSI fallback with pkg/analytics' semantics (give up and
consume only the ESC, rather than swallowing partially-scanned params).

Also:
- Widened pkg/analytics/escape_code_parser.go's parseCSI terminator
  range to match (it was cited as the reference implementation for
  this fix but had the same narrower gap for '~' and other non-letter
  finals in 0x5B-0x60/0x7B-0x7E).
- Added a size cap (mirroring escape_code_parser.go's existing 65536
  bound) to scanUntilTerminator so an unterminated/adversarial OSC or
  DCS payload can't force an unbounded scan.
- Pre-size the bytes.Buffer in stripANSIBytes/sanitizeUTF8Bytes with
  Grow(len(b)) to avoid reallocation growth in this hot path.
- Removed the now-vestigial (*StateGenerator).stripANSIBytes wrapper
  method now that its only caller can use the shared free function
  directly.
- Added regression tests for all of the above, including a mid-buffer
  (start > 0) case and the new size-cap 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>

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update go tier2 baseline [skip ci]

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* chore(demos): update E2E feature GIFs [skip ci]

* fix(nav): add Feature Flags to navigation menu

settingsFeatures route existed but was never registered in NAV_PAGES,
making the /settings/features page unreachable from the sidebar.

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

* docs(claude): prune CLAUDE.md and rule files for token efficiency

- CLAUDE.md: removed inline bundling-tmux and concurrency patterns code
  blocks; replaced with reference links to new .claude/docs/ files
- feature-testing-registry.md: removed illustrative TS code blocks (~51%
  reduction); kept checklists and decision tree
- session-creation-registry.md: minor condensation
- .claude/docs/bundling-tmux.md: extracted bundling commands
- .claude/docs/concurrency-patterns.md: extracted double-checked locking pattern

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

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update go tier2 baseline [skip ci]

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* chore(demos): update E2E feature GIFs [skip ci]

* feat(backlog): import backlog items from GitHub issues

Adds an ImportGitHubIssue RPC that shells out to `gh issue view` to
populate title, description, labels, and URL from any GitHub issue,
then creates a BacklogItem and optionally triggers auto-triage.

Frontend adds a mode toggle to the "New Backlog Item" modal:
- Manual: existing BacklogItemForm (unchanged)
- Import from GitHub Issue: URL field → ImportGitHubIssue RPC

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

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update go tier2 baseline [skip ci]

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* chore(demos): update E2E feature GIFs [skip ci]

* chore(main): release 1.34.0 (#158)

* Brew formula update for stapler-squad version v1.34.0

* chore(demos): update E2E feature GIFs [skip ci]

* chore(sdd): planning artifacts for github-issue-picker

* feat(backlog): GitHub issue picker — browse repos and issues to import

Adds an interactive two-phase picker (repo selection → issue list) to
the backlog import flow, powered by native Go GitHub HTTP client calls
instead of gh CLI subprocess invocations.

Backend (Epic 1):
- github/http_client.go: export GhBaseURL for test injection
- github/repos.go: SearchUserRepos, ListRepoIssues domain functions
- proto/session/v1/backlog.proto: SearchGitHubRepos + ListGitHubIssues
  RPCs and supporting message types
- server/services/backlog_service.go: handler implementations with
  input validation and ownerRepoPattern guard
- server/services/backlog_github_rpc_test.go: 9 handler tests via
  httptest.Server interception

Frontend (Epics 2–3):
- useBacklogService.ts: GitHubRepo, GitHubIssue, GitHubAuthError types
  and searchGitHubRepos / listGitHubIssues hook methods
- lib/utils/issuePickerCache.ts: localStorage TTL cache (5 min)
  for repos and issues, origin-scoped keys, last-used repo
- lib/hooks/useGitHubIssuePicker.ts: two-phase picker state — debounce,
  generation counter, AbortController, local-repo tier from Redux
- components/backlog/GitHubIssuePicker.css.ts: vanilla-extract styles
- components/backlog/GitHubIssuePicker.tsx: RepoSelector + IssueList
  with keyboard nav, ARIA attrs, two-level Escape, auth error state
- backlog/page.tsx: replaces URL text input with GitHubIssuePicker

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

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update go tier2 baseline [skip ci]

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* chore(demos): update E2E feature GIFs [skip ci]

* chore: merge tstapler/main → upstream (20260702) (#159)

* refactor(session-types): unify SessionType, promote one_off to proto enum, split config

Eliminates three sources of type duplication:

1. config/types.go + config/executor.go extracted from the 1031-line config/config.go
   (SRP fix — config.go now contains only factory functions and the Config struct)

2. session.SessionType is now a Go type alias for config.SessionType, removing the
   duplicate type that required aliasSessionTypeToSessionType no-op conversions

3. bool one_off = 14 promoted to SESSION_TYPE_ONE_OFF = 5 in the SessionType proto enum;
   field 14 is reserved for wire compatibility. All call sites updated: backend handler,
   workflow scheduler, alias defaults service, and all frontend contexts/hooks/tests.

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

* fix(headless): use Setsid instead of Noctty for headless runner subprocess

WithNoControllingTerminal() sets SysProcAttr.Noctty=true on Linux, which
calls ioctl(0, TIOCNOTTY) in the child after fork. This returns ENOTTY when
the parent process has no controlling terminal — the case when stapler-squad
runs as a systemd service — causing every headless triage call to fail with
"fork/exec .../claude: inappropriate ioctl for device" (exit code 1).

Replace WithNoControllingTerminal() with WithNewSession() in ProcessRunner.Run.
Setsid creates a new process session (implying no controlling terminal) without
invoking TIOCNOTTY, so it works regardless of whether the parent has a TTY.

Also corrects the misleading comment in managed_process_linux.go that claimed
Noctty was safe without a controlling terminal.

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

* feat(omnibar): replace Create shortcut hint with clickable Create Session button

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

* feat(alias): add name_prefix field + fix session name oscillation

- Add `name_prefix` to AliasConfig (Go), AliasProto (proto field 12),
  and AliasEntry (TypeScript) — wired through the full stack
- In the detection effect, skip the generic suggestedName update for
  aliases; the alias block now derives the session name as
  namePrefix + typedLabel, falling back to namePrefix alone or the
  alias name — eliminates the oscillation between alias name and
  prefix+label on each keystroke
- AliasesManager settings form now has a Name prefix field with a live
  preview hint
- Create Session button in shortcuts bar uses compact styling on desktop
  and expands to touch-friendly size on coarse-pointer (mobile) devices

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

* fix(detection): detect dynamic workflows + expand turn-marker to ✦

- Add "dynamic workflow" alternate to waiting_for_background_agent pattern
  so "✻ Waiting for N dynamic workflow(s) to finish" → StatusWaitingForAgent
- Expand [✻◉] → [✻◉✦] in verb_duration_completion and
  waiting_for_background_agent to cover ✦ (U+2726, Claude Code primary spinner)
- Add test cases for all three bullet variants on both waiting and completion lines

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

* fix(review-queue): show INPUT_REQUIRED items + UX improvements

- Fix invisible INPUT_REQUIRED/APPROVAL_PENDING items: deriveWorkingState
  maps these to PROCESSING, which was being filtered out; now always
  passes items through when their reason requires user action
- Fix workingCount to exclude INPUT_REQUIRED/APPROVAL_PENDING from the
  "working" tally (they need attention, not patience)
- Fix summaryCount grammar ("input neededs", "task completes", "timed outs")
  by replacing tuple pluralization with per-reason formatter functions
- Fix filter empty state: show "no items match" when a filter is active,
  not the generic "all done" message
- Move auto-advance checkbox into the panel title row (was orphaned above
  the card in page.tsx toolbar div)
- Hide floating help button on mobile (keyboard shortcuts are irrelevant
  on touch devices)
- Increase filter button / toggle touch targets to 44px on mobile
- Downgrade oldest-item callout from alarming orange to neutral muted style
- Show filter toggle whenever any items exist (not only when server
  totalItems > 0)

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

* fix(alias): default session type + name oscillation

- Fix session type not applying for aliases configured as
  "Default (directory)": that option stores SessionType.UNSPECIFIED,
  which the detection effect was explicitly skipping — form stayed at
  the initial "new_worktree" value instead. Now maps UNSPECIFIED → "directory".
- Fix session name oscillating every other keystroke: the generic
  suggestedName block was running for InputType.Alias results and
  resetting lastSuggestedNameRef to the alias slug (e.g. "pw"),
  causing the alias-specific name block to fail its staleness check
  and alternate on each input event. Fixed by skipping the generic
  block for Alias inputs entirely — the alias block below handles naming.

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

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* chore(demos): update E2E feature GIFs [skip ci]

* chore(sdd): planning artifacts for review-queue-jump-fix

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

* fix(review-queue): suppress auto-advance on session status transitions

The "deleted externally" effect in ReviewQueueContent used reviewQueueItems
(the filtered visible list) to check if the selected session still existed.
When a session transitioned to ACTIVE/PROCESSING, it was filtered from the
visible list but remained in the Redux store — the effect incorrectly fired
handleAutoAdvance(id, true), jumping to the next queue item immediately after
the user opened a session and clicked into the terminal.

Fix: use allQueueItems from useReviewQueueContext().items (the unfiltered
Redux store) as the existence oracle. A session filtered from the visible queue
due to status transition stays in the store and no longer triggers auto-advance.
Genuine removals (removeItem Redux events) still fire auto-advance correctly.

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

* fix(sessions): prevent Claude process orphaning after server restart

Three-part fix for tmux session / Claude process accumulation:

**Fix 1 — DeleteSession fallback (session_service.go)**
When FindLiveInstance returns nil (e.g. server restarted since the session
was created, so the in-memory poller is empty), fall back to
KillTmuxSessionByTitle which kills by the deterministic tmux session name.
Previously the DB record was deleted but the Claude process kept running
indefinitely.

**Fix 2 — Startup orphan sweep (session/orphan_sweep.go)**
ReconcileOrphanedTmuxSessions runs as Step 6d of BuildRuntimeDeps, after
the re-adoption passes (6/6b) that hot-attach DB sessions to their live
tmux panes. It enumerates all staplersquad_* tmux sessions, reads the
STAPLER_SESSION_UUID env var from each, and kills any whose UUID (or
sanitized title) has no match in the current workspace DB. The keepalive
sentinel is always preserved.

**Fix 3 — MCPServerURL backfill (session_service.go)**
loadInstancesWithWiring now backfills inst.MCPServerURL from the server's
configured URL for sessions created before MCP integration was wired up.
Without this, buildLaunchCommand omits --mcp-config entirely and Claude
restarts without a session UUID, making it impossible to identify from the
process list or MCP request headers.

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

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* fix(lint): return empty map instead of nil in GetAllInstanceArtifacts

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

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* fix(backlog): harden triage parser and add repoPath UI gate

ParseHeadlessTriageResult now uses brace-scan (strings.Index/LastIndex)
to tolerate natural-language preamble before the JSON block, fixing
silent parse failures on multi-step triage runs. The "Trigger Triage"
button in BacklogItemDetail and BacklogItemCard is now disabled with a
tooltip when repoPath is not set, preventing the confusing
CodeFailedPrecondition server error.

Adds 3 new unit tests for the parser and a Playwright e2e gate test
that creates an item without repoPath and asserts the button is
disabled.

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

* chore(demos): update E2E feature GIFs [skip ci]

* feat(harness): headless triage test harness + alias kebab-case fix

Adds a build-tagged Go harness (go:build harness) that exercises the
backlog triage feature end-to-end via the ConnectRPC HTTP layer with no
browser or UI. Four sub-tests cover distinct phases runnable individually:
Gate (repoPath precondition), TriggerAndPoll (async completion), ParserRobust
(preamble tolerance), and FullFlow (full user journey). Makefile targets
added for each phase.

Also converts alias namePrefix label to kebab-case lowercase
(spaces/underscores → hyphens) before concatenating with the prefix, so
"@ssq My New Feature" produces "ssq-my-new-feature" instead of
"ssq-My New Feature". Two new tests added to Omnibar.alias.test.tsx.

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

* chore(sdd): planning artifacts for nav-redesign

Navigation redesign: group 16+ flat nav items into 4 sections (Work,
Automation, Insights, Settings & Tools), restore mobile access for 8
currently-hidden routes, and consolidate Settings/Config Files/Features.

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

* feat(nav): group navigation into 4 sections, restore mobile access

Reorganise the 15 nav pages into Work / Automation / Insights / Settings
groups rendered in both DrawerNav (desktop sidebar) and BottomNav More
sheet (mobile).  All 8 routes that were hidden from mobile (Settings,
Insights, Logs, Errors, Help, Escape Analytics, Files, Workflows/Rules)
are now reachable on every screen size.  Removes the redundant Config
Files and Features top-level entries; fixes a DrawerNav bug where items
were shown regardless of feature-flag state.

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

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* chore(demos): update E2E feature GIFs [skip ci]

* chore: commit in-progress work from previous sessions

Includes executor fixes (WithProcessDir support, Linux setsid/Setpgid
EPERM fix), backlog triage harness test expansions, rate-limit
integration test, Makefile test-triage-real target, and planning
artifacts for backlog-triage-e2e-hardening and
put-backlog-behind-a-feature-flag-by-default.

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

* feat: support Antigravity CLI hooks.json format in ssq-hooks

* fix(pane): restore session peek modal integration in pane picker

* feat(files): wire up the premium LocalFileBrowser component to the files page

* chore: commit in-progress work from previous sessions

- ssq-hooks: Antigravity CommandLine/Cwd normalization, workspace-aware
  DB path resolution from cwd, WorkspacePaths fallback
- session service: ForkSession fully wired (callbacks, hook config,
  controller, driver, autonomous mode); ResumeHibernated wires review
  queue poller and autonomous driver
- ent schema: autonomous_mode bool field + generated ORM files
- instance_hibernate: start controller + session driver on resume
- omnibar: initialTitle prop pre-populates session name; OmnibarContext
  threads title through openOmnibar(); page.tsx passes ?title param
- LocalFileBrowser: CSS and component updates
- scripts: find-orphaned-features.py, find-unmerged-commits.py

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

* feat(omnibar): replace Create shortcut hint with clickable Create Session button

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

* feat(settings): add UpsertAlias and DeleteAlias RPCs with AliasesManager UI

Implements full CRUD for alias session presets in Settings > General, removing
the need to manually edit config.json. Adds UpsertAlias/DeleteAlias ConnectRPC
handlers (case-insensitive name matching, slice-scan upsert, validation via
aliasNameRE) and a React AliasesManager component with inline 3-second delete
confirmation, env-var editor, tag management, and ARIA accessibility.

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

* fix(registry): add alias RPCs to scanner methodToID map

UpsertAlias, DeleteAlias, ListAliases were missing from the methodToID
map, causing the scanner to use fallback raw-name IDs (UpsertAlias,
DeleteAlias, ListAliases) instead of canonical kebab-case IDs
(alias:upsert, alias:delete, alias:list). This caused Registry
Validation CI to fail with 3.36% divergence.

Removes the duplicate fallback JSON files from the registry root that
were generated under the old behavior.

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

* feat(analytics): program detail panel with subcommand drill-down

Add DB-backed time-windowed analytics queries and an inline program
detail panel so operators can see exactly which sub-operations are
causing escalations before writing a rule.

Backend (Go):
- Add compound index on (command_program, created_at) to ent schema
- Replace full-table-scan ListAnalytics with time-windowed
  ListAnalyticsSince (WHERE created_at >= ?) — AC-1, AC-2
- Add GetSubcommandBreakdown aggregation query using ent GroupBy — AC-4
- Add ListRecentCommandsByProgram returning last N command previews — AC-5
- Add GetSubcommandTrend returning per-day counts — AC-6
- Add GetProgramAnalytics ConnectRPC method returning SubcommandBreakdown,
  ExampleCommands, RuleCoverage, DailyTrend — AC-7

Frontend (React/TypeScript):
- New ProgramDetailPanel component with subcommand frequency table
  (count, %, decision breakdown), example commands, rule coverage
  summary, trend sparklines, and "Add rule →" links — AC-8 through AC-13
- New useProgramAnalytics hook with AbortController cleanup
- ApprovalAnalyticsPanel: clicking program row opens inline detail panel
- ApprovalRulesPanel: fix panel crush in flex container (flexShrink: 0),
  use window.location.search in useEffect for URL param pre-fill
  (avoids useSearchParams/Suspense issues in Next.js static export)

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

* feat(backlog): gate backlog behind feature flag on all layers

- Frontend layout guard: backlog/layout.tsx redirects to / when flag off
- Backend interceptor: FeatureFlagInterceptor wired to BacklogService only
- E2E tests: beforeAll/afterAll enable+restore the backlog flag

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

* fix: address copilot review comments on analytics drill-down

- Fix 1: exclude NULL command_subcategory rows in GetSubcommandBreakdown
  to avoid sql.ScanSlice scan errors on nullable GROUP BY columns
- Fix 2: replace strings.Fields tokenizer in coveredSubcommands() with
  regexp.Compile + synthetic "<program> <subcommand>" matching so
  regex-style patterns (e.g. \bgit\b.*\bpush\b) work correctly
- Fix 3: add TestGetProgramAnalytics_ReturnsExpectedFields unit test
  covering window_days=7 and non-nil response fields
- Fix 4: add escapeRegex() helper in ApprovalRulesPanel and use it when
  prefilling commandPattern to avoid metacharacter injection; switch word
  boundaries from \b to (?:^|\s)/(?:\s|$) for hyphenated program names
- Fix 5: add e.stopPropagation() on Suggest Rule button and "add manually"
  link so clicking them does not toggle the parent <tr> drill-down row
- Fix 6: add tabIndex, role=button, aria-expanded, aria-label, and
  onKeyDown (Enter/Space) to the clickable <tr> for keyboard accessibility
- Fix 7: call setData(null) before setIsLoading(false) in error path of
  useProgramAnalytics to clear stale data on refresh failure
- Fix 8: render per-program daily trend sparkline in ProgramDetailPanel;
  note that trend data is per-program not per-subcommand (backend limit)
- Fix 9: thread caller context through LoadProgramWindow,
  GetSubcommandBreakdown, and ListRecentCommands instead of context.Background()

* fix(review-queue): resolve UUID→Title before Remove so approved/deleted sessions leave the queue

Queue items are keyed by inst.Title but approval-response and session-deleted
events arrive with UUID. resolveQueueKey() looks up the instance via FindInstance
(which handles both UUID and Title) and returns Title, falling back to the raw
value if the instance is no longer loaded.

Also removes duplicate SubcommandDecisionCount declaration in repository.go
introduced by the analytics cherry-pick merge.

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

* chore(bench): update go tier1 baseline [skip ci]

* chore: sync upstream → personal fork (20260629) (#132)

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* chore(demos): update E2E feature GIFs [skip ci]

* refactor(session): apply type-driven design to buildLaunchCommand

Replace the 8x isClaudeProgram bool check with a sealed programKind sum
type (claudeProgram / plainProgram). classifyProgram() parses once at the
boundary; holding claudeProgram is proof the program invokes claude, so
buildClaudeCommand needs zero isClaude guards — they are enforced by the
type system, not by runtime checks.

- Add programKind interface with claudeProgram / plainProgram variants
- Add classifyProgram() smart constructor (parses once; trust downstream)
- buildLaunchCommand: switches on type, delegates to buildClaudeCommand or
  returns plain cmd unchanged
- buildClaudeCommand: no guards — the type makes invalid states
  unrepresentable (a plainProgram can never reach this function)
- Extract claudeMCPConfigFlag() helper for the MCP config flag string
- TestClassifyProgram: table test for the sum type classification
- TestBuildLaunchCommand_PlainProgramIgnoresClaudeFlags: proves that a
  non-claude program with all claude-related Instance fields set still
  returns the bare program, enforced by the type routing

* feat(backlog): implement CancelTriage RPC and session delete button

Adds CancelTriage endpoint that stops any active triage sessions for a
backlog item. Wires up the previously-TODO cancel button in
BacklogItemDetail and adds a per-session delete button in the session list.

* fix(install): skip FDA prompt for non-admin users with cert-signed binary

Non-admin users cannot read either TCC database (authorization denied),
causing fda_is_granted() to always return false and show the 15s prompt
on every reinstall even when FDA is already granted.

When all TCC databases exist but are unreadable, fall back to a heuristic:
if the installed binary is cert-signed (designated requirement includes
"certificate root"), assume FDA was previously granted. The TCC grant is
tied to the signing identity (com.stapler-squad + cert), which is stable
across rebuilds, so no new grant is needed on reinstall.

* perf(tmux): add semaphore to cap concurrent capture-pane subprocesses

capturePaneSem (size 8) limits concurrent CapturePaneContent calls to
avoid circuit-breaker lock contention and OS process table pressure.
Control-mode fast path bypasses the semaphore entirely.

* perf(vcs): cache reachableSet results and batch-read blobs under single lock

- reachableSetCache (sync.Map, 30s TTL) eliminates O(N) commit walk on
  repeated calls — was the #1 pprof hotspot (47.4B cycles, 38 events)
- diffShortstatUnderLock batch-reads all needed blobs in one lock hold,
  replacing N lock-acquire/release cycles — was the #2 hotspot (9.87B
  cycles, 1641 events)

* chore(proto): regenerate types bindings after rebase

Types were out of sync (DetectedStatus missing from Go/TS bindings)
after the CancelTriage commit was rebased onto upstream.

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* chore(demos): update E2E feature GIFs [skip ci]

* fix(terminal): repair escape code pipeline for new Claude Code renderer (#139)

* feat(onboarding): offer to install Claude Code hooks during onboarding

Adds a final onboarding step that asks whether to install the global
Claude Code hooks, with two independent toggles:
  - Rule enforcement (PreToolUse -> `ssq-hooks check`)
  - Notifications (Notification/Stop -> `ssq-hook-handler`)

Previously these hooks were discoverable only via docs / a manual
`ssq-hooks install` invocation; nothing prompted the user.

Backend:
- New internal/claudehooks package: idempotent, atomic install + detection
  of the two global hooks in ~/.claude/settings.json. cmd/ssq-hooks now
  reuses it (InstallRules) instead of its private patchClaudeSettings.
- New SessionService RPCs GetHookStatus and InstallHooks. InstallHooks
  resolves the ssq-hooks binary and ssq-hook-handler from ~/.local/bin
  (then $PATH / exe-relative scripts); when a binary is unavailable it
  returns a manual-fallback message rather than failing.
- `make install` now also copies ssq-hook-handler to ~/.local/bin so the
  server can register a stable path.

Frontend:
- OnboardingModal gains step 5: prefilled from GetHookStatus (a toggle is
  pre-checked only when its hook is available and not already installed),
  installs via InstallHooks, and disables toggles whose binary is missing.

Tests: unit tests for the package and the two handlers; Jest tests for the
onboarding step. Feature registry updated (GetHookStatus, InstallHooks,
onboarding-hook-install).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(onboarding): address review — concurrency, async guards, e2e

- claudehooks.mutate: serialize read-modify-write with a package mutex and
  write via a unique temp file (os.CreateTemp) so two concurrent installs
  (double-click) can't corrupt or clobber settings.json. Add a -race test.
- OnboardingModal: guard async setState with a mounted ref (removes the
  after-unmount update / act warning) and seed the toggle defaults only once
  so navigating Back→forward no longer discards the user's toggle edits;
  reset the seed guard on a fresh open.
- Jest: await the status fetch in gotoHooksStep to remove flakiness.
- Add Playwright e2e (tests/e2e/onboarding-hook-install.spec.ts) covering the
  hooks step render + finish-without-install (does not mutate global settings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(sdd): planning artifacts for new-renderer terminal fix

Research, implementation plan, adversarial/architecture reviews, validation
plan, and architecture-performance deep-dive for fixing escape code stripping
caused by the new Claude Code renderer.

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

* fix(terminal): repair escape code pipeline for new Claude Code renderer

The new Ink-based renderer emits escape sequences that exposed four latent
bugs in the terminal streaming pipeline, causing garbled output in xterm.js:

1. TextDecoder reuse without {stream:true}: multi-byte UTF-8 characters
   (é, €, CJK, emoji) split across consecutive proto frames emitted U+FFFD.
   Fix: StateApplicator and useTerminalStream now pass {stream:true} on
   all streaming decode calls; separate lineDecoder for complete line content.

2. EscapeSequenceParser lookback too short (20→256): OSC window titles and
   DCS payloads from the new renderer exceed 20 bytes, causing incomplete
   sequences to be flushed as garbage.

3. ED2+ED3 stripping: parser stripped \x1b[3J when paired with \x1b[2J,
   bleed-through of previous session history. xterm.js v6 handles this
   correctly without intervention.

4. RedrawThrottler over-classification: any \x1b[\d+A was treated as a
   full-screen redraw; Ink emits cursor-up on every incremental line
   update, causing most progress/spinner frames to be dropped.
   Fix: only classify cursor-up + erase-screen as a genuine redraw.
   Also: 100→33ms cap (30fps) to match Ink render cadence.

Adds 84 tests including a combined pipeline integration suite covering
the full TerminalDiff→StateApplicator→EscapeSequenceParser→TerminalStreamManager
chain.

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

* fix(terminal): address code review - decoder isolation, test ESC prefix, timer cleanup

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

* chore(registry): regenerate after merge with main

* fix(a11y): remove aria-selected from listitem div; aria-checked on checkbox is correct

* chore(registry): remove stale entries for RPCs removed from main

---------

Co-authored-by: Tyler Stapler <tystapler@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(bench): update go tier1 baseline [skip ci]

* chore(bench): update e2e latency baseline [skip ci]

* chore(bench): update frontend throughput baseline [skip ci]

* feat(rules): auto-suggest rule name from criteria inputs (#140)

* feat(rules): auto-suggest rule name from criteria inputs

Generates a "Allow/Block/Escalate {target}" name as the user fills
in tool target, category, pattern, or programs. The suggestion only
applies when the name field is empty or still matches the previous
auto-suggestion, so manual edits are never overwritten.

Also scopes golangci-lint to the current module root to avoid
scanning files in external workspace paths (../../../../../WorkProjects).

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

* fix(tests): resolve TypeScript errors in ArtifactsTab tests and tighten RuleBuilderForm auto-suggest

- Add makeArtifacts() cast helper in ArtifactsTab.test.tsx to satisfy
  protobuf Message<> type requirements without importing the full runtime
- Fix computeSuggestedName category branch: check cat existence, not
  cat?.value (avoids truthiness trap on empty-string values)
- Move nameRef sync to useLayoutEffect to avoid render-phase ref mutation
  in React concurrent mode

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

* fix: resolve CI failures in multib…
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.

2 participants