Files
nandan_prabhu 1128024c02 feat: Add Auth0 Flutter Windows desktop reference + evals (#192)
* feat: Add Auth0 Flutter Windows desktop reference

auth0_flutter reached Windows GA in v2.1.0 with a distinct API
(windowsWebAuthentication(), custom URL scheme + Registry protocol
handler, no CredentialsManager) that neither the existing
flutter-native (iOS/Android) nor flutter-web references cover.

- Add references/framework-flutter-windows/index.md, verified against
  auth0/auth0-flutter's raw README/FAQ/EXAMPLES and example runner
  source (vcpkg setup, CMakeLists toolchain wiring, Registry scheme
  registration, main.cpp mutex/named-pipe integration, manual
  credential storage).
- Wire flutter-windows into SKILL.md's Tier 1/2/3 router tables and
  cross-link it from framework-flutter-native/-web's "When NOT to Use".
- Add flutter-windows to validate-skill.sh's EXPECTED_FRAMEWORKS and
  the root README coverage table.
- Add routing, activation, and behavioral eval coverage for the new
  reference.

* fix: trim SKILL.md prose to stay under skillsaw's context-budget warn limit

The flutter-windows router wiring pushed SKILL.md's estimated token count
over skillsaw's 4,300 warn threshold. Drop the added explanatory prose
(a lead-in sentence and a callout note) and keep only the three backticked
table rows needed for routing/reachability, matching the terse,
table-only convention most other framework rows already use.

* fix: trim SKILL.md prose to stay under skillsaw's context-budget warn limit

CI flagged 4,305 tokens against the 4,300 warn limit after the flutter-windows
rebase. Shortened five prose passages without touching rules or tables.

* fix: address CodeRabbit review findings on Flutter Windows reference

- Split the weak appCustomURL grader into separate login()/logout() call-shape
  checks and add Info.plist to the forbidden mobile-only list.
- Require an explicit Windows-target signal before routing to flutter-windows,
  since flutter create scaffolds windows/ alongside every other platform dir.
- Derive the single-instance mutex and named-pipe names from the app's custom
  scheme instead of a fixed string shared by every app using the example runner.
- Implement real credential persistence (save/restore/clear via
  shared_preferences) in the AuthService example instead of leaving TODOs.
- Validate the OAuth state parameter server-side in the /callback handler
  before forwarding to the custom scheme.

* fix Flutter Windows router and callback follow-ups

Co-authored-by: sanchitmehtagit <246215521+sanchitmehtagit@users.noreply.github.com>

* fix: address remaining CodeRabbit and human-reviewer findings

- Remove duplicate H1 heading in framework-flutter-windows/index.md
- Replace assert() with ArgumentError for the redirectUrl/returnTo
  pairing check, since assert() is stripped from production builds
- Widen the login()/logout() implementation-check regexes so they
  also match the receiver-variable style (webAuth.login(...)) used
  by the reference's own AuthService example, not just direct chaining
- Scope the intermediary eval's appCustomURL/redirectUrl/returnTo
  graders to the correct login()/logout() call instead of matching
  those tokens anywhere in the output

* fix: address remaining CodeRabbit findings on Flutter Windows reference

- Check credential expiry before restoring a session
- Guarantee local cleanup in logout() via try/finally, and surface
  remote logout failures at the call site
- Actually invoke restoreSession() on startup with a loading gate
- URL-encode code/state/error before redirecting in the /callback handler
- Fix MD031 blank lines around two fenced code blocks
- De-conflate the fw-flutter-windows activation case from generic Auth0 intent

* fix: address latest CodeRabbit review round on Flutter Windows reference

- Replace shared_preferences with flutter_secure_storage for Credentials
  persistence (CWE-922: refresh token via offline_access must not be
  stored in plaintext)
- Handle malformed/outdated persisted credential records in
  restoreSession() instead of leaving the app stuck on the loading screen
- Bind login()/logout() eval graders to the windowsWebAuthentication()
  receiver so an unrelated .login()/.logout() call can no longer
  false-pass the grader

* revert: restore SKILL.md prose trimmed for since-raised context-budget limit

e60b760 shortened five unrelated prose passages (Step 1 intro, "if nothing
clearly matches", Step 2 skip note, Tier 2 intro, "if still ambiguous",
Conflicts section) to stay under skillsaw's context-budget warn limit, which
was 4,300 tokens at the time. #197 later raised that limit to 5,000. With the
flutter-windows content included, the file is ~4,575 estimated tokens — safely
under the current limit — so the trims are no longer necessary and were
unrelated to this branch's actual scope.

* Update SKILL.md

Removes flutter windows framework detection

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sanchitmehtagit <246215521+sanchitmehtagit@users.noreply.github.com>
2026-09-03 16:37:38 +05:30
..

Activation evals — the auth0 skill description

Three layers of eval guard this skill. This directory is the activation layer — it tests the one thing the other two can't see.

Layer Question Runs in CI? Needs a live model?
Routing (../routing-cases.json) Does each intent/framework map to reference files that exist? yes no — deterministic
Activation (here) Does the description make the agent load the skill on the right prompts — and leave it alone on the wrong ones? manual yes — claude CLI
Behavioral (../behavioral/) Does the loaded skill make the agent generate correct SDK code? manual yes — claude CLI

Why this layer exists

The description in SKILL.md frontmatter is the only signal an agent uses to decide whether to load this skill. If it's wrong, nothing else matters — the best router in the world never runs.

Neither other layer touches it:

  • scripts/check_routing_evals.py parses the SKILL.md body (the Step 4 table). It never reads frontmatter.
  • ../behavioral/ loads the plugin explicitly via --plugin-dir and grades the code produced after the skill is already in play.

So a description edit could destroy discoverability with every other check green. That was the gap; this closes it.

What it measures

For each case in cases.json, both description variants are scored on the same prompt, and the run reports recall on positives, false positives on negatives, and explicit REGRESSED / IMPROVED lists.

Two stages:

Stage A — classifier (default). Renders a realistic skill menu (auth0 plus seven distractor skills, name + description only, exactly as skills appear in a system prompt), then the user's request, and asks which skills it would invoke. Cheap, and the description is the only variable that changes between the two runs.

The distractor menu is not decoration. Asking "would you invoke auth0?" in isolation primes YES and makes every negative case meaningless. Several distractors are auth-adjacent on purpose (kubernetes-ops, api-hardening, aws-infra) so the near-miss negatives have a correct home to go to.

Stage B — real activation (--real). Ground truth. Builds two temp copies of plugins/auth0 differing only in the description line, runs the prompt with --plugin-dir and --output-format stream-json, and detects an actual Skill(auth0) tool call in the event stream (not prose about using it). A full agent run per case per variant, so it's reserved for the cases tagged spot_check plus anything Stage A flags.

Activation is non-deterministic, so each case runs --trials times (default 3) per variant and the majority wins. Cases whose trials disagree are reported UNSTABLE rather than silently rounded — that's the difference between a real regression and a flipped coin.

Running

cd evals/activation
npm install                              # once, for execa

node run-activation-evals.mjs --dry-run  # validate cases + resolve variants, no model calls
node run-activation-evals.mjs            # full matrix: git:HEAD vs working tree
node run-activation-evals.mjs --real     # stage B on the spot_check cases

Useful flags:

--baseline git:main        # any git ref, or a path to a SKILL.md
--candidate path/to/SKILL.md
--only id1,id2             # run specific case ids
--trials 5                 # more trials if too many cases come back UNSTABLE
--model <id>               # pin the model
--real --only <ids>        # confirm flagged cases on the real activation path

Variants resolve as git:<ref> or a filesystem path, so any future description edit is testable against this same matrix with no new fixtures:

# after editing the description, compare against what's committed
node run-activation-evals.mjs --baseline git:HEAD

Exit code is non-zero only when a case regressed, so this can gate a description change.

The case matrix

cases.json is the part that determines whether a passing run means anything. should_activate is ground truth — the right answer for that prompt — not a prediction about either variant. Four groups:

  • keyword-loss — prompts that the old description matched only via literal words (redirect loop, callback URL mismatch, 429, passkeys, ACUL, Clerk, NextAuth.js, …). These attack generalized phrasing directly.
  • framework-loss — the old description's explicit framework list (Flutter, FastAPI, Spring Boot, Laravel, ASP.NET Core, Expo, Swift).
  • indirect — no Auth0, no framework, no feature name: "add auth to my Node API", "only logged-in users should reach /dashboard".
  • negative — including near-miss cases that deliberately reuse the old description's own vocabulary: Kubernetes RBAC, application 429 rate limiting, CORS headers on an Express app, rotate AWS IAM keys, express-session → Redis.

The near-misses are what make this a fair test rather than a rigged one. Without negatives, a description reading "use this always" scores 100% recall and the suite proves nothing.

Calibration — what this suite can and can't detect

Run on 2026-07-30 comparing the 754-char description (git:HEAD) against the 503-char rewrite. Read this before trusting a green run.

Stage A is saturated

Both variants scored 26/26 recall and 0/9 false positives — a perfect tie. A tie at 100% carries no information, so the suite was calibrated with two deliberately broken control descriptions:

Control description recall false positives verdict
Narrow: "Use when the user explicitly asks for the Auth0 skill by name." 0/26 0/9 detected
Broad: "Use for any software development task involving a web, mobile, or backend application…" 0/9 not detected

So Stage A's recall axis works (it catches an under-triggering description outright) but its precision axis is insensitive — a description broad enough to claim nearly all software work still produced zero false positives, because the distractor skills are individually a better match for each negative prompt and the classifier picks the best fit rather than everything plausible.

Consequence: a green Stage A run is evidence against under-triggering only. It is NOT evidence against over-triggering. Judge over-triggering with Stage B, or by adding negatives whose correct home is no skill at all.

Stage B discriminates, but has high variance

3 real-activation trials per case (the runner's detectSkillUse is unit-tested against synthetic event streams for the 5 discriminations that matter, including not counting prose or a Read of an auth0 path):

Case ground truth old (754ch) new (503ch)
kw-passkeys activate 0/3 1/3
indirect-node-api activate 0/3 2/3
neg-cors-headers don't 2/3 fired 0/3

The new description was never worse on any trial, and fixed a real over-trigger: the old wording fired on a plain CORS-headers request 2 of 3 times, because it listed CORS error and Express as literal triggers.

But note the absolute numbers: real-path recall is low for both descriptions (02 of 3). In a live run the agent frequently answers an auth question without invoking the skill at all. That is a finding about the skill's discoverability in general, not about this edit — and it's the most useful thing this harness surfaced. Worth investigating separately.

Practical guidance

  • Never read a Stage A tie as "the change is safe" — it mostly means both descriptions clear a low bar. Look at Stage B and the control table.
  • Use --trials 5+ on Stage B before believing any single-case difference.
  • If you add over-triggering negatives, verify the suite can actually fail by re-running the broad control: node run-activation-evals.mjs --baseline /tmp/SKILL-broad.md --trials 1

Adding a case

{
  "id": "kw-something",
  "prompt": "What the developer actually types.",
  "should_activate": true,
  "why": "Why this is the ground truth — required, it's what makes the case reviewable.",
  "tags": ["keyword-loss", "debug"],
  "spot_check": true          // optional: include in the --real stage
}

Guidance:

  • Write the prompt as a developer would type it, not as a description-shaped sentence. Echoing the description's vocabulary tests string matching, not intent recognition.
  • Every positive needs a plausible negative twin using the same vocabulary. A description can only be shown to discriminate if something nearby doesn't match.
  • Skip prompts with no defensible ground truth. cases.json documents the ones excluded on purpose (e.g. "hash passwords with bcrypt in my hand-rolled auth" — both activating and respecting the user's stated choice are defensible). Scoring those measures taste, not activation.
  • Don't tune the description until the suite is green. Fitting the wording to 35 known prompts is overfitting; the goal is a description that generalizes to the prompts nobody wrote down.