signal-triage

$npx mdskill add expo/expo-harbor-eval/signal-triage

A feedback signal only becomes an eval when it measures a **durable model capability**. Most signals are instead (or additionally) product feedback that belongs upstream. Run every signal through this procedure before writing any task files; record the verdict as a row in `tasks/TRIAGE.md` before starting authoring.

SKILL.md

.github/skills/signal-triageView on GitHub ↗
---
name: signal-triage
description: Decide whether an Expo CLI/skills feedback signal becomes a tasks/codegen/feedback-* eval, an upstream product fix, or both. Use BEFORE authoring any eval from a field report — when reading new submit-expo-feedback signals, eval-candidate submissions, or bug reports about models failing on Expo work.
---

# Signal triage: eval, upstream fix, or both

A feedback signal only becomes an eval when it measures a **durable model
capability**. Most signals are instead (or additionally) product feedback that
belongs upstream. Run every signal through this procedure before writing any
task files; record the verdict as a row in `tasks/TRIAGE.md` before starting
authoring.

## Gate 0 — dedupe

Grep the signal's feedback id against existing tasks first:

    grep -rl "<feedback_id>" tasks/

One report can hold several findings (bca74ecdb803 produced feedback-01 AND
feedback-02) — dedupe per *finding*, not per report. Anything already covered
needs no new task.

A hit may land in `tasks/TRIAGE.md` rather than a task dir — that is the
verdict ledger, and it means the id was already triaged (possibly dropped or
held). Read its row instead of re-deriving the verdict; dropped and held ids
exist ONLY there.

## The three questions

**Q1 — Who failed: the model, or the product?**

- A model chose a wrong approach while a correct one was available with the
  tools at hand → eval material.
- Expo-owned code (eas-cli, an SDK package, skill text) misbehaved and the
  agent merely *discovered* it → product material. A polished root-cause
  report means the agent **succeeded**; that is not a failure signal at all.
- Both (misleading skill text that models follow into a real bug) → both
  tracks: fix the guidance upstream AND build the eval as its regression
  harness.

**Q2 — Who owns the root cause? (decides durability)**

- Apple/UIKit/Yoga/React Native platform behavior: durable. Expo cannot fix
  it, models must handle it forever, the eval stays valid.
