Skip to content

feat: display MCP servers in profile Statistics panel - #1

Closed
t1000040 wants to merge 81 commits into
mainfrom
feat/profile-mcp-servers
Closed

feat: display MCP servers in profile Statistics panel#1
t1000040 wants to merge 81 commits into
mainfrom
feat/profile-mcp-servers

Conversation

@t1000040

@t1000040 t1000040 commented Jun 5, 2026

Copy link
Copy Markdown

Summary

Collect MCP server names from local config during CLI submit and display them as badges in the Statistics panel on user profile pages, below the existing Clients section.

Changes

CLI (Rust)

  • Add mcp module to tokscale-core that discovers MCP server names from Claude, Cursor, and OpenCode configs
  • Include mcp_servers field in the submit payload

Backend (Next.js API)

  • Add mcp_servers JSONB column to submissions table (migration 0013)
  • Parse and store MCP servers on submit
  • Return mcpServers in the user profile API response

Frontend

  • Display MCPs as badges in the Statistics panel (below Clients)
  • Thread mcpServers prop through ProfileActivity → GraphContainer → StatsPanel

Misc

  • Fix leaderboard hydration issue (mounted check)
  • Fix profile Last Updated timezone to UTC

junhoyeo and others added 30 commits May 26, 2026 11:50
The Rust CLI and core changes need to compile cleanly while keeping Trae account sync idempotent and daily active-time aggregation aligned to local calendar days.

Constraint: Trae reports account-level sessions from a synced cache rather than root-command live reads.
Rejected: Keep append-only Trae manifests | repeated syncs would duplicate the same sessions and inflate reports.
Confidence: high
Scope-risk: moderate
Tested: cargo fmt --check; targeted Trae manifest, Trae dedupe, and local active-time tests
Not-tested: Full native-target release matrix
Group invite creation should treat malformed non-empty JSON as a client error while preserving the empty-body default invite flow.

Constraint: Empty request bodies are a supported shorthand for default invite settings.
Rejected: Treat every JSON parse failure as an empty object | malformed payloads would be silently accepted.
Confidence: high
Scope-risk: narrow
Tested: bunx vitest run __tests__/api/groupInviteCreateRoute.test.ts
Not-tested: Browser-level invite form submission
Leaderboard URLs should survive server reloads with validated custom date/search parameters, and the groups browser should load additional pages without replacing the current tab state.

Constraint: Invalid custom date ranges must fall back safely instead of leaking raw URL values into client state.
Rejected: Client-only date validation | shared URLs would still hydrate from inconsistent server data.
Confidence: high
Scope-risk: moderate
Tested: bunx vitest run __tests__/api/leaderboard.test.ts __tests__/lib/leaderboardDateRange.test.ts
Not-tested: Manual browser click-through for group load-more pagination
The READMEs should advertise the currently supported Trae sync flags and include Zed and Kiro wherever client filters and support tables enumerate available clients.

Constraint: Trae variant selection is scoped to credential login/logout, not sync.
Rejected: Leave translated READMEs stale | users would copy unsupported `trae sync` flags.
Confidence: high
Scope-risk: narrow
Tested: grep for stale Trae sync flags and updated client-filter lists
Not-tested: Rendered README table layout in GitHub UI
The leaderboard API route should let Next.js handle request URL dynamic detection instead of catching the framework signal as an application error during production builds.

Constraint: The route still needs normal application errors to return the existing JSON 500 response.
Rejected: Force the route fully dynamic | moving request URL parsing outside the catch avoids the build-time false error without changing cache policy.
Confidence: high
Scope-risk: narrow
Tested: bun run build
Not-tested: Manual API request in a deployed Next runtime
The Trae implementation now uses IDE and Solo only as credential sources while storing synced account-level usage under the single `trae` client, so the module comments should describe that boundary directly.

Constraint: Trae IDE and Solo share the international account-level usage API.
Rejected: Reintroduce per-variant report clients | that would duplicate the same account usage when both desktop apps are installed.
Confidence: high
Scope-risk: narrow
Tested: cargo fmt --check
Not-tested: Live Trae account sync
The users/groups segmented links should switch only the leaderboard view while carrying the active date, sort, and search filters forward; page is intentionally dropped because pagination state is scoped to each view.

Constraint: The view selector is link-based SSR navigation, so the href must be computed from server search params.
Rejected: Keep hardcoded links | toggling views would silently reset shared leaderboard URLs.
Confidence: high
Scope-risk: narrow
Tested: bunx vitest run __tests__/lib/leaderboardViewSelector.test.ts
Not-tested: Manual browser click-through
Malformed JSON was rejected, but valid JSON primitives could still reach invite field extraction; null could throw and arrays or strings would silently behave like default payloads.

Constraint: Empty bodies must keep creating a default member invite.
Rejected: Coerce non-object JSON to `{}` | that would hide bad client payloads and weaken the malformed-body guard.
Confidence: high
Scope-risk: narrow
Tested: bunx vitest run __tests__/api/groupInviteCreateRoute.test.ts
Not-tested: Browser form submission with a corrupted request body
The custom date range parser already validates both endpoints before comparing the range, so the second identical validation check after the comparison was unreachable defensive noise.

Confidence: high
Scope-risk: narrow
Tested: bunx vitest run __tests__/lib/leaderboardDateRange.test.ts
Not-tested: Manual custom date filter entry
Repeated overlapping Trae syncs can fetch older copies of sessions that do not win the manifest merge. Delaying the batch write until after the merge avoids creating an artifact that the same GC pass immediately removes, and millisecond filenames reduce same-second overwrite risk for artifacts that do win.

Constraint: Trae sync stores one JSON artifact per fetched batch and uses the manifest as the referential-integrity index.
Rejected: Keep writing every fetched batch | losing batches remain unreferenced and are intentionally removed by manifest GC.
Confidence: high
Scope-risk: narrow
Tested: cargo fmt --check
Tested: cargo test -p tokscale-cli test_manifest_reference
The active-time splitter intentionally uses local calendar days, matching the date filter semantics. Document the v2.2.0-visible alignment and leave an inline note for the DST gap path where a local midnight cannot be represented.

Constraint: Existing date filtering docs already state local-time semantics, but active-time cache/readers may have assumed UTC day keys.
Rejected: Revert to UTC buckets | that would reintroduce mismatch with local report dates.
Confidence: high
Scope-risk: narrow
Tested: cargo fmt --check
Tested: cargo test -p tokscale-core test_compute_daily_active_time_matches_local_day_boundaries_for_fixed_offset
Local main carries five fixes from the prior audit cycle:
- 013552e fix(leaderboard): preserve filters across view toggles
- 6d0fe74 fix(groups): reject non-object invite payloads
- 1a1efd8 refactor(leaderboard): remove duplicate date validation
- 8e5778d fix(cli): write Trae sync artifacts only when referenced
- 1ac617e docs(cli): document local active-time day buckets

Incoming from origin:
- 9228a8a ci: update coverage badge [skip ci] (49% -> 47%)

No content overlap: origin only touches .github/badges/coverage.svg, which
none of the local commits modify. Three-way merge resolves automatically by
taking origin's badge update verbatim. Verified pre-merge with
`git merge-tree` against the divergence base c9e051a; no conflict markers
emitted.

Constraint: keep the audited fix chain intact while picking up the bot-pushed badge
Rejected: rebase onto origin/main | user asked for a merge commit, and rebasing the badge bot's commit onto local would orphan its [skip ci] semantics
Confidence: high
Scope-risk: very narrow (one file changes, mechanical merge)
Validation

* Validation tier: Tier 4 - CI/test tooling, because this adds a focused frontend CI workflow plus migration replay verification without changing runtime behavior.

* bun install --frozen-lockfile: PASS

* bun run --cwd packages/frontend test: PASS, 29 files and 251 tests passed.

* DATABASE_URL=postgres://postgres:postgres@localhost:<docker-port>/tokscale_ci NODE_ENV=test bun run --cwd packages/frontend test:migrations: PASS against postgres:16, migrations applied and schema smoke checks passed.

* ruby -e 'require "yaml"; YAML.load_file(".github/workflows/frontend_ci.yml"); puts "yaml ok"': PASS

* perl -ne 'print if /[\x{0400}-\x{04FF}]/' .github/workflows/frontend_ci.yml packages/frontend/package.json packages/frontend/scripts/check-migrations.ts: PASS, no output.

* git diff --check: PASS

* git diff --cached --check: PASS

* Ledger: not applicable - not required for selected validation tier/change family.

* Version: not applicable - not required for selected validation tier/change family.

* Non-gating observation: bun run --cwd packages/frontend lint currently fails on pre-existing unrelated app lint errors outside this diff; the new workflow does not introduce lint as a frontend gate.

* Not run: actionlint - not installed locally.

Rollback

