Files
orca/mobile/src/tasks/task-runtime-reply-schema.ts
T
Jinwoo Hong 6142657d7a refactor(mobile): checked reply readers for the tasks domain's board, runtime, search and create (step 7) (#21246)
* test(mobile): record main's agent.launch create receipt before checking it

`agent.launch` is the one read site in the tasks domain's project-board,
runtime, source-search and workspace create/source files with no recording
family at all, so main's answer to a malformed launch receipt was undocumented
and a checked reader would have had nothing to move.

One family, one scenario, two goldens: `worktree.agent-launch-create` drives
`createWorktreeWithNameRetry` down the `agent.launch` arm instead of
`worktree.create`, which needs an `agentLaunch` argument on the existing
worktree-create-retry adapter. The agent is a constant there on purpose — which
agent is picked changes only the params, and the arm under test is which method
the create is issued on.

A separate family rather than an eighth `worktree.create-retry` scenario:
`familyGoldens` drives its reply matrix over the family's FIRST scenario, so
adding to that family would have recorded a pilot golden and left the launch
receipt with no partitions. As its own base it gets all eleven.

Recorded from a detached worktree at the pinned baseline with this branch's
`rpc-recording/` and manifest copied in, per the recipe in the recorder README:
`mobile/pnpm-lock.yaml` has drifted past `4b876758d3` on main, so `--record`
refuses on this branch's tree even though `mobile/src` and `src/shared` are
byte-identical to the pin.

Thirty-four existing goldens move on `adapterSha256` and nothing else — the six
families mounted through the edited adapter module. No body moves.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): checked reply readers for the tasks domain's board, runtime, search and create

Forty-three unchecked reply readers across five files become checked zod readers,
so a malformed host reply surfaces as one readable error naming the method
instead of a downstream TypeError, a rendered `undefined`, or a screen left ready
over garbage. Deliberately a behaviour change on malformed replies only.

Five schema modules, each recording the consumer line behind every requirement
and the host handler it was checked against:

- `task-project-board-reply-schema.ts` — the sixteen `github.project.*`
  envelopes. Where a consumer reads a member off BOTH arms unguarded the schema
  is a union on `ok`; where it guards everything (`result.error?.message ?? '…'`,
  `result.labels ?? []`) it is a flat passthrough and requires only the
  container, because a requirement on a member the consumer already defaults
  would refuse a reply main rendered.
- `task-runtime-reply-schema.ts` — the hydration reads. The three preference
  writes read `z.unknown()`: no call site interprets their body.
- `task-source-search-reply-schema.ts` — the provider searches and the pasted
  single-item lookups. The Linear union replaces the hand reader in
  linear-mobile-issue-read.ts, whose own copy reached the screen unattributed.
- `workspace-source-reply-schema.ts` — SSH state, agent detection, orca.yaml
  hooks, sparse presets and base-ref search.
- `workspace-create-reply-schema.ts` — the create receipt, the launch receipt and
  the hosted-base union.

Requirements are exactly the members a consumer reads unguarded AND a recorded
golden shows the host sending. That second half is load-bearing: the recorded
GitHub search row is `{ number, title }`, the recorded Linear issue is `{ id }`,
the recorded project is missing `id`/`url`/`source` and the recorded sparse
preset is missing `repoId`/`createdAt`/`updatedAt` — requiring what the shared
types declare would have dropped rows main renders. Where the value therefore
stays looser than the screen's own state type, the call site keeps one narrowing
cast with that reason on it rather than a default that would fabricate state.

Two enum decisions, both pinned:

- `ownerType` is CLOSED with no fallback. It is echoed into the next
  `github.project.listViews` params, and remote-wire-compatibility.md rule 4
  forbids a reply-schema fallback from shaping a param; the host's own listing
  handler answers `validation_error` for any other value.
- `ssh` `status` is OPEN and degrades to `disconnected`, main's own answer for a
  state it did not receive. The readiness gate is an equality test against
  `connected`, so an arm this build has not heard of can never grant a create,
  and the record survives with its Connect affordance.
- Every other host vocabulary a consumer equality-tests — the project view
  `layout`, the `setupRunPolicy` — stays `z.string()` for the same rule.

Tri-states are preserved, not collapsed: the row detail's `reviewDecision`, a
work item's `author` and the SSH record's `error` each keep explicit `null`
distinct from absent, with a unit pin on each.

`blank-workspace-create.test.ts` splits one `it.each` in two. The two create
routes now answer a workspace-less reply differently: `agent.launch` still
reports "Failed to create workspace", because its reader guards `worktreeId`
itself, while `worktree.create` is named as unreadable, because the create screen
reads `result.worktree.id` unguarded into the session route. Both reach the same
catch; only the sentence changes.

`mobile-tasks-refactor-parity.test.ts` moves four hashes and no count. Hooks hold
at 350 with 28 bodies edited and no dependency array moved; statements hold at
417 and declarations at 194; `semantics` loses exactly four lines, all four
string literals that lived inside the one deleted inline cast type. No method
literal and no `rpc:` call signature moves.

The inventory loses its five tasks lines; the boundary test stays green.

Goldens are refreshed in the next commit, which is where the disclosed behaviour
change is proved.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin and re-record the corpus over the tasks domain's checked readers

Repins `baseline` to d4cfac98b4, the commit that landed the checked readers, and
re-records all 760 goldens. The repin rewrites that header on every file; the
recorder edit below rewrites `recorderSha256` on every file too.

The disclosed behaviour change is the body-moved set and nothing else: the
malformed reply partitions of the families whose readers this branch converted.

Two recorder files move with it, both re-anchoring evidence the checked readers
displaced rather than deleted:

- `pilot-recordings.test.ts` restates the b2 seed. The shipped null result is
  still the seed and the screen still reports an error the user can see; what
  moved is the sentence, from V8's "Cannot read properties of null (reading
  'ok')" to the reply and method the reader names.
- `operation-mutations.ts` re-anchors that seed's `acceptance` mutant. Its defect
  is a null envelope reaching the metadata sheet, and the call-site guard it was
  injected at can no longer see one, because the reader refuses the envelope
  first. The anchor is the schema now, and loosening it to `z.unknown()` puts the
  null back on the path to `result.ok` — the same defect at its new home.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): assert the reply-schema pins without type assertions

The changed-code casting gate counts a `as` in a test like any other, and eight
of them had crept into the new schema pins. Each is replaced by an assertion that
reads the same fact off the typed value: the schema already declares
`worktreeCreateIdempotency`, `glab`, `status` and `error`, so the narrowing was
never needed, and the two "is this key present" checks are JSON comparisons,
which is the honest way to ask — `JSON.stringify` drops an absent key and keeps
an explicit null, which is the whole distinction a tri-state pin is making.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the corpus to the tree it was recorded from

The previous repin named d4cfac98b4, and the assertion cleanup that followed it
touched `mobile/src` — a fenced path — so the header pointed at a tree the
working copy no longer was. Repins to 6b740c3f61 and re-records.

Bodies are unchanged: only `baseline` moves, on all 760 goldens. Four test files
cannot reach a recording, which is the point — the fence does not know that, and
a header that names a tree nobody can reproduce is the one claim it exists to
make.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): point the reply schemas' consumer citations at the landed lines

Every requirement in the five schema modules names the consumer line that
justifies it, and the migration moved those lines: deleting a thirty-line inline
cast type shifts everything under it. The citations now resolve against the tree
they ship in.

Comment-only. No schema, no reader and no consumer changes.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the corpus to the tree the citations landed in

`baseline` follows the last commit to touch a fenced path, and the citation fix
did. Bodies unchanged: `baseline` moves on all 760 goldens and nothing else.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin both reader vocabularies against the host's own unions

The two enums these readers declare were checked against mobile's restatement
of the wire, not against the types the handlers return. A closed enum written
from the wrong vocabulary drops every row that carries an arm it omits, and no
golden can catch it when no fixture carries one.

Both arm sets are now keyed by the host type in a Record, so an arm added to or
removed from SshConnectionStatus or GitHubProjectOwnerType fails tsc before any
test runs. The SSH degrade's inertness is pinned at the gate that reads it
rather than argued in a comment: an arm a newer host sends and the degraded
value reach the same label, the same readiness verdict and the same error.

Also corrects a comment claiming the file-mutation owner check reads members
this schema forwards. It asks ssh.getState through a reader of its own, and no
mobile code reads providerEpoch, supportsFolderDownload or remotePlatform.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the corpus to the tree the vocabulary pins landed in

Comments and tests cannot change a decoded value, so the whole delta is the
baseline header key: 760 goldens, one line each, no body moves and no scenario
or adapter change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): record real provider rows in the smart-search and paste fixtures

The smart-search and paste-lookup scenarios carried hand-written stubs, not
rows any handler can build: Linear issues of `{ id }` alone, GitHub items of
`{ number, title }`, and a GitLab item keyed by `iid`, a member neither work-item
type declares. Every one of them omits members the host's own types declare
non-optional and mobile then reads with no guard, so the corpus was evidence for
a requirement it could never have justified.

