fix(golf): stop the keyboard covering the distance field you're typing into - #1659
Conversation
…g into Reported from the app: on the live shot-entry screen, focusing "Distance remaining" opens the numeric keypad directly over the input, so you cannot see the number you are entering. Root cause is a half-shipped design, not a missing one. `capacitor.config.ts` sets `resize: 'ionic'`, which does NOT resize the WebView — it expects the app to react to the keyboard itself. Ionic's framework does that; this is not an Ionic app, so nothing did. The layout viewport stays full height, the browser therefore believes every input is still visible, and the keyboard just covers the bottom ~45% of the screen. globals.css already carries the correct landing rule for a keyboard-aware scroll — `scroll-margin-bottom: calc(var(--keyboard-height) + 40px)` on every input under `body.capacitor` — and `--keyboard-height` is already maintained by CapacitorProvider's keyboardWillShow listener. That CSS was dead: it shapes where a scroll lands, and no keyboard-aware scroll was ever triggered. So the fix is the missing trigger, not new machinery. - CapacitorProvider: on keyboardWillShow, after setting --keyboard-height, scroll the focused element into view. Global on purpose — a scroll on keyboardWillShow is user-initiated by definition (the keyboard only opens because someone tapped an input), which is a different event from the unrequested programmatic scroll `shouldAutoScrollDistanceInput` guards against during putt tagging. That guard is untouched. - use-shot-state-machine: drop `preventScroll: true` from the auto-focus. It was added to avoid double-scrolling against the `scrollIntoView` above it, but that call used `block: 'nearest'`, which is a no-op when the element is already inside the pre-keyboard viewport — so all it actually suppressed was the one scroll that could have known about the keyboard. Also `nearest` -> `center`, so the pre-keyboard scroll leaves the field above where the keypad will land. Verified: tsc clean, eslint --max-warnings 0 clean, 112 tests pass across the touched areas. NOT verified on a device — keyboardWillShow does not fire in jsdom or a desktop browser, and this session has no iOS runtime. The behaviour is reasoned from the config and the existing CSS, so it needs a look on a phone before it is called done. The new tests assert against source text for the same reason: the effect sits behind two setTimeouts inside a useEffect and only matters against a real iOS keyboard. They cannot prove the field is visible; they prove the two lines that made it invisible do not come back unnoticed, and that the CSS half and the listener half are never removed as "unused" independently. Both were checked to fail against the previous code rather than passing vacuously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcCZsEmEgQ4VD9EPMTcDX7
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
`Database types drift` is failing on every open PR — #1659, #1660, and it would have taken #1661 next. None of them touch the database. The entire drift is one metadata line: __InternalSupabase: { - PostgrestVersion: "14.17" + PostgrestVersion: "14.5" } That is the version string `supabase gen types` stamps into its header, not schema. No table, column, enum, function or relationship differs — the CI diff prints exactly one hunk and stops. Production now reports 14.5, the committed file says 14.17, and the gate compares generated output byte-for-byte. So a platform-side version change on Supabase's side turned every unrelated PR red. WHY THIS IS HAND-EDITED RATHER THAN REGENERATED `npm run db:types` needs SUPABASE_ACCESS_TOKEN, which is a CI secret and is not in the local env — `scripts/check-types-drift.sh` degrades to an advisory skip without it, so the drift is not reproducible locally at all. The authoritative value came from CI's own diff output, which is the same generator this gate runs. The next full regeneration will produce this byte-identically. Verified by CI on this branch: the gate that was failing is the proof. Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three leaks with one cause: worktree, branch, PR and dependency tree were treated as one object with one lifetime. Splitting them is the whole change, and the plan is explicit that separate PRs for parking, branch GC and dependency bloat would generate exactly the residue being fixed. 1 — REMOVING A CHECKOUT NO LONGER ABANDONS ITS BRANCH The old model made "remove worktree" imply "abandon branch", so a checkout could only go once its PR merged. #1659 has been open for days awaiting a physical-iPhone test and held ~3.8 GiB the entire time for no reason: its work was pushed, and the checkout was reproducible from a ref. PARK remove the disposable checkout, KEEP the branch (no PR needed) RETIRE park, AND delete a branch proven merged Parking asks a different question than retiring. Not "has this work landed" but "is this checkout reproducible from a pushed ref". That is the only place a remote tip is required, and it is required in the safe direction. 2 — BRANCH DELETION, PROVEN BY PR HEAD OID Nothing deleted branches, so they accumulated invisibly: `gh pr merge --delete-branch` fails while a worktree holds the branch, and the worktree removal path never touched refs. Ten local branches for merged PRs were sitting in this repo. The evidence is `PR MERGED` + `local tip === PR head OID`, and deliberately NOT a remote tip. `delete_branch_on_merge: true` removes origin/<branch> at merge time, so requiring it is guaranteed false for exactly the branches that are safe — #1654's shipped defect, whose first dry run printed "Nothing retirable" with 11.6 GiB in front of it. Verified before writing the classifier: for #1669/#1670/#1671/#1672 the remote branch is gone, GitHub still reports .head.sha, and it matches the local tip exactly in all four. The counterexample that earns its own verdict: someone commits to a branch AFTER its PR merged. The PR proves the old tip landed and says nothing about the new commits, which exist nowhere else once the remote is gone. KEEP_DIVERGED_AFTER_PR. `git branch -D` is unavoidable here — squash-merge means `-d` always refuses — which is precisely why it is gated behind that proof, re-verified immediately before deleting, and never exposed as a bare command a caller can aim. 3 — A CHECKOUT IS NO LONGER A ~3.8 GiB COMMITMENT new-worktree.sh installed dependencies unconditionally. Most control-plane, docs and config work never runs a test. That coupling is why six worktrees in one day took the volume to ZERO BYTES FREE, where nothing runs at all because writing a command's output needs disk. Installs are now opt-in (--install) or on demand (scripts/ensure-worktree-deps.mjs), behind a two-number policy: HELM_DISK_RESERVE_GIB 12 the floor that keeps the MACHINE usable HELM_INSTALL_BUDGET_GIB 5 conservative estimate of one install An install needs reserve+budget = 17 GiB. The measured cost was 3.8 GiB and the budget is deliberately larger: one day's observation is not a promise about the next. Unknown free space fails closed. new-worktree.sh's own precheck drops to the reserve alone, because creating a checkout no longer costs GiB. ONE AUTHORITY, TWO LAYERS scripts/lib/worktree-lifecycle.mjs decides; scripts/worktree-lifecycle.mjs gathers. Pure classifier means every verdict is covered without a git fixture per case, and a wrong verdict is a logic bug rather than a shell-quoting bug. retire-worktrees.sh becomes a forwarding shim — two implementations of a rule that deletes things is how they drift. A NEAR MISS, FOUND BY MY OWN TEST The first draft resolved the repository from `import.meta.url`. A test that built a throwaway git fixture and ran the CLI with cwd set to it therefore operated on the REAL repository, and --park removed a live worktree (keyboard-covers-distance, PR #1659). Nothing was lost, and the reason is the design rather than luck: parking keeps the branch, and PARKABLE requires the tip already equal its pushed remote. Verified after the fact — local == remote == 03a1307, PR still open, branch intact. The checkout was recreatable the whole time. It was still wrong. A tool that deletes things must take its target from where the caller is standing, not from where the script lives. Fixed, and proven by running it inside a fixture and seeing only the fixture. The bug also cost 140s of test time — the CLI was scanning 40 real branches through `gh` on every case. The suite now runs in 2s. TWO SUITES MIGRATED, NOT DELETED Both encoded the previous model and failed correctly: retire-worktrees.test.ts a clean pushed worktree with an OPEN PR was KEEP. It is now PARK. That IS the redesign. All 13 refusals are preserved in the new suite; the HELM_PR_LOOKUP seam is kept, now carrying a head SHA so exact-match cases are expressible. new-worktree-precheck.test.ts asserted the refusal names retire-worktrees.sh and that ENOSPC cleanup lives in new-worktree.sh. Both moved. Each assertion has a successor and got one; none was dropped. AGENTS.md says the standing grant RELIES on these refusals firing, so quietly weakening either file would quietly weaken that authorization. 30 lifecycle cases + 5 precheck cases. The classifier layer covers all four worktree verdicts and all nine branch verdicts, including the ones that only appear when evidence is MISSING — a failed lookup is UNKNOWN_PR and never "no PR", which is #1668's whole subject. DOCS AGENTS.md and autonomy.md both enumerated five guarantees including "an isolated dependency install". That is now four, and the install is separate. Shipping the tool without that edit would have shipped the contradiction this program exists to remove. DELIBERATELY NOT DONE HERE: the sweep across the 38 local branches. The plan sequences it after this merges, report-only first, every branch re-verified. Being right about the logic is not the same as being right about the branches. CI caught one more: the Lint ratchet, a gate distinct from `npm run lint`, went +1 on an unused `HERE` constant left over from an earlier draft of ensure-worktree-deps.mjs. `npm run preflight` is the blocking static set and runs it; I had run the individual gates instead. Fixed, and preflight is green. Verified: npm run preflight 0; npm test 1274 files / 12042 passed; typecheck 0; lint 0; check:cycles 0; enforcement:check 0; shellcheck 0; repo:doctor 0; markdown:ratchet 0; both drift gates 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
…les (#1676) Three leaks with one cause: worktree, branch, PR and dependency tree were treated as one object with one lifetime. Splitting them is the whole change, and the plan is explicit that separate PRs for parking, branch GC and dependency bloat would generate exactly the residue being fixed. 1 — REMOVING A CHECKOUT NO LONGER ABANDONS ITS BRANCH The old model made "remove worktree" imply "abandon branch", so a checkout could only go once its PR merged. #1659 has been open for days awaiting a physical-iPhone test and held ~3.8 GiB the entire time for no reason: its work was pushed, and the checkout was reproducible from a ref. PARK remove the disposable checkout, KEEP the branch (no PR needed) RETIRE park, AND delete a branch proven merged Parking asks a different question than retiring. Not "has this work landed" but "is this checkout reproducible from a pushed ref". That is the only place a remote tip is required, and it is required in the safe direction. 2 — BRANCH DELETION, PROVEN BY PR HEAD OID Nothing deleted branches, so they accumulated invisibly: `gh pr merge --delete-branch` fails while a worktree holds the branch, and the worktree removal path never touched refs. Ten local branches for merged PRs were sitting in this repo. The evidence is `PR MERGED` + `local tip === PR head OID`, and deliberately NOT a remote tip. `delete_branch_on_merge: true` removes origin/<branch> at merge time, so requiring it is guaranteed false for exactly the branches that are safe — #1654's shipped defect, whose first dry run printed "Nothing retirable" with 11.6 GiB in front of it. Verified before writing the classifier: for #1669/#1670/#1671/#1672 the remote branch is gone, GitHub still reports .head.sha, and it matches the local tip exactly in all four. The counterexample that earns its own verdict: someone commits to a branch AFTER its PR merged. The PR proves the old tip landed and says nothing about the new commits, which exist nowhere else once the remote is gone. KEEP_DIVERGED_AFTER_PR. `git branch -D` is unavoidable here — squash-merge means `-d` always refuses — which is precisely why it is gated behind that proof, re-verified immediately before deleting, and never exposed as a bare command a caller can aim. 3 — A CHECKOUT IS NO LONGER A ~3.8 GiB COMMITMENT new-worktree.sh installed dependencies unconditionally. Most control-plane, docs and config work never runs a test. That coupling is why six worktrees in one day took the volume to ZERO BYTES FREE, where nothing runs at all because writing a command's output needs disk. Installs are now opt-in (--install) or on demand (scripts/ensure-worktree-deps.mjs), behind a two-number policy: HELM_DISK_RESERVE_GIB 12 the floor that keeps the MACHINE usable HELM_INSTALL_BUDGET_GIB 5 conservative estimate of one install An install needs reserve+budget = 17 GiB. The measured cost was 3.8 GiB and the budget is deliberately larger: one day's observation is not a promise about the next. Unknown free space fails closed. new-worktree.sh's own precheck drops to the reserve alone, because creating a checkout no longer costs GiB. ONE AUTHORITY, TWO LAYERS scripts/lib/worktree-lifecycle.mjs decides; scripts/worktree-lifecycle.mjs gathers. Pure classifier means every verdict is covered without a git fixture per case, and a wrong verdict is a logic bug rather than a shell-quoting bug. retire-worktrees.sh becomes a forwarding shim — two implementations of a rule that deletes things is how they drift. A NEAR MISS, FOUND BY MY OWN TEST The first draft resolved the repository from `import.meta.url`. A test that built a throwaway git fixture and ran the CLI with cwd set to it therefore operated on the REAL repository, and --park removed a live worktree (keyboard-covers-distance, PR #1659). Nothing was lost, and the reason is the design rather than luck: parking keeps the branch, and PARKABLE requires the tip already equal its pushed remote. Verified after the fact — local == remote == 03a1307, PR still open, branch intact. The checkout was recreatable the whole time. It was still wrong. A tool that deletes things must take its target from where the caller is standing, not from where the script lives. Fixed, and proven by running it inside a fixture and seeing only the fixture. The bug also cost 140s of test time — the CLI was scanning 40 real branches through `gh` on every case. The suite now runs in 2s. TWO SUITES MIGRATED, NOT DELETED Both encoded the previous model and failed correctly: retire-worktrees.test.ts a clean pushed worktree with an OPEN PR was KEEP. It is now PARK. That IS the redesign. All 13 refusals are preserved in the new suite; the HELM_PR_LOOKUP seam is kept, now carrying a head SHA so exact-match cases are expressible. new-worktree-precheck.test.ts asserted the refusal names retire-worktrees.sh and that ENOSPC cleanup lives in new-worktree.sh. Both moved. Each assertion has a successor and got one; none was dropped. AGENTS.md says the standing grant RELIES on these refusals firing, so quietly weakening either file would quietly weaken that authorization. 30 lifecycle cases + 5 precheck cases. The classifier layer covers all four worktree verdicts and all nine branch verdicts, including the ones that only appear when evidence is MISSING — a failed lookup is UNKNOWN_PR and never "no PR", which is #1668's whole subject. DOCS AGENTS.md and autonomy.md both enumerated five guarantees including "an isolated dependency install". That is now four, and the install is separate. Shipping the tool without that edit would have shipped the contradiction this program exists to remove. DELIBERATELY NOT DONE HERE: the sweep across the 38 local branches. The plan sequences it after this merges, report-only first, every branch re-verified. Being right about the logic is not the same as being right about the branches. CI caught one more: the Lint ratchet, a gate distinct from `npm run lint`, went +1 on an unused `HERE` constant left over from an earlier draft of ensure-worktree-deps.mjs. `npm run preflight` is the blocking static set and runs it; I had run the individual gates instead. Fixed, and preflight is green. Verified: npm run preflight 0; npm test 1274 files / 12042 passed; typecheck 0; lint 0; check:cycles 0; enforcement:check 0; shellcheck 0; repo:doctor 0; markdown:ratchet 0; both drift gates 0. Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
) Two days of repairs left the pieces better and unable to check each other. This makes drift observable, so the next forensic pass is a command instead of a week. THE PATTERN, STATED ONCE control written -> documented -> assumed -> stops running -> docs remain -> agent trusts docs -> failure Observed, all of it: guard-bash.sh existed, unwired, deleted guard-sql.sh existed, unwired, deleted retire-worktrees.sh existed, never invoked four rule files claimed hooks that were gone worktree GC keyed on a ref GitHub deletes on merge Docs Regen opened a PR that sat open while main went stale a fixture test operated on the real repository Not one was a missing control. Every one was a control that stopped running while its documentation stayed put. WHAT THIS ADDS npm run control-plane:verify static + runtime npm run control-plane:verify:static CI-safe, wired as its own required job READ-ONLY by construction. It regenerates nothing, deletes nothing, mutates no config. The previous docs gate re-ran the generator and diffed, so the only way to learn the docs were stale was to have already fixed them — a verifier that repairs what it inspects cannot tell you what was wrong. 0 VERIFIED passed, or a named acknowledged gap 1 CONTROL FAILURE a control that should hold does not 2 UNKNOWN a required state could not be established UNKNOWN outranks FAIL and never becomes PASS. If something could not be established, "these specific things are broken" would imply everything else was checked. ACKNOWLEDGED GAP != UNKNOWN. Four gaps carry an id, an owner, a date, a reason and a closing condition in config/control-plane-gaps.json, and every run prints them so they cannot fade. MUTATION BUDGET, ENFORCED BEFORE ALLOCATION HELM_MAX_MUTATION_WORKTREES defaults to 1. Refused before `git worktree add` and before any install, so a refusal costs nothing. Classification FAILS TOWARD MUTATION: an unreadable or undeclared workspace counts. A declared release workspace escapes the budget only if it is ALSO detached — on a branch it can still be committed to. The disk reserve stopped a catastrophe; it never stopped waste. Six worktrees in one day was a concurrency problem that only became visible as a disk problem. BRANCH-GC AUTHORIZATION, RECORDED IN THE TOOL ITSELF Verdict renamed DELETE_SAFE -> DELETE_MERGED_EXACT so it states its own evidence, and the grant lives in the authority, not in a paragraph elsewhere: PR MERGED + local tip === PR head OID + not protected + not checked out Nine verdicts are explicitly excluded. The sharpest is new: NO_UPSTREAM_UNIQUE_WORK. Measured — ten branches hold up to 19 commits with no upstream, existing nowhere else. Folding those into UNKNOWN_PR conflated "the lookup failed" with "this is the only copy", and only one of those is permanent. THE GATE-LIE FROM #1672, ACTUALLY CLOSED #1672 raised the timeout and its own PR said that makes the unknown RARER, not DISTINGUISHABLE. The eleven repo sweeps now run in a dedicated `guards` project under scripts/repo-guards.mjs: PASS ran, found nothing POLICY_FAILURE ran, found violations exit 1 INFRASTRUCTURE_FAILURE did not finish — UNKNOWN exit 2 Proven end to end by injecting a real 200s hang into a real guard: exit 2, never reported as a violation. The guards are UNTOUCHED. They are 116-357 lines of bespoke matching logic each; rewriting them to fix a REPORTING defect would risk weakening the guards themselves. The lie was at the reporting boundary and is fixed there. Worth recording: vitest's JSON reporter does not carry the timeout text — a real timeout arrives as `Error: STACK_TRACE_ERROR`. A first draft scraped the human-readable stream to recover it. That did not match, and it was the wrong idea: parsing a tool's console output to make a safety decision is the brittle-regex pattern this repo has already deleted guards for. The guarantee is the DEFAULT — an unrecognised failure is INFRASTRUCTURE_FAILURE — not a pattern. TOOL AUTHORITY, AND EVIDENCE THAT EXPIRES docs/TOOL_AUTHORITY_MATRIX.md, generated. Five words that are not synonyms: CONFIGURED, CONNECTED, EXPOSED, ALLOWED, EXERCISED. ET-4 lost two days reading the first as the last. Probed live, not inferred: Sentry BOTH namespaces connected, find_organizations byte-identical. Redundant -> mcp__claude_ai_Sentry__* is authority, plugin denied. Reversible: one deny line restores it. Vercel plugin exposes ONLY authenticate -> CONFIGURED, not CONNECTED. Account connector is authority; dead namespace denied. GitHub the gh CLI is the lifecycle path, exercised three ways: MERGED #1676, OPEN #1659, and a branch with no PR. A failed lookup stays UNKNOWN_PR (#1668). Supabase sanctioned mcp__supabase__* is installed but NOT connected. Every observation carries a fingerprint of the configuration that produced it. Change a deny rule and the matching EXERCISED claims become STALE on the next regeneration. Nobody has to remember to invalidate them. Two bugs found by running it: Sentry and GitHub produced an IDENTICAL fingerprint (both slices empty), which would have made drift undetectable for both; and a service governed by nothing now says `ungoverned` rather than implying coverage it does not have. SUPABASE — A STRUCTURAL FINDING Starting the sanctioned server's OAuth flow shows its requested scopes: organizations:read projects:read database:read analytics:read secrets:read edge_functions:read environment:read storage:read Every scope is :read. That is connector-enforced capability restriction — real evidence, not a URL string, and obtainable without any write probe. It also means mcp__supabase__apply_migration almost certainly CANNOT function under this grant, while an explicit owner authorization says it may. Surfaced as gap SUPABASE_MIGRATION_GRANT_VS_READ_SCOPES rather than resolved, because resolving it means attempting a production migration to answer a documentation question. USER-GLOBAL, UNDER EXPLICIT AUTHORIZATION AND WITH A BACKUP removed mcp__plugin_supabase_supabase__{execute_sql,apply_migration} — no such plugin is installed; a grant waiting to activate on install is a trap, not a capability removed mcp__claude_ai_Supabase__apply_migration — account-WIDE mutation nobody authorized kept mcp__supabase__apply_migration — the owner's explicit grant kept mcp__claude_ai_Supabase__execute_sql as TEMPORARY_FALLBACK, named UNENFORCED, because it is the only working query path until the sanctioned one connects fixed the autoMode prose claiming a PreToolUse hook blocks destructive SQL. It never did; it had been copied from this repo's own docs. SANDBOX — MEASURED, NOT ASSUMED, AND NOT ENABLED Ten disposable probes: writes inside the project, outside it, into .claude/settings.json and .claude/hooks, reads of .env.local, and ../ traversal ALL SUCCEEDED — including paths the config lists under denyRead and denyWithinAllow. sandbox.filesystem.disabled is true and authoritative. Network sandboxing IS enforced (observed: deny network-outbound analytics.brew.sh:443). NOT enabled. The configured allowWrite covers /private/tmp/helmv3-*, ~/.npm and ~/Library/Caches/deno — not the repository — so flipping it as configured would block every worktree write, user-globally, for every project and any concurrent session. Recorded as gap SANDBOX_FILESYSTEM_DISABLED with the closing condition. No fake Bash parser was added as compensation. DOCS REGEN — created != integrated #1623 existed because main carried a stale route count (234 vs 235, after /admin/self-heal shipped) and the fix lived in a bot PR that nothing merged. That workflow's own header recorded that GITHUB_TOKEN PRs may receive no required CI, so it could not be merged confidently either. regen-docs.mjs gains a genuinely NON-MUTATING --check that computes expected blocks, diffs them, names the first differing line, and leaves the tree byte-identical. It is now a required PR-time gate. The auto-PR workflow is verification-only and creates nothing. #1623's diff is byte-for-byte what regeneration produces here, so it closes as superseded. FAILURE INJECTION — 15 cases, every one in a disposable COPY delete a configured hook -> FAIL point a hook at a nonexistent script -> FAIL break the blocking hook's matcher -> FAIL reintroduce a false safety claim -> FAIL the same claim, QUOTED -> PASS (corrections stay writable) declare a namespace never observed -> FAIL move the budget check after allocation -> FAIL remove the budget check -> FAIL change a service's config -> its observations STALE change one service -> others NOT staled a service governed by nothing -> ungoverned, not fresh inject a real 200s guard hang -> INFRASTRUCTURE_FAILURE, exit 2 inject a real banned import -> POLICY_FAILURE, exit 1 Plus a SENTINEL proving the injections cannot reach the live checkout. That is the direct regression for #1676, where a fixture test resolved its target back to the real repository and parked a live worktree. THREE THINGS THE VERIFIER CAUGHT IN ITS OWN AUTHORS' WORK 1. It reported mutation-budget-enforced FAIL against a correctly-ordered script, because indexOf matched `git worktree add` in the script's own HELP TEXT at line 15. Substring is not mechanism — the same error the enforcement generator made on its first run. Now line-based and comment-aware. 2. The matrix rendered the whole Supabase namespace as DENIED when only its ten mutators are, and folded DENIED_BY_POLICY and NOT_EXERCISED into "FAILED" — which reads as "we tried and it broke", untrue of both. 3. #1672's own test asserted `unit` must be no stricter than its siblings, because `unit` held the sweeps. They moved. The failure was the premise announcing itself, and the test now pins the invariant that survives: whichever project runs the sweeps must be the most generous. DELIBERATELY NOT DONE No UNKNOWN or no-upstream branch deleted. No production SQL write as a probe. No regex Bash/SQL pseudo-guard. #1638 not merged. sandbox.filesystem not enabled. The Repair LaunchAgent not re-armed — a green control plane is not evidence about a different unresolved runtime problem. Verified: npm run preflight 0; npm test 1265 files / 12049 passed; control-plane:verify 0 (20 pass, 0 fail, 0 unknown, 1 acknowledged gap); control-plane:verify:static 0; enforcement:check 0; docs:inventory-check 0; tool-authority:check 0; guards 32/32; check:cycles 0; shellcheck 0; repo:doctor 0. Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lsof's silence (#1683) * fix(lifecycle): an OPEN PR's checkout needs its owner's consent, not lsof's silence Reproduced 2026-08-30: `npm run worktrees:retire` removed a concurrent session's checkout (agent/round-type-reclassify, PR #1681, OPEN). Every mechanical signal said disposable — clean, tip identical to its pushed remote, and no process whose cwd `lsof` could see. Nothing was lost, because parking is defined to keep the branch and PARKABLE already required the tip to match its remote. But the checkout had an owner and the tool could not tell, so this is a classifier defect, not a slip. The unsound step is reading silence as absence. `lsof +D` samples one instant: hasLiveProcess === true proof of activity — a sound veto hasLiveProcess === false NOT proof of inactivity — an agent session between two tool calls has no visible cwd classifyWorktree's own header stated the defect as design — "parking does NOT consult the PR at all" — so the comment is rewritten in the same commit. A file whose prose contradicts its code is the failure this program exists to remove. WHAT CHANGED New worktree verdict KEEP_PR_OWNER_INTENT_REQUIRED, deliberately neither ACTIVE (nothing proved anyone is using it) nor UNKNOWN (the PR read fine; what is missing is a decision). It is in REQUIRES_HUMAN_VERDICTS, so the standing authorization already excludes it. config/open-pr-dispositions.json gains `worktree_policy`, because the disposition LABEL was being asked to imply an ACTION readers had to infer: KEEP never parked automatically PARK_IF_REPRODUCIBLE park once clean and pushed; the branch stays A missing row, an unrecognised policy, or a disposition of ACTIVE/UNKNOWN all KEEP. #1659 — an open PR waiting on a physical-device test — is released explicitly by its owner and stays parkable, which is the case the park/retire split exists for. The registry is now current state in BOTH directions. It carried ACTIVE rows for #1623, #1638, #1679 and #1680 after they closed or merged; control-plane -verify now fails on a stale row as well as a missing one, and on a worktree_policy outside the vocabulary. That vocabulary check lives in the verifier because the lifecycle tool must fail SAFE on a malformed entry (unrecognised policy => KEEP) and would therefore never report one. #1681's row is recorded ACTIVE/KEEP, transcribed from observable state — PR OPEN, worktree checked out — by this session and not by its owner, who should confirm it. Recorded rather than left blank because leaving it blank is exactly what let --retire park it. WHAT THIS DOES NOT COVER A worktree whose branch has NO PR is still parked on reproducibility alone — the same defect class, with no PR to key a disposition on. Requiring recorded intent for every branch would make --park unable to act at all, which defeats the reason parking exists. Registered as WORKTREE_PARK_NO_PR_OWNERSHIP rather than quietly accepted. VERIFIED preflight 0 · 1269 files / 12,101 tests 0 · docs:check 0 · knowledge:check 0 · markdown ratchet 30515, unchanged · control-plane:verify VERIFIED exit 0 (was CONTROL FAILURE on open-pr-residue) · lifecycle suite 60 passed. Failure injection: disabling the ownership gate turns 8 of them red, including the end-to-end CLI case where --park refuses a fixture worktree whose PR is OPEN with no disposition. The lib was restored from a hashed backup and re-verified byte-identical. Assumption stated once: this ran in the canonical checkout rather than a worktree. HELM_MAX_MUTATION_WORKTREES=1 and the other session holds the only mutation workspace, so new-worktree.sh refuses by design; AGENTS.md permits a single session in canonical, and the concurrency hazard the worktree rule addresses does not apply because the other session is isolated in its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH * chore(dispositions): record this PR's own row — the new rule's first cost Opening #1683 made control-plane:verify red on open-pr-residue immediately, because the key set must now equal the live open-PR set exactly. That is the chosen tradeoff, not a surprise: a PR number does not exist until the PR does, so the row can only ever land in a follow-up commit. KEEP rather than PARK_IF_REPRODUCIBLE: this work runs in the canonical checkout, which classifyWorktree holds ACTIVE before consulting anything else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The registry is CURRENT STATE, so a merge makes its own row stale — this is the two-ended cost ADR-2026-08-30 records, paid on schedule. control-plane:verify reported it as a CONTROL FAILURE the moment #1687 merged (open-pr-residue: "disposition rows for PRs no longer open: #1687"), which is the check working: a current-state registry that only failed on MISSING rows would have carried an ACTIVE row for a merged PR indefinitely, exactly as this file did for #1623, #1638, #1679 and #1680 before this run. After: 3 open PRs (#1659, #1678, #1681), all owned elsewhere, all classified. control-plane:verify VERIFIED, exit 0 — 21 pass, 0 fail, 0 unknown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
…1689) * fix(lifecycle): a checkout is disposable only if it says so itself The #1681 fix made an OPEN PR's checkout unparkable without recorded owner intent. It never covered the window BEFORE a PR exists — and that window is where a session actually starts. A worktree five minutes old is clean, pushed, has no PR to key a disposition on, and is invisible to `lsof` between two tool calls: every signal the old rule read as "disposable", and every one of them wrong. That residue was registered as WORKTREE_PARK_NO_PR_OWNERSHIP. Disposability now comes from the workspace's own identity, in the marker scripts/new-worktree.sh already wrote: .helm/workspace.json -> { "parkPolicy": "KEEP" } at creation, always "PARK_IF_REPRODUCIBLE" only if a human sets it No new store, no new registry — one field on a file that existed. Releasing a checkout is a positive act; everything else KEEPS: no marker, no key, an unknown value, a file that will not parse. Verdict KEEP_WORKSPACE_INTENT_REQUIRED, which sits outside the standing autonomous authorization. The gate runs BEFORE the reproducibility checks and independently of any PR, so both gates must permit. An OPEN PR its owner released still cannot override a workspace KEEP. The two questions stay separate, because conflating them is what caused this: workspace identity may this CHECKOUT go? PR state may this BRANCH be deleted? Branch retirement is untouched: PR MERGED plus an exact head-OID match, exactly as before. CLOSED DIFFERENTLY FROM THE CONDITION IT WAS OPENED WITH, deliberately. That asked for a session id checked for LIVENESS — the same unsound negative-evidence inference in a new costume, since a session between two tool calls looks dead. Declared intent needs no probe. WHAT THIS CHANGES FOR THE ONE LIVE WORKTREE: nothing observable, and the reason matters. agent/round-type-reclassify (#1681) predates the marker entirely, so it now reports KEEP_WORKSPACE_INTENT_REQUIRED with "no .helm/workspace.json" rather than KEEP_PR_OWNER_INTENT_REQUIRED. Still kept, for a stronger reason. worktree_policy in open-pr-dispositions.json is NOT removed here, though it is now the weaker of two gates. #1659 is PARKED — there is no checkout on disk to carry a marker — so deleting the field would delete the only recorded authorization for it. The file says so, with the condition for removing it. Verified: three injections, each red for its own reason — unwiring the gate (8 tests), letting a missing marker mean released (5), and moving the gate after the reproducibility checks (1) — with the library restored byte-identical (sha256). The end-to-end tests run real `--park` against real git worktrees with the marker absent, KEEP, and released; the fixture repo gained the `.helm/` gitignore line the real repo has at .gitignore:257, without which the marker itself reads as uncommitted work. preflight 0, 220 tests across src/test/scripts, control-plane:verify VERIFIED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH * chore(control-plane): record #1689's disposition row Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH * chore(control-plane): clear #1688's transitional row — the designed workflow #1688 shipped the transitional merge-identity exception so a PR's own row could survive its own merge without a cleanup push to main. That exception is scoped to exactly one commit: it holds while main's HEAD IS the merge commit, and stops the moment anything else lands. From this branch, HEAD is this branch's tip, so #1688's row is stale again and the check says so, naming the merge SHA it no longer matches. Clearing it here IS the workflow — the next ordinary PR removes the previous row while adding its own. First exercise of it, and it behaved exactly as designed rather than needing a bypass, which was the whole point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(#1697) All three PRs that were open in this repo — #1659, #1678, #1681 — are merged, along with #1696, whose transitional grace had expired. The residue check flagged every one of them by name, with the remedy text added in #1692 telling a session exactly what to do about it. Also records a gap the merges exposed. `gh pr merge --squash --delete-branch` removes the remote head ref, and the lifecycle tool looks a PR up by `head={owner}:<branch>` — which then matches nothing, so a worktree whose PR just merged classifies UNKNOWN_PR ("no PR found — cannot prove the work landed") and `--retire` correctly takes no action. Observed on #1681: merged, tip exact, checkout clean, ~3.8 GiB left behind. The refusal is right — UNKNOWN_PR means evidence was unavailable, and that is never a licence to remove anything. The defect is upstream of the verdict: the LOOKUP depends on a ref that delete_branch_on_merge removes at exactly the moment the branch becomes safe to retire. Same shape as #1654's shipped defect, one level down. Not fixed here. A lookup that also searched merged PRs by head SHA would close it, since the classifier already keys on `local tip === PR head OID` — but that changes how the tool gathers evidence for branch deletion, and that deserves its own change rather than riding along in a bookkeeping commit. Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported from the app with a screenshot: on the live shot-entry screen, focusing Distance remaining opens the numeric keypad directly over the input, so you can't see the number you're entering.
Root cause — a half-shipped design, not a missing one
capacitor.config.tssetsresize: 'ionic', which does not resize the WebView. It expects the app to react to the keyboard itself. Ionic's framework does that; this is not an Ionic app, so nothing did. The layout viewport stays full height, the browser therefore believes every input is still visible, and the keyboard simply covers the bottom ~45% of the screen.The other half was already shipped and correct:
globals.csssetsscroll-margin-bottom: calc(var(--keyboard-height) + 40px)on every input underbody.capacitorCapacitorProvideralready maintains--keyboard-heightfromkeyboardWillShowThat CSS was dead. It shapes where a keyboard-aware scroll lands, and no such scroll was ever triggered. So this adds the missing trigger rather than new machinery.
Changes
CapacitorProvider.tsx— onkeyboardWillShow, after setting--keyboard-height, scroll the focused element into view.Global on purpose. A scroll on
keyboardWillShowis user-initiated by definition — the keyboard only opens because someone tapped an input. That is a different event from the unrequested programmatic scroll thatshouldAutoScrollDistanceInputguards against during putt tagging, so that guard is untouched and still returnsfalsefor putting.use-shot-state-machine.ts— droppreventScroll: truefrom the auto-focus, andblock: 'nearest'→'center'.preventScrollwas added to avoid double-scrolling against thescrollIntoViewon the line above it. But that call usedblock: 'nearest', which is a no-op when the element is already inside the pre-keyboard viewport — so there was no second scroll to collide with, and allpreventScrollactually suppressed was the one scroll that could have known about the keyboard.Verification, and its limit
tsc clean · eslint
--max-warnings 0clean · 112 tests pass across the touched areas.Not verified on a device.
keyboardWillShowdoes not fire in jsdom or a desktop browser, and this session has no iOS runtime. The behaviour is reasoned from the Capacitor config and the existing CSS — it needs a look on a phone before it's called done.The new tests assert against source text, for the same reason: the effect sits behind two
setTimeouts inside auseEffectand only matters against a real iOS keyboard. They can't prove the field is visible; they prove the two lines that made it invisible don't come back unnoticed, and that the CSS half and the listener half are never deleted as "unused" independently. Both guards were checked to fail against the previous code rather than passing vacuously.Worth a second opinion
The
CapacitorProviderchange affects every input in the native app, not just this screen. That's what makes it the right fix — but it's also the part to disagree with if you'd rather scope it to the golf screen.🤖 Generated with Claude Code
https://claude.ai/code/session_01BcCZsEmEgQ4VD9EPMTcDX7