* git revert HEAD
Validation
* Validation tier: Tier 2 - narrow runtime change, route-level permission check for group invite creation.
* bun x vitest run __tests__/api/groupInviteRoute.test.ts: PASS after fix; RED before fix failed as expected with admin-created admin invite returning 201 instead of 403.
* bun x vitest run __tests__/api/groupInviteRoute.test.ts __tests__/api/groupInviteJoinRoute.test.ts __tests__/lib/groupInvites.test.ts __tests__/lib/groupHelpers.test.ts: PASS, 14 tests.
* bun x vitest run __tests__/api/group*.test.ts __tests__/lib/group*.test.ts: PASS, 20 tests.
* bun x eslint __tests__/api/groupInviteRoute.test.ts 'src/app/api/groups/[slug]/invite/route.ts': PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.
* Not required: full frontend lint/typecheck for this targeted route change; when probed, bun run lint and bun x tsc -p tsconfig.json --noEmit fail on unrelated existing frontend issues outside this diff.

Rollback
* git revert HEAD
Validation

* Validation tier: Tier 2 - narrow frontend submit validation runtime change.

* bun install --frozen-lockfile: PASS.

* bun x vitest run __tests__/api/submit.test.ts: PASS, 37 tests. RED step before implementation failed on the new high-volume acceptance test as expected.

* bun run --cwd packages/frontend lint -- src/lib/validation/submission.ts __tests__/api/submit.test.ts: PASS.

* git diff --check: PASS.

* git diff --cached --check: PASS.

* Ledger: not applicable - not required for selected validation tier/change family.

* Version: not applicable - not required for selected validation tier/change family.

* Additional check: bun x tsc -p packages/frontend/tsconfig.json --noEmit reported a pre-existing unrelated packages/frontend/src/components/BlackholeHero.tsx PNG module resolution error, left untouched.

* Not run: full frontend/backend suites - not required for selected validation tier.

Rollback

* git revert HEAD
* fix(submit): adopt legacy rows for first device submit

Validation
* Validation tier: Tier 3 - submit data-integrity change; the diff changes how legacy daily_breakdown rows are attributed during the device-aware submit transition.
* git diff --check: PASS
* git diff --cached --check: PASS
* bun x vitest run __tests__/api/submitAuth.test.ts: PASS (7 tests)
* bun x vitest run __tests__/api/submitAuth.test.ts __tests__/api/usersProfile.test.ts: PASS (13 tests)
* bun x vitest run __tests__/api/submitAuth.test.ts __tests__/api/usersProfile.test.ts __tests__/lib/getLeaderboard.test.ts __tests__/lib/getGroupLeaderboard.test.ts: PASS (22 tests)
* bun x vitest run __tests__/lib/getLeaderboardAllTime.test.ts: PASS (4 tests)
* bun run lint -- src/app/api/submit/route.ts __tests__/api/submitAuth.test.ts __tests__/api/usersProfile.test.ts: PASS
* bash scripts/check-version-coherence.sh: PASS (Version coherence OK: 2.1.3)
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: PASS, bash scripts/check-version-coherence.sh
* Not run: full frontend suite and Rust suite - not required for selected validation tier; targeted submit/profile/leaderboard coverage exercises the changed write path and public totals.

Rollback
* git revert HEAD

* test(submit): cover legacy rows after modern device submit

Add a regression test for the device-aware submit cutover path where legacy daily rows remain separate after another modern device has already submitted.

Constraint: Preserve legacy daily rows when modern-device attribution is ambiguous

Confidence: medium

Scope-risk: narrow

Not-tested: DB-backed legacy adoption with real Postgres rows

---------

Co-authored-by: Junho Yeo <i@junho.io>
Preserve same-device totals when parser regressions produce lower token reports.
Commit the release provenance manifests before package publishing so npm provenance matches the repository state.
Honor explicit --home diagnostics paths without leaking host environment roots.
Include Hermes profile databases configured through scanner extra paths.
Record the repository rule that automated commits must use Junho Yeo's maintainer identity and that pull requests should be squash-merged with conventional titles, so future agent sessions do not leak worker identities into history.

Constraint: User explicitly required this rule to be captured in AGENTS.md after worker identity leaked into a PR branch commit.
Rejected: Leave the rule only in conversation context | future sessions would not inherit it.
Confidence: high
Scope-risk: narrow
Tested: git config user.name and user.email verified before commit.
Not-tested: No code tests run; documentation-only change.
Surface Cursor setup diagnostics consistently for explicit Cursor reports and add coverage for the warning paths.
…ium-tool-call` (junhoyeo#612)

* fix(submit): bypass cost-without-tokens check for Cursor legacy premium-tool-call

Cursor's pre-2025-05 usage exports include `premium-tool-call` rows that are billed per tool invocation and carry no token attribution at all (input/output/cache columns are empty). PR junhoyeo#557 introduced a sanity check that rejects any submission where cost > 0 and tokens === 0, which permanently locks out any user with historical Cursor data — even a few cents of legacy tool-call charges block the entire upload (we observed a real submission where ~$45 of premium-tool-call rows blocked $45,618 of otherwise valid data, ~0.1% of the payload).

Allow `cursor/premium-tool-call` specifically by adding a single legacy carve-out. The check still fires for every other `(client, model)` combination, including other cursor models, so genuine parser regressions remain visible.

Also enrich the cost-without-tokens error messages so operators can read the offending row directly out of the CLI output:
- Client-level errors now include the client, modelId, providerId, full cost (`cost=$X.XXXX`), and the full token breakdown (`input/output/cacheRead/cacheWrite/reasoning`).
- Day-level errors list which clients on that day were responsible (`offending clients: cursor/premium-tool-call (provider=cursor) cost=$2.0500; ...`).
- Submission-summary errors include the same per-client offender list.

Constraint: cannot drop Cursor `premium-tool-call` rows in the CLI parser because their cost is a legitimate spend the user wants reflected in totals.
Rejected: Drop tokenless rows in the CLI scanner | loses real cost attribution that the leaderboard depends on.
Rejected: Loosen the check globally (cost > 0 && tokens === 0 → warning) | re-opens the original PR junhoyeo#557 vulnerability (implausible cost-only submissions).
Confidence: high
Scope-risk: narrow
Directive: Only add new entries to CURSOR_LEGACY_TOKENLESS_MODELS for Cursor billing events that legitimately lack token attribution — never for parser bugs (fix the parser instead).
Not-tested: A Cursor user whose entire history is exclusively `premium-tool-call` (summary-level legacy subtraction path is exercised only by unit tests, not by an end-to-end submit).

* fix(submit): exclude Cursor legacy cost from cost-per-million sanity cap

Address Codex review (P1) on PR junhoyeo#612: when a legacy `cursor/premium-tool-call` row shares a day with a small amount of token-bearing usage, the day-level branch falls through to the cost-per-million check because `day.totals.tokens > 0`. Using the full day cost (legacy + real) as the numerator meant tiny token counts tripped the $10k/M ceiling even though the legacy row is supposed to be skipped — e.g. $2.05 in legacy tool calls plus 100 normal tokens computed $20,600/M and was rejected.

Subtract the legacy tokenless Cursor cost before applying the cost-per-million cap at both day and summary levels (was previously only subtracted for the cost-without-tokens branch). The cost-without-tokens branch behavior is unchanged because both branches now read from the same `checkableCost` value.

Add a regression test (`excludes cursor legacy cost from the cost-per-million sanity cap`) that pins the exact mixed-day shape from the review.

Confidence: high
Scope-risk: narrow
Directive: Any future cost-related sanity check on day/summary aggregates must also subtract `legacyCost` first — wire it off the same `checkableCost` local instead of recomputing.

* fix(submit): use float-epsilon when subtracting Cursor legacy cost

Address cubic review (P2) on PR junhoyeo#612: the new legacy-cost subtraction used strict `> 0` float comparison at both the day and summary cost-without-tokens checks. Floating-point summation residue could trip the check on a valid all-legacy submission — e.g. `0.1 + 0.2 === 0.30000000000000004` while IEEE `0.3 ≈ 0.299999999999999988`, so `totalCost - legacyClientCost ≈ 5.5e-17 > 0` even though the user truly has $0.30 of legacy cost and nothing else.

Introduce `LEGACY_COST_FLOAT_EPSILON = 1e-6` and use it as the threshold on both check sites. 1e-6 is well below any realistic LLM charge, so any legitimate non-legacy cost still trips the check; only FP rounding noise is absorbed.

Add a regression test (`tolerates floating-point residue when subtracting cursor legacy cost`) that constructs the exact `0.1 + 0.2` vs `0.3` scenario and asserts the residue is positive but the submission still validates.

Confidence: high
Scope-risk: narrow
Directive: Any cost-vs-cost float comparison on aggregated sums should use LEGACY_COST_FLOAT_EPSILON (or a comparable epsilon) — strict `> 0` is a footgun on summed IEEE 754 values.
…pagination drift, CSRF origin gate, CI coverage floors (junhoyeo#611)

* fix(submit,db): close data-integrity gaps from post-batch audit

- helpers.ts: regression guard now triggers on token decrease alone,
  removing the dead hasLowerCoverage AND-clause that let parser
  regressions through whenever message/model counts stayed equal
- submit/route.ts: legacy-row adoption UPDATE is now race-safe via a
  NOT EXISTS guard + try/catch fallthrough so concurrent submits cannot
  violate the (submission_id, submitted_device_id, date) unique constraint
- submit/route.ts: LEGACY_DEVICE_KEY now sourced from devices/shared.ts
  instead of a duplicate local constant
- validation/submission.ts: MAX_DAILY_TOKENS lowered back to 10B (from
  the 10x widening in f6aeca7); added WARN_DAILY_TOKENS soft band at 5B
  with a structured warn so legitimate high-volume users still pass
- validation/submission.ts: removed provenance from submission hash
  inputs so resubmits stay idempotent across schemaVersion-2 clients
- settings/devices/[deviceId]/route.ts: cache invalidation now goes
  through normalizeUsernameCacheKey, matching submit/route.ts
- users/[username]/devices/[deviceId]/route.ts: per-device totals are
  now aggregated in SQL instead of pulling every daily_breakdown row
  into JS and summing client-side
- submitAuth.test.ts + submit.test.ts: removed stale buildModelBreakdown
  mocks (export was dropped by 157ab04) and updated three submission-cap
  tests for the new 10B threshold
- helpers.test.ts (new): five regression tests for the guard, including
  the equal-coverage + lower-tokens case that previously slipped
- 0011_drop_dead_columns.sql: added comment block documenting the
  multi-table ACCESS EXCLUSIVE lock window for future migrators
- AGENTS.md: new "Migration journal hygiene" section forbidding
  hand-edited _journal.json timestamps

Constraint: 0011 already deployed; cannot retroactively split it
Constraint: Existing schemaVersion-2 hashes were never persisted as
  idempotency keys, so removing provenance is a no-op on stored data
Rejected: drizzle onConflictDoNothing on the UPDATE | drizzle pattern
  does not apply to UPDATE; used NOT EXISTS subquery instead
Rejected: Keep MAX_DAILY_TOKENS at 100B with raised cost ceiling |
  10x larger abuse window without proportional sanity check
Confidence: high
Scope-risk: moderate
Directive: When adding a new field to UnifiedMessage or
  ClientBreakdown, audit every test-helper constructor and the
  submission-hash inputs together — they are coupled by design
Not-tested: Postgres-backed concurrent legacy-adoption race under
  real load (only modelled in unit tests)

* fix(groups): close invite-acceptance privilege escalation and harden routes

- invites.ts: acceptGroupInvite now re-checks the inviter's CURRENT
  role inside the transaction via canManageGroupRole. Without this,
  Alice could promote Bob to admin, Bob could mint an admin invite for
  Carol, Alice could demote Bob, and Carol would still land as admin
  on accept. Now: the accept fails with forbidden if the inviter has
  been demoted below the invite's role.
- groups/route.ts, groups/[slug]/route.ts, role/route.ts: payload
  guard (null / non-object / array -> 400) consistent with the
  invite-create route from 6d0fe74. Eliminates the 500-on-bad-payload
  class of bug across all mutating group routes.
- groups/route.ts: new URL(request.url) hoisted above the GET try
  block so Next.js dynamic-rendering bailout no longer gets swallowed
  (same anti-pattern e683f62 fixed on the leaderboard route)
- invite/route.ts: Cache-Control: no-store, private on the 201
  response so raw invite tokens cannot be cached by browsers,
  reverse proxies, or error trackers
- requestSession.ts: cookie-based sessions on POST/PUT/PATCH/DELETE
  now reject when Origin is set and not in CSRF_ALLOWED_ORIGINS
  (defaults to https://tokscale.dev + http://localhost:3000). Bearer
  token clients (mobile, CLI) bypass the check entirely.
- transfer-ownership/route.ts (new): owners can now hand off ownership
  atomically. Validates caller is owner, target is a non-owner member,
  and runs the dual role update in one transaction. leave/route.ts
  and role/route.ts error messages now point users at this endpoint.
- groupInvitesDemotedInviter.test.ts (new): four-case coverage for
  the inviter re-check
- groupsPayloadGuard.test.ts (new): 3 routes x 3 bad payload shapes
- groupTransferOwnership.test.ts (new): six scenarios including the
  atomic-update success path
- requestSessionCsrf.test.ts (new): eight scenarios pinning the
  origin allowlist + Bearer bypass
- groupInviteCacheControl.test.ts (new): no-store + member->member
  and member->admin rejection pinning

Constraint: Bearer-token clients must keep working without CSRF
Constraint: ownership transfer must be atomic (no
  zero-owner-window even on partial failure)
Rejected: server-side full CSRF token pattern | overkill for an
  Origin-vs-allowlist surface; Bearer clients already exempt
Rejected: include UI prompt for transfer-ownership | UI work is
  out of scope for this PR; endpoint ships first
Confidence: high
Scope-risk: moderate
Directive: When changing canManageGroupRole's comparator (e.g. >
  to >=), add explicit member->member and admin->admin tests so the
  redundant member-role check in invite/route.ts does not silently
  start passing
Directive: CSRF_ALLOWED_ORIGINS env var must be set in any new
  deployment environment; do not ship without it
Not-tested: real demoted-inviter race against acceptGroupInvite
  under concurrent role changes (unit tests model this but no integration test)

* fix(leaderboard): stabilize pagination and preserve filters across views

- queries.ts: listPublicGroups and listUserGroups now order by
  updatedAt DESC, id DESC. Without the secondary sort, "Load more"
  duplicated or dropped rows whenever updatedAt tied or shifted
  between page fetches.
- LeaderboardClient.tsx: the replaceState URL effect now reads and
  preserves view= from useSearchParams. Previously the rebuilt URL
  dropped view entirely, which was safe today only because the
  default view was "users" — adding any new view (e.g. by-client)
  would have silently rebounced the user back to users on every
  state change.
- ViewSelector.tsx: buildLeaderboardViewHref now strips from/to
  unless period === "custom". Switching from a custom-period users
  view to groups and back no longer carries stale date params into
  non-custom views.
- ViewSelector.tsx: added a sr-only aria-live="polite" announcement
  so AT users hear which leaderboard view is active after a switch
- GroupsBrowser.tsx: per-tab AbortController via a ref. Rapid tab
  toggles between Public / Mine no longer let a late response stomp
  the active tab's data. handleTabChange also skips the redundant
  page-1 refetch when initial SSR data is still valid.
- dateRange.ts: one-line comment pinning why lexicographic from > to
  is safe (isValidDateString enforces YYYY-MM-DD)
- groupsQueries.test.ts (new): three tests verifying stable two-column
  sort on both listPublicGroups and listUserGroups across paginated
  fetches with tied updatedAt values

Constraint: GroupsBrowser tab refetch must not refresh on initial
  mount when SSR data is still valid (cheaper, but means "Mine" tab
  is stale until the user paginates)
Rejected: aria-live="assertive" for view change | too disruptive
  for navigational announcements; polite is the right register
Rejected: Add from/to validation to ViewSelector | already enforced
  upstream in dateRange.ts; would duplicate the validator that
  1a1efd8 just removed
Confidence: high
Scope-risk: narrow
Directive: GroupsBrowser AbortController ref must be reset on
  unmount AND on tab change; the cleanup in useEffect handles
  unmount only — do not remove the manual abort in handleTabChange
Not-tested: real Chrome refresh-on-focus behavior with the
  page-1 SSR skip in place

* chore(ci,cli): enforce coverage floors, pin supply chain, atomic Trae writes

- vitest.config.ts: coverage block with v8 provider + 40% floor for
  lines/functions/branches/statements. Previously no coverage was
  collected on the frontend at all, so PRs deleting tests merged green.
- test_coverage.yml: cargo tarpaulin now runs with --fail-under 45
  so coverage regressions break the build instead of silently
  dropping the badge percentage
- test_coverage.yml: emibcn/badge-action pinned to
  f6f18dd5b9f95a97e6bb87de327f3e12f4b5e462 (v2.0.4) so a hijacked
  mutable tag cannot push to main via the workflow's contents:write
- check-migrations.ts: now rejects duplicate idx values and
  non-contiguous sequences in drizzle/meta/_journal.json. Heuristic
  warn when a single migration file contains CREATE INDEX on more
  than one table (drizzle wraps each migration in one tx; multi-
  table index builds hold ACCESS EXCLUSIVE across all of them).
- trae.rs: artifact write is now tempfile-plus-rename, POSIX-atomic.
  Concurrent sync passes hitting the same millisecond filename can
  no longer half-truncate a file that the manifest already references.
- trae.rs: credential-label doc clarified — cache filenames are
  fixed (credentials-solo.json / credentials-ide.json), not derived
  from client_str()
- scanner.rs: tracing::warn! when a TOKSCALE_EXTRA_DIRS or
  ScannerSettings extra path escapes $HOME. Liveness test confirms
  the scan still completes (warn-only, never block).

Constraint: tarpaulin --fail-under threshold is set 2 points below
  the current real coverage so this PR itself does not fail CI;
  tighten when coverage rises
Constraint: vitest threshold is conservative (40%) for the same
  reason — raise it as the suite grows
Rejected: pin badge-action to a Dependabot-tracked floating tag |
  defeats the supply-chain pin; the SHA is the safety
Rejected: use std::fs::File::create_new + manual rename | OK on
  POSIX but ugly cross-platform; tempfile-like pattern is shorter
  and equivalent here
Confidence: high
Scope-risk: narrow
Directive: When adding a new GitHub Action third-party dep, pin
  to a commit SHA with a trailing comment of the version, never
  to a moving tag (especially for any action that has write
  scopes on main)
Directive: When adding a new migration, run scripts/check-migrations.ts
  locally first — the multi-CREATE-INDEX warn now flags the lock
  pattern that bit 0011
Not-tested: actual coverage threshold breach in CI (would require
  intentionally lowering coverage, not done)

* fix(ci): correct emibcn/badge-action SHA pin

The SHA in c79881a (f6f18dd5...) did not exist on emibcn/badge-action,
so the Code Coverage job failed at "Prepare all required actions" with
"Unable to resolve action ... unable to find version".

Real SHA for v2.0.4 is f9150fde070fcca0c4e832437611b44838fcd325,
verified via `git ls-remote https://github.com/emibcn/badge-action
refs/tags/v2.0.4`.

Constraint: Must stay pinned to a commit SHA, not a moving tag
Confidence: high
Scope-risk: narrow
Directive: When pinning a third-party action, always verify the
  resolved SHA with `git ls-remote ... refs/tags/<tag>` before
  pushing — do not type the SHA from memory

* fix(groups,ci): resolve PR junhoyeo#611 review feedback

- transfer-ownership/route.ts: caller and target UPDATEs are now
  predicate-guarded inside the transaction (role = 'owner' on the
  caller demotion, role != 'owner' on the target promotion). Without
  this, two concurrent POSTs from the same owner to different targets
  could both pass the pre-tx owner check and both commit, leaving
  multiple owners. The new guards make whichever transaction commits
  second match zero rows and abort with HTTP 409. Resolves
  chatgpt-codex-connector P2 on PR junhoyeo#611.
- groupTransferOwnership.test.ts: added `ne` to the drizzle-orm mock
  (route now imports it) and two race-loss regression tests covering
  the caller-demoted-by-other-tx and target-already-promoted paths.
- test_coverage.yml: reverted emibcn/badge-action back to the
  @v2.0.4 tag. The repo has no Dependabot/Renovate automation, so
  SHA pins stagnate without recourse — and the rest of the
  workflow uses tags. Resolves cubic-dev-ai P2 on PR junhoyeo#611.

Constraint: Postgres unique constraint on groupMembers is (groupId,
  userId), not (groupId, role='owner'), so the only enforcement of
  the single-owner invariant is at the application layer
Constraint: ne() requires drizzle-orm >= 0.30 (already on this branch)
Rejected: SELECT ... FOR UPDATE on the caller's row | adds round-trip
  latency; predicate-guarded UPDATE achieves the same atomicity
Rejected: Keep SHA pin and ask the team to add Dependabot | scope
  creep; tag was the existing convention
Confidence: high
Scope-risk: narrow
Directive: When adding a third-party GitHub Action to this repo,
  use @vX.Y.Z tags, not SHAs — there is no Dependabot here
Directive: Any future write to groupMembers.role must run inside a
  transaction with a predicate guard if it affects the single-owner
  invariant
Not-tested: real Postgres-backed concurrent transfer race under load

* fix: round-2 PR junhoyeo#611 review feedback from cubic AI

Resolves nine threads opened against commit 1be9af0:

P1 fixes:
- submit/route.ts: legacy-adoption UPDATE now runs inside a nested
  drizzle transaction (Postgres SAVEPOINT). When a concurrent submit
  triggers a unique-constraint violation the savepoint rolls back
  instead of poisoning the outer tx, so the fall-through fetch and
  insert path stays usable.
- invites.ts: inviter membership SELECT is now FOR UPDATE so a
  concurrent role change between the read and the membership INSERT
  cannot escalate the new member to a role the (now-demoted)
  inviter can no longer grant.
- requestSession.ts: cookie-authenticated mutating requests now
  reject when the Origin header is MISSING, not just when it's
  outside the allowlist. Modern browsers always set Origin on
  cross-origin mutations; absence is treated as untrusted (non-
  browser clients should authenticate with a Bearer token).
- transfer-ownership/route.ts: confirmed already covered — the
  predicate-guarded UPDATEs in 1be9af0 (role = 'owner' on the
  caller, role != 'owner' on the target) plus the !callerRow /
  !targetRow throw → 409 path satisfy both "re-check ownership
  inside the tx" and "validate both rows before returning."

P2 fixes:
- GroupsBrowser.tsx: per-tab AbortController via a Map ref. Aborted
  requests no longer leave a tab stuck with loading=true, and same-
  tab Load More clicks cancel the previous fetch without disturbing
  the other tab's request.
- check-migrations.ts: regex now handles schema-qualified table
  names ('ON public.users') correctly, including quoted identifiers.
  File-read catch block now throws on missing files (ENOENT) and
  surfaces every other errno instead of silently skipping.

P3 fixes:
- trae.rs: credential cache directory doc no longer claims a hard-
  coded ~/.config path. It now references paths::get_config_dir(),
  TOKSCALE_CONFIG_DIR, and XDG resolution.

requestSessionCsrf.test.ts: flipped the "no Origin allowed" case to
"no Origin rejected" to match the stricter posture.

Constraint: drizzle nested transactions must map to a SAVEPOINT,
  not a new connection — verified against the drizzle-orm docs
Constraint: Stricter CSRF check assumes Bearer clients exist for
  every non-browser caller; confirmed CLI + mobile + service
  workers all use Authorization headers
Rejected: Wrap the legacy adoption UPDATE in raw SAVEPOINT/RELEASE
  SQL | drizzle's tx.transaction() abstracts this and gives a
  proper rollback path on throw
Rejected: Per-tab loading via global state | per-tab refs match
  the per-tab data state already in this component
Confidence: high
Scope-risk: moderate
Directive: Any cookie-authenticated mutating route added in this
  repo must keep going through getSessionFromRequest — do not
  read raw cookies, or the CSRF Origin gate is bypassed
Directive: When adding a new migration with multi-table CREATE
  INDEX, expect the check-migrations heuristic to warn — silence
  it by splitting the migration, not by relaxing the regex
Not-tested: real Postgres-backed savepoint-rollback under genuine
  concurrent constraint violation
Not-tested: browser tab switch with a slow network and rapid
  toggling — only modeled in vitest with abort mocks

* fix(groups): use post-lock timestamp for invite expiry check

Cubic flagged that `acceptedAt = new Date()` was captured before the
transaction started, so the FOR UPDATE lock acquisition window could
push the actual wall-clock time past `invite.expiresAt` while the
stored timestamp stayed in the past — letting an effectively-expired
invite pass `gt(expiresAt, acceptedAt)` and be accepted.

Moved the `new Date()` capture to AFTER the FOR UPDATE returns, so
the value reflects wall-clock time at the moment the row is locked.
The expiry comparison and the audit-trail `accepted_at` column now
use the same fresh timestamp.

Tests:
- groupInvites.test.ts + groupInvitesDemotedInviter.test.ts: added
  `.for("update")` to the mock chain so `tx.select().from()
  .where().limit().for("update")` resolves the inviter rows.

Constraint: Drizzle's FOR UPDATE binding waits on the lock, which
  can take arbitrary time in production; the timestamp must be
  re-read after the lock is held
Rejected: Use Postgres `clock_timestamp()` in the WHERE predicate
  | drizzle's expression builder is cleaner; equivalent semantics
Confidence: high
Scope-risk: narrow
Directive: When any future code adds a FOR UPDATE / NOWAIT / SKIP
  LOCKED clause, capture wall-clock-sensitive comparisons AFTER
  the lock has been acquired
Not-tested: real Postgres-backed slow-lock-acquisition race

* test(submit): mock tx.transaction for the savepoint added in e64e3c7

CI vitest started failing on e64e3c7 because the savepoint wrap
(`tx.transaction(async (sp) => sp.execute(...))`) hit a missing
mock method — local `tx` literals in submitAuth.test.ts did not
expose `.transaction()`. The exception was swallowed by the
outer try/catch and the tests counted 0 execute calls instead of
the expected legacy-adoption path.

Added a `transaction` field on each local `tx` mock that just
invokes the callback with the same `tx`, so calls inside the
savepoint still count toward `tx.execute` / `tx.update` and the
existing assertions hold.

Confidence: high
Scope-risk: narrow
Directive: When a Drizzle production code path adds a nested
  transaction (savepoint), the corresponding test must mock the
  inner `transaction` method as a passthrough, otherwise the
  invariants the test enforces silently disappear
Not-tested: real Postgres savepoint semantics (the mock is a
  passthrough — only the call counts are verified)
…y constant (junhoyeo#613)

The TUI cache reader (`tui::run` in `tui/mod.rs`) loaded the cache with the hard-coded `GroupBy::Model` while `run_warm_tui_cache` in `main.rs` — fired as a detached subprocess after every successful `tokscale submit` — wrote the cache with `GroupBy::default()` (= `GroupBy::ClientModel`). `load_cache` does a strict inequality check on the cached vs. requested `group_by`, so the two never matched and every submit silently invalidated the next TUI launch's cache. The "show cached data immediately while a fresh load runs in the background" contract never triggered for any user who had ever run `tokscale submit`.

Empirically reproduced on a real user cache:
- On-disk `groupBy`: `"client,model"` (written by warm-tui-cache)
- TUI reader compared against: `"model"`
- Result: `CacheResult::Miss`, 266 days / 2,346 hourly / 117 models of valid cached data dropped on every launch

Introduce `tui::cache::TUI_DEFAULT_GROUP_BY: GroupBy = GroupBy::Model` as a single source of truth and route every TUI-cache-touching site through it:
- `tui::mod.rs` cache lookup
- `tui::app.rs` runtime `App.group_by` default
- `main.rs::run_warm_tui_cache` save (the bug site — was using `GroupBy::default()`)

The value matches the prior TUI runtime default (`Model`), so there is no user-visible presentation change — only the writer's key gets corrected.

Add two regression tests in `tui::cache::tests`:
- `warm_cache_round_trip_under_canonical_key_is_fresh` — save with `TUI_DEFAULT_GROUP_BY`, load with `TUI_DEFAULT_GROUP_BY`, must be `Fresh` (not `Miss`).
- `pre_fix_writer_key_misses_under_canonical_reader_key` — saves with `GroupBy::default()` and asserts the reader (using `TUI_DEFAULT_GROUP_BY`) returns `Miss`. Documents the historical bug as a frozen assertion: any future re-introduction of `GroupBy::default()` at a TUI cache write site is caught immediately.

Other `GroupBy::default()` sites in `main.rs` (CLI subcommand loaders at 2407, 2706, 4341, 4466, 4668) are intentionally left alone — they're report-computation defaults, not TUI cache writes, and changing them would alter unrelated user-visible CLI output.

Constraint: must not change the default TUI presentation (Models tab grouping stays "by model").
Constraint: must not require existing users to delete their on-disk cache — the first launch after upgrade will still Miss (cache key was "client,model", reader now still wants "model"), but the very next BG load writes "model" and the cache self-heals from there.
Rejected: change `tui::mod.rs` reader + `app.rs` runtime to `GroupBy::default()` | user-visible presentation change (Models tab default flips from `model` to `client,model`).
Rejected: change `impl Default for GroupBy` to return `Model` | broader blast radius — affects every `GroupBy::default()` call site in CLI subcommand loaders.
Confidence: high
Scope-risk: narrow
Directive: never key the TUI cache on `GroupBy::default()`. The `pre_fix_writer_key_misses_under_canonical_reader_key` test will fail if you do. Always use `tui::cache::TUI_DEFAULT_GROUP_BY`.
Not-tested: the cache self-healing path across a real upgrade (cache file on disk written by the pre-fix binary, then loaded by the post-fix binary). Covered conceptually by the unchanged Miss→BG load→save path but not by an integration test.
… submission

Cursor's usage export records historical request/On-Demand charges with
empty token columns, which the parser turns into cost > 0 / tokens = 0
rows. The server rejects any such row, so a few cents of legacy Cursor
data (auto, claude-3.5-sonnet, o3, ...) blocked the entire submission.

The CLI now drops these rows in the submit path, recomputes day/summary/
year totals, and reports exactly what was excluded ("the rest is
submitted"). The cursor `premium-tool-call` carve-out is preserved so
that grandfathered cost the server still accepts isn't silently dropped.

Constraint: Server validation stays strict to catch genuine parser regressions
Rejected: Broaden server-side carve-out to all cursor tokenless rows | would mask real parser bugs
Confidence: high
Scope-risk: narrow
Directive: is_tokenless_costed_row must stay in sync with CURSOR_LEGACY_TOKENLESS_MODELS in packages/frontend/src/lib/validation/submission.ts
Not-tested: end-to-end submit against the live server (verified via --dry-run only)
Satisfies the strict `cargo fmt --all -- --check` lint gate; no behavior change.
The 3.0.0 release (2026-05-27) committed the version bump but every npm
publish job failed auth (invalid NPM_TOKEN), so nothing shipped and npm is
still at 2.1.3. Reset all version manifests to 2.1.3 so the Publish
workflow's bump=major recomputes 3.0.0 and clears the provenance no-op
guard (prepare-release-provenance.sh aborts when no manifest diff is staged).

Constraint: prepare-release-provenance.sh requires a real version diff to commit
Scope-risk: narrow
Confidence: high
crhan and others added 27 commits June 1, 2026 04:44
…junhoyeo#646)

* feat(codex): detect turn starts so the Turn column counts codex turns

Codex sessions never set `is_turn_start`, so the TUI/CLI Turn column was
always 0 ("—") for codex while ClaudeCode and Kiro reported real counts
(turn_count is gated on msg.is_turn_start during daily/hourly/model
aggregation). The codex parser never flipped the flag — verified across all
branches and history; turn detection existed only in claudecode.rs and
kiro.rs.

Detect human turns from `event_msg` `user_message` events: set a deferred
`pending_turn_start` on CodexParseState, then mark the next
token_count-derived message (the assistant's reply, which carries the
tokens) as a turn start. System-injected messages whose body begins with
`<` (e.g. <environment_context>, <user_instructions>) are excluded as
non-human input, mirroring claudecode::is_human_turn. The flag is
`#[serde(default)]` so a pending turn survives incremental cache re-parses.