The rows are now the shapes the corpus already uses elsewhere (`tk-list-linear`,
`tk-provider-load`, `tk-list-gitlab-items`), checked member for member against
LinearIssue (src/shared/linear/issue-types.ts:3), GitHubWorkItem
(src/shared/github/work-item-types.ts:17) and GitLabWorkItem
(src/shared/gitlab-types.ts:165).

No schema moves in this commit. It records what main renders for a real row, so
the requirement that follows can be read against main's own behaviour rather
than against a stub.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): require the members the tasks readers' consumers read unguarded

Round-1 review found four unguarded consumer reads at members the corpus proves
the host sends, plus two the corrected fixtures now prove. Each one ends in a
TypeError inside a render or a useMemo, which is the defect class this migration
exists to close.

Required, each because a consumer reads it with no guard and the host's own type
declares it non-optional:

  title on an accessible project  project.title.toLowerCase()
  name, directories on a preset   localeCompare, and two joins
  labels on a work-item row       item.source.labels.filter, both label editors
  state.name, team.name, priority createLinearTask, and the reviewer sort

All six sit inside a salvagingArray, so a row that lacks one drops and the list
survives. The single-row paste lookup names the reply instead, because there is
no list for it to survive in.

Loosened in the other direction: the SSH record no longer requires `error` or
`reconnectAttempt`. Nothing reads either one — the gate spells
`matchingState?.error ?? null` and nothing anywhere reads reconnectAttempt — and
the record is a salvagedOptional, so requiring an unread member drops the WHOLE
record, whose fallback on the connect path is `fallbackSshState(id,'connected')`.
A reply of `{ targetId, status: 'auth-failed', error }` would have shown the
drawer as connected. Mobile's own stored type is widened to match; the shared
wire type is untouched.

The `iid` extension on the lookup row goes with it. Neither work-item type
declares such a member and every GitLab consumer builds its iid param out of
`item.source.number`.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): repin the screen parity hashes over the merged tree

Main landed the sibling tasks lane, which edits the same screen hook files this
branch does, so the merged tree hashes to neither side's constant. Both inputs
are legitimate: main's reply-schema conversions and this branch's WorkspaceSshRecord
rename.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): justify the smart-source casts by the schema, not the fixture

Five SAFETY notes argued from the corpus back to the wire: they justified a
cast by the stub rows the fixtures used to carry (`{ iid, title }`,
`{ number: 12, title: 'twelve' }`). Those rows were the defect corrected in
6763ff12e9, so the claims are now false, and the reasoning was never sound —
a fixture cannot say what the host may send. Each note now cites the schema's
own requirement rule, the host type and the consumer read.

Comment-only; no golden moves. The hook and statement parity hashes move
because `normalized` hashes a statement's full span, comments included.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): repair the line citations this lane's own edits aged

Eleven citations across five reply-schema modules pointed at the wrong line.
Every one was correct when written and rotted afterwards: the SAFETY-note
rewrites, the F4 dedupe's deleted casts and the sibling lane's merge each
shifted the files being cited. A citation is the whole argument for a
requirement, so a stale one reads as a fabricated one.

Found by resolving every `file.ts:line` in the five modules against the merged
tree and comparing the line's text to the claim beside it, not by reading them.
The ones that still resolve correctly are left alone, including three that
looked stale and were not.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep the two advisory task probes total so a nullish payload cannot unhydrate the screen

`preflight.check` and `linear.status` are read under `success-result-or-skip` and documented as
advisory. That policy accepts an envelope whose `result` is absent or null, then asks the reader to
decode it; a `looseObject` refuses, the throw leaves the reader, and the caller's catch discards the
entire hydration. The corpus records the difference: on the `result-absent` and `result-null`
partitions main hydrates the Tasks screen and lists one provider, and the checked readers left it
unhydrated with no providers.

`.catch` restores main's answer exactly. Every consumer guards to the leaf and compares to `true`,
so absence, null and a garbage payload have always meant "not installed" and "not connected".

Four cases pin it, and removing either catch fails all four.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the RPC corpus at this lane's head

Repins the recording baseline to b354d1338a (the advisory-probe totality
fix) and records all 382 scenarios from that tree.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the RPC corpus after merging main

Repins the recording baseline to the merge commit and records all 386
scenarios from that tree, so the corpus carries both main's step-7 batch
and this lane's.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): drop the Linear row cast the checked schema made unnecessary

`found` is already assignable to the mobile `LinearMobileIssue` alias once
`linearIssueRowSchema` requires its nine members, so the assertion and its
disable line carried no type error. The sibling cast in
smart-source-search-requests.ts stays: it targets the shared `LinearIssue`,
whose `labelIds` is required where the schema leaves it optional.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): rewrite the SAFETY lines that argued from deleted fixtures