- Expo-owned bug: transient. Expo will ship a fix, and the eval's ground
  truth — including its distractor — can invert with one release. Do not
  build an eval whose correct answer depends on an upstream bug staying
  unfixed. (Sharp test: "would the distractor become the *right* answer
  after the fix ships?" If yes, no eval.)
- Pinning versions does not rescue transient signals: pinned SDK tasks stay
  representative of code models must still write; an archived tool bug is
  representative of nothing once fixed.

**Q3 — Is it on-mission for a family?**

- expo-codegen / expo-feedback: writes correct Expo app code, especially
  where popular guidance misleads. The best tasks are traps (CONTRIBUTING.md,
  "Field-sourced tasks").
- simbench: operates a fixed app on the simulator with programmatic state
  verification. For a coding signal with runtime-visible symptoms, add native
  verification to that same codegen task; do not duplicate it as a device-use
  task. Simbench evaluates tool operation, while native coding checks evaluate
  the submitted app.
- Debugging Expo's own toolchain is currently no family's mission — routing
  upstream is not a loss.

## Verify before authoring

- The reporter is often the failing (or auto-submitting) agent itself.
  Self-diagnoses are unproven: reproduce every code-level claim against the
  pinned source before it becomes rubric ground truth (e.g. fetch the exact
  npm tarball and read the shipped code; for platform behavior, check the
  report tested it on real devices/OS versions).
- If the report contains no verified correct fix, the reference solution is
  yours to reconstruct — budget for self-verification and say so in the
  task's motivation.
- Check `latest` as well as the pinned version: a trap still shipping in the
  current release (slider falsy-zero, present in 5.0.1 AND 5.2.0) hardens
  durability and usually warrants a parallel upstream issue.
- Maintainer intent is durability evidence: an explicit wontfix or a
  merged-then-reverted fix on the platform tracker (RN modal overlap,
  facebook/react-native#50399) is the strongest possible Q2 answer.
- A report marked "resolution pending" blocks authoring only while the
  mechanism is unverifiable. If public platform docs pin the accounting and
  arithmetic (EAS Hosting subrequest budget), ground truth can be built
  without the reporter's fix — but parameterize every number the platform
  can change (the paid subrequest limit went 1,000→10,000 on 2026-02-11).
- After shipping, probe the task with a mid-tier agent and READ its passing
  submission. A pass through a shape the rubric never considered is a
  rubric gap, not a solve: feedback-09's sonnet probe scored 1.0 by
  switching web output to 'single' (an SPA shell that cannot fix link
  previews). Harden the criterion, re-hold all brackets, then re-judge the
  same submission to prove the fix bites (it dropped to 0.75).

## Verdicts and required artifacts

| Verdict | When | Artifacts |
| --- | --- | --- |
| Eval (feedback-NN) | Model failed, durable root cause, on-mission | Task per CONTRIBUTING.md: `metadata.motivation` + `feedback_id`, environment reproducing the trap, constraints that block deleting the triggering feature, `solution/distractor/` = the fix the field tested and found insufficient, full calibration bracket (empty=0, baseline=0, reference=1.0, distractor<1.0) |
| Upstream fix | Product failed (Q1) or transient root cause (Q2) | Issue/PR against the owning repo with the verified causal chain; workaround into skill text if users need it now |
| Skill edit + eval | Guidance misled AND the underlying behavior is platform-owned | Both artifact sets; the eval tests handling the *behavior*, never "recite that the old rule was wrong" |
| Drop | Not reproducible, already covered, or no capability signal | One-line note with the feedback id and reason |

## Worked examples (2026-08-06 batch)

- **bca74ecdb803** (headerTransparent + automatic content inset): skill text
  misled, but the cold-launch inset behavior is Apple-owned → skill edit +
  evals feedback-01/02. Valid because the rubric tests the explicit-inset
  fix, not knowledge of the rule's wrongness.
- **4111d2c27a14** (eas-cli capability sync disables iCloud for
  kvstore-only apps): Expo-owned CLI bug, agent succeeded in diagnosing it,
  and the portal-re-enable distractor becomes correct once the one-line
  mapping fix ships → upstream issue/PR only, no eval.
- **911b52e0e368** (percentage-height grid + sticky chrome coupled to a
  native Animated.Value across freezeOnBlur): model failed twice
  (GPT-5.6, physical-device repro), root causes are Yoga/native-driver
  mechanics → eval feedback-03. Report shipped no verified fix, so the
  reference was reconstructed and the rubric judges properties (intrinsic
  heights, visibility derived from offset, no imperative native resets),
  not one library choice.

## Worked examples (2026-08-12 batch)

- **029b2a2da3d2** (overlapping Modals freeze touch, SDK 56): verified in RN
  0.85.3 source — `_isPresented` flips before presenting with no failure
  check, and the iOS `isRendered` latch keeps an invisible screen-sized,
  touch-claiming host view mounted; the frozen state is absorbing and the
  sibling-overlap repro is fully deterministic. Identical code in RN 0.87,
  Meta explicitly declined to fix (#50399) → eval feedback-04. Rubric judges
  properties (single visibility source of truth, completion-signal
  sequencing, product behaviors preserved); the
  editor-nested-inside-the-open-modal pattern is legit and must pass, as is
  an onDismiss-gated handoff. Strong later simbench variant. Authored as
  feedback-04-modal-editor-touch-freeze; calibrated empty=0, baseline=0,
  reference=1.0, distractor=0.0 on two stable judged runs.
- **3892d1c10f3c** (slider recenter, 5.0.1): three verified layers — the JS
  wrapper coerces falsy 0→undefined (in 5.0.1 AND latest 5.2.0), the Fabric
  codegen default is 0 so the native `oldProps.value != newProps.value`
  guard never fires, and no `prepareForRecycle` means remounts can recycle
  stale thumbs → eval feedback-05 (pin 5.0.1) + upstream issue to
  react-native-slider. The reference makes every recenter a real prop
  transition: controlled onValueChange tracking with a reset to the
  non-default positive midpoint (a keyed remount with a truthy initial
  value is the equivalent alternative) — a constant midpoint prop alone
  never re-sends to native. Authored as
  feedback-05-slider-relative-recenter with the real 5.0.1 wrapper vendored
  into environment/node_modules/ so the read-the-installed-source capability
  is exercisable; calibrated empty=0, baseline=0, reference=1.0,
  distractor=0.5 on two stable judged runs.
- **1610fc625460** (sortable grid): glitch is inherent — layout transitions
  are commit-triggered snapshots and cannot track a continuous gesture;
  every credible implementation (react-native-sortables,
  draggable-flatlist, Candillon) keeps layers stable, drives motion from
  shared values, and commits order once on drop → eval feedback-06. Rubric
  bans mid-gesture commit-driven sibling motion and multiple order commits —
  NOT LinearTransition per se (post-drop settle and add/remove transitions
  are legitimate). Authored as feedback-06-sortable-grid-stable-layers;
  calibrated empty=0, baseline=0, reference=1.0, distractor=0.25 on two
  stable judged runs.
- **019ff4bd-822c…** (EAS Hosting subrequest budget): upgraded HOLD → eval
  feedback-07. Cloudflare docs pin the accounting the model ignored (every
  fetch, every redirect hop, `connect()` per connection, the 6-connection
  concurrency gate) and Google Calendar docs pin the arithmetic (2,500
  maxResults/page, 1,000 calls per batch = 1 subrequest, `nextSyncToken`) —
  an offline fetch-counting mock with a stipulated budget B separates
  batched from naive syncs by >1000x. The reporter's pending resolution is
  not load-bearing.

## Worked examples (2026-08-13 batch)

- **aa72f3888091** (modal keyboard, Android edge-to-edge): triage verified
  the window-level trap in RN 0.85.3 source (the modal dialog requests
  adjustResize but is forced edge-to-edge, which Android ignores), and the
  authoring research then OVERTURNED the planned design: core Keyboard
  events fire for modal-window IME (activity-insets emitter) and core
  KeyboardAvoidingView consumes exactly them, so the dependency-free fix is
  a full-window behavior='padding' KAV directly inside the Modal — and the
  distractor is a KAV misplaced around the centered card → eval
  feedback-08-modal-keyboard-viewport, calibrated 1.0/0.25 on two stable
  judged runs. Lesson: verify the FIX mechanism, not only the bug
  mechanism — the research step can flip the reference/distractor design.
- **4711ed525356** (invite links 404 outside the app): the model treated
  platform association as a replacement for serving the canonical URL;
  Apple/Android/Chrome docs pin every browser-landing case, so the
  contract is permanent → eval feedback-09-invite-links-web-fallback
  (env = association-only + static web output; reference = server output +
  a web-branched invite route with a gesture-driven scheme attempt, store
  links, and context), calibrated 1.0/0.5 on two stable judged runs.
  Lesson: knowledge-contract signals verify from vendor docs alone — no
  pinned-source repro required.