`codex exec` one-shots count too: headless but carrying a real human
prompt, so each is exactly one turn. Verified end-to-end against a real
`codex exec` session (1 user_message -> turn_count 1), including the
agent_message that interleaves between the prompt and the token_count.

Adds 4 unit tests: human turn, system-injected (xml), exec one-shot (with
interleaved agent_message), and incremental-parse continuity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(codex): bump message-cache schema for pending_turn_start field

The incremental Codex parse state gained a serde(default) pending_turn_start field, but old cache files written before it existed load the field as false. If the cache boundary fell between a human user_message and the token_count line that closes that turn, re-parsing the appended chunk would start with pending_turn_start=false and silently drop the turn boundary. Bumping CACHE_SCHEMA_VERSION discards stale caches so the first run after upgrade re-parses from scratch with the field present.

* fix(codex): match only known system-injected tags as non-human turns

codex_message_is_human_turn rejected every message whose trimmed body starts with '<', which also drops legitimate human prompts that begin with markup (asking about a <div>, pasting an XML snippet, etc.). Match the specific known injected prefixes (<environment_context>, <system-reminder>, <user_instructions>) instead, and add a unit test covering both the markup-prompt and injected-context cases.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…eo#647)

* test(cli): isolate fixtures from inherited scan-path env vars

