Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .kilo_workflow/dispatch-role.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ STRIP='$(env | grep -oE "^(KILO|OPENCODE)[A-Za-z0-9_]*" | sed "s/^/-u /" | tr "\
# Redirection below means an attached pane shows nothing at all. Say so in the
# pane itself — a blank window reads as a dead agent otherwise. This prints to
# the terminal only, never into the log, so the EXITCODE contract is untouched.
CMD="echo $(printf '%q' "$NAME: output goes to $LOG — this pane stays blank by design; watch with: tail -f $LOG") && cd $(printf '%q' "$WT") && env $STRIP kilo run $(printf '%q' "$MSG") --agent $(printf '%q' "$ROLE") --title $(printf '%q' "$NAME")"
# `--auto`: a headless `kilo run` auto-REJECTS every permission ask it cannot
# pre-empt (e.g. protected ~/.config/kilo reads), and the reject kills the
# round silently with EXITCODE=0 and no sentinel (see
# learnings/system/headless-role-dispatch-dies-on-permission-ask.md in the main
# checkout). Role definitions already grant what these agents need; --auto
# stops the CLI answering "no" on the agent's behalf.
CMD="echo $(printf '%q' "$NAME: output goes to $LOG — this pane stays blank by design; watch with: tail -f $LOG") && cd $(printf '%q' "$WT") && env $STRIP kilo run $(printf '%q' "$MSG") --agent $(printf '%q' "$ROLE") --auto --title $(printf '%q' "$NAME")"
for arg in "$@"; do CMD+=" $(printf '%q' "$arg")"; done
CMD+=" > $(printf '%q' "$LOG") 2>&1; echo EXITCODE=\$? >> $(printf '%q' "$LOG")"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# mobile: Android a11y bounds are clamped to the visible viewport — clip probes need pixels or control offsets

Symptom: on Android, `maestro hierarchy` bounds for a partially off-screen React Native
card report only the VISIBLE band (clamped at the screen's content edge, e.g. y=444 on a
2560px-tall emulator), unlike iOS which reports window/content coordinates past the edge.
Consequences for E2E geometry probes:

- A card's true top cannot be read once it crosses the viewport edge; repeated dumps show
the card "stuck" at the edge value. Compute the true top from a child control's reported
offset (PR-review thread cards: control row sits ~35px below the card's true top).
- Taps on a control whose band is clamped to a few px (e.g. reported `[72,444][775,453]`)
are unreliable and usually miss — Maestro and ADB/uiautomator taps alike.
- Zero-motion assertions remain valid with clamped bands: if neither the card's true rect
nor the viewport moved, the clamped band is byte-identical; any true motion changes it.

For header-visibility questions (does a title render behind the app chrome), don't trust
a11y at all: take a screenshot and scan pixels — locate the card background color's first
visible row and count dark text pixels in the header band (PR-review card bg is
(240,238,230), page bg (251,250,245); the chrome is opaque, not blurred).
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# mobile: Android Maestro hierarchy invents phantom-tall bounds for clamped expanded cards — verify with uiautomator text rows

Symptom: after expanding a PR-review thread whose header flew off-screen, `maestro
hierarchy` reports the card as e.g. `[37,444][1043,1496]` (a 1052px-tall band starting at
the a11y clamp edge), suggesting the card is merely clamped at top. `adb uiautomator dump`
on the same screen shows the truth: the card's tail (last comment + Reply row) ends ~y510
and the next thread starts at y520 — the reported 1052px band is phantom.

Consequence: never read expand-state or card geometry from Maestro bounds once the a11y
clamp (y=444 on pixel9 API35) is involved. The reliable probes:

- `adb shell uiautomator dump /sdcard/window.xml` — text rows keep true y per visible row;
a thread's title ABSENT from the dump means its header is off-screen (the flow-4 failure
signature). Identify which expanded thread a clamped tail belongs to by matching its
comment text against the fixture (`grep body: server.mjs`), not by position.
- The thread's Expand/Collapse control DISAPPEARS from both dumps when its true bounds are
fully above the clamp edge — a missing control is itself evidence the header is off-screen.