Three casts still justified themselves with the `{ id }` and
`{ number, title }` rows this branch replaced in round 1, which reads as a
licence to loosen the requirements that close the reproduced crashes. Each
now names what its schema requires and what the cast actually covers, each
verified by deleting the cast and reading the error:

- Linear rows: all nine read members are required, so `labelIds` alone is
  the gap between the schema and the shared LinearIssue.
- GitHub search: `items` and eight row members are required; the salvaged
  `T | undefined` types and the deliberately opaque `sources`/`errors` are
  what remain.
- Sparse presets: `id`, `name` and `directories` are required; the cast
  covers the three SparsePreset declares that the reply omits.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): resolve the bare :NN citations the first audit could not see

The earlier repair resolved only qualified `file.ts:line` citations, and
these doc blocks name a file once and then reference it as a bare `:NN`,
so every continuation reference went unchecked. Re-running the audit with
a resolver that carries the last-named file and directory forward finds
twelve stale line numbers and four references whose nearest named file is
the wrong one.

Fixed: the four `find`/`filter` lines and the layout equality tests under
the `views` requirement, the settings commit, the detail refusal throw,
and the three metadata guarded reads, all shifted by one to five lines.
The ui-state, paste-resolved and host-method references are now qualified,
because an intervening citation to another file silently reassigned them.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-pin the tasks parity hashes the round-2 fixes moved

One statement changed (the Linear list cast is gone) and three SAFETY
comments nested inside statements were rewritten, so the hook and
statement hashes move. Counts hold at 350 and 417, and the declaration,
semantic, render and style hashes do not move, which is what shows no
type, call or rendered tree changed with them.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): delete the Linear list reader the checked schema replaced

`extractLinearIssueReadItems` lost its only caller when the smart-search
operation moved to `rpcResultVariant('linear-issues', …)`. What remained
was a function no screen can reach, a suite reporting coverage for it, and
a second 'Unexpected Linear tasks response' string competing with the
named reply error. The `LinearMobileIssue` type stays: it is the mobile
`LinearIssue` alias.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): say what a refused sparse-preset list actually does to the screen

"Reports the named error" overstates it: the error setter's value is
destructured with a leading underscore and read by nobody, here and on
main. The visible delta is `presetsLoaded` staying false, which disables
"New preset" and both draft entry points where main let the user create
one. No shipped host reaches the state: `repo.sparsePresets` has no
refusal arm.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the RPC corpus after the round-2 fixes

Repins to the round-2 head and records all 386 scenarios from it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the RPC corpus after merging main at 7a1f55c52a

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep the persisted-ui-state reader total so an unreadable ui reply cannot unhydrate the Tasks screen

Main boxed the payload and read `undefined` off a string, number or array, so the screen
hydrated; a refusal here threw out of hydrateTaskState and failed the settings, preflight and
Linear legs beside it. Null and absent now hydrate with the defaults too, since every read of
the state is optional. The GitHub search SAFETY line separates the members the schema requires
from the ones it only types, and the parity hashes follow that comment text.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the RPC corpus over the total ui-state reader

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-17 14:08:59 -04:00

121 lines
6.2 KiB
TypeScript

import { z } from 'zod'
import { salvagedOptional, salvagingArray } from '../../../src/shared/zod-salvage'
// What the Tasks screen reads once per host to hydrate, and the preferences it writes back.
// Checked against src/main/runtime/rpc/methods/status.ts:6-16 (RuntimeStatus, declared in
// src/shared/runtime-session-contracts.ts:64), client-ui.ts:22-74 (the `{ settings }` / `{ ui }`
// envelopes), preflight.ts:17 (PreflightStatus) and linear.ts:35-42 (the connection status).
/**
* The runtime status, read for a capability list and nothing else.
*
* `capabilities` is the only member any consumer here reaches for, and every one of them guards it
* — `status.capabilities?.includes(…)` in use-mobile-tasks-runtime-hydration.tsx:205 and in
* src/shared/file-mutation-ownership.ts:10, `result.capabilities ?? []` in
* worktree-create-capability.ts:51. So the requirement is the container: main read `.capabilities`
* off whatever the payload was, and a string or a number reply was a property read that answered
* `undefined` and silently downgraded the host to "Tasks unsupported".
*
* Elements are strings and a non-string element drops. `includes` over a mixed array already never
* matched a capability id, so the drop changes no verdict; it is what makes the drop visible in
* the salvage report instead of invisible in an `includes` that quietly answers false.
*
* `worktreeCreateIdempotency` is `unknown` on purpose. worktree-create-capability.ts:55-70 does its
* own `typeof`/`Array.isArray` triage over it and treats absent, null, non-object and object as
* four different answers; a schema that narrowed it would have to pick one of those apart here and
* change which branch a host lands in.
*/
export const taskRuntimeStatusSchema = z.looseObject({
capabilities: salvagedOptional('capabilities', salvagingArray(z.string())),
worktreeCreateIdempotency: z.unknown().optional(),
hostPlatform: salvagedOptional('hostPlatform', z.string())
})
/**
* Persisted client UI state, answered under a `ui` member.
*
* The reader yields `ui` itself, which is what the member reader it replaces did, and it is total
* like the two probes below it. Main's `rpcPayloadMember` boxed the payload and read `undefined`
* off a string, a number or an array, so the Tasks screen hydrated on any of those; a refusal here
* would instead throw out of `hydrateTaskState` and leave the whole screen unhydrated (the outer
* `.catch` at use-mobile-tasks-runtime-hydration.tsx:349). Main did throw on a null or absent
* payload, and that is the one place this reader is deliberately kinder: persisted UI state is
* optional at every read (:283 spells `uiState?.trustedOrcaHooks ?? {}`, :284
* `uiState?.taskResumeState ?? {}`), so "no ui" hydrates with the defaults rather than failing the
* settings, preflight and Linear legs that arrived beside it. The
* `matrix-settings.task-hydration-ui.get-1` golden records that move on `result-absent` and
* `result-null`.
*
* Both members are `unknown`, and the call site keeps one narrowing cast over them. They are
* opaque forwards: `trustedOrcaHooks` goes straight into state, and `taskResumeState` is the
* screen's whole persisted view state, re-read field by field with its own defaults at :286 and
* across use-mobile-tasks-client-settings-actions.tsx. Declaring either here would restate a
* twelve-member union that nothing in this reader reads.
*/
export const taskUiStateSchema = z
.looseObject({
ui: salvagedOptional(
'ui',
z.looseObject({
taskResumeState: z.unknown().optional(),
trustedOrcaHooks: z.unknown().optional()
})
)
})
.nullish()
.catch(undefined)
.transform((reply) => reply?.ui)
/**
* The provider preflight, read only for whether `glab` is installed.
*
* Three consumers and all three guard to the leaf: `preflight?.glab?.installed === true`
* (use-mobile-tasks-runtime-hydration.tsx:295, mobile-home-host-requests.ts:95) and the
* `readProbeMember` pair in use-new-workspace-runtime-context.ts:92. `git` and `gh` are declared
* non-optional by PreflightStatus but no mobile consumer reads them, so they pass through.
*
* Total on purpose. This read is advertised as advisory, but `success-result-or-skip` accepts an
* envelope whose `result` is absent or null and then asks the reader to decode it, and a refusal
* there throws out of the whole hydration -- the corpus records main hydrating the Tasks screen on
* that reply and this reader leaving it unhydrated. `.catch` restores main's answer exactly,
* because every consumer guards to the leaf and reads absence as "not installed".
*/
export const taskPreflightSchema = z
.looseObject({
glab: salvagedOptional(
'glab',
z.looseObject({ installed: salvagedOptional('installed', z.boolean()) })
)
})
.catch({ glab: undefined })
/**
* Whether Linear is connected.
*
* `connected` is compared to `true` at use-mobile-tasks-runtime-hydration.tsx:293,
* mobile-home-host-requests.ts:96 and use-new-workspace-runtime-context.ts:96, so absence and
* `false` already mean the same thing and nothing is required. `workspaces` is not declared: the
* picker reads it through a different operation on this same method, and listing a member ahead of
* a reader is how a schema starts refusing replies no consumer here would have noticed.
*
* Total for the same reason as the preflight read above: absence and `false` already mean the same
* thing here, so a nullish payload must read as "not connected" rather than throw.
*/
export const taskLinearStatusSchema = z
.looseObject({
connected: salvagedOptional('connected', z.boolean())
})
.catch({ connected: undefined })
/**
* The three writes whose reply body no call site reads.
*
* `ui.set` is interpreted for its verdict alone and the value discarded
* (use-mobile-tasks-client-settings-actions.tsx:201, setup-hook-trust.ts:49-51); `settings.update` is
* fire-and-forget at five sites and never interpreted; `linear.selectWorkspace` chains the context
* reload off the send without interpreting it. Declaring a member on any of them would be a
* requirement with no reader, and would make a refusal visible for the first time at a site whose
* whole documented behaviour is that it is not.
*/
export const taskPreferenceWriteSchema = z.unknown()