cmd_with_home / cmd_with_home_hermetic set HOME and XDG dirs but left
TOKSCALE_EXTRA_DIRS, TOKSCALE_HEADLESS_DIR and CODEX_HOME inherited from
the developer's shell. A dev who exports e.g.
TOKSCALE_EXTRA_DIRS=~/.codex/sessions (for codefuse mirror tracking) makes
the codex scanner read real session data inside the fixtures, so
fixture-count assertions like
test_models_group_by_workspace_model_surfaces_workspace_fields_for_codex
fail (entries 50 vs 1) locally while passing on CI's clean env.

Clear those three scan-path overrides in both helpers so tests are
hermetic regardless of the dev's shell. Tests that need CODEX_HOME set it
explicitly afterwards, so the removal is overridden where intended.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(cli): also isolate GEMINI_CLI_HOME, HERMES_HOME, TOKSCALE_CONFIG_DIR

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Junho Yeo <i@junho.io>
…eo#642)

* fix antigravity ide sync

* fix(antigravity): disable proxy for loopback HTTPS sync client

---------

Co-authored-by: Junho Yeo <i@junho.io>
fix(codex): skip replayed parent usage in fork logs

Validation
* Validation tier: Tier 3 - shared parser/accounting change affecting submit totals.
* cargo fmt --all -- --check: PASS
* cargo test -p tokscale-core codex: PASS, 72 passed.
* cargo test -p tokscale-core: PASS, 817 passed, 1 ignored.
* cargo clippy -p tokscale-core --all-targets -- -D warnings: PASS
* Focused local Codex 2026-05-24 verifier over real ~/.codex JSONL: PASS, 177,833,822 tokens after skipping 614,239 inherited replay token_count rows.
* git diff --check: PASS
* git diff --cached --check: PASS
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.

Rollback
* Revert this squash merge commit.

Co-authored-by: Junho Yeo <i@junho.io>
fix(claude): surface desktop usage diagnostics

Validation
* Validation tier: Tier 3 - shared pricing resolver plus CLI report diagnostics.
* git diff --check origin/main...HEAD: PASS
* git diff --cached --check: PASS
* cargo fmt --all -- --check: PASS
* cargo test -p tokscale-cli claude_desktop_diagnostic: PASS, 3 tests.
* cargo test -p tokscale-cli models: PASS, 44 tests.
* cargo test -p tokscale-cli clients: PASS, 24 tests.
* cargo test -p tokscale-core opus_4: PASS, 12 tests.
* cargo test -p tokscale-core claude: PASS, 70 tests.
* cargo test -p tokscale-core pricing::lookup::tests: PASS, 137 tests.
* cargo clippy -p tokscale-cli --all-targets -- -D warnings: PASS.
* cargo clippy -p tokscale-core --all-targets -- -D warnings: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - no release manifests changed; repository release workflow owns version bumps.
* Not run: full workspace cargo test - not required for selected validation tier; targeted CLI/core tests cover the changed surfaces.

