From f8c8ee037dcd8eec5edbe2b0684031a935fcba6f Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:25:06 -0700 Subject: [PATCH] refactor(mobile): parse the launch outcome into a named type at its boundary anti-slop/no-object-parameters flagged terminalLaunchWarning's `result: object`. The rule is pointing at a real seam rather than a style nit: the helper advertised a loose object and did the narrowing inside itself, so every caller handed it unparsed wire data and nothing downstream held a real type. Parsed at the boundary instead. parseTerminalLaunchOutcome takes `unknown` and returns TerminalLaunchOutcome | null, so the narrowing happens once, where the untrusted payload enters, and the consumer works with a named type. The type is taken from the shared contract rather than restated - a Pick over the terminal member of AgentLaunchOutcome - so a change to that union fails here instead of flowing through. `handle` is deliberately excluded: nothing reads it, and requiring it would drop the warning off a reply that omitted one, which is a behaviour change smuggled in under a typing change. No assertion and no config exemption: reintroducing `as Partial` would trade this finding for the defect removed earlier in this branch, and the rule is correct here. The rule arrived with the merge-forward (#20781, newer than this branch's merge-base), and anti-slop is not one of the changed-code gate's six scans - it runs only repo-wide - which is why a clean local gate did not predict it. Behaviour is unchanged across all five warning cases, and the positive case was re-ablated on the new parser: dropping the warning reddens exactly it, 1 failed | 18 passed, restored byte-identical to 19 passed. --- .../src/tasks/agent-launch-worktree-create.ts | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/mobile/src/tasks/agent-launch-worktree-create.ts b/mobile/src/tasks/agent-launch-worktree-create.ts index 0503e561310..dc4faf53cc8 100644 --- a/mobile/src/tasks/agent-launch-worktree-create.ts +++ b/mobile/src/tasks/agent-launch-worktree-create.ts @@ -14,6 +14,7 @@ import { withoutReservedAgentCreateFields, + type AgentLaunchOutcome, type AgentLaunchResult } from '../../../src/shared/agent-launch-intent' import type { TuiAgent } from '../../../src/shared/tui-agent' @@ -59,27 +60,46 @@ export function readAgentLaunchCreateOutcome(result: unknown): AgentLaunchCreate if (typeof worktreeId !== 'string' || !worktreeId.trim()) { return null } - const warning = terminalLaunchWarning(result) + const warning = parseTerminalLaunchOutcome( + 'outcome' in result ? result.outcome : undefined + )?.warning?.trim() return { worktreeId, ...(warning ? { warning } : {}) } } /** - * The startup failure a terminal launch reports: the workspace exists, the agent did not start - * (pty exhaustion). Dropping it is what lands the phone on an unexplained empty session. + * The terminal outcome, narrowed to the fields this reader consumes. Taken from the shared union + * rather than restated, so a change to the contract fails here instead of flowing through. * - * Narrowed rather than asserted, because the payload is whatever the host sent — a reader that - * claims the contract's shape without checking it is how a malformed reply reaches the UI as a - * TypeError instead of a message. + * `handle` is deliberately not required: nothing here reads it, and demanding it would drop the + * warning off a reply that omitted it — a behaviour change smuggled in under a typing change. */ -function terminalLaunchWarning(result: object): string { - if (!('outcome' in result) || !result.outcome || typeof result.outcome !== 'object') { - return '' +type TerminalLaunchOutcome = Pick< + Extract, + 'kind' | 'warning' +> + +/** + * Parses the launch outcome, which arrives as whatever the host sent. + * + * A terminal launch reports its startup failure here: the workspace exists, the agent did not + * start (pty exhaustion). Dropping it is what lands the phone on an unexplained empty session. + * + * Parsed into a named type at this boundary rather than read off a loose `object`, and narrowed + * rather than asserted — a reader that claims the contract's shape without checking it is how a + * malformed reply reaches the UI as a TypeError instead of a message. + */ +function parseTerminalLaunchOutcome(outcome: unknown): TerminalLaunchOutcome | null { + if ( + !outcome || + typeof outcome !== 'object' || + !('kind' in outcome) || + outcome.kind !== 'terminal' + ) { + return null } - const outcome = result.outcome - if (!('kind' in outcome) || outcome.kind !== 'terminal') { - return '' - } - return 'warning' in outcome && typeof outcome.warning === 'string' ? outcome.warning.trim() : '' + const warning = + 'warning' in outcome && typeof outcome.warning === 'string' ? outcome.warning : undefined + return { kind: 'terminal', ...(warning === undefined ? {} : { warning }) } } /**