Tap geometry note (Android, same runs): the expand pressable is the title row — its band is
title-text-top +20 to +63 (e.g. title y555 → tappable ~555-598). Taps even ~10px below the
band hit the badge/meta row and silently do nothing; derive the tap point from the CURRENT
uiautomator title row (+35/+50), never from a stale dump or a previous thread's offsets.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# mobile: D3 drag-cancel race (7b) — iOS drag NEVER lands mid-settle with Maestro; Android adb combined instrument works

Symptom: verifying "drag the list mid-settle cancels the deferred expand" (D3 guard). Tap a
top-clipped thread's expander, then swipe. The thread expands anyway. On iOS this is NOT a
product defect and NOT (as an earlier version of this learning claimed) a drag that "lands
in time".

Cause, measured on-device 2026-07-29 (pr-review-d957 r4, iOS 26.5 sim): the deferred settle
completes in **~316ms** (scrollToIndex animated park + promise resolution). Maestro's
tap→swipe turnaround in ONE flow file is **~700ms+** on iOS. The drag's `onScrollBeginDrag`
arrives ~400ms AFTER the expansion applied; the "failure" is the product correctly
expanding post-settle and the drag then scrolling the expanded list. r2's iOS 7b "pass" was
the same artifact (the cancel verdict was luck, not mechanism).

Decisive readout (use it again): temporary `console.log` in `invalidateSettle(source)` and
around the scrollToIndex await in `pr-review-discussion-tab.tsx` (byte-restored after) —
Metro captures `[D3R4] begin gen N` / `resolved gen N current M match <bool>` /
`invalidateSettle from drag|retap at <epochMs>`. `match false` = cancel won;
`invalidate` timestamp after `resolved` = instrument too slow.

Working instruments:
1. **Android only**: one `adb shell "input tap x y && input swipe 540 1400 540 1100 250"`
(~30-50ms gap) on an UNclamped control (top > a11y clamp edge, e.g. y=444 on pixel9
API35). Confirmed again in r4: drag landed 51ms into the settle, `match false`, thread
stayed collapsed.
2. iOS: NO sanctioned instrument lands mid-settle (simctl has no touch injection; a second
concurrent Maestro process wedges the driver). Report iOS 7b as instrument-blocked with
the trace, and rely on (a) the wiring trace (`invalidateSettle from drag` DOES fire for
real drags) plus (b) Android 7b for the cancel verdict. Do NOT classify an iOS expansion
after a Maestro tap→swipe as a product failure without the trace.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# mobile: iOS FlashList blanks entirely when maintainVisibleContentPosition is toggled off for one commit and back on (Android tolerates it)

Symptom (pr-review-d957 r5, head df4b6c379): with the deferred-expand path suppressing
`maintainVisibleContentPosition` for exactly the expand commit (`{disabled: true}` for one
auto-batched render, re-enabled on a 150ms timeout), EVERY fired top-clipped deferred
expand on iOS (4/4: gamma, epsilon, zeta geometries, incl. one uninstrumented) leaves the
discussion list COMPLETELY BLANK — no thread rows in the a11y tree, uniform page-colored
viewport, not recoverable by swipes; only a remount (tab switch) brings content back.
`getAbsoluteLastScrollOffset()` at +600ms post-expand reads the deterministic garbage
value `-997949.6666666666` (identical across runs). The settle itself is healthy
(`scrollToIndex resolved … match true` at +330-400ms, offset ~786 — the pre-expand park
lands correctly); the corruption happens at/after the suppressed expand commit or at
re-enable. Android on the same build PASSES all geometries: offset stable post-expand,
header parked at y1300 px on screen (gamma 1248 / epsilon 1314 / zeta 1140 dark px in the
header band, uiautomator title row present).

Consequence for E2E: an iOS blank list after a deferred expand is reproducible 100% and is
NOT the r1-r4 flight signature (header off-screen by expansion height) — it is a distinct,
more severe failure mode. Diagnose with a temporary console.log of
`getLayout(index).y` / `getFirstItemOffset()` / `getAbsoluteLastScrollOffset()` at tap,
settle-resolved, and +600ms post-expand (the D3R5 trace pattern, 7 anchored insertions in
`pr-review-discussion-tab.tsx`); the -997949 offset is the smoking gun. Verify blank vs
flown with a screenshot pixel scan (uniform (251,250,245) below the chrome = blank) plus
`maestro hierarchy` showing no `Discussion thread` rows.

iOS tap gotcha recorded on the same runs: coordinate taps at y ≤ ~182pt just under the PR
screen's tab chrome are silently swallowed (Maestro logs COMPLETED, handler never runs);
taps at y ≥ 187pt land. Place clipped-thread expand taps at ≥ y190pt.

Fix direction for the product (not applied): do not toggle `disabled` around the commit on
iOS — e.g. gate the suppression to Android, or replace it with the recorded fallback
(exact `scrollToOffset` settle, completion-polled) so mVCP is never cycled.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# mobile: never run two maestro commands concurrently on one UDID; screenshot loops finish before maestro's slow startup

Symptom: `IOSDriverTimeoutException: iOS driver not ready in time` mid-run on iOS when a
background `maestro test` (tap) overlapped a `maestro hierarchy` started 1.5s later on the
same simulator. Two XCUITest channels on one UDID wedge the driver.

Recovery: kill only the stale `xcodebuild test-without-building` PID bound to the UDID
(see `mobile-maestro-ios-driver-timeout-kill-stale-only.md`); app state, scroll position,
and login all survive. Retry with `MAESTRO_DRIVER_STARTUP_TIMEOUT=300000`.

Second, related timing fact (iOS, machine under parallel-workflow load): maestro's JVM +
driver startup takes 15-25+s before the tap lands. A `xcrun simctl io screenshot` loop
(10-30 shots, ~0.5s each) started concurrently with the tap command ALWAYS finishes before
the tap lands — every frame is pre-tap. Timed screenshot/hierarchy capture of sub-second
windows (e.g. an 800ms probe delay) is not achievable this way, and concurrent maestro
for the window is what causes the timeout above.

What works instead: instrument the app with temporary `console.log` probes (captured from
`pnpm dev:capture mobile`), take hierarchy dumps at leisure before and after the
interaction, and DERIVE mid-window screen geometry from the logged scroll values:
screen_y = (layoutY + firstItemOffset) - absoluteLastScrollOffset + K, where K (the
screen-y of the list viewport's content-top edge) is calibrated from a moment where both a
hierarchy bound and the logged offsets are known. On pr-review-d957 iOS K=367, confirmed
within ±1.3pt at five independent moments across three runs and scroll regions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# mobile: positioning a thread at the top clip — FlashList max-offset clamp and sub-threshold swipes

Symptom: E2E flows that need a collapsed resolved thread clipped ~5-20pt above the visible
content top (PR-review flows 4 / 7a / 7b) cannot nudge the card high enough: swipes stop
moving the list, always at the same card position.

Cause: two independent constraints.
1. The list is at FlashList's computed max scroll offset. The clamp releases only when the
content BELOW the target card exceeds one viewport height AND has been measured. With
freshly mounted data (collapsed cards + unmeasured conversation comments), a mid-list
card simply cannot reach the clip. In the pr-review-d957 fixture (iOS), gamma clamped at
y228 until page 2 was loaded via "Load more" — r1's own flow-4 setup dump shows page-2
threads (zeta/eta) already loaded for the same reason. On Android (2424px), zeta could
not be clipped until theta (below it) was expanded, adding ~850px below.
2. Short Maestro swipes (<~10% of screen height, e.g. 1-6%) are silently ignored on both
platforms (velocity/distance below the scroll-recognition threshold); hierarchy bounds
stay byte-identical. Do not retry them in a loop — they never land.

Working sequence per platform:
- Load page 2 first (`scrollUntilVisible` 'Load more comments' + tap), or expand a thread
below the target to grow content.
- Approach with 30% Maestro swipes, one command at a time with a hierarchy probe between
(back-to-back commands after big swipes get ignored intermittently).
- Final 40-100px positioning: iOS — a 6-8% swipe usually lands once the clamp is released;
Android — `adb shell input swipe x y1 x y2 300` works reliably but ONLY once the offset
can grow (it is also clamped, not broken).
- Android reminder: the a11y clamp edge (y=444 on pixel9 API35, 1080x2424) is NOT the
opaque-chrome bottom (~y536 pixels); verify clip geometry with a screenshot pixel scan,
not bounds alone.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# mobile: devSeedUserGithubToken 500s when a sibling e2e user already owns github_user_id 999001

Symptom: `githubApps.devSeedUserGithubToken` with `githubUserId: "999001"` returns a 500
("Failed query: insert into user_github_app_tokens ... on conflict ..."), even though the
runbook says a sibling seed should surface as a benign `upserted: false`.

Cause: the local postgres on this machine is SHARED across worktrees (this worktree's stack
used the base `postgres` DB on localhost:5432 — the `postgres-N` DBs belong to other flows;
find yours via `select count(*) from user_auth_provider where provider_account_id='<e2e email>'`
across DBs). A previous section seeded github_user_id 999001 for a DIFFERENT kilo_user_id
(`e2e-mobile-mobile-audit-w1-pr-safety@example.com`). The upsert targets
(kilo_user_id, github_app_type); with no row for YOUR user it attempts a plain insert, which
violates unique index `UQ_user_github_app_tokens_github_user_app` on (github_user_id, app_type).

Fix: list existing ids first —
`psql -h localhost -p 5432 -U postgres -d postgres -c "select github_user_id, github_login from user_github_app_tokens"`
— and seed with a FREE id (999003 worked). The github_user_id value only feeds the token
envelope AAD, which round-trips through the same row, so any unique value works against the stub.

Also: the seed mutation needs an authenticated call. The web fake-login cookie path did NOT
satisfy tRPC (`UNAUTHORIZED`). Working path with curl, same as the app's own native login:
`POST /api/auth/native/otp {email}` → read the 6-digit code from the newest `dev/logs/emails/*.html`
→ `POST /api/auth/native/token {provider:'email', email, code}` → `{token}` → call tRPC with
`Authorization: Bearer <token>`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# mobile: Maestro childOf never matches RN a11y cards on iOS — tap by bounds-derived point

Symptom: `tapOn: {text: 'Expand thread', childOf: {text: 'Discussion thread src/gamma.ts L33 (RIGHT)'}}`
fails with "Element not found: Text matching regex: Expand thread", while `maestro hierarchy`
clearly shows both labels on screen.

Cause: in Maestro's flattened iOS a11y tree, a RN View with `accessibilityLabel` (the thread card)
and the Pressables INSIDE it (`Expand thread`, `Unresolve thread`) are SIBLINGS at the same depth,
not parent/descendant — verified by walking the hierarchy JSON. `childOf` finds zero parents, and
the error misleadingly names the child selector.

Fix: parse `maestro --device <udid> hierarchy` output — every element carries `bounds` as
`[x1,y1][x2,y2]` — and `tapOn: {point: <center-x>,<center-y>}`. Verify positions with a fresh
hierarchy dump immediately before the point tap; positions were stable across repeated dumps.

Second trap: taps whose y is under the app's fixed header chrome are silently swallowed — Maestro
logs COMPLETED but nothing happens (same signature as the keyboard swallow). On the PR-review
screen the tab selector row ends at y≈166 (iPhone 17 Pro) and the effective dead zone extends to
about y≈178-180; a tap at y=174 was eaten, y=180 worked. When positioning a row for a header tap,
leave its control at y≳185; a collapsed thread card's expand control sits only 13pt below the card
top, so a "partially clipped at the list top" card's control is always inside the dead zone.

Also: Maestro text matching treats the pattern as regex — literal parens in labels like
`L33 (RIGHT)` still MATCH because the matching is not strict full-string in every code path
(`scrollUntilVisible` found the element with the unescaped pattern).
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# mobile: IOSDriverTimeoutException — killing ONLY the stale xcodebuild sufficed (no sim reboot)

Update to `mobile-maestro-ios-driver-timeout-stale-xcodebuild.md` (which prescribes
kill xcodebuild + `simctl shutdown && boot`).

Observed 2026-07-29 (pr-review-d957 repro run, sibling section thrashing Maestro driver restarts
on their own UDID every ~8s): `IOSDriverTimeoutException: iOS driver not ready in time` mid-run.
`ps aux | grep "xcodebuild test-without-building" | grep <UDID>` showed exactly one stale driver
bound to my UDID. Killing just that PID (no simulator shutdown, no reboot) and retrying with
`MAESTRO_DRIVER_STARTUP_TIMEOUT=300000` recovered immediately — app state, scroll position, and
login all survived, so the in-flight E2E probe sequence continued without re-navigation.

Try the kill-only path first; keep the learning's shutdown+boot as the fallback when the driver
still won't come up.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# mobile: Maestro `direction: DOWN` swipe is eaten by a sticky FlashList header overlay

Symptom: on a list with `stickyHeaderIndices` (PR-review Files tab), swipes that scroll
content DOWN (offset decreases, e.g. `direction: DOWN` or start≈30% end≈70%) silently do
nothing — Maestro logs COMPLETED, hierarchy is byte-identical. Up-swipes work fine.

Cause: FlashList's StickyHeaders overlay is an absolute-positioned sibling ON TOP of the
list, not part of the scroll content. A gesture starting inside the overlay's frame (on the
PR Files screen it sits at the list top, y≈233-267 ≈ 27-30% of an 874pt screen) does not
scroll the list. Maestro's directional DOWN swipe starts near y≈30%, inside the overlay.

Fix: use explicit-coordinate swipes whose start is below the overlay
(`start: 50%, 40%` → `end: 50%, 75%`). Verify movement with a before/after hierarchy grep
(bounds of a stable row). Note: real users dragging from the stuck header row hit the same
non-scrollable strip — standard FlashList StickyHeaders behavior, not a defect to report
unless the product wants drag-from-header scrolling.
17 changes: 17 additions & 0 deletions .kilo_workflow/learnings/mobile-tablet-app-startup-broken.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# mobile: app startup/consent unusable on tablet simulators (iPad + tablet AVD) — blocks tablet E2E login

Symptom (observed 2026-07-29, pr-review-d957, both surfaces on the same day):
- iPad Pro 11-inch (iOS 26.5): app boots to "Welcome to Kilo Code" but the consent body
and `Accept and continue` button never enter the a11y tree (logo + title only, "1 page"
scroll bars). `login.sh` and its cold-relaunch retry both fail — never reaches the email
field.
- kilo_pixel_tablet_api35 (Android): after the dev-client deep link, the resumed
MainActivity exposes NO app content (a11y tree = status bar only; screenshot pixels are
mostly black + page-bg). Metro serves the Android bundle fine. force-stop + deep-link
relaunch (the one supported recovery) does not recover.

The phone surfaces (iPhone 17 Pro iOS 26.5, kilo_pixel9_api35) work the same day, so this
is tablet-form-factor specific. The welcome/consent screen and startup are outside any
PR-review change area. Impact: tablet side-by-side regression checks (PR-review flow 9)
are environment-limited until app startup on tablets is fixed; do not classify PR-area
failures from tablet evidence until login works there.
Loading
Loading