Rollback
* Revert this squash merge commit.
fix(pricing): skip unusable exact price entries

Validation
* Validation tier: Tier 2R - post-merge rebase correction for pricing lookup conflict with junhoyeo#614.
* git diff --check origin/main...HEAD: PASS.
* git diff --cached --check: PASS.
* cargo fmt --all -- --check: PASS.
* cargo test -p tokscale-core pricing::lookup::tests: PASS, 143 passed.
* cargo clippy -p tokscale-core --all-targets -- -D warnings: PASS.
* Remote CI: Test & Coverage: PASS.
* Remote CI: Build Native (Test Only): PASS, all targets.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - release workflow owns package version bumps.
* Not run: full workspace cargo test - not required for selected validation tier; targeted lookup tests cover resolver behavior and remote CI passed on the final PR SHA.

Rollback
* Revert this squash merge commit.

Co-authored-by: Junho Yeo <i@junho.io>
feat(pricing): add models.dev fallback source

Validation
* Validation tier: Tier 2R - post-review narrow runtime correction for models.dev retry failure handling.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* cargo fmt --all -- --check: PASS.
* cargo test -p tokscale-core models_dev: PASS, 8 tests.
* cargo test -p tokscale-core pricing::: PASS, 213 tests.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.
* Not run: cargo check -p tokscale-cli - not required for this correction; touched code is internal to tokscale-core models.dev fetch handling and targeted pricing tests cover it.

Rollback
* Revert this squash merge commit.

Co-authored-by: Junho Yeo <i@junho.io>
fix(auth): apply CSRF gate to cookie mutations

Validation
* Validation tier: Tier 3 - security-sensitive auth mutation behavior.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* bun --cwd packages/frontend test __tests__/api/settingsTokensDelete.test.ts: PASS, 6 tests.
* bun --cwd packages/frontend test __tests__/api/settingsTokensList.test.ts __tests__/api/settingsTokensDelete.test.ts __tests__/api/settingsSubmittedDataDelete.test.ts __tests__/api/deviceAuthorizeCsrf.test.ts __tests__/api/settingsDeviceRenameCsrf.test.ts __tests__/lib/requestSessionCsrf.test.ts: PASS, 37 tests.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.
* Not run: full frontend test suite - not required locally because targeted API and auth CSRF tests cover the changed routes.

Rollback
* Revert this squash merge commit.
fix(auth): allow tokscale.ai CSRF origin

Validation
* Validation tier: Tier 3 - high-risk runtime/security change because the default CSRF allowlist affects cookie-authenticated mutating requests.
* TDD red: bun run test __tests__/lib/requestSessionCsrf.test.ts: FAIL before implementation, 1 failed and 12 passed; the new tokscale.ai production-origin case returned null.
* bun run test __tests__/lib/requestSessionCsrf.test.ts: PASS, 13 tests.
* bun run test __tests__/api/groupRoute.test.ts: PASS, 5 tests.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - no release/version manifests changed and the documented version coherence check applies to CLI release manifests.
* Not run: full frontend suite, lint, build - not required for the focused local proof requested for this scoped allowlist fix.

Rollback
* Revert this squash merge commit.

Co-authored-by: Junho Yeo <i@junho.io>
fix(auth): claim device codes atomically

Validation
* Validation tier: Tier 3 - auth-route runtime change because this touches device-code authorization semantics.
* TDD red: bun run test __tests__/api/deviceAuthorize.test.ts: FAIL before implementation; update predicate used deviceCodes.id only instead of userCode/expiresAt/userId guards.
* bun run test __tests__/api/deviceAuthorize.test.ts: PASS, 2 tests.
* bun run test __tests__/api/deviceAuthorize.test.ts __tests__/api/devicePoll.test.ts: PASS, 7 tests.
* bun run lint src/app/api/auth/device/authorize/route.ts __tests__/api/deviceAuthorize.test.ts: PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - no CLI package or release manifest changed.
* Not run: full frontend test suite - not required for selected validation tier; targeted authorize and poll route tests cover the changed auth path.

Rollback
* Revert this squash merge commit.

Co-authored-by: Junho Yeo <i@junho.io>
fix(auth): hash browser session tokens at rest

Validation
* Validation tier: Tier 3 - auth/session storage and DB migration affect authentication security behavior.
* bun install: PASS, installed missing workspace dependencies before tests.
* TDD red: bun --cwd packages/frontend test __tests__/lib/session.test.ts: FAIL before implementation, 4 failed and 1 passed.
* bun --cwd packages/frontend test __tests__/lib/session.test.ts __tests__/lib/personalTokens.test.ts __tests__/api/authToken.test.ts __tests__/lib/requestSessionCsrf.test.ts: PASS, 30 tests.
* bunx drizzle-kit check --config drizzle.config.ts: PASS from packages/frontend.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* Migration replay: not run locally - DATABASE_URL is not set in this worktree.
* TypeScript check: not used as merge proof - bunx tsc -p packages/frontend/tsconfig.json --noEmit fails on a pre-existing unrelated missing asset module in packages/frontend/src/components/BlackholeHero.tsx.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.

Rollback
* Revert this squash merge commit.
Co-authored-by: Junho Yeo <i@junho.io>
fix(auth): constrain GitHub OAuth return paths

Validation
* Validation tier: Tier 3 - auth redirect safety affects security-sensitive runtime behavior.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* bun x vitest run __tests__/api/githubAuthReturnTo.test.ts: PASS.
* bun x eslint src/app/api/auth/github/route.ts src/app/api/auth/github/callback/route.ts src/lib/auth/returnTo.ts __tests__/api/githubAuthReturnTo.test.ts: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.
* Not run: bun x tsc --noEmit --pretty false - project-wide check is blocked by unrelated existing frontend errors outside this diff.

Rollback
* Revert this squash merge commit.
Co-authored-by: Junho Yeo <i@junho.io>
fix(leaderboard): use competition ranks for all-time ties

Validation
* Validation tier: Tier 2 - narrow runtime change focused on frontend leaderboard/embed rank semantics.
* bun install --frozen-lockfile: PASS.
* bun x vitest run __tests__/lib/getLeaderboardAllTime.test.ts __tests__/lib/getUserEmbedStats.test.ts: PASS, 10 tests.
* bun x vitest run __tests__/lib/getLeaderboardAllTime.test.ts __tests__/lib/getLeaderboard.test.ts __tests__/lib/getUserEmbedStats.test.ts __tests__/api/usersProfile.test.ts: PASS, 22 tests.
* bun x eslint src/lib/leaderboard/getLeaderboard.ts src/lib/embed/getUserEmbedStats.ts __tests__/lib/getLeaderboardAllTime.test.ts __tests__/lib/getUserEmbedStats.test.ts: PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.
* Not run: full frontend test suite - not required for selected validation tier.

Rollback
* Revert this squash merge commit.
fix(claude): preserve cc-mirror tool result attribution

Validation
* Validation tier: Tier 2 - narrow runtime change, localized Claude parser attribution correction.
* TDD red: cargo test -p tokscale-core sessions::claudecode::tests::test_cc_mirror_tool_result_keeps_variant_client_and_provider: FAIL before implementation, expected client attribution mismatch.
* cargo test -p tokscale-core sessions::claudecode::tests::test_cc_mirror_tool_result_keeps_variant_client_and_provider: PASS.
* cargo test -p tokscale-core sessions::claudecode: PASS.
* cargo test -p tokscale-core cc_mirror: PASS.
* cargo fmt --all -- --check: PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* scripts/check-version-coherence.sh: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: PASS, scripts/check-version-coherence.sh.
* Not run: frontend validation tests - frontend files were not touched.

Rollback
* Revert this squash merge commit.
Co-authored-by: Junho Yeo <i@junho.io>
fix(core): derive active time from message durations

Validation
* Validation tier: Tier 2 - narrow runtime change, localized sessionization metric behavior.
* cargo test -p tokscale-core sessionize::tests --lib: PASS, 16 tests.
* cargo test -p tokscale-core: PASS, 818 unit tests passed, 1 ignored; integration tests passed; doc-tests passed.
* cargo fmt --all -- --check: PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.
* Not run: full workspace tests - not required for selected validation tier.

Rollback
* Revert this squash merge commit.
fix(graph): reuse local Cursor auto-sync path

