Skip to content

Split touch interaction orchestration into semantic modules #1691

Description

@thymikee

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:

    // src/daemon/handlers/interaction-touch.ts:77-95
    export async function handleTouchInteractionCommands(
      params: InteractionHandlerParams & {
        captureSnapshotForSession: CaptureSnapshotForSession;
        refSnapshotFlagGuardResponse: RefSnapshotFlagGuardResponse;
      },
    ): Promise<DaemonResponse | null> {
      switch (params.req.command) {
        case 'press':
          return await dispatchTargetedTouchViaRuntime(params, 'press');
        case 'click':
          return await dispatchTargetedTouchViaRuntime(params, 'click');
        case 'longpress':
          return await dispatchTargetedTouchViaRuntime(params, 'longpress');
        case 'fill':
          return await dispatchFillViaRuntime(params);
        default:
          return null;
      }
    }
  • 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:

    // src/daemon/handlers/interaction-touch.ts:97-104
    // fallow-ignore-next-line complexity
    async function dispatchTargetedTouchViaRuntime(
      params: InteractionHandlerParams & {
        captureSnapshotForSession: CaptureSnapshotForSession;
        refSnapshotFlagGuardResponse: RefSnapshotFlagGuardResponse;
      },
      command: TargetedTouchCommand,
    ): Promise<DaemonResponse> {
  • Existing function seams at planned-at commit:

    Current range Current responsibility
    interaction-touch.ts:98-341 targeted press/click/longpress admission, execution, and payloads
    interaction-touch.ts:345-550, 612-629 direct-iOS selector eligibility, dispatch, fallback/corroboration, point/reference decoding
    interaction-touch.ts:552-610 public response transform and Maestro fallback disclosure
    interaction-touch.ts:632-770 fill admission, ref preamble, execution, and payloads
    interaction-touch.ts:772-945, 1031-1049 shared runtime lifecycle, warning composition, iOS corroboration, error/retry helpers
    interaction-touch.ts:948-1030 Android blocking-dialog readiness and ref snapshot freshness
  • The shared runtime must append warnings rather than overwrite builder output:

    // src/daemon/handlers/interaction-touch.ts:818-832
    const { result, responseData, recordedTarget } = await options.buildPayloads(runtimeResult);
    // Append, don't clobber — the builder may already carry a warning
    // (e.g. stale-refs, #1076).
    const appendedWarnings = [
      ...(readiness.status === 'recovered' ? [readiness.warning] : []),
      ...(afterRunWarning ? [afterRunWarning] : []),
    ];
    if (appendedWarnings.length > 0) {
      const warning = [
        ...(typeof responseData.warning === 'string' ? [responseData.warning] : []),
        ...appendedWarnings,
      ].join(' ');
      result.warning = warning;
      responseData.warning = warning;
    }
  • Android dialog recovery is a ref-frame side-effect seam, not a generic retry:

    // src/daemon/handlers/interaction-touch.ts:971-990
    const readiness = await ensureAndroidBlockingSystemDialogReady({
      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') {
      const abort = 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.

  • Keep tests on the stable public handler path:

    // src/daemon/handlers/__tests__/interaction.test.ts:1-9
    import type { CommandFlags } from '@agent-device/contracts/command';
    import { test, expect, vi, beforeEach } from 'vitest';
    import { handleInteractionCommands } from '../interaction.ts';
    import type { SessionStore } from '../../session-store.ts';
    import type { SessionState } from '../../types.ts';
    import { attachRefs, type SnapshotBackend } from '@agent-device/kernel/snapshot';
    import { AppError } from '@agent-device/kernel/errors';
    import { buildSnapshotState } from '../snapshot-capture.ts';
    import { setSessionSnapshot, STALE_SNAPSHOT_REFS_WARNING } from '../../session-snapshot.ts';
  • The ADR 0011 construction guard automatically discovers every
    interaction-touch*.ts source file:

    // src/daemon/handlers/__tests__/interaction-response-construction-guard.test.ts:15-26
    const HANDLERS_DIR = path.resolve(import.meta.dirname, '..');
    const BUILDER_FILE = 'interaction-touch-response.ts';
    
    function touchHandlerSourceFiles(): string[] {
      return fs
        .readdirSync(HANDLERS_DIR)
        .filter(
          (file) =>
            (file.startsWith('interaction-touch') || file === 'interaction-common.ts') &&
            file.endsWith('.ts') &&
            file !== BUILDER_FILE,
        );
    }
  • 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.

Commands you will need

Purpose Command Expected on success
Dependency impact pnpm depgraph affected src/daemon/handlers/interaction-touch.ts --json --limit 25 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/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 existing
    response 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-touch
    get/is public-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; 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.
  • packages/contracts/src/interaction-guarantees.ts, contract fixtures,
    command descriptors, daemon registry, public types, response/wire shapes.
  • Apple runner, Android platform/helper, web/Linux/Vega, provider adapters, and
    live-device behavior.
  • ADRs, CONTEXT.md, README, website docs, skills, changelog.
  • 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.
  • plans/README.md and every other plan file.

Git workflow

  • Branch: advisor/008-split-touch-interaction-orchestrator
  • Suggested commits:
    1. test(daemon): split touch interaction coverage by module
    2. refactor(daemon): split touch interaction orchestration
  • Keep each commit green. The temporary planted errors/violations described
    below are verification only and must never be committed.
  • Do NOT push or open a pull request unless the operator explicitly instructs it.

Target module topology

Use direct imports; do not add an internal barrel. The dependency direction is:

interaction-touch.ts
  -> interaction-touch-press.ts
  -> interaction-touch-fill.ts

interaction-touch-press.ts
  -> interaction-touch-direct-ios.ts
  -> interaction-touch-runtime.ts
  -> interaction-touch-android-readiness.ts
  -> interaction-touch-response.ts

interaction-touch-fill.ts
  -> interaction-touch-runtime.ts
  -> interaction-touch-android-readiness.ts
  -> interaction-touch-response.ts

interaction-touch-direct-ios.ts
  -> interaction-touch-response.ts

interaction-touch-runtime.ts
  -> interaction-touch-android-readiness.ts
  -> interaction-touch-response.ts

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.

Verify:

mkdir -p .tmp/plan-008
pnpm depgraph affected src/daemon/handlers/interaction-touch.ts --json --limit 25
pnpm exec vitest list --project unit-core src/daemon/handlers/__tests__/interaction.test.ts src/daemon/handlers/__tests__/interaction-android-recovery-abort.test.ts | rg -c 'src/daemon/handlers/__tests__/(interaction|interaction-android-recovery-abort)\.test\.ts'
pnpm exec vitest list --project unit-core src/daemon/handlers/__tests__/interaction.test.ts src/daemon/handlers/__tests__/interaction-android-recovery-abort.test.ts --json=.tmp/plan-008/baseline.json
node -e '
const assert = require("node:assert/strict");
const fs = require("node:fs");
const tests = JSON.parse(fs.readFileSync(".tmp/plan-008/baseline.json", "utf8"));
const names = tests.map((entry) => entry.name);
assert.equal(tests.length, 87);
assert.equal(new Set(names).size, 87);
console.log("87 unique baseline test names recorded");
'
pnpm exec vitest run --project unit-core src/daemon/handlers/__tests__/interaction.test.ts src/daemon/handlers/__tests__/interaction-android-recovery-abort.test.ts src/daemon/handlers/__tests__/interaction-response-construction-guard.test.ts

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;
  • the fixture module ≤300 lines.

Verify discovery preservation:

pnpm exec vitest list --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 | rg -c 'src/daemon/handlers/__tests__/interaction(-touch[^ ]*)?\.test\.ts'
pnpm exec vitest list --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 --json=.tmp/plan-008/final.json
node -e '
const assert = require("node:assert/strict");
const fs = require("node:fs");
const names = (path) =>
  JSON.parse(fs.readFileSync(path, "utf8"))
    .map((entry) => entry.name)
    .sort();
const baseline = names(".tmp/plan-008/baseline.json");
const final = names(".tmp/plan-008/final.json");
assert.equal(baseline.length, 87);
assert.equal(new Set(baseline).size, 87);
assert.equal(final.length, 87);
assert.equal(new Set(final).size, 87);
assert.deepEqual(final, baseline);
console.log("87 unique test names preserved");
'

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:

  1. 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.

  2. 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.ts

    Expected: exit nonzero with seven planted failures, one naming each file.

  3. Remove every planted test with apply_patch; rerun the same command and
    expect exit 0.

  4. 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.

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:layering

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:

  • session/capability/surface checks;
  • recorded fill parameter assertion;
  • settle flag validation;
  • fill parsing;
  • findResolvedTarget stale-warning/admission bypass;
  • ref flag guard and Android refresh order;
  • fill options (delayMs, Maestro fallback, verify, settle, replay guard,
    pre-resolved target);
  • 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.

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:layering

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:

  1. Using apply_patch, temporarily add an unexported function containing
    const responseData = { planted: true }; to
    interaction-touch-direct-ios.ts.
  2. Run the response-construction guard.
  3. Confirm it exits nonzero and names
    interaction-touch-direct-ios.ts: responseData = ....
  4. 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

First inspect line counts and the module DAG:

wc -l src/daemon/handlers/interaction-touch.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/interaction-touch-response.ts
wc -l src/daemon/handlers/__tests__/interaction.test.ts src/daemon/handlers/__tests__/interaction-touch*.test.ts src/daemon/handlers/__tests__/interaction-touch-fixtures.ts
rg -n 'fallow-ignore-next-line complexity' src/daemon/handlers/interaction-touch*.ts

Expected:

  • interaction-touch.ts ≤100 lines;
  • every new implementation module ≤300 lines;
  • interaction-touch-response.ts ≤500 lines;
  • 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.

Verify:

rg -n 'function (dispatchTargetedTouchViaRuntime|dispatchFillViaRuntime|dispatchDirectIosSelectorInteraction|dispatchRuntimeInteraction|runWithAndroidDialogReadinessCheck|refreshAndroidRefSnapshotIfFreshnessActive)' src/daemon/handlers/interaction-touch*.ts
rg -n 'interaction-android-recovery-abort' src test scripts
node -e 'require("node:fs").rmSync(".tmp/plan-008", { recursive: true, force: true })'
git status --short

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.
  • Baseline discovery contained exactly 87 unique names and baseline focused
    tests passed.
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions