You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Plan 008: Split touch interaction orchestration into semantic modules
Executor instructions: Follow this plan step by step. Run every
verification command and confirm the expected result before moving to the
next step. This is a behavior-preserving refactor: moved assertions must not
be weakened and runtime semantics must not change. If anything in the "STOP
conditions" section occurs, stop and report — do not improvise. A reviewer
maintains the plan index for this work; do not create or edit plans/README.md.
Drift check (run first): git diff --stat 13bc70f24..HEAD -- src/daemon/handlers/interaction-touch.ts src/daemon/handlers/interaction-touch-response.ts src/daemon/handlers/interaction-touch-press.ts src/daemon/handlers/interaction-touch-fill.ts src/daemon/handlers/interaction-touch-direct-ios.ts src/daemon/handlers/interaction-touch-runtime.ts src/daemon/handlers/interaction-touch-android-readiness.ts src/daemon/handlers/__tests__/interaction.test.ts src/daemon/handlers/__tests__/interaction-android-recovery-abort.test.ts src/daemon/handlers/__tests__/interaction-touch.test.ts src/daemon/handlers/__tests__/interaction-touch-press.test.ts src/daemon/handlers/__tests__/interaction-touch-fill.test.ts src/daemon/handlers/__tests__/interaction-touch-direct-ios.test.ts src/daemon/handlers/__tests__/interaction-touch-runtime.test.ts src/daemon/handlers/__tests__/interaction-touch-android-readiness.test.ts src/daemon/handlers/__tests__/interaction-touch-response.test.ts src/daemon/handlers/__tests__/interaction-touch-fixtures.ts
If any in-scope file changed since this plan was written, compare the
"Current state" excerpts against the live code before proceeding; on a
mismatch, treat it as a STOP condition.
Status
Priority: P2
Effort: L
Risk: MED
Depends on: none
Category: tech-debt
Planned at: commit 13bc70f24, 2026-08-08
Why this matters
interaction-touch.ts is a 1,049-line hotspot that currently answers six
different questions: targeted press routing, fill admission, direct-iOS fast
path behavior, shared runtime finalization, Android readiness, and response
projection. Its 3,992-line test file mixes those paths with unrelated get and is routing. Splitting along the already-visible policy seams makes each
interaction guarantee cheaper to locate and change while preserving the public
daemon path, session semantics, and platform behavior.
This plan does not introduce a new abstraction layer, provider port, fallback,
or command surface. It is a local module-DAG extraction inside the existing
touch command family.
Current state
The implementation mixes several independently changing policies
src/daemon/handlers/interaction-touch.ts is 1,049 lines. Its public router
is already thin, but all behavior sits below it:
Android dialog recovery is a ref-frame side-effect seam, not a generic retry:
// src/daemon/handlers/interaction-touch.ts:971-990constreadiness=awaitensureAndroidBlockingSystemDialogReady({
session,
command,phase: 'before-command',});// ADR 0014: blocking-dialog recovery is itself device-mutating and expires the// frame at its own seam. A ref action admitted against the pre-recovery frame// must NOT continue against the recovered UI...if(options.refContext&&readiness.status==='recovered'){constabort=refMutationAdmissionResponse({
session,ref: options.refContext.ref,mintedGeneration: options.refContext.mintedGeneration,staleRefsWarning: options.refContext.staleRefsWarning,});if(abort)return{aborted: true,response: abort};}
The behavior is constrained by accepted contracts
ADR 0011 separates completeness from behavioral truth and requires shared
rule implementations plus contract scenarios:
<!-- docs/adr/0011-interaction-guarantee-contract.md:47-50 -->
Make the path × guarantee matrix a first-class, machine-checked artifact, with
three enforcement layers. Types enforce **completeness** of declarations,
shared implementations prevent **drift**, and generated test coverage enforces
**truth**.
ADR 0011 requires the shared interaction response builder:
<!-- docs/adr/0011-interaction-guarantee-contract.md:153-157 -->
For `responseFields`, one `buildInteractionResponseData(...)` becomes the only
construction site for interaction response payloads ... A small guard test ...
fails if an interaction handler contains a hand-rolled `responseData = {` literal.
ADR 0014 fixes the mutation boundary that the extraction must preserve:
<!-- docs/adr/0014-session-ref-frame-lifetime.md:18-35 -->- A session owns at most one **ref frame** ... kept separate from the latest
operational observation (`session.snapshot`).
- Every mutating leaf expires the frame at the side-effect seam — after all
pre-action guards, immediately before the device op — with no success-only
rollback; a post-dispatch failure still leaves it expired.
- Mutation admission requires an active frame whose epoch and issuance scope
authorize the ref.
CONTEXT.md:439-480 names the Selector Capture Reliability Contract.
Preserve these terms and rules: direct iOS is a narrow fast path; regular
selector paths are capture-backed; sparse failures are observable; an XCTEST_RECORDED_FAILURE can become warning-success only after one same-
presentation changed digest; Android helper reuse is not result caching; and
pending outcome retry precedes post-gesture stabilization.
CONTEXT.md:492-503 says provider scenarios exercise the public daemon path,
while unit tests remain appropriate for pure logic and important edges. New
handler tests must continue to enter through handleInteractionCommands;
do not export private helpers or add test-only dependency injection.
Repository shape and test constraints
AGENTS.md:128-143 says a file should answer one question, implementation
files target 300 LOC, files over 1,000 are architecture debt, tests mirror
source topology 1:1, interaction.test.ts must shrink rather than grow, and
shared fixtures are named exports in a sibling fixture module.
src/daemon/handlers/__tests__/interaction.test.ts currently has 3,992 lines
and 82 top-level test declarations. Its two parameterized declarations expand
that source to 86 cases under Vitest discovery. It also owns 178 lines of
shared mocks/factories before its first test (interaction.test.ts:1-178).
src/daemon/handlers/__tests__/interaction-android-recovery-abort.test.ts
contains one additional touch-readiness regression. The redistribution
baseline is therefore 87 discovered tests across those two files.
Fallow currently ratchets three moderate CRAP findings on interaction-touch.ts and six on interaction.test.ts at fallow-baselines/health.json:156-159,223-226. This refactor should shrink
those counts. Do not regenerate or loosen the baseline.
exit 0; bounded dependents/gates report for planning
Test discovery
pnpm exec vitest list --project unit-core <test paths>
one line per discovered test; baseline and final totals match 87
Focused handler tests
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction
exit 0; all interaction handler tests pass
Response-construction guard
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction-response-construction-guard.test.ts
exit 0; every touch source uses the shared builder
Guarantee gates
pnpm exec vitest run --project unit-core src/__tests__/contracts/interaction-guarantees.test.ts src/__tests__/contracts/interaction-contract-coverage.test.ts
exit 0; matrix references and coverage remain honest
Interaction contracts
pnpm exec vitest run --project interaction-contract
exit 0; all path × guarantee scenarios pass
Provider scenarios
pnpm test:integration:provider
exit 0; public daemon/provider paths pass
Layering
pnpm check:layering
exit 0; no value cycle, back-edge, type-cycle growth, or module-policy breach
Fast static gates
pnpm check:quick
exit 0; lint and TypeScript pass
Broad deterministic gate
pnpm check
exit 0; tooling, fallow, unit, build, and smoke aggregates pass
Affected gates
pnpm check:affected --run
exit 0; selected local checks pass and CI-only checks are reported
Dependencies must already be installed. Use pnpm only. No live device is
needed; GitHub remains authoritative for native/device lanes reported by the
affected selector.
Suggested executor toolkit
Read docs/adr/0011-interaction-guarantee-contract.md, docs/adr/0014-session-ref-frame-lifetime.md, CONTEXT.md sections Selector Capture Reliability Contract and Testing Principles, and docs/agents/testing.md before editing.
Use pnpm depgraph affected ... before moving shared code.
Use rg for symbol/caller searches and apply_patch for edits.
Do not use a code-generation script to split the test file; preserve each
named test and assertion through reviewable moves.
Scope
In scope (the only production files you should modify or create):
src/daemon/handlers/interaction-touch.ts — retain as the thin public router.
src/daemon/handlers/__tests__/interaction-touch-fixtures.ts — create; named
pure factories/data only, not global mock installation.
Out of scope (do NOT touch, even though they are adjacent):
src/daemon/handlers/interaction.ts — it must continue importing the same handleTouchInteractionCommands entrypoint.
Other existing interaction-*.ts implementation modules and their tests,
including targets, policy, reference frame, flags, common finalization,
gesture, recorded input, iOS tap outcome, and runtime creation.
src/daemon/selector-runtime.ts and its tests; get/is behavior is not
being refactored.
fallow-baselines/**, layering/type-cycle baselines, coverage thresholds, or
any allowlist. New findings must be fixed in the extraction, not baselined.
New compatibility wrappers, fallbacks, test-only DI seams, package façades,
barrels, or speculative interfaces.
No arrow may point back upward. Use import type where a symbol is type-only,
but remember that the repo also ratchets type-only cycles: the layering gate
must show no growth.
Production module
One question it answers
Matching test file
interaction-touch.ts
Which touch command handler owns this request?
interaction-touch.test.ts
interaction-touch-press.ts
How are press/click/longpress targets admitted and executed?
interaction-touch-press.test.ts
interaction-touch-fill.ts
How is fill admitted, parameterized, executed, and projected?
interaction-touch-fill.test.ts
interaction-touch-direct-ios.ts
When and how does the direct-iOS selector path run, delegate, or corroborate?
interaction-touch-direct-ios.test.ts
interaction-touch-runtime.ts
How does the shared runtime finalize outcomes, warnings, retries, and iOS corroboration?
interaction-touch-runtime.test.ts
interaction-touch-android-readiness.ts
How do Android readiness and freshness compose with ref admission?
interaction-touch-android-readiness.test.ts
interaction-touch-response.ts
How are touch results normalized and built through the one response site?
interaction-touch-response.test.ts
Steps
Step 1: Capture the green baseline and the exact discovery count
Run the dependency report and baseline tests before moving anything. Count the
86 cases Vitest discovers from interaction.test.ts plus the one
recovery-abort case through Vitest discovery, not by grepping source text.
Expected: the count pipeline prints 87, the JSON assertion prints 87 unique baseline test names recorded, and the focused baseline exits 0.
The ignored .tmp/plan-008/baseline.json is the name manifest used after the
move. Record the count and test total in the handoff. If the count differs or
the baseline contains a duplicate name, STOP before editing and report the
current names/count.
Step 2: Split tests by the target production topology without changing assertions
Create interaction-touch-fixtures.ts from the reusable setup at interaction.test.ts:84-162:
export named session factories for iOS, Android, macOS desktop, and macOS
menubar;
export makeVisibleButtonSnapshot and contextFromFlags;
turn emulateCaptureSnapshotForSession into a factory that accepts the
calling test file's mocked dispatch function rather than importing or
installing a global mock;
keep vi.mock declarations and beforeEach resets local to each test file,
and include only mocks that file actually needs.
Redistribute, do not rewrite, all 87 discovered cases:
Keep only top-level get and is routing/read cases in interaction.test.ts.
Put one existing representative for each of press, click, longpress,
and fill in interaction-touch.test.ts to pin router ownership.
Move direct selector eligibility, native dispatch, semantic delegation,
Maestro fallback, pending-stabilization, and fused-seam cases to interaction-touch-direct-ios.test.ts.
Move ordinary press/click/longpress validation, macOS surface/button policy,
ref targeting, non-hittable/off-screen guards, and pre-resolved find cases
to interaction-touch-press.test.ts.
Move all fill target/admission, ref, option-forwarding, off-screen, and
recorded-parameter cases to interaction-touch-fill.test.ts.
Move generic finalization, warning composition, retry-positionals, runtime
iOS corroboration, and cross-command ref-frame lifetime sequences to interaction-touch-runtime.test.ts.
Move Android ref refresh, suspicious-tree fallback, blocking-dialog recovery,
launcher/Settings escape, and the existing recovery-abort regression to interaction-touch-android-readiness.test.ts; then delete interaction-android-recovery-abort.test.ts.
Move recording/touch-visualization/coordinate, verification evidence,
response identity, public transform, Maestro disclosure, reference-frame,
settle-ref issuance, and result-shape cases to interaction-touch-response.test.ts.
Every moved behavior test must still import and call handleInteractionCommands from ../interaction.ts. Pure response-builder
tests may import production exports from interaction-touch-response.ts, as
those exports are used by production modules; never export a symbol only to
make a test possible. Preserve every existing test name and assertion. Do not
convert tests into a table merely to reduce lines unless the assertions and
case names remain individually visible.
Size targets after fixture extraction:
At the planned-at commit, the recording/verification response cases occupy
722 lines at interaction.test.ts:1018-1368,1921-2224,2348-2414 before the
new file's local imports. Therefore 500 lines is a target, not a truthful hard
gate, unless a second production seam is demonstrated outside this plan.
target ≤500 lines for each new touch test file, with a hard ceiling below
1,000 lines; preserve the 1:1 source/test topology instead of inventing a
second test shard solely to satisfy the target;
interaction.test.ts <1,000 lines and contains no touch-command test;
Expected: the regex count prints exactly 87 and the manifest comparison
prints 87 unique test names preserved, with no old interaction-android-recovery-abort.test.ts discovery. The regex matches the
retained interaction.test.ts plus all seven intended interaction-touch*.test.ts filenames; the JSON comparison proves the count
was not preserved by replacing or duplicating a test name.
Prove the new files are actually executed:
Using apply_patch, temporarily add one test named PLANTED plan-008 discovery failure to each new interaction-touch*.test.ts; each must throw an Error naming its file.
Expected: exit nonzero with seven planted failures, one naming each file.
Remove every planted test with apply_patch; rerun the same command and
expect exit 0.
Temporarily add const __plan008TypeDiscovery: never = 'planted'; to one new
test file and run pnpm typecheck. Expect a TypeScript error naming that
file. Remove the planted line immediately and rerun pnpm typecheck to green.
Never commit a planted failure. If any new file is absent from the failure
report, STOP: the split test is not owned by the expected gate.
Step 3: Extract response projection and Android readiness leaves
Move buildTargetedTouchResponsePayloads from interaction-touch.ts:311-339 and the response-related symbols from interaction-touch.ts:552-610 into interaction-touch-response.ts beside the
one response construction site:
buildTargetedTouchResponsePayloads
transformTouchResponseData
readInteractionResponseDataTransformCommand
MaestroFallbackResponseFields
MaestroFallbackDisclosure
maestroFallbackDisclosure
Keep their names, inputs, output shapes, normalization calls, and Maestro
disclosure semantics unchanged. Export only symbols needed by production
siblings or the matching pure response test. Define the targeted-result union
from the existing contracts inside the response module (or reuse its existing
contract union); do not import a type from interaction-touch-press.ts, which
would create a response → press back-edge against the target DAG.
Create interaction-touch-android-readiness.ts from current lines 948-1030:
RefAdmissionContext
ReadinessOutcome
runWithAndroidDialogReadinessCheck
refreshAndroidRefSnapshotIfFreshnessActive
The ref context is the shared typed value crossing into runtime readiness.
Preserve lease-provider bypass, before/after phases, recovery-triggered shared
ref admission rejection, comparison-safe freshness baseline, diagnostics phase,
and best-effort refresh failure behavior exactly.
Update the still-unsplit orchestrator to import the moved production symbols.
Delete moved definitions; do not leave forwarding wrappers.
Expected: exit 0; no new value/type cycle or baseline change.
Step 4: Extract direct-iOS and shared runtime orchestration
Create interaction-touch-direct-ios.ts from current lines 345-550 and 612-629:
direct-selector eligibility and Maestro selector decoration;
non-default click-option exclusion;
direct selector dispatch;
fused ADR 0014 frame-expiry seam immediately before runner dispatch;
response normalization and finalization;
same-scope iOS failure corroboration;
ADR 0011 semantic delegation to the runtime path;
point and reference-frame decoding.
Do not broaden the fast path. In particular, preserve all current exclusions:
recording, replay target guard, non-selector targets, non-default click options, --verify, and --settle. Preserve Maestro's native error shape. The generic
direct helper currently accepts 'press' | 'fill'; do not create a new direct
fill call path or narrow behavior as part of this refactor.
Create interaction-touch-runtime.ts from current lines 772-945 and 1031-1049:
dispatchRuntimeInteraction
runtime iOS corroboration and payload construction
normalized error response
retry-positionals and point-positionals helpers
Import Android readiness from the leaf created in Step 3. Preserve action
timestamps, pre/post readiness order, Android escape rethrow, warning append
order, shared finalization, scheduleInteractionOutcomeRetry: false on
corroborated outcomes, retained recordedTarget, Android freshness baseline,
and normalized error payloads.
Delete moved definitions from interaction-touch.ts; do not add compatibility
wrappers or an internal barrel.
Verify:
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction-touch-direct-ios.test.ts src/daemon/handlers/__tests__/interaction-touch-runtime.test.ts src/daemon/handlers/__tests__/interaction-response-construction-guard.test.ts
pnpm exec vitest run --project interaction-contract
pnpm check:layering
Expected: exit 0; all interaction contract scenarios remain green.
Step 5: Extract press and fill handlers, leaving a thin router
Create interaction-touch-press.ts from current lines 98-341, excluding buildTargetedTouchResponsePayloads already moved in Step 3. Preserve:
public capability and macOS surface checks;
click-button validation and exact error details;
parsing and stale-ref warning before any internal recapture;
ref flag guard and mutation admission order;
Android freshness baseline;
replay-target guard forcing the runtime tree path;
direct-iOS attempt/delegation;
click/press/longpress option projection, including preresolvedTarget only
for click/press;
Android in-app assertion and the exact inputs passed to the targeted response
payload builder;
direct coordinate reference-frame resolution and settle-ref issuance.
Create interaction-touch-fill.ts from current lines 632-770. Preserve:
response projection, fallback identity, reference frame, Maestro disclosure,
stale warning, and settle refs.
Reduce interaction-touch.ts to imports, the existing parameter type
intersection, and the four-way switch. It should delegate to production exports
from the new press/fill modules and return null for every other command.
Remove the obsolete fallow-ignore-next-line complexity comment. Do not move
command routing to interaction.ts, a registry, or a new abstraction.
Expected: exit 0. No assertion or wire snapshot changes are allowed.
Step 6: Prove the structural response guard sees the new files
The guard's directory scan should automatically include every new interaction-touch*.ts module except the builder file. Prove it rather than
assuming it:
Using apply_patch, temporarily add an unexported function containing const responseData = { planted: true }; to interaction-touch-direct-ios.ts.
Run the response-construction guard.
Confirm it exits nonzero and names interaction-touch-direct-ios.ts: responseData = ....
Remove the planted function with apply_patch and rerun the guard to green.
Verify red then green:
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction-response-construction-guard.test.ts
Never commit the planted violation. If the guard stays green with the violation
present, STOP and report the discovery gap; do not add an allowlist.
Step 7: Enforce module/test shape and run all semantic gates
every new test file targets ≤500 lines and is strictly below 1,000 lines;
a file over the target must still answer only its matching production
module's question rather than mixing categories to balance line counts;
retained interaction.test.ts <1,000 lines and contains no top-level touch
command case;
fixture module ≤300 lines;
the complexity-waiver search prints no matches.
Then run all owning gates:
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction
pnpm exec vitest run --project unit-core src/__tests__/contracts/interaction-guarantees.test.ts src/__tests__/contracts/interaction-contract-coverage.test.ts
pnpm exec vitest run --project interaction-contract
pnpm test:integration:provider
pnpm check
pnpm check:affected --run
Expected: every command exits 0. pnpm check:fallow inside the broad gate must
accept the reduced baseline counts without a baseline edit. The affected gate
may report native/device lanes left to GitHub.
Step 8: Perform the tightening and scope pass
Search for duplicate moved definitions, upward imports, stale old test
discovery, and unexpected files. There must be one production definition per
symbol and no forwarding compatibility layer.
Expected: each function is defined once in its target module; the removed test
filename has no references; the temporary discovery manifests are removed;
status lists only the in-scope production/test files plus this plan if it is
intentionally uncommitted.
Test plan
Characterization baseline: 87 discovered tests across interaction.test.ts and interaction-android-recovery-abort.test.ts, all
uniquely named and green before extraction.
Retain or move all 87 discovered cases without renaming or weakening them;
final discovery remains exactly 87 and its sorted name manifest is identical.
Keep public-path handler tests calling handleInteractionCommands; use direct
imports only for pure response helpers already exported for production use.
Prove every new test file is executed with one temporary named failure per
file, then remove all planted failures and return green.
Prove TypeScript includes a new test file with a temporary never assignment,
then remove it and return pnpm typecheck to green.
Prove the response-construction guard scans new production files with one
temporary hand-rolled response literal, then remove it and return the guard
to green.
Run unit, guarantee, interaction-contract, and provider integration suites.
No new device-specific scenario is needed because behavior is unchanged; any
changed platform-visible result is a regression and a STOP condition.
Done criteria
The planned-at excerpts matched and the baseline dependency report was read.
Final discovery contains exactly 87 unique names; the JSON name-manifest
comparison is identical, and every original assertion is preserved.
Temporary planted failures proved all seven new test files execute; all
planted code was removed before commit.
A temporary planted type error proved TypeScript includes a new test file;
it was removed and typecheck returned green.
A temporary hand-rolled response proved the construction guard scans a
new source file; it was removed and the guard returned green.
interaction-touch.ts is a ≤100-line router.
Each new implementation module answers the one question in the topology
table and is ≤300 lines; interaction-touch-response.ts is ≤500 lines.
New test files mirror source topology, target ≤500 lines, and are all
<1,000 lines; interaction.test.ts is <1,000 lines and no longer contains
touch tests.
There is no new internal barrel, provider port, test-only DI seam,
fallback, compatibility wrapper, value cycle, type-cycle member, or
layering back-edge.
ADR 0011 path guarantees, ADR 0014 ref-frame seams, warning composition,
recording, retries, response identity, and platform behavior are unchanged.
pnpm check:quick, pnpm check:layering, guarantee tests, and the
response-construction guard exit 0.
pnpm exec vitest run --project interaction-contract exits 0.
pnpm test:integration:provider exits 0.
pnpm check exits 0 without changing Fallow/layering baselines.
pnpm check:affected --run exits 0 for local gates.
git status --short contains no out-of-scope source changes.
Docs and skills are unchanged because this plan changes no command or
runtime behavior; plans/README.md is unchanged by instruction.
STOP conditions
Stop and report back; do not improvise if:
Any in-scope current-state excerpt has drifted since 13bc70f24.
Baseline or final discovery is not exactly 87 unique names, the sorted name
manifests differ, or a moved assertion must change to pass.
Any temporary planted test/type/response violation is not caught by its
expected gate.
The extraction requires changing interaction.ts, a command descriptor,
daemon registry, guarantee matrix, contract fixture, public type, response
shape, platform implementation, or SessionState owner.
Direct iOS, replay guard, ref admission, warning composition, Android
readiness, recording, retry, or corroboration behavior changes even if tests
can be updated to accept it.
A new implementation file exceeds 300 lines, the response module exceeds 500
lines, any new test reaches 1,000 lines, or retained interaction.test.ts
remains at/above 1,000 lines after the prescribed partition. Do not create a
second test shard without a matching production seam merely to hit 500 lines.
pnpm check:layering reports a new value cycle, type-cycle member, inversion,
or module-policy breach. Do not edit a ratchet baseline.
pnpm check:fallow requests new baseline findings. Improve the split; do not
regenerate fallow-baselines/health.json.
A provider or interaction-contract test changes its result or wire shape.
A verification command fails twice after one reasonable correction.
Any fix requires touching a file outside the in-scope list.
Maintenance notes
Reviewers should inspect imports as a DAG, not just line counts. A set of small
files with mutual imports is worse than the original module and may expand the
ratcheted type-only cycle.
Future touch behavior should land in the module named for its policy:
eligibility/delegation in direct-iOS, admission/execution in press or fill,
lifecycle/finalization in runtime, Android recovery in readiness, and wire
projection in response.
Keep interaction-touch.ts a router. Do not let it reaccrete validation or
platform policy.
interaction-response-construction-guard.test.ts intentionally discovers
new interaction-touch*.ts files by name. Preserve that automatic discovery.
Any future semantic change still needs a regression proven red against
pre-fix code, the appropriate provider/public path, and the ADR 0011 contract
scenario where the path × guarantee cell applies.
This plan deliberately leaves selector-runtime.ts, its tests, and the known
ratcheted type-only cycle untouched; they are separate architecture work.
Plan 008: Split touch interaction orchestration into semantic modules
Status
13bc70f24, 2026-08-08Why this matters
interaction-touch.tsis a 1,049-line hotspot that currently answers sixdifferent questions: targeted press routing, fill admission, direct-iOS fast
path behavior, shared runtime finalization, Android readiness, and response
projection. Its 3,992-line test file mixes those paths with unrelated
getandisrouting. Splitting along the already-visible policy seams makes eachinteraction guarantee cheaper to locate and change while preserving the public
daemon path, session semantics, and platform behavior.
This plan does not introduce a new abstraction layer, provider port, fallback,
or command surface. It is a local module-DAG extraction inside the existing
touch command family.
Current state
The implementation mixes several independently changing policies
src/daemon/handlers/interaction-touch.tsis 1,049 lines. Its public routeris already thin, but all behavior sits below it:
The main targeted dispatcher carries an explicit complexity waiver and owns
capability checks, click options, ref admission, Android refresh, replay
identity, direct-iOS eligibility, runtime dispatch, and response building:
Existing function seams at planned-at commit:
interaction-touch.ts:98-341press/click/longpressadmission, execution, and payloadsinteraction-touch.ts:345-550,612-629interaction-touch.ts:552-610interaction-touch.ts:632-770filladmission, ref preamble, execution, and payloadsinteraction-touch.ts:772-945,1031-1049interaction-touch.ts:948-1030The shared runtime must append warnings rather than overwrite builder output:
Android dialog recovery is a ref-frame side-effect seam, not a generic retry:
The behavior is constrained by accepted contracts
ADR 0011 separates completeness from behavioral truth and requires shared
rule implementations plus contract scenarios:
ADR 0011 requires the shared interaction response builder:
ADR 0014 fixes the mutation boundary that the extraction must preserve:
CONTEXT.md:439-480names the Selector Capture Reliability Contract.Preserve these terms and rules: direct iOS is a narrow fast path; regular
selector paths are capture-backed; sparse failures are observable; an
XCTEST_RECORDED_FAILUREcan become warning-success only after one same-presentation changed digest; Android helper reuse is not result caching; and
pending outcome retry precedes post-gesture stabilization.
CONTEXT.md:492-503says provider scenarios exercise the public daemon path,while unit tests remain appropriate for pure logic and important edges. New
handler tests must continue to enter through
handleInteractionCommands;do not export private helpers or add test-only dependency injection.
Repository shape and test constraints
AGENTS.md:128-143says a file should answer one question, implementationfiles target 300 LOC, files over 1,000 are architecture debt, tests mirror
source topology 1:1,
interaction.test.tsmust shrink rather than grow, andshared fixtures are named exports in a sibling fixture module.
src/daemon/handlers/__tests__/interaction.test.tscurrently has 3,992 linesand 82 top-level test declarations. Its two parameterized declarations expand
that source to 86 cases under Vitest discovery. It also owns 178 lines of
shared mocks/factories before its first test (
interaction.test.ts:1-178).src/daemon/handlers/__tests__/interaction-android-recovery-abort.test.tscontains one additional touch-readiness regression. The redistribution
baseline is therefore 87 discovered tests across those two files.
Keep tests on the stable public handler path:
The ADR 0011 construction guard automatically discovers every
interaction-touch*.tssource file:Fallow currently ratchets three moderate CRAP findings on
interaction-touch.tsand six oninteraction.test.tsatfallow-baselines/health.json:156-159,223-226. This refactor should shrinkthose counts. Do not regenerate or loosen the baseline.
Commands you will need
pnpm depgraph affected src/daemon/handlers/interaction-touch.ts --json --limit 25pnpm exec vitest list --project unit-core <test paths>pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interactionpnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction-response-construction-guard.test.tspnpm exec vitest run --project unit-core src/__tests__/contracts/interaction-guarantees.test.ts src/__tests__/contracts/interaction-contract-coverage.test.tspnpm exec vitest run --project interaction-contractpnpm test:integration:providerpnpm check:layeringpnpm check:quickpnpm checkpnpm check:affected --runDependencies must already be installed. Use
pnpmonly. No live device isneeded; GitHub remains authoritative for native/device lanes reported by the
affected selector.
Suggested executor toolkit
docs/adr/0011-interaction-guarantee-contract.md,docs/adr/0014-session-ref-frame-lifetime.md,CONTEXT.mdsectionsSelector Capture Reliability Contract and Testing Principles, and
docs/agents/testing.mdbefore editing.pnpm depgraph affected ...before moving shared code.rgfor symbol/caller searches andapply_patchfor edits.named test and assertion through reviewable moves.
Scope
In scope (the only production files you should modify or create):
src/daemon/handlers/interaction-touch.ts— retain as the thin public router.src/daemon/handlers/interaction-touch-press.ts— create.src/daemon/handlers/interaction-touch-fill.ts— create.src/daemon/handlers/interaction-touch-direct-ios.ts— create.src/daemon/handlers/interaction-touch-runtime.ts— create.src/daemon/handlers/interaction-touch-android-readiness.ts— create.src/daemon/handlers/interaction-touch-response.ts— extend with the existingresponse projection/disclosure helpers.
In scope (the only test/support files you should modify, create, or delete):
src/daemon/handlers/__tests__/interaction.test.ts— retain only non-touchget/ispublic-router cases; shrink below 1,000 lines.src/daemon/handlers/__tests__/interaction-android-recovery-abort.test.ts—delete after moving its single regression.
src/daemon/handlers/__tests__/interaction-touch.test.ts— create.src/daemon/handlers/__tests__/interaction-touch-press.test.ts— create.src/daemon/handlers/__tests__/interaction-touch-fill.test.ts— create.src/daemon/handlers/__tests__/interaction-touch-direct-ios.test.ts— create.src/daemon/handlers/__tests__/interaction-touch-runtime.test.ts— create.src/daemon/handlers/__tests__/interaction-touch-android-readiness.test.ts— create.src/daemon/handlers/__tests__/interaction-touch-response.test.ts— create.src/daemon/handlers/__tests__/interaction-touch-fixtures.ts— create; namedpure factories/data only, not global mock installation.
Out of scope (do NOT touch, even though they are adjacent):
src/daemon/handlers/interaction.ts— it must continue importing the samehandleTouchInteractionCommandsentrypoint.interaction-*.tsimplementation modules and their tests,including targets, policy, reference frame, flags, common finalization,
gesture, recorded input, iOS tap outcome, and runtime creation.
src/daemon/selector-runtime.tsand its tests;get/isbehavior is notbeing refactored.
packages/contracts/src/interaction-guarantees.ts, contract fixtures,command descriptors, daemon registry, public types, response/wire shapes.
live-device behavior.
CONTEXT.md, README, website docs, skills, changelog.fallow-baselines/**, layering/type-cycle baselines, coverage thresholds, orany allowlist. New findings must be fixed in the extraction, not baselined.
barrels, or speculative interfaces.
plans/README.mdand every other plan file.Git workflow
advisor/008-split-touch-interaction-orchestratortest(daemon): split touch interaction coverage by modulerefactor(daemon): split touch interaction orchestrationbelow are verification only and must never be committed.
Target module topology
Use direct imports; do not add an internal barrel. The dependency direction is:
No arrow may point back upward. Use
import typewhere a symbol is type-only,but remember that the repo also ratchets type-only cycles: the layering gate
must show no growth.
interaction-touch.tsinteraction-touch.test.tsinteraction-touch-press.tsinteraction-touch-press.test.tsinteraction-touch-fill.tsinteraction-touch-fill.test.tsinteraction-touch-direct-ios.tsinteraction-touch-direct-ios.test.tsinteraction-touch-runtime.tsinteraction-touch-runtime.test.tsinteraction-touch-android-readiness.tsinteraction-touch-android-readiness.test.tsinteraction-touch-response.tsinteraction-touch-response.test.tsSteps
Step 1: Capture the green baseline and the exact discovery count
Run the dependency report and baseline tests before moving anything. Count the
86 cases Vitest discovers from
interaction.test.tsplus the onerecovery-abort case through Vitest discovery, not by grepping source text.
Verify:
Expected: the count pipeline prints
87, the JSON assertion prints87 unique baseline test names recorded, and the focused baseline exits 0.The ignored
.tmp/plan-008/baseline.jsonis the name manifest used after themove. Record the count and test total in the handoff. If the count differs or
the baseline contains a duplicate name, STOP before editing and report the
current names/count.
Step 2: Split tests by the target production topology without changing assertions
Create
interaction-touch-fixtures.tsfrom the reusable setup atinteraction.test.ts:84-162:menubar;
makeVisibleButtonSnapshotandcontextFromFlags;emulateCaptureSnapshotForSessioninto a factory that accepts thecalling test file's mocked dispatch function rather than importing or
installing a global mock;
vi.mockdeclarations andbeforeEachresets local to each test file,and include only mocks that file actually needs.
Redistribute, do not rewrite, all 87 discovered cases:
getandisrouting/read cases ininteraction.test.ts.press,click,longpress,and
fillininteraction-touch.test.tsto pin router ownership.Maestro fallback, pending-stabilization, and fused-seam cases to
interaction-touch-direct-ios.test.ts.ref targeting, non-hittable/off-screen guards, and pre-resolved
findcasesto
interaction-touch-press.test.ts.recorded-parameter cases to
interaction-touch-fill.test.ts.iOS corroboration, and cross-command ref-frame lifetime sequences to
interaction-touch-runtime.test.ts.launcher/Settings escape, and the existing recovery-abort regression to
interaction-touch-android-readiness.test.ts; then deleteinteraction-android-recovery-abort.test.ts.response identity, public transform, Maestro disclosure, reference-frame,
settle-ref issuance, and result-shape cases to
interaction-touch-response.test.ts.Every moved behavior test must still import and call
handleInteractionCommandsfrom../interaction.ts. Pure response-buildertests may import production exports from
interaction-touch-response.ts, asthose exports are used by production modules; never export a symbol only to
make a test possible. Preserve every existing test name and assertion. Do not
convert tests into a table merely to reduce lines unless the assertions and
case names remain individually visible.
Size targets after fixture extraction:
722 lines at
interaction.test.ts:1018-1368,1921-2224,2348-2414before thenew file's local imports. Therefore 500 lines is a target, not a truthful hard
gate, unless a second production seam is demonstrated outside this plan.
1,000 lines; preserve the 1:1 source/test topology instead of inventing a
second test shard solely to satisfy the target;
interaction.test.ts<1,000 lines and contains no touch-command test;Verify discovery preservation:
Expected: the regex count prints exactly
87and the manifest comparisonprints
87 unique test names preserved, with no oldinteraction-android-recovery-abort.test.tsdiscovery. The regex matches theretained
interaction.test.tsplus all seven intendedinteraction-touch*.test.tsfilenames; the JSON comparison proves the countwas not preserved by replacing or duplicating a test name.
Prove the new files are actually executed:
Using
apply_patch, temporarily add one test namedPLANTED plan-008 discovery failureto each newinteraction-touch*.test.ts; each must throw anErrornaming its file.Run:
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction-touch.test.ts src/daemon/handlers/__tests__/interaction-touch-press.test.ts src/daemon/handlers/__tests__/interaction-touch-fill.test.ts src/daemon/handlers/__tests__/interaction-touch-direct-ios.test.ts src/daemon/handlers/__tests__/interaction-touch-runtime.test.ts src/daemon/handlers/__tests__/interaction-touch-android-readiness.test.ts src/daemon/handlers/__tests__/interaction-touch-response.test.tsExpected: exit nonzero with seven planted failures, one naming each file.
Remove every planted test with
apply_patch; rerun the same command andexpect exit 0.
Temporarily add
const __plan008TypeDiscovery: never = 'planted';to one newtest file and run
pnpm typecheck. Expect a TypeScript error naming thatfile. Remove the planted line immediately and rerun
pnpm typecheckto green.Never commit a planted failure. If any new file is absent from the failure
report, STOP: the split test is not owned by the expected gate.
Step 3: Extract response projection and Android readiness leaves
Move
buildTargetedTouchResponsePayloadsfrominteraction-touch.ts:311-339and the response-related symbols frominteraction-touch.ts:552-610intointeraction-touch-response.tsbeside theone response construction site:
buildTargetedTouchResponsePayloadstransformTouchResponseDatareadInteractionResponseDataTransformCommandMaestroFallbackResponseFieldsMaestroFallbackDisclosuremaestroFallbackDisclosureKeep their names, inputs, output shapes, normalization calls, and Maestro
disclosure semantics unchanged. Export only symbols needed by production
siblings or the matching pure response test. Define the targeted-result union
from the existing contracts inside the response module (or reuse its existing
contract union); do not import a type from
interaction-touch-press.ts, whichwould create a response → press back-edge against the target DAG.
Create
interaction-touch-android-readiness.tsfrom current lines 948-1030:RefAdmissionContextReadinessOutcomerunWithAndroidDialogReadinessCheckrefreshAndroidRefSnapshotIfFreshnessActiveThe ref context is the shared typed value crossing into runtime readiness.
Preserve lease-provider bypass, before/after phases, recovery-triggered shared
ref admission rejection, comparison-safe freshness baseline, diagnostics phase,
and best-effort refresh failure behavior exactly.
Update the still-unsplit orchestrator to import the moved production symbols.
Delete moved definitions; do not leave forwarding wrappers.
Verify:
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction-touch-response.test.ts src/daemon/handlers/__tests__/interaction-touch-android-readiness.test.ts src/daemon/handlers/__tests__/interaction-response-construction-guard.test.ts pnpm check:layeringExpected: exit 0; no new value/type cycle or baseline change.
Step 4: Extract direct-iOS and shared runtime orchestration
Create
interaction-touch-direct-ios.tsfrom current lines 345-550 and 612-629:Do not broaden the fast path. In particular, preserve all current exclusions:
recording, replay target guard, non-selector targets, non-default click options,
--verify, and--settle. Preserve Maestro's native error shape. The genericdirect helper currently accepts
'press' | 'fill'; do not create a new directfill call path or narrow behavior as part of this refactor.
Create
interaction-touch-runtime.tsfrom current lines 772-945 and 1031-1049:dispatchRuntimeInteractionImport Android readiness from the leaf created in Step 3. Preserve action
timestamps, pre/post readiness order, Android escape rethrow, warning append
order, shared finalization,
scheduleInteractionOutcomeRetry: falseoncorroborated outcomes, retained
recordedTarget, Android freshness baseline,and normalized error payloads.
Delete moved definitions from
interaction-touch.ts; do not add compatibilitywrappers or an internal barrel.
Verify:
Expected: exit 0; all interaction contract scenarios remain green.
Step 5: Extract press and fill handlers, leaving a thin router
Create
interaction-touch-press.tsfrom current lines 98-341, excludingbuildTargetedTouchResponsePayloadsalready moved in Step 3. Preserve:preresolvedTargetonlyfor click/press;
payload builder;
Create
interaction-touch-fill.tsfrom current lines 632-770. Preserve:findResolvedTargetstale-warning/admission bypass;delayMs, Maestro fallback, verify, settle, replay guard,pre-resolved target);
stale warning, and settle refs.
Reduce
interaction-touch.tsto imports, the existing parameter typeintersection, and the four-way switch. It should delegate to production exports
from the new press/fill modules and return
nullfor every other command.Remove the obsolete
fallow-ignore-next-line complexitycomment. Do not movecommand routing to
interaction.ts, a registry, or a new abstraction.Verify:
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction.test.ts src/daemon/handlers/__tests__/interaction-touch.test.ts src/daemon/handlers/__tests__/interaction-touch-press.test.ts src/daemon/handlers/__tests__/interaction-touch-fill.test.ts src/daemon/handlers/__tests__/interaction-touch-direct-ios.test.ts src/daemon/handlers/__tests__/interaction-touch-runtime.test.ts src/daemon/handlers/__tests__/interaction-touch-android-readiness.test.ts src/daemon/handlers/__tests__/interaction-touch-response.test.ts pnpm check:quick pnpm check:layeringExpected: exit 0. No assertion or wire snapshot changes are allowed.
Step 6: Prove the structural response guard sees the new files
The guard's directory scan should automatically include every new
interaction-touch*.tsmodule except the builder file. Prove it rather thanassuming it:
apply_patch, temporarily add an unexported function containingconst responseData = { planted: true };tointeraction-touch-direct-ios.ts.interaction-touch-direct-ios.ts: responseData = ....apply_patchand rerun the guard to green.Verify red then green:
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction-response-construction-guard.test.tsNever commit the planted violation. If the guard stays green with the violation
present, STOP and report the discovery gap; do not add an allowlist.
Step 7: Enforce module/test shape and run all semantic gates
First inspect line counts and the module DAG:
Expected:
interaction-touch.ts≤100 lines;interaction-touch-response.ts≤500 lines;a file over the target must still answer only its matching production
module's question rather than mixing categories to balance line counts;
interaction.test.ts<1,000 lines and contains no top-level touchcommand case;
Then run all owning gates:
Expected: every command exits 0.
pnpm check:fallowinside the broad gate mustaccept the reduced baseline counts without a baseline edit. The affected gate
may report native/device lanes left to GitHub.
Step 8: Perform the tightening and scope pass
Search for duplicate moved definitions, upward imports, stale old test
discovery, and unexpected files. There must be one production definition per
symbol and no forwarding compatibility layer.
Verify:
Expected: each function is defined once in its target module; the removed test
filename has no references; the temporary discovery manifests are removed;
status lists only the in-scope production/test files plus this plan if it is
intentionally uncommitted.
Test plan
interaction.test.tsandinteraction-android-recovery-abort.test.ts, alluniquely named and green before extraction.
final discovery remains exactly 87 and its sorted name manifest is identical.
handleInteractionCommands; use directimports only for pure response helpers already exported for production use.
file, then remove all planted failures and return green.
neverassignment,then remove it and return
pnpm typecheckto green.temporary hand-rolled response literal, then remove it and return the guard
to green.
changed platform-visible result is a regression and a STOP condition.
Done criteria
tests passed.
comparison is identical, and every original assertion is preserved.
planted code was removed before commit.
it was removed and typecheck returned green.
new source file; it was removed and the guard returned green.
interaction-touch.tsis a ≤100-line router.table and is ≤300 lines;
interaction-touch-response.tsis ≤500 lines.<1,000 lines;
interaction.test.tsis <1,000 lines and no longer containstouch tests.
fallback, compatibility wrapper, value cycle, type-cycle member, or
layering back-edge.
recording, retries, response identity, and platform behavior are unchanged.
pnpm check:quick,pnpm check:layering, guarantee tests, and theresponse-construction guard exit 0.
pnpm exec vitest run --project interaction-contractexits 0.pnpm test:integration:providerexits 0.pnpm checkexits 0 without changing Fallow/layering baselines.pnpm check:affected --runexits 0 for local gates.git status --shortcontains no out-of-scope source changes.runtime behavior;
plans/README.mdis unchanged by instruction.STOP conditions
Stop and report back; do not improvise if:
13bc70f24.manifests differ, or a moved assertion must change to pass.
expected gate.
interaction.ts, a command descriptor,daemon registry, guarantee matrix, contract fixture, public type, response
shape, platform implementation, or SessionState owner.
readiness, recording, retry, or corroboration behavior changes even if tests
can be updated to accept it.
lines, any new test reaches 1,000 lines, or retained
interaction.test.tsremains at/above 1,000 lines after the prescribed partition. Do not create a
second test shard without a matching production seam merely to hit 500 lines.
pnpm check:layeringreports a new value cycle, type-cycle member, inversion,or module-policy breach. Do not edit a ratchet baseline.
pnpm check:fallowrequests new baseline findings. Improve the split; do notregenerate
fallow-baselines/health.json.Maintenance notes
files with mutual imports is worse than the original module and may expand the
ratcheted type-only cycle.
eligibility/delegation in direct-iOS, admission/execution in press or fill,
lifecycle/finalization in runtime, Android recovery in readiness, and wire
projection in response.
interaction-touch.tsa router. Do not let it reaccrete validation orplatform policy.
interaction-response-construction-guard.test.tsintentionally discoversnew
interaction-touch*.tsfiles by name. Preserve that automatic discovery.pre-fix code, the appropriate provider/public path, and the ADR 0011 contract
scenario where the path × guarantee cell applies.
selector-runtime.ts, its tests, and the knownratcheted type-only cycle untouched; they are separate architecture work.