Validation
* Validation tier: Tier 2 - narrow runtime change; graph now reuses the existing local-report Cursor sync helper and adds a CLI regression test for the fresh-cache warning path.
* Red test: cargo test -p tokscale-cli test_graph_fresh_cursor_cache_skips_auto_sync_warning: FAIL before fix; exposed `Cursor sync failed; using cached data` despite fresh cache.
* cargo test -p tokscale-cli test_graph_fresh_cursor_cache_skips_auto_sync_warning: PASS.
* cargo test -p tokscale-cli cursor_auto_sync: PASS.
* cargo test -p tokscale-cli graph_cursor: PASS.
* cargo fmt --all -- --check: PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* Version: PASS, bash scripts/check-version-coherence.sh.
* Ledger: not applicable - not required for selected validation tier/change family.
* Additional lint: not run locally - cargo clippy -p tokscale-cli -- -D warnings could not run because local cargo-clippy used rustc 1.86.0 while dependencies require rustc 1.88.0+.
* Not run: full test suite - not required for selected validation tier.

Rollback
* Revert this squash merge commit.

Co-authored-by: Junho Yeo <i@junho.io>
fix(headless): preserve capture timeout state

Validation
* Validation tier: Tier 2R - post-review narrow runtime correction for headless subprocess timeout handling.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* cargo test -p tokscale-cli --test cli_tests headless_capture: PASS, 3 tests.
* cargo check -p tokscale-cli: PASS.
* cargo fmt --all -- --check: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.
* Not run: full workspace test suite - not required locally for selected validation tier; targeted headless capture tests and CLI check cover this correction and remote CI is expected to run final PR gates.

Rollback
* Revert this squash merge commit.

Co-authored-by: Junho Yeo <i@junho.io>
fix(antigravity): include cache rows in default submit

Validation
* Validation tier: Tier 2 - narrow runtime change; core client registry flag changes submit default selection and targeted tests cover Antigravity parsing/submit graph behavior.
* Red test: cargo test -p tokscale-core antigravity: FAIL before implementation with test_antigravity_submit_default_is_true and test_submit_default_graph_includes_antigravity_cache_rows.
* cargo test -p tokscale-core antigravity: PASS, 14 tests.
* cargo test -p tokscale-cli default_submit_clients: PASS, 2 tests.
* cargo fmt --all -- --check: PASS.
* cargo clippy -p tokscale-core --all-features -- -D warnings: PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* bash scripts/check-version-coherence.sh: PASS, Version coherence OK: 3.0.0.
* Ledger: not applicable - scripts/ledger is absent and no ledger policy applies to this change family.
* Version: PASS, bash scripts/check-version-coherence.sh; no version bump required because release workflow owns manifest version bumps.
* Not run: frontend tests - not required because the current frontend registry already accepts every core client id and no frontend files changed.
* Not run: full workspace test suite - not required for selected validation tier.

Rollback
* Revert this squash merge commit.

Co-authored-by: Junho Yeo <i@junho.io>
…o#671)

test(auth): align device authorize mocks with guarded claims

Validation
* Validation tier: Tier 2 - test-only CI correction for security-sensitive device authorize route coverage; no runtime files changed.
* TDD red: bun --cwd packages/frontend test __tests__/api/deviceAuthorize.test.ts __tests__/api/deviceAuthorizeCsrf.test.ts: FAIL before fix, 3 failed and 2 passed, matching the Frontend CI device authorize failures.
* bun --cwd packages/frontend test __tests__/api/deviceAuthorize.test.ts __tests__/api/deviceAuthorizeCsrf.test.ts: PASS, 5 tests.
* bun --cwd packages/frontend test: PASS, 48 test files and 391 tests.
* bun run lint -- __tests__/api/deviceAuthorize.test.ts __tests__/api/deviceAuthorizeCsrf.test.ts (cwd packages/frontend): PASS.
* Remote Frontend CI / Frontend Vitest: PASS.
* Remote Frontend CI / Frontend Migration Replay: PASS.
* Remote cubic AI code reviewer: PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - no release manifests changed.

Rollback
* git revert HEAD
Validation
* Validation tier: Tier 2R - post-review test-only correction for the cost-only active-day regression fixture; no runtime files changed.
* git diff --check: PASS
* git diff --cached --check: PASS
* cargo test -p tokscale-core test_calculate_summary_counts_cost_only_days_as_active: PASS
* cargo fmt --check: PASS
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.
* Not run: full workspace test suite - not required locally for selected validation tier; the targeted aggregation regression test covers this correction and mandatory remote CI will run final PR gates.

Rollback
* git revert HEAD

Co-authored-by: Junho Yeo <i@junho.io>
Validation
* Validation tier: Tier 4 - CI/release tooling, touched publish workflow and release helper scripts.
* git diff --check: PASS
* git diff --cached --check: PASS
* bash scripts/test-calculate-release-version.sh && bash scripts/test-check-version-coherence.sh && bash scripts/test-npm-release-state.sh && bash scripts/test-prepare-release-provenance.sh: PASS
* bash -n scripts/check-version-coherence.sh scripts/test-check-version-coherence.sh: PASS
* bash scripts/check-version-coherence.sh: PASS
* ruby -e 'require "yaml"; ARGV.each { |path| YAML.load_file(path); puts "#{path}: OK" }' .github/workflows/publish-cli.yml .github/workflows/test_coverage.yml: PASS
* Remote Test & Coverage / Lint: PASS.
* Remote Test & Coverage / Code Coverage: PASS.
* Remote Vercel: PASS.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.
* Not run: scripts/test-package-launchers.sh - not required for selected validation tier because launcher runtime code was unchanged.

Rollback
* git revert HEAD

Co-authored-by: Junho Yeo <i@junho.io>
Validation
* Validation tier: Tier 2R - post-merge test-only CI correction for a process-global XDG_DATA_HOME race in the Zed client path fixture; no runtime files changed.
* TDD red: XDG_DATA_HOME=/tmp/xdg-data-home cargo test -p tokscale-core clients::tests::test_zed_data_dir_path -- --exact --nocapture: FAIL before fix, reproduced the GitHub coverage failure with /tmp/xdg-data-home/zed/threads/threads.db instead of /tmp/home/.local/share/zed/threads/threads.db.
* XDG_DATA_HOME=/tmp/xdg-data-home cargo test -p tokscale-core clients::tests::test_zed_data_dir_path -- --exact --nocapture: PASS, 1 test.
* cargo test -p tokscale-core clients::tests -- --test-threads=4: PASS, 26 tests.
* cargo test -p tokscale-core: PASS, 838 lib tests passed, 1 ignored; codebuff/hermes integration tests passed; doc-tests passed.
* cargo fmt --check: PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* Remote CI on PR junhoyeo#672 at fbdb509: PASS, including Test & Coverage, Code Coverage, Lint, and Build Native matrix.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - no release manifests changed.
* Not run: local cargo tarpaulin - not required for this test-only correction because mandatory remote Test & Coverage passed on the final PR SHA.

Rollback
* git revert HEAD
Validation
* Validation tier: Tier 4 - CI/release tooling; touched publish workflow, native-build workflow, npm publish helper, launcher smoke helper, and release workflow safety checks.
* git diff --check origin/main...HEAD: PASS.
* git diff --check: PASS.
* git diff --cached --check: PASS.
* bash scripts/test-calculate-release-version.sh && bash scripts/test-check-version-coherence.sh && bash scripts/test-npm-release-state.sh && bash scripts/test-prepare-release-provenance.sh && bash scripts/test-release-workflow-safety.sh: PASS.
* bash scripts/test-package-launchers.sh: PASS.
* python3 scripts/check-release-workflow-safety.py: PASS.
* python3 -m py_compile scripts/check-release-workflow-safety.py: PASS.
* bash -n scripts/check-version-coherence.sh scripts/test-check-version-coherence.sh scripts/check-npm-release-state.sh scripts/test-npm-release-state.sh scripts/publish-npm-package.sh scripts/test-package-launchers.sh scripts/test-release-workflow-safety.sh scripts/calculate-release-version.sh scripts/test-calculate-release-version.sh scripts/prepare-release-provenance.sh scripts/test-prepare-release-provenance.sh: PASS.
* ruby -e 'require "yaml"; ARGV.each { |path| YAML.load_file(path); puts "#{path}: OK" }' .github/workflows/publish-cli.yml .github/workflows/build-native.yml: PASS.
* Remote CI on PR junhoyeo#666 at b030c90: PASS, including Launcher Smoke, Test & Coverage, Code Coverage, and Lint.
* Version: PASS, bash scripts/check-version-coherence.sh.
* Ledger: not applicable - not required for selected validation tier/change family.
* Not run: full workspace cargo test - not required for selected validation tier because runtime Rust code was not changed and targeted release/workflow validation covers the touched tooling.

Rollback
* git revert HEAD

Co-authored-by: Junho Yeo <i@junho.io>
test(codex): cover fork replay submit cap failures

Add a focused Codex parser regression for forked child sessions that replay large inherited cached-input rows before a small child-local delta. The current parser behavior already skips the inherited replay rows; this test locks the submit-cap failure shape reported in junhoyeo#650 without changing runtime semantics.

Validation
* Validation tier: Tier 2 - test-only regression coverage for localized Codex parser behavior.
* cargo fmt --check: PASS
* cargo test -p tokscale-core sessions::codex::tests::test_forked_child_submit_cap_regression_skips_large_inherited_cache_replays -- --exact: PASS
* cargo test -p tokscale-core sessions::codex::tests::test_forked_child -- --nocapture: PASS
* Remote CI: PASS - Test & Coverage, Build Native, Vercel, WIP, Can I merge this PR?, cubic.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - test-only change.

Rollback
* git revert HEAD

Co-authored-by: minislively <minislively@users.noreply.github.com>
feat(tui): keep hourly and daily dates readable in compact layouts

Compact the Hourly time column to `%H:00` and move repeated dates into muted `%m/%d` day-boundary separators, so narrow terminals no longer crush full timestamps into unreadable fragments. The Hourly renderer now budgets by rendered lines, including separators, and keeps the selected data row visible when a day boundary falls into a one-line viewport.

Also make the Daily tab drop the year to `%m-%d` only when the full table layout does not fit, while preserving `%Y-%m-%d` on wide terminals.

Validation
* Validation tier: Tier 2 - narrow TUI rendering behavior change with targeted render regression coverage.
* cargo fmt --check: PASS
* cargo clippy --all-targets -- -D warnings: PASS
* cargo test -p tokscale-cli: PASS
* Remote CI: PASS - Test & Coverage, Build Native, Vercel, WIP, Can I merge this PR?.
* Ledger: not applicable - not required for selected validation tier/change family.
* Version: not applicable - not required for selected validation tier/change family.

Rollback
* git revert HEAD

Co-authored-by: ruohan.chen <crhan123@gmail.com>
Collect MCP server names from local config during CLI submit and store
them in the submissions table. Show them as badges under Clients in the
Statistics section of the profile page.
@t1000040 t1000040 closed this Jun 5, 2026
t1000040 pushed a commit that referenced this pull request Jun 30, 2026
junhoyeo#713)

* feat(sessions): read Antigravity CLI usage from local SQLite databases

The Antigravity CLI (the terminal agent that stores its data under `~/.gemini/antigravity-cli/`) was never counted. tokscale only knew two Gemini-family sources: the Gemini CLI (scans `~/.gemini/tmp/*.{json,jsonl}`) and Antigravity (pulls usage from a running IDE language server over RPC and caches it under the config dir). The Antigravity CLI fell into neither bucket, so its on-disk usage was invisible — `tokscale antigravity sync` found the filesystem candidates but cached zero because its only artifact path still requires a live language-server RPC connection.

This adds Antigravity CLI as a first-class local scan source so its usage updates automatically like every other file-based source — no RPC, no `antigravity sync`. A new `antigravity-cli` client globs `~/.gemini/antigravity-cli/conversations/*.db` (honoring `GEMINI_CLI_HOME`) and a new parser reads each conversation database directly.

Each `gen_metadata` row is one generation encoded as the same `GeneratorMetadata` protobuf the IDE returns over `GetCascadeTrajectoryGeneratorMetadata`. The repository has no `.proto`/prost decoder (the IDE path receives JSON because the language server does the proto-to-JSON conversion), so the parser ships a tiny dependency-free wire-format reader and pulls only the fields it needs. The field numbers were reverse-engineered from real databases and cross-checked across 6 sessions / 140 turns: `chatModel.#19` is the response model, `usage.#5`/`#9`/`#10` are cacheRead/output/thinking (verified by the invariant `#9 + #10 == #3`, the stored total output), `#11` is the responseId used for dedup, and input combines the fixed system-prompt count `#1` with the newly-processed input `#2`. The session timestamp and workspace come from `trajectory_metadata_blob`.

Adding the new `ClientId` variant fans out to the usual registration points: the scanner gains a `*.db` glob arm (which naturally rejects `.db-wal`/`.db-shm` sidecars), both local-parse dispatch paths gain a branch, and the CLI `ClientFilter`, client labels, TUI picker, and frontend source maps gain entries. The deprecated per-client boolean flags intentionally do not, since `antigravity-cli` is reachable only via the canonical `--client antigravity-cli`.

Closes junhoyeo#712.

* fix(sessions): handle file:// authority/UNC paths and test Antigravity CLI wiring

Addresses the cubic review on junhoyeo#713.

`file_uri_to_path` previously stripped `file://` and only special-cased the leading slash before a Windows drive letter, so a non-empty authority (`file://host/share/...`, the UNC form) lost its host and collapsed into a bare path. It now treats an empty-authority remainder as before (`/C:/x` → `C:/x`, `/home/x` kept) and reconstructs a non-empty authority as a UNC path (`host/share/x` → `//host/share/x`) so `normalize_workspace_key` preserves the `//` prefix. A unit test covers the Windows-drive, POSIX, UNC, and percent-encoded-CJK cases.

The new `AntigravityCli` client wiring is now asserted in `test_client_as_str`, `test_client_key`, and `test_client_from_key` (display name "Antigravity CLI", hotkey `f`, and the reverse hotkey mapping).

* style: rustfmt antigravity_cli.rs

* fix(antigravity-cli): add gemini-3-flash-a pricing alias and harden parser tests

Map the raw #19 responseModel `gemini-3-flash-a` onto the priced
`gemini-3-flash-preview` so Antigravity CLI cost no longer resolves to 0.
Add alias-resolution, #9/#10==#3 field-mapping invariant, and
malformed-protobuf bounds tests.

Constraint: must not weaken the junhoyeo#707 brand-token fuzzy-match guard in lookup.rs

Confidence: high

Scope-risk: narrow

---------

Co-authored-by: Junho Yeo <i@junho.io>
t1000040 pushed a commit that referenced this pull request Jul 10, 2026
junhoyeo#713)

* feat(sessions): read Antigravity CLI usage from local SQLite databases

The Antigravity CLI (the terminal agent that stores its data under `~/.gemini/antigravity-cli/`) was never counted. tokscale only knew two Gemini-family sources: the Gemini CLI (scans `~/.gemini/tmp/*.{json,jsonl}`) and Antigravity (pulls usage from a running IDE language server over RPC and caches it under the config dir). The Antigravity CLI fell into neither bucket, so its on-disk usage was invisible — `tokscale antigravity sync` found the filesystem candidates but cached zero because its only artifact path still requires a live language-server RPC connection.

This adds Antigravity CLI as a first-class local scan source so its usage updates automatically like every other file-based source — no RPC, no `antigravity sync`. A new `antigravity-cli` client globs `~/.gemini/antigravity-cli/conversations/*.db` (honoring `GEMINI_CLI_HOME`) and a new parser reads each conversation database directly.

Each `gen_metadata` row is one generation encoded as the same `GeneratorMetadata` protobuf the IDE returns over `GetCascadeTrajectoryGeneratorMetadata`. The repository has no `.proto`/prost decoder (the IDE path receives JSON because the language server does the proto-to-JSON conversion), so the parser ships a tiny dependency-free wire-format reader and pulls only the fields it needs. The field numbers were reverse-engineered from real databases and cross-checked across 6 sessions / 140 turns: `chatModel.#19` is the response model, `usage.#5`/`#9`/`#10` are cacheRead/output/thinking (verified by the invariant `#9 + #10 == #3`, the stored total output), `#11` is the responseId used for dedup, and input combines the fixed system-prompt count `#1` with the newly-processed input `#2`. The session timestamp and workspace come from `trajectory_metadata_blob`.

Adding the new `ClientId` variant fans out to the usual registration points: the scanner gains a `*.db` glob arm (which naturally rejects `.db-wal`/`.db-shm` sidecars), both local-parse dispatch paths gain a branch, and the CLI `ClientFilter`, client labels, TUI picker, and frontend source maps gain entries. The deprecated per-client boolean flags intentionally do not, since `antigravity-cli` is reachable only via the canonical `--client antigravity-cli`.

Closes junhoyeo#712.

* fix(sessions): handle file:// authority/UNC paths and test Antigravity CLI wiring

Addresses the cubic review on junhoyeo#713.

`file_uri_to_path` previously stripped `file://` and only special-cased the leading slash before a Windows drive letter, so a non-empty authority (`file://host/share/...`, the UNC form) lost its host and collapsed into a bare path. It now treats an empty-authority remainder as before (`/C:/x` → `C:/x`, `/home/x` kept) and reconstructs a non-empty authority as a UNC path (`host/share/x` → `//host/share/x`) so `normalize_workspace_key` preserves the `//` prefix. A unit test covers the Windows-drive, POSIX, UNC, and percent-encoded-CJK cases.

The new `AntigravityCli` client wiring is now asserted in `test_client_as_str`, `test_client_key`, and `test_client_from_key` (display name "Antigravity CLI", hotkey `f`, and the reverse hotkey mapping).

* style: rustfmt antigravity_cli.rs

* fix(antigravity-cli): add gemini-3-flash-a pricing alias and harden parser tests

Map the raw #19 responseModel `gemini-3-flash-a` onto the priced
`gemini-3-flash-preview` so Antigravity CLI cost no longer resolves to 0.
Add alias-resolution, #9/#10==#3 field-mapping invariant, and
malformed-protobuf bounds tests.

Constraint: must not weaken the junhoyeo#707 brand-token fuzzy-match guard in lookup.rs

Confidence: high

Scope-risk: narrow

---------

Co-authored-by: Junho Yeo <i@junho.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants