mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
b5869eeaae72dbf2b7a4bd409f2dd66860bfcf08
88
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
85f8d6b5f5 |
test: retire long-tail cases whose assertion is decided by the test itself (#24132)
Resumes the backlog sweep at a chunk size that actually gets read. Six auditors, 84 files
each, and all six read their full scope case-by-case against production — the first wave
where every chunk closed with no gap. 33 case declarations removed across 22 files, 1 test
file deleted, 826 lines gone. No production code touched.
This wave exists because a conclusion of mine was wrong. I had recorded that yield collapsed
~36x and that deletion was no longer the high-value work. I was dividing cases removed by
files IN SCOPE while the fraction auditors actually READ fell from 100% to about 4%, because
I kept handing them 300-800 files. Recomputed against files read, yield has been flat at 4-7
per 100 with no downward trend. This wave came in at 8.2.
The most instructive removal looked like the most valuable test in scope.
`orchestration-worker-release-reap-fixed.func.test.ts` cites a production bug by two
identifiers, describes orphaned PTYs accumulating until `TasksMax=4096` aborts processes on
EAGAIN, and advertises itself as the functional tier wiring the real orchestration RPC
surface, the real `OrchestrationDb` and the real release modules. Deleting it leaves no
reference to that bug anywhere in `src`.
It still had to go: its fake runtime performed the fence it asserted —
if (pty.incarnationId !== inc) { return null }
handleTable.set('term_reminted', { ptyId, epoch: rendererGraphEpoch })
— so the case checking that a reused ptyId with a mismatched incarnation does not resolve was
checking a decision its own spy made twenty lines earlier. The real fence is owned by
`orca-runtime-terminal-handle-incarnation.test.ts:257`, and the other two cases replay
`orchestration-worker-release-incarnation-fallback.test.ts` (which uses a plain
`mockReturnValue` rather than reimplementing the remint) and `worker/worker-release.test.ts:23`.
"Integration test" and "wires real modules" describe the scaffolding, not the asserted step.
Other removals: a self-comparison disguised by an alias, where
`export const getIssueOwnerRepo = getOwnerRepo` makes a case asserting the two "agree" into
`f(x) === f(x)`; four cases whose `vi.mock` of `resolveIssueSource` made both the preference
value and the topology inert; five verdict-precedence cases owned by a verdict-agnostic block;
three call-shape probes on one-line store pass-throughs whose real contracts are driven by
behavioural neighbours; and a `export type _Ref = [...]` declaration whose own comment admits
it exists only to preserve test-only module-surface references.
Kept after checking production rather than shape. An auditor found two near-identical
ten-reconnect loops and kept both: one uses a test-local live-lease filter, the other the
shipped `sshRemotePtyLeaseAllowsReattach` predicate, and the file's own comment explains the
duality is deliberate "so the two cannot drift". Another kept a paths-alignment case that
looks like a validator tested against its own list, because adding a generated file without
registering its path does fail it — and `shellReadyWrappersExist` uses that registered list to
decide whether a partial tree needs regeneration.
Production duplication is now confirmed four times over, and it is why mirrored tests exist:
`createUpdateWorktreeLineage`/`createAssignWorktreeParent` differ by one `console.error`
string; `terminal-path-tap.ts` and `document/path-tap.ts` carry hand-maintained copies of
`matchFilePathAtColumn` under a docblock reading "keep the two in sync". In those cases both
test sides are load-bearing and the duplication belongs on a refactor list.
`mobile/tests-typecheck-baseline.txt` loses one entry. Trimming
`relay-host-signed-out-verdict.test.ts` made it typecheck clean, so the ratchet required
pruning its grandfathered entry — the file graduates from exempt to enforced. Baseline is now
124 entries, down from 125.
Verified: 690 test files / 7,560 cases pass across the touched desktop areas; the modified
mobile files pass (162 cases); `check-tests-typecheck-ratchet.mjs` OK (898 files in program,
124 grandfathered); `check-reliability-gates.mjs` 140 gates; the deleted file is absent from
the gate manifest, `cloud/package.json` and the mobile baseline; nothing under
`mobile/src/test-support/rpc-recording/` or `mobile/rpc-foundation/goldens/` touched.
|
||
|
|
ca7c14db08 |
fix(mobile): start + menu, quick command and diff-note agents through agent.launch (#22954)
* fix(mobile): start + menu, quick command and diff-note agents through agent.launch
The session screen's + menu, agent quick commands and diff notes' New agent
session now ask the host to start the agent with agent.launchReplay, so the
host picks chat or terminal from the desktop's default and delivers any
prompt. Hosts without the launch capabilities keep today's paths.
The phone's pending tab choice is one value (a tab, a terminal by handle, or a
launched surface) instead of two refs, and a launched chat is found by its
session id in the next snapshot rather than a predicted tab id. A launched
surface waits a bounded number of snapshots for its tab.
* test(mobile): add the + menu and diff-note launch scenarios to the recording corpus
* test(mobile): repin bridged-parity tallies for the four launch goldens; drop test casts
The corpus grows from 790 to 794 goldens; all four new ones replay identically.
* fix(mobile): show a refused agent launch as a toast beside open tabs
The inline create error renders only in an empty session, so a host refusal
(for example a disabled agent) from the + menu in a session with tabs showed
nothing. Always toast the failure: the caller's own copy when it gave one,
otherwise the host's reason.
* test(mobile): type the launch reply helper with the shared launch outcome types
* fix(mobile): record a launched agent's tab as this device's pick on the host
A launch carries no navigation, so the phone selected the new tab only
locally while the host kept this device on the tab it had before. Leaving
the session and coming back, or a reconnect that reset the screen, reopened
that old tab. The "+" terminal path this replaced asked the host to select
the tab for the caller.
When a launched surface's tab lands in a snapshot, activate it for the
caller exactly as a tap does. The resolver now names the landed tab in
place of the unused `missed` flag. Route parity re-pinned for the new
activation body, identity payload and strings.
* fix(mobile): land on a launched agent's tab without a 500 ms wait or a blank pane
The host publishes a launched tab before it replies, so the tab list the
phone already holds usually has it by the time the reply arrives. The
launch paths still waited for a refetch 500 ms later, leaving the phone on
the old tab for that long after every launch. Read the tab list at once.
On hosts without agent.launch, the chat path also unsubscribed the open
terminal and cleared its handle before the chat's tab landed, while the old
terminal tab stayed selected: a blank pane until the next tab list. Leave the
open tab live until the chat lands, as the launch path does; applying that
tab list tears the old terminal down.
Route parity re-pinned for the two bodies.
* fix(mobile): keep a tab the user picked while a prompted launch was still replying
A quick command or review-notes launch now waits for the host to deliver the prompt, which can take up to a minute. The launched tab shows up in the tab row well before that, so a user who tapped another tab meanwhile was pulled back onto the launched one when the reply arrived, and that pick was recorded on the host.
The launch now remembers which tab the phone was on when it started and only takes focus if the phone is still there when the reply lands. Any move made in between, by a tap or by the computer navigating this phone, wins. Session route parity re-pinned for the handleCreateTerminal body only.
* fix(mobile): name a launched agent's tab before asking, and land on it when it is listed
A "+" menu, quick-command or review-notes launch now reserves its tab before it asks the host: a fresh pane key (tab and leaf UUIDs) and, for an agent the host may start as a chat, a session id. Both are minted once per launch and sent unchanged on every replay, since the host's replay fingerprint covers them.
The phone arms its pending selection with that reservation before sending, so it lands on the terminal (matched by pane halves) or chat (matched by session id) as soon as the tab is listed. For an agent whose prompt is pasted after start, that is long before the reply, which waits for delivery. Landing also frees the "+" lock; the lock holds the create's id, so an older launch's reply cannot free a newer one's. The reply now only adds its own handle or session id (an older host ignores the reservation), starts the fallback countdown, and reports prompt delivery.
A tab the user picks mid-launch replaces the pending selection, so the launch-start tab check is gone. A reservation the host refuses as already taken reads "Couldn't start the agent. Try again." on the first send, and as unconfirmed after a replay. The mobile UUID fallback now yields a v4 UUID, because a pane key's leaf must be one. The host launch path moved to new-tab-agent-host-launch.ts; session route parity re-pinned for that move and the landing's lock release.
* test(mobile): expect the launch reservation in the four launch scenarios
The four launch scenarios now expect the pane key and session id the phone sends (the scripted ids come first, so the operation id moves from ...001 to ...004).
* fix(mobile): don't say an agent may not have started while the user is looking at it
When a launch's reply was lost after its tab had already landed, the phone said "Couldn't confirm the agent started", although the listed tab proves it did. Now a listed tab narrows the doubt to the prompt or notes ("The agent started, but couldn't confirm the notes were sent."), the notes stay unsent, and a bare launch says nothing. Only the nested-function parity pin moves, for handleCreateTerminal passing the tab list to the launch.
* test(mobile): read the launch's sent reservation through the host's params schema
The anti-slop audit rejects Reflect.get; parsing with AgentLaunchReplay also
asserts the host accepts the params the phone sent.
* test(mobile): check the launch reservation against the host without importing its schema
Mobile code may import the params contract only as types. The phone's tests
now read the sent reservation by narrowing, a chat reservation is checked
through the real host dispatcher, and the older-host drop is pinned host-side.
* fix(mobile): don't send the same review notes to a second new agent
The "+" lock is now freed when the launched tab lands, but review notes are
only cleared when the launch's reply confirms delivery, which for a prompted
launch can take up to a minute. In that window "Send review notes to AI" still
offered the same notes, and choosing a new agent session started a second
agent with them.
The notes a new agent session is being started with are now held from the tap
until that launch settles: the Send button no longer counts them, the sheet no
longer offers them, and a stale tap on the old sheet starts nothing. Notes the
host did not deliver become sendable again once the reply arrives.
* test(mobile): record the + menu and diff-note launch goldens
4 added (+ as a terminal, + as a chat, notes delivered, notes not delivered). 10 existing create-terminal goldens move only because the recorded state now shows one pending selection instead of two refs; their requests are unchanged.
|
||
|
|
75040eba5a |
test: open, seed and read the agent-session record store through one test harness (#23986)
* test: open, seed and read the agent-session record store through one harness Tests that open the durable agent-session record store, seed it, or read back what it persisted now go through agent-session-record-store-test-harness.ts instead of calling AgentSessionRecordStore.open or touching agent-sessions.json themselves. A later change that moves the store into the chat database then changes the harness instead of every test. No production code changes. Tests whose subject is the JSON file itself (its .bak recovery, salvage, schema versions, permissions, and what older builds read back) keep reading and writing the file directly; the storage move rewrites or deletes them. * test: address the record-store harness by the host's state directory The harness took the store's own folder, so each caller picked one (join(root, 'store'), or 'agent-sessions' where a test read the store the runtime owns). A later change that moves the store into the state directory's journal database could not tell those apart, and would have had to edit every caller again. Every harness function now takes the state directory, the one the test's journal database and recovery capsule already live in, and keeps the store in the same subfolder the runtime uses. Callers pass that directory; store-only tests pass their temp directory unchanged. Format tests that share a directory with harness calls take the file path from testAgentSessionStoreFilePath. The folder name moves from a private constant in the runtime to AGENT_SESSION_STORE_DIR_NAME beside the store's file name, so the harness shares it without importing the runtime. Its value and every path built from it are unchanged. |
||
|
|
45c63a66e9 |
test: delete the source-grep tests an earlier detector's regex missed (#23976)
A rebuilt detector found 195 source-grep candidates where the original found 111.
The gap was one over-specific regex: the first scanner required a literal `.ts`
path inside `readFileSync(...)`, so every test that built its path from variables
(`join(dirname, '..', 'foo.tsx')`) was invisible to it. Roughly 84 files of a
pattern an earlier wave reported as cleared had in fact survived.
Deleted whole, every case asserting on production source text:
- `app-startup-routing.test.ts` (27 cases) — exact import statements
(`"import('../components/UpdateCard').then"`), relative-path spelling, and
`indexOf` source ordering. A file move or a `lazy()` refactor breaks it.
- `pull-request-page-host-boundary.test.ts` (13) — `toContain` on whole argument
expressions concatenated across 20+ component files.
- `SmartWorkspaceNameField-source-boundaries.test.ts` (7) — placeholder copy, a
Tailwind class string, and `not.toContain` on an already-deleted symbol.
- `github-project-repo-list-load.test.ts` (9) — `indexOf` statement ordering
inside `loadTasks`.
- `github-enterprise-slug-routing-boundary.test.ts` (4) —
`toContain('host: githubProjectHost(parsed?.slug.host)')`.
- `web-viewport-shell.test.ts` (3) — a regex demanding exact CSS selector-list
ordering and whitespace.
- `agent-catalog-links.test.ts` (1) — restates two `homepageUrl` literals straight
out of `agent-catalog.ts` with nothing in between.
Trimmed, keeping only what nothing else can reach:
- `desktop-startup-ordering.test.ts` 549 -> 66 lines, retaining the three cases
named as `assertionRefs` by the `ssh-filesystem.stream-inactivity-lifecycle` and
`agent-browser.owner-boundary-cleanup` gates; 15 source-order greps went.
- `ResourceUsageStatusSegment.session-polling.test.ts` keeps its census that no
`setInterval` exists and `listSessions()` is called exactly once — an added poll
multiplies a global daemon scan and no behavioral test sees it. The
`indexOf('if (!open)')` ordering pair and four `not.toContain` lines went.
- `agent-skill-installed-command-callers.test.ts` 231 -> 86, keeping the
`readdirSync` census that discovers every `<AgentSkillSetupPanel` caller and
asserts set equality against the allowlist, so a new panel host cannot silently
show a default Update action.
Also in this wave, from the renderer lib/runtime sweep: 22 cases whose routing
signal the production path never reads — verified by mutation, stripping
`connectionId`, the WSL preference and the UNC path from four of them left all 29
tests passing — plus braille-spinner rows collapsed onto one regex range, copied
`WELL_KNOWN_LABELS` rows, and a whole `resolveAiVaultResumeStartupShell` describe
whose four darwin/linux fixtures all return before the login shell is read.
`config/reliability-gates.jsonc` drops the two `app-startup-routing.test.ts`
references; the manifest still validates for 140 gates.
|
||
|
|
6194a7a1b6 |
test: drop private-internal and boundary-census tests with behavioral owners (#23941)
Fourth audit wave, cut short by a session restart, so this lands the verified subset rather than the full batch. Removes private-predicate cases whose behavior is already covered through the module's real entry point, and de-exports the seams they reached for. Also drops three whole files whose every case was a duplicate or a call-shape grep. The source-grep vein is close to exhausted. One auditor reviewed 15 remaining flagged files and deleted nothing: what is left is mostly legitimate architectural ratchets that no type checker and no behavioral test can reach — AST fences banning `as`/`any` in an RPC operation region, discovered-vs-listed set equality over subscription sites, count ceilings on unchecked reply readers, and assertions on generated WebView bundles (no CDN URL, no `</script` tokenizer escape, parses at the Chrome 74 floor). Those stay. |
||
|
|
6e1b7e7fa3 |
test: remove junk tests that assert source text instead of behavior (#23815)
Deletes 101 test files and trims 112 more, all matching documented junk patterns: exact source/import/string greps, copied inventories and export lists, duplicate invocations of a contract another test already owns, typeof-shape checks TypeScript already enforces, and self-comparisons. The largest group read a production `.ts` file and asserted on its text — for example a TaskPage test that required the source to contain `selectedRepos.find((r) => r.id === newIssueRepoId) ?? selectedRepos[0] ?? null`. Any behavior-preserving rename broke it; no behavior change ever did. Production-side follow-through: exports that only these tests imported are de-exported or deleted, stale comments pointing at removed censuses are dropped, and the reliability-gate registry, `cloud/package.json` test lists, and orphaned source-reading helpers are updated so nothing references a deleted file. Two files kept their real coverage and lost only the census scaffolding: `agent-status-producer-census.test.ts` now drives all five producers end to end instead of grepping the source tree, and `config-toml-trust-stale-writes` replaces an export-list parity check. |
||
|
|
ccdb324b63 |
Add CodeBuddy as a built-in coding agent (#23740)
* feat(agents): integrate CodeBuddy launch, status and session history * docs: record CodeBuddy lifecycle verification * fix(codebuddy): backfill scoped history and negotiate remote resume * test(cli): include CodeBuddy in known search agents |
||
|
|
29c7d5d983 |
fix(mobile): start AI-button agents through agent.launch, never a bare shell (#22762)
* fix(mobile): start AI-button agents through agent.launch, never a bare shell "Fix checks with AI", "Resolve conflicts with AI", commit-failure recovery and diff review's "New Agent Session" created a terminal with no agent and typed the multi-line prompt into the shell, so each line ran as a shell command. They now call agent.launchReplay into the existing workspace with the prompt; the host picks chat or terminal from the user's default and delivers the prompt. Hosts without the launch capabilities get the buttons disabled with update copy. The agent comes from the desktop's own resolution (moved to src/shared). The replay loop and capability read are shared with the workspace-create launch. * test(mobile): repin bridged-parity tallies for the AI-button launch goldens The corpus goes from 787 to 790 goldens: five shell-path goldens are removed and eight agent.launch ones added; one lands in identical and two in result-absent-settlement. * test(mobile): re-record goldens for AI-button launches through agent.launch Repinned baseline to |
||
|
|
0034ede120 |
fix(mobile): left-align every line of the desktop host card (#23673)
* fix(mobile): left-align every line of the desktop host card StatusDot carried its own marginRight on top of each row's spacing, so the host card's status text sat 14 px in and the worktree line was hand-indented to match. Move the dot spacing to the row gap in every consumer and drop the worktree-line indent. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-pin tasks style parity hash for the title-row gap Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
8b410b4893 |
feat: add first-class Qoder CLI support (#23581)
feat: add first-class Qoder CLI support Integrate Qoder launch, identity, canonical hook status, trust and resume. Verify with captured Qoder 1.1.64 transcripts and hidden Electron sidebar checks. Builds on and cross-reviews #7502, #8611, #9655, #12910, #13311 and #15291. Co-authored-by: dalveytech-vincent <vincent@dalveytech.com> Co-authored-by: Eridanus117 <45489268+Eridanus117@users.noreply.github.com> Co-authored-by: xingqingzzp-gif <xingqingzzp-gif@users.noreply.github.com> Co-authored-by: jyang2004 <jyang2004@users.noreply.github.com> Co-authored-by: yunqian <yunqian@alibaba-inc.com> Co-authored-by: huzhening.hzn <huzhening.hzn@alibaba-inc.com> |
||
|
|
400e4e7957 |
feat(agents): add Freebuff launch and sidebar status support (#23567)
Add Freebuff launch support and execution-host status reporting for the sidebar, including running, question, blocked, and settled states. Validate against captured CLI transcripts and real rendered sidebar evidence. Cross-referenced community implementations #17065, #20839, and the Freebuff portion of #18790. Preserve their agent/catalog/mobile/documentation coverage and add canonical status publication and regression tests. Co-authored-by: Harkaran Brar <18134082+harkaranbrar7@users.noreply.github.com> Co-authored-by: Prarambha369 <98906077+Prarambha369@users.noreply.github.com> Co-authored-by: Lesley Murfin <260182349+LesleyMurfin@users.noreply.github.com> |
||
|
|
45f3512a33 |
feat(agents): add first-class DeepSeek Harness (dsh) support (#22468)
* feat(agents): add first-class DeepSeek Harness (dsh) support Register DSH as a supervised Orca agent: catalog entry and detection for its dsh-tui profile, status/question hooks through DeepSeek's own Claude-Code hook bridge, composer-ready prompt delivery, session resume, headless Source Control AI, and title identity that no longer collides with Gemini's. * fix(dsh): reach Orca through DSH's credential scrub and stop reading its title as Gemini DSH runs command hooks through its own shell executor, which drops every env var whose name contains KEY, TOKEN, SECRET or PASSWORD — taking ORCA_PANE_KEY and ORCA_AGENT_LAUNCH_TOKEN with it, so every hook exited without posting. Mirror both onto scrub-safe aliases at spawn and restore them at the top of the DSH hook script. Its title collided too: DSH rests on the same glyph Gemini works on, so a resting DSH pane was relabelled Gemini CLI and reported working forever. Defer both the Gemini classifier and the title status detector on DSH's whale, in the base module both copies of that classifier read. * test(mobile): repin the session-route closure for the DSH agent icon * fix(dsh): address review — never splice user rows, cover remote panes, keep the diff off argv - findManagedDshPatchRegion paired an orphan start marker with a later block's end, so a truncated write made install/remove delete the user's own rows. Pair each end with the nearest preceding start; regression test fails without the fix. - The relay PTY env builder never applied the scrub-safe aliases, so remote DSH status silently never appeared even with the remote hook installed. - Source Control AI sent the whole diff on argv; send it over stdin with DSH's '-' marker. - dsh-tui/dst already chose the interactive profile, so a workspace folder named 'web' or 'plugin' no longer marks a live agent pane non-interactive. - Isolate USERPROFILE as well as HOME so a Windows run cannot edit the real home. - Drop the duplicate README badge and revert an incidental doc reformat. * refactor(dsh): share the managed-hooks reader and tighten the new modules Reuse before reimplementing: readManagedDshHookEvents was a near-verbatim copy of Muse's, with byte-identical private helpers. Both now call one readManagedHookEventsFromJson. Also: one readTextOrAbsent instead of two spellings of the same read (dropping an existsSync TOCTOU), one status() builder instead of four inline literals, rmSync(force) instead of exists-then-unlink, and a redundant empty-string guard before JSON.parse. The patch-file transforms lose their index juggling for a predicate plus a filter. * fix(dsh): refuse a flow-style patch file, keep its mode, and stop the relay inheriting a pane - applyManagedDshPatch matched only an exact `[]`, so `[] # keep empty` or a non-empty flow sequence got a block entry appended after it — invalid YAML that would leave DSH unable to load the user's own patch layer either. It now strips the token from an empty sequence (keeping a trailing comment) and returns null for a non-empty one; install reports that and changes nothing. - The patch rewrite dropped an owner-only file to the umask default (CWE-732); pass preserveMode. - The relay PTY env never dropped inherited pane identity the way the local and daemon builders do, so a spawn that specified none could inherit the relay's own and every agent's hook would report against that pane. * fix(dsh): keep the flow-style refusal in every status read, and scope the mode test to POSIX A refused patch file carries no managed region, so getStatus() fell through to a bare not_installed with detail null — the actionable 'rewrite it as a block sequence' message only ever reached the one-shot install() return. Export the predicate and check it first, behind one shared message constant. The owner-only mode assertion cannot hold on Windows, where chmod only toggles the read-only attribute and mode & 0o777 reads 0o666 for any writable file. * docs(readme): restore the DeepSeek Harness badge lost in the rebase * test(mobile): repin the session-route closure to the measured 4221 Measured, not derived: 4220 without the DSH icon entry, 4221 with it. Two of the three modules above main's 4218 pin are not this change's — they arrived with the mobile work after #22570 and were never repinned; the changelog records that split explicitly. * fix(dsh): settle tui-idle on the agent's own hook, so supervised workers see it ready Reported by a tester on the adhoc build: `terminal wait --for tui-idle` ran to its 90s timeout against an already-ready DSH composer, so a supervised worker never sees the agent as ready. Every existing tier reads the title, and DSH deliberately carries no title status: its rest prefix is Gemini's working glyph, so the detector reports none. A fresh first-party `done` is better evidence than any title anyway — it is the agent's own account of its own turn, and normalizeDshEvent drops subagent events, so it is the lead's. Scoped to DSH: for agents whose hooks report child turns, a mid-turn `done` is the #6011 class this file prevents. * test(daemon): record the DSH transcript's true-colour I2 divergences Adding the dsh-tui capture to __fixtures__ enrolled it in the serialize replay sweep, where it reports 10 I2 divergences and failed the unlisted-transcript default of 0. Every one is the same shape — visible-grid row=0, a 24-bit background the round trip does not restore to default — which is DSH's whale intro painting whole rows of true colour. Verified as an upstream limitation rather than a regression by replaying against the previous build (build-serialize-addon-at-ref.mjs --ref origin/main): I1 and I3 both hold. * fix(dsh): return the new tui-idle verdict from the first-party done lane Main refactored isTuiIdleSatisfied into evaluateTuiIdle, which returns a verdict rather than a boolean. The DSH lane still returned `true`; it is tier-1 positive evidence, so it returns READY_STRONG like the title/body lane above it. Re-verified the regression test still fails without the lane. * test(relay): pin the scrub-safe pane-identity aliases on the relay spawn path The relay builds a remote pane's env itself, so the alias mirroring there had no test: removing the call left every suite green while remote DSH status silently vanished. Both cases fail without it. * docs(dsh): point the hook service at the integration reference The reference doc had no inbound link from anywhere in the repo. |
||
|
|
d06b43e634 |
fix(tasks): read a malformed saved Linear team selection as sticky-all instead of crashing the page (#22279)
* fix(tasks): read a malformed saved Linear team selection as sticky-all instead of crashing the page A persisted defaultLinearTeamSelection that is not a string array (a string reached 1.4.207, report 0a2b6e7f) threw '(t ?? []).filter is not a function' inside a commit-phase effect and tripped the page.tasks error boundary. The value is now normalized where the page reads it and where a host projects it to paired clients; anything but a string array means sticky-all. * fix(mobile): read a malformed saved Linear team selection as sticky-all A host that predates the desktop fix projects its raw store value, so the mobile Linear list must tolerate the same string shape. Also trims the desktop helper's comments to the why. * fix(tasks): validate projected Linear team IDs and refresh parity contract --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
90801e2deb |
feat(agents): add first-class ZCode harness (#22464)
* feat(agents): add first-class ZCode harness Add ZCode (Z.ai's `zcode` CLI) as a supervised Orca agent: managed lifecycle hooks on local, SSH and Windows hosts; status, question and approval reporting; synthetic status titles; session resume; orchestration worker launch options; and desktop + mobile agent-picker registration. Written against the newly open-sourced `zai-org/ZCode` (agent CLI 0.16.9), not against a remembered screen: - ZCode's hook runner writes a Claude-compatible stdin alias set, so it routes through the existing Claude-compatible vendor path while keeping its own identity in the sidebar. - `PermissionRequest` fires only once the approval card is on screen and racing the user's answer, so it is proof the pane is blocked, not an auto-approval. - ZCode's clarification tool is literally `AskUserQuestion` with Claude's questions/options shape, so Orca's question card renders it unchanged. - ZCode's `hooks.enabled` defaults to false, which is why configured hooks were reported as never firing; the installer sets it. - ZCode renames its own process to `zcode-cli`, so the expected foreground process cannot be the launch command or dispatch refuses the pane. - ZCode emits no OSC title in any state and repaints its ASCII banner forever, so readiness comes from Orca's synthetic hook title and launch drafts wait on the composer box rather than on a quiet render window. Three files crossed their max-lines limit, so each is split along a real seam: command-line entrypoint parsing out of agent process recognition, skill classification out of skill root discovery, and registry coverage out of the remote hook installer tests. Refs #10564 * fix(zcode): drop the session-option catalog and pin the orchestration contract ZCode's CLI exposes no `--model` flag at all, and the session-option launch path refuses to apply any option until a model id is chosen. A catalog therefore could not deliver `--mode` per worker, and would have accepted `--model` only to drop it silently. Take opencode's position instead: no catalog, so `worker-start --model` is refused with a clear message and ZCode launches with the model from its own config. `--mode` stays reachable through agent args, which is also how the yolo default is applied. Add a contract test covering the parts that make ZCode a usable worker: dispatchable foreground process, stdin prompt delivery, the prompt staying out of the launch command, and the composer-gated draft paste. * refactor(zcode): reuse shared helpers and cut the harness down No behaviour change; every ZCode test still passes. - Use installer-utils' own `hookDefinitionHasManagedCommand` instead of re-walking a hook definition by hand, which also drops a local string reader. - Share one `readZCodeEventMap` instead of keeping the same narrowing in both hook-settings and hook-config-json. - Collapse five identical error returns into one `zcodeHookError` builder, and return early from the status branches instead of assigning through `let`. - Split the event-to-status decision out of `normalizeZCodeEvent` into a pure `readZCodeTurn`, so the normalizer reads as decide-then-build and stops computing the tool name for events that never look at it. - Take a script file name in `readManagedZCodeHookEvents` like its siblings, which removes a `Parameters<typeof …>` indirection at the call site. - Drop the unused `ZCodeHookEvent` export and inline a single-use path helper. - Correct a stale comment: ZCode's loader is a strict `JSON.parse`, so the in-place edit preserves key order and indentation, not comments. * fix(zcode): address review — keep unmanaged event keys, correct comment, de-dupe README - `removeZCodeManagedHooks` deleted any event key whose list ended up empty, so an unrelated `"Notification": []` the user wrote was removed as collateral whenever a managed hook elsewhere made the write happen. Only touch an event Orca actually owned something in; covered by a new regression test. - The `isNewTurnEvent` comment claimed UserPromptSubmit was ZCode's only turn boundary while the expression below it also returned true for SessionStart. Say what the code does: SessionStart lands the idle boundary, UserPromptSubmit is the turn boundary (the Codex/Claude shape). - ZCode appeared twice in the README's single agent-badge block; keep the local-icon entry the link checker validates and drop the favicon duplicate. * docs(zcode): call out that the desktop bundle's CLI cannot open a session From live testing on #22464: pointing `zcode` at the desktop app's bundled `glm/zcode.cjs` installs Orca's hooks fine but then fails with `Cannot find package '@zcode/tui'`, so the pane never opens a session. The symptom reads as a broken harness when the CLI simply has no TUI. Say which build to use and how to check before reporting a problem. Reported-by: JWu527 |
||
|
|
b419b3183e | test: remove redundant mobile and GitLab checks (#22748) | ||
|
|
7a4f080086 |
revert: #18790 (orchestration incarnation reap fallback and bundled Freebuff agent) (#22601)
This reverts commit
|
||
|
|
069dc8a1d8 |
feat(agent-launch): let a caller reserve the chat session, and start terminal launches with the session picks (#22523)
* feat(agent-launch): let a caller reserve the chat session and carry session picks to a terminal launch * fix(agent-launch): keep a caller-minted session id named for its agent, and mint the fallback the same way * test(mobile): model the older host from the launch fields, not the refined schema * docs(agent-launch): describe the reserved session id as conversation identity, not placement The caller mints the session id so it knows which conversation it started; tab placement is not keyed on it. Also puts the terminal surface's doc comment back on createTerminalSurface. * docs(agent-launch): say a terminal launch reads the session picks on the wire contract The `sessionOptions` field doc still said a terminal launch ignores them, which this branch changed. * fix(agent-launch): check a reserved session id's token after the agent name, not the whole id A hyphenated agent name failed the one-token check, so any session id for such an agent was refused at the wire, while every other agent without a chat has its id ignored on the terminal. |
||
|
|
ebed0964a2 |
feat(agents): add first-class Muse Code harness (#22216)
* feat(agents): add first-class Muse Code harness Add Muse as a supervised Orca agent across desktop, mobile, session history, source control, local hooks, SSH, WSL, and native Windows. Preserve user settings, support Muse 1.3 hook environment allowlists, and recognize versioned foreground processes. Include question, waiting, completion, resume, and readiness coverage. Co-authored-by: homesh-dev <300847526+homesh-dev@users.noreply.github.com> Co-authored-by: jeffhuen <32542276+jeffhuen@users.noreply.github.com> Co-authored-by: John Cusack <johncusackccm@gmail.com> Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com> * test(agents): cover Muse remote hook registration * test(agents): cover Muse hook and source-control contracts * test(agents): exclude Muse hook metadata from script mode check * test(agents): keep Muse skill picker coverage stable * test(ai-vault): include Muse in every-agent fixture * test(mobile): repin Muse agent icon closure * fix(muse): detect questions and approvals from structured Muse signals Muse 1.3 fires no hook for request_user_input, so a pending question left the pane "working". Its internal reminder subagents also post hooks with their own session ids (even after Stop), which surfaced "tool failed" rows and flipped finished panes back to working. - Read pending questions from Muse's session log (user_input_prompt_requested/settled) via the existing transcript poll, now generalized from Codex subagents to Muse on main and relay. - Drop child-session hooks (SubagentStart ids, or turn_id === session_id). - Treat Notification permission_prompt as the approval wait; PermissionRequest also fires for auto-approved calls, so it only caches the approval card. - Ignore Notification copy as the prompt; poll replays are not new prompts or turn boundaries. - Allowlist USERPROFILE so Windows cmd AutoRun doesn't fail every hook. * perf(muse): parse only question events from the session log Most Muse session-log lines are large model/tool records. Filter raw lines by the user_input_prompt_ marker before JSON.parse via an optional readJsonlCursor line filter. * fix(muse): unwrap batched log records and scope questions to the live turn Review follow-ups: question events inside retained_frame batches were skipped, and a question left open by a crash or interrupt stayed pending for the pane's life. Share the history scanner's retained_frame unwrapper, and only report a pending question whose run_id matches the hook turn_id. * refactor(muse): drop type assertion in retained_frame unwrap * fix(agent-hooks): satisfy exhaustive-switch lint in transcript poll policy --------- Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com> |
||
|
|
0677271709 |
fix(orchestration): reap leaked worker terminals via process-incarnation fallback — stops an unbounded PTY/process leak on Remote Server (OOM / cgroup PID exhaustion) (#18790)
* fix(orchestration): remint live handle from process incarnation on worker release When a durable terminal handle goes stale (rendererGraphEpoch fence), inspectWorkerTerminal re-mints a live handle via resolveTerminalHandleByProcessIncarnation + matchesProcessIncarnation so release/stop/read act on the still-running PTY instead of reporting missing and leaking the agent process tree. - keep main shared host-scope re-exports; add matchesProcessIncarnation - wire observation.terminalHandle through control/stop/release - rebuild release-completion on main structured paths - on missing/unattached + provably exited: settleDead fence first, then same-incarnation settleWorker fall back (archive may block settleDead mid-request); settle before recovery defer * fix(orchestration): derive SSH host scope from the reminted handle; reuse fresh-request recovery guidance for structured workers Addresses two open CodeRabbit review comments on PR #18790. inspectWorkerTerminal read the dispatch authority with the stale durable terminalHandle, so after a remint the lookup resolved nowhere and currentHostScope was always undefined — an SSH worker with no liveness verdict and no persisted host_scope got classified from terminal.connected instead of unverifiable. It now reads the same effectiveHandle every other observation in the function uses. stopStructuredWorkerForRelease told the caller to repeat the release with the same --retry-request, which only replays the stale release_unknown receipt and made a structured-worker close failure permanently unretryable. It now sources releaseUnknownRecovery from worker-release-completion so the fresh-request-ID guidance lives in one place. Pre-commit lint-staged (oxlint + oxfmt) run manually: clean. * test(orchestration): exercise incarnation recovery through runtime paths * test(orchestration): pin the incarnation read scenario to the reminted terminal The read scenario only asserted that the call resolved, so it documented nothing about which handle the read reached. Assert that the handle readTerminal received resolves to the registered pane and incarnation, so the scenario proves the read went through the reminted terminal instead of passing on the incarnation fence's throw. * refactor(orchestration): drop redundant incarnation prefix check; require liveTerminalHandle * feat: add freebuff as a first-class TUI agent (#42) <!-- orca-pr-loc --> <!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. --> | | Files | Added | Deleted | Net | | :--- | ---: | ---: | ---: | ---: | | Test | 0 | 0 | 0 | 0 | | Prod | 28 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$37 | 0 | $\color{#1a7f37}{\Huge{\mathbf{+}}}$37 | <!-- /orca-pr-loc --> ## ELI5 Add Freebuff (`freebuff`) as a recognized first-class TUI coding agent in Orca alongside Codebuff and other supported agents. ## What Changed - Registered `freebuff` across shared TUI agent definitions, configuration catalogs, display names, and telemetry schemas. - Added agent icons, favicons, status mappings, and mobile asset references for Freebuff. - Added localization strings across supported language packs (`en`, `es`, `fr`, `ja`, `ko`, `zh`) and updated locale translation policy. - Documented Freebuff CLI in README agent table (`npm i -g freebuff`). ## Why Freebuff is a CLI coding agent twin of Codebuff (`npm i -g freebuff`). Adding it to the catalog enables users to launch worktrees, run automated sessions, and pick Freebuff directly within Orca. ## Linked Issue N/A ## Visual Proof `N/A` - Catalog registration and metadata definition for CLI agent launch; UI rendering uses existing TUI agent picker and status components. ## Testing - Verified TypeScript contracts, schemas, and catalog configurations. - Tested CLI detection / agent picker integration locally on Linux (`worktree create --agent freebuff`). ## AI Disclosure Assisted by AI coding tooling. ## Checklist - [x] This PR is small and focused - [x] I explained what changed and why (including ELI5) - [x] Before/after screenshots or videos attached for UI changes, or `N/A` with reason - [x] Self-reviewed for correctness, security, and performance - [x] Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A) --------- Co-authored-by: Lesley Murfin <lesley@revivebusiness.ca> * test(orchestration): erase method overloads in worker reap fixtures * test: document worker fixture type boundaries * test: simplify worker fixture typing --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: svc-orca[bot] <313947298+svc-orca[bot]@users.noreply.github.com> Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
335d111265 |
fix(mobile): name the Back control in every page-served screen (OTA phase C, C2.8) (#21715)
* fix(mobile): name the Back control in every page-served screen The C2.7 device proof could only reach the tasks Back by tapping the native control's coordinates: inside the shell there is no native chrome behind it, so a bare Pressable is absent from the accessibility tree and a screen reader has nothing to announce. The agent-history panel's Back carries a label but no role; the tasks one carries neither. Both gain `accessibilityRole="button"`, and the tasks one the app's own wording, `accessibilityLabel="Back"` — the spelling its thirteen siblings use, including the agent-history panel this screen sits beside. The host screen's Back already carried both and did not move. The new census is a rule rather than a count: it finds every Back affordance under the screen tree each page route serves — a Pressable rendering a ChevronLeft that either carries the back-button style or calls back — and requires a button role and a Back-shaped label on each. Its coverage assertion reads MOBILE_WEB_PAGE_ROUTES, so a route registered without a screen entry fails here, the way the flag census fails for a switched route. Red first on both offenders by name. Two tasks parity hashes move, and only the two a JSX prop must move: `semantics` 3,272 -> 3,274 for the widened host signature and the two new runtime strings, and the render-token stream 35,195 -> 35,203 for the eight tokens the two attributes are. Hooks, statements, declarations and styles hold, which is the evidence that nothing executable changed. Recorded in the test's comment. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): judge a Back control by what it does, and name the files pair Round 1 found the census discriminating on the ChevronLeft glyph, which separates nothing: dismisses sit in the same header slot with the same back-button style, so the rule claimed a dismiss and told it to be called Back, while the explorer's Close escaped only by drawing an X. The predicate is now the behaviour. A Back control is a Pressable whose press handler reaches a back call — a `back()`, `goBack()` or `onBack()` call, a handler named for one, or a local handler whose declaration makes one — or whose label already says Back. A control matching neither is outside the rule whatever it renders. The reviewer's mutation, a dismiss with the back-button style wrapping a ChevronLeft behind `onDismiss`, is claimed by the old predicate and passed over by this one, which is the whole of the difference. A spread now reads as unknown rather than absent, so a Pressable that hides these props behind one is a Back control that fails both rules instead of a control the scan never sees. No Pressable in the five trees spreads today, so the branch is inert and load-bearing. C3.1 registered the files explorer and preview, so the coverage assertion went red on its merge, as designed. Both screens join PAGE_SERVED_SCREENS and both Back controls gain `accessibilityRole="button"`; they already carried `Back to session` and `Back to files`. The tasks Back gains the `hitSlop={8}` its four siblings carry, so its touch target is no longer the glyph alone. Nine mutations: the dismiss stays out; a dropped role on each of the five controls and a dropped label on the tasks one go red by name; a spread fails both rules; a dropped table entry fails coverage; and dropping the host screen's label — its only detector, since `actions.leaveHost` is opaque — empties that tree and fails the presence assertion rather than passing in silence. Two parity hashes move for the one added prop and no count does: `semantics` holds at 3,274 lines with one changed, the host signature gaining `hitSlop`, and the render-token stream gains that attribute's four tokens, 35,203 -> 35,207. Recorded in the test's comment. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): assert a Back per page route, not per screen directory Round 2 found the presence assertion reading directories while two routes share `src/files`, so the explorer's Back answered for the preview's as well: rewriting the preview's control to `onPress= {handleClose}` with `accessibilityLabel="Close"` left that route with no accessible Back and the suite green. Reproduced exactly, then made red by this change, which names the route and the module it failed in. Each PAGE_SERVED_SCREENS entry now names the module that renders its route's Back, and presence is asserted per entry. The rules still read whole trees, so a Back added beside a screen is ruled as well as the screen's own; the tree is derived from the module's directory rather than declared twice. The failure prints the offending list instead of comparing two values a reader cannot tell apart. The doc comment claimed the preview was carried by the label half of the predicate. It is not: `requestBack` is named for what it does, so the preview is found behaviourally like the explorer, the tasks screen and the agent-history panel. Dropping the label from each of those three leaves it detected and failing the naming rule, which is the evidence. Only the host screen depends on the label half, because `actions.leaveHost` is a member access this rule does not follow. The admitted residual gap stays admitted and the predicate is not widened for it: a second Back control in a screen that already has one, with an opaque handler and a label that does not say Back, is still outside what this rule sees. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
b6e8b1a7b2 |
feat(mobile): serve the tasks screen from the page, with its seams (OTA phase C, C2.1 + C2.5) (#21694)
* fix(mobile): encode the host id in the tasks workspace-creation href (OTA phase C, C2.1)
`use-mobile-tasks-workspace-create-actions.tsx` built
`/h/${hostId}/session/...` with the host id interpolated raw — the C1.2 class.
A host id carrying `/`, `#`, `?` or whitespace reaches the wire as an href
`BRIDGE_ROUTE_HREF_PATTERN` refuses, the handoff falls through to the local
router, and expo-router's Unmatched paints over the page.
Deleted rather than patched: `hostNewWorktreeSessionRoute` already builds
this exact href with both segments encoded, and already has the test that
pins it. The screen now calls it.
The census that caught it stays: no module under `src/tasks` may interpolate
into `/h/${...}` without encoding, which is the rule rather than this one
line. Three refactor-parity hashes move with the statement change and are
recorded in that file the way every earlier movement is.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): route the tasks tree's external links through the seam (OTA phase C, C2.1)
Ten of the twelve call sites in the tasks page closure: the nine under
`src/tasks`, swapped by one export in the dependency barrel, and
`MobileMarkdown.tsx`, which imports react-native directly and is edited in
place.
Inside the shell's WebView react-native-web's `openURL` calls
`window.open(url, '_blank')`, which both shells refuse — iOS returns nil from
`createWebViewWith`, Android false from `onCreateWindow` — and resolves
regardless. Every one of these sites would have reported success into a tap
that opened nothing.
The barrel's `Linking` is typed `{ openURL: (url: string) => void }`, so a
`.catch` on it is a compile error rather than a handler for a rejection that
cannot arrive; the seam names its own failures. `MobileMarkdown`'s own
`.catch(() => {})` goes with the swap for the same reason.
No parity hash moved: the barrel and `MobileMarkdown` are outside the
refactor-parity family's source set.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): route the shared screens' external links through the seam, with a census (OTA phase C, C2.1)
The last two of the twelve call sites in the tasks page closure:
`ProtocolBlockScreen.tsx` and the `openExternalUrl` prop wiring at
`host-screen-overlays.tsx`.
Both are shared with native routes and with the already-live `/h/[hostId]`
page, so this changes that page too: its external links go from the measured
`window.open` no-op — which both shells refuse and which resolves anyway — to
a URL handed to the shell. Nothing changes on a phone, where the seam is
`Linking.openURL` unchanged.
The `openExternalUrl` prop chain is retyped `(url: string) => void` with it,
and `SmartWorkspaceSourceField`'s `.catch(() => {})` goes: the seam names its
own failures and never rejects, so that was a handler for a rejection that
cannot arrive.
The census is the rule rather than today's twelve sites: no module in the
tasks page closure may reach react-native's `Linking`, by name or through a
namespace import. It reads the closure from a new builder export —
`metafile.inputs` for `_layout` plus the route, which is one definition of
what a page contains — and checks which module the name comes from, not which
text a call site writes, since the tasks tree still calls `Linking.openURL`
and that `Linking` is now the barrel's seam-backed export. Confirmed to
discriminate: restoring one react-native import turns it red.
A second case pins that the seam is in the closure, so an empty offender list
cannot also mean a page that reaches no link code at all.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): write the tasks clipboard through the shell's verb (OTA phase C, C2.1)
The two `Clipboard.setStringAsync` sites in the tasks page closure move onto
a seam, `src/platform/clipboard.ts` with a `.web.ts` sibling, registered in
the overrides.
A hook rather than a function because the web form needs the page's bridge
client, which is React context. Native is `expo-clipboard` unchanged. Web
calls `native.clipboard.write` through `useNativeVerbs`, because
`expo-clipboard` on the web is `navigator.clipboard` and needs a secure
context: the iOS shell serves the page from a custom scheme and Android from
`https`, so that path would work on one platform and silently not on the
other, with nothing at the call site able to tell.
Both seams reject rather than return false, and both call sites already wrap
the write in a `catch` that puts the message on screen — so a write that did
not land says so instead of showing "Copied". A route that has not declared
`native.clipboard.write` is refused before a frame is sent and lands in that
same `catch`; the route declares it in the entry commit.
Two parity hashes move, the hook list and the statement hash, each by one
entry, and are recorded in that file. `semantics` holds, as do render and
style: no RPC call, method literal or JSX host signature changed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): hand the tasks Back button to the shell (OTA phase C, C2.1)
The tasks header's `router.back()` reached expo-router through the dependency
barrel, and inside the page that moves nothing: the document holds the single
history entry the entry wrote with `replaceState`. The stack with somewhere
to go is the native one the shell pushed the page onto.
One line in the barrel, as with `Linking`: `useRouteHandoff` is router-shaped,
so every call site is unchanged. On a phone it is expo-router. Inside the page
it keeps a route the page renders and posts `navigate-back` for a Back the
document cannot serve — the C2.2 seam, which until now had no consumer.
No parity hash moved: the barrel is outside the refactor-parity source set,
and no call site changed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): render mermaid as its own source box on the web (OTA phase C, C2.5)
`MermaidDiagram` is in the tasks page closure, reached through
`MobileMarkdown`, and it renders the diagram inside a sandboxed `WebView`.
`react-native-webview` is a native component with no browser counterpart:
importing it runs a codegen lookup that throws, and the route manifest imports
every route, so one such import takes the whole page down rather than one
diagram.
The web sibling renders the labelled source box the native component already
falls back to on a parse or render error, with that component's own styles, so
the degradation looks like a state the product already has rather than a
second design.
Not a browser renderer, and the reason is not reach: mermaid is a browser
library and the engine bundle is vendored. It is that the native path's safety
comes from the WebView it runs in — `buildHtml` escapes `</script>` and the
U+2028/U+2029 separators because diagram source is untrusted agent and PR
content — and a DOM path has no such sandbox, so it needs its own escaping and
its own proof. That is a change of its own, not a smaller version of this one.
Registered in the overrides, whose gate fails on an unlisted `.web.*` file.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): turn the tasks route on for the page (OTA phase C, C2.1)
The entry: `/h/[hostId]/tasks` joins `MOBILE_WEB_PAGE_ROUTES`, the route file
becomes the shell's flag switch in `index.tsx`'s shape, and a `.web.tsx`
sibling renders the screen directly, registered in the overrides.
The screen moves to `src/tasks/MobileTasksScreen.tsx` first, verbatim — body
byte-identical, imports rewritten to `./`. It has to: under the builder's
`resolveExtensions` a web sibling importing `./tasks` resolves back to
itself, which is why every other shell route's screen already lives in `src`.
The parity family follows the file rather than the path. `TASKS_ROUTE` leaves
`MOBILE_TASKS_SOURCE_FILES` — `SOURCE_PATTERN` already matches
`MobileTasks*.tsx`, so listing it too would double-count — and the execution
reader points at the new file. Measured rather than predicted: all six
refactor-parity cases pass unchanged. No hash moved, including the family
text and declaration list, because the new name sorts where the route path
sat.
The route declares `navigate`, `storage`, `externalLink` and
`native.clipboard.write`, which the grammar fold made expressible and
per-route scoping makes meaningful: it is granted those and not the rest of
what this shell implements.
The browser check covers what only a browser answers — every module in the
closure evaluating under React Native Web, `taskSource` surviving the
handshake into the page's own URL, and the route's chunk arriving on a
client-side navigation. It states plainly what it does not cover: the three
seams are reached from controls that need provider data the double does not
serve, so a case posting those frames directly would prove the transport and
read as a tap it never performed. Both new checks join the `mobile_web_app`
job.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(config): resolve a route closure the way the bundle ships it (OTA phase C, C2.1)
`mobileWebAppRouteClosure` took the route's explicit `.tsx` path as an entry
point, so esbuild used that file directly and `resolveExtensions` never ran.
For a route with a `.web.tsx` sibling that measured the native switch, which
no browser loads: the tasks closure came back carrying
`MobileWebShellScreen`, and with it a `Linking` import the census then
reported as an offender.
Extensionless now, so the closure is the one the page actually contains:
3775 modules, 428 local, with `external-link.web.ts` and `clipboard.web.ts`
in it and the shell screen out.
The route-manifest pins move with the tasks route joining
`MOBILE_WEB_PAGE_ROUTES`, in both the declaration check and the built
manifest.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): cover the clipboard seam, close two page escapes, share the mermaid props (OTA phase C, C2.1)
Four from round 1.
The clipboard seam shipped untested. Both halves have one now: the native
form rejects when `setStringAsync` answers false and resolves when it does
not, and the web form is driven through the real port pair — resolving on a
reply, rejecting when the shell says the pasteboard refused, and rejecting on
an ungranted route without putting a frame on the wire.
The tasks barrel still re-exported `expo-clipboard` with no consumer, which
kept `ExpoClipboard.web.js` — the `navigator.clipboard` path this series
exists to avoid — inside the page closure. Deleted, and asserted as the
module's absence from that closure rather than as a count of importers: a new
import puts the file back whoever writes it.
`ProtocolBlockScreen` reached expo-router's singleton for its way out to the
host list. A singleton is the one shape the handoff cannot intercept — it is
not a hook, so the page's bridge client is never consulted — and `/` is a
route the page does not carry, so inside the shell that replace rendered the
root route in the WebView instead of leaving it. Pre-existing and live via
`/h/[hostId]`; routed through the handoff now. Two suites' `expo-router`
mocks gain the hook the handoff reads.
`MermaidDiagram.web.tsx` redeclared its props; it imports the native
component's type, so drift fails tsc.
No parity hash moved: none of these files is in the refactor-parity source
set.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* style(config): use endsWith for the clipboard module check
The changed-code gate refuses a dollar-anchored regex where `String#endsWith`
says the same thing. No behaviour change.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): close the href census gap, read route params through firstParam (OTA phase C, C2.1)
Five from round 2, two of them real.
The raw-interpolation census inspected only the leading `${...}`, so
`` `/h/${encodeURIComponent(hostId)}/session/${worktreeId}` `` passed it — and
a worktree id carrying `/`, `#`, `?` or whitespace breaks the href exactly as
a host id does. It now refuses any hand-built `/h/...` template with any
interpolation left raw, whichever segment it is. Proved against exactly that
shape in a throwaway before the change, which the old rule admitted.
The tasks switch read `hostId` and `taskSource` as plain strings. expo-router
hands back an array for a repeated query key, so a duplicate `?hostId=` built
`/h/host-a%2Chost-b/tasks`; both go through `firstParam` now, as the
agent-history switch does. `index.tsx` is untouched, per the Phase D list.
Three in the render check's prose: the header claimed the browser proves the
three seams fire from a tap, which the file's own closing note denies; a
module count repeated a number the closure test already pins; and a `replies`
parameter was threaded through without ever being supplied.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
|
||
|
|
40b2230508 |
test(mobile): typecheck the test files on a ratchet, and pin the reply enums where tsc looks (#21298)
* fix(mobile): move the last six reply-enum pins where tsc looks mobile/tsconfig.json excludes *.test.ts, so a `Record<HostUnion, true>` coverage record in a schema test is never typechecked: the two that existed (SshConnectionStatus, GitHubProjectOwnerType) checked nothing, and the four closed enums beside them had only a doc citation of the host type. Each arm list moves into its schema module as hostUnionArms<Union>(), which #21269 introduced for the same reason, and each test iterates the exported list instead of holding its own copy: - SSH_CONNECTION_STATUS to SshConnectionStatus - PROJECT_OWNER_TYPE to GitHubProjectOwnerType - DETAIL_FILE_STATUS to GitHubPRFile['status'] - PUSH_TEST_REFUSAL_REASONS and PUSH_REGISTER_REFUSAL_REASONS to the refusal arms of MobilePushTestResult and MobilePushRegisterResult - SETUP_RUN_POLICIES to SetupRunPolicy openEnum's parameter widens from a non-empty tuple to `readonly string[]` so a hostUnionArms list can feed it. z.enum already accepts the same, so the tuple constraint only excluded callers zod itself takes; behaviour unchanged. Twelve mutations prove the pins: dropping one arm and adding a bogus one each fail mobile tsc in all six places. Zero goldens move, the schemas' behaviour being unchanged, and the 21 recording suites pass at the existing baseline. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): fix the type errors in eighteen test files Found by typechecking the tests for the first time (see the config that follows). All mechanical, none weakens a product type: - 67 `act(() => vi.advanceTimersByTime(...))` callbacks return VitestUtils where act wants void, so each becomes a block. The async ones await only a genuinely promise-returning call, so no extra microtask tick is introduced. - Four fixtures were stale against a product type that gained a required member: MobileViewState.alwaysShowDefaultBranch, PrSidebarData.checksError, the branch-compare summary's errorMessage, and SessionOptionDescriptor's transport, which #20884 added precisely so a producer could not inherit the wrong lane's rendering by omission. - `getLastConnectedAt` on the shared relay fake was typed `() => null`, which refused the timestamp two escalation suites assign to it. - Two holders used before assignment take `!`, one `advance!.kind === ...` becomes `advance?.kind`, one widened status arm takes `as const`, and the Expo notification fixture keeps `data` required because the dismissal cases assign through it. 631 test files pass, 6222 tests, unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): typecheck the test files, on a ratchet mobile/tsconfig.json excludes *.test.ts so Metro never compiles tests into the release bundle, and vitest transpiles without checking types. Nothing had ever typechecked a mobile test, which is why a `Record<HostUnion, true>` pin written in one proved nothing and why 144 of the 630 test files had drifted. tsconfig.test.json is that program with the tests put back, behind `typecheck:tests`. Four files stay out: they import the desktop main process or src/shared/child-process, which are written against @types/node, and this program's libs are React Native's, where setTimeout answers a number rather than a NodeJS.Timeout. Pulling that graph in reports ~280 errors about the desktop rather than about mobile; vitest runs those four under Node, which is where they belong. The CI gate is a ratchet rather than the raw typecheck, modelled on check-ts-nocheck-ratchet.mjs: 126 files still fail, so the gate freezes that set and fails when a file that checks today stops checking, or when a baseline entry starts checking and was not pruned. The list may only shrink. Why not zero: 180 of the remaining 510 errors are one seam — tests locate mocked react-native components by string name, which `ElementType` does not admit — and closing it means either 180 casts or a global JSX declaration for the mocked names. That is a design decision, not a mechanical fix, so it is left for a follow-up rather than made here. The rest are smaller clusters of the same kind: vi.fn mocks assigned into typed slots, call-arg tuple indexing, and createElement props fixtures. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile-recorder): correct the corpus counts and the salvage claim The oracle section still quoted the corpus as 368 scenarios and 727 goldens; it is 393 and 778, and the three replay suites report 781 tests. Each number now names the command that measures it. "No golden carries one" was the load-bearing error: 44 goldens carry a recorded `reply-salvage` today, starting with the push-test unknown-reason scenario #21176 added for exactly that purpose. The paragraph claimed the observation pins an absence when on those families it pins a recorded drop. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the tests-typecheck ratchet's parser The gate reads tsc's output, and tsc indents the "Overload 1 of 2, ..." detail under an error. Counting those as filenames would write unparseable entries into the baseline and leave the gate unprunable, so the parser is pinned on that shape as well as on the added/stale diff. Written against the gate itself: it flagged this file before the directive it carried was removed, which is the end-to-end proof the spawn half works. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): await the timer advances the act() rewrite dropped Rewriting `await act(async () => vi.advanceTimersByTimeAsync(n))` into a braced body left the returned promise floating at 27 sites, so the advance was no longer ordered before the assertions that follow it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): unshadow MobileHostCard's .tsx suite A wildcard `include` keeps only the higher-priority extension, so MobileHostCard.test.tsx sat outside every tsc program while MobileHostCard.test.ts existed beside it. Its one error is the same react-test-renderer seam its sibling is baselined for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): census every test file into the typecheck program The ratchet diffs only files that error, so a test excluded from tsconfig.test.json or shadowed by a sibling extension left the gate silently. Every *.test.ts(x) on disk must now be in the program or named in TESTS_OUTSIDE_PROGRAM with its reason. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(shared): make the enum helpers refuse the ways they can prove nothing openEnum takes a `const` T so a bare literal keeps its arms rather than widening to string. hostUnionArms blocks inference of U with NoInfer and defaults it to never, so a call that omits the host union — where the record would only pin itself — no longer compiles. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): describe the census and correct the baseline count Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): give the push fixture cast its SAFETY rationale Widening the pre-existing cast made the changed-code gate attribute it as a new finding. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): build the push fixtures as typed notifications Replaces the `as unknown as` cast with Expo's own types, filling FirebaseRemoteMessage and its notification once in two builders, and passes the data payload in rather than mutating through an optional member. Typing the fixture showed one assertion comparing the scheduled content against the whole arriving content, which only held while the cast let the fixture omit the two members the presenter drops; it now names the four members the presenter forwards. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): keep the grouped-question advance read non-optional `advance?.kind` let an absent advance take the null-draft branch instead of failing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): run the tests-typecheck ratchet on Windows Spawns tsc's JS entry on this Node instead of the node_modules/.bin shim, which is a POSIX shell script that Windows resolves to tsc.CMD and then appends .exe to. Parsed paths are normalised to POSIX so a Windows run does not read every baseline entry as both stale and added. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): close the ratchet's @ts-nocheck hole and read tsc once tsc exits 0 on a @ts-nocheck file, so a baselined test could be "fixed" with one line, pruned, and never checked again; the census now names any program test file whose leading comment carries the directive. `--noEmit --listFiles` answers both questions in one pass, so the gate spawns tsc once rather than twice. Corrects the two stale counts, and states hostUnionArms' real reason for living in the schema module now that tests are typechecked. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
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 |
||
|
|
abc8386e14 |
fix(mobile): name a create's launch so a lost reply cannot build two workspaces (#21137)
* fix(mobile): name a create's launch so a lost reply cannot build two workspaces `agent.launch` admits a caller-supplied `operationId` through a durable ledger, so exactly one execution happens and every replay returns the recorded answer. No client sent one, so the machinery was inert and the original defect was still live: mobile retries a lost create by design, and a retried launch built a second agent in a second workspace. Mobile now mints an operation id per create candidate and sends it whenever the host advertises `agent.launch.replay.v1`. The invariant is one operation per candidate. `computeAgentLaunchFingerprint` folds `target` whole, so the workspace name is inside the fingerprint; carrying one id across a name-collision bump would meet its own row under a differing fingerprint and refuse `agent_session_operation_conflict`, failing the create outright on the second candidate. The id is therefore minted beside `clientMutationId` at the top of each loop iteration and reused verbatim by every retry arm inside that candidate — never re-minted, since a new id is a new operation. Admission runs ahead of every effect, so `_invalid` / `_expired` / `_capacity` prove nothing launched: those re-send the same candidate unnamed rather than let bookkeeping fail a create the host would have performed. `_unknown` is the one refusal that is not safe to re-send, and it surfaces. Also corrects a false comment: the legacy path caches the whole launch under `clientMutationId`, so inside its 60s window a replay adds neither a workspace nor a surface, and outside it adds both — not "a second surface, never a second workspace". * fix(mobile): preserve launch identity on refusals * fix(mobile): use launch receipts to authorize replay * test: move mobile launch replay coverage outside node project * fix(mobile): enforce replay-safe launch delivery at the host * test: run mobile launch contracts in mobile checks * test: cover mobile launch contract workflow dependencies |
||
|
|
01a1b6b024 |
refactor(mobile): checked reply readers for the tasks item and list domain (step 7) (#21169)
* refactor(mobile): checked reply readers for the tasks item and list domain (step 7)
Thirty-eight unchecked reply readers across four tasks files become checked zod
readers, so a malformed host reply surfaces as one `RpcIncompatibleReplyError`
naming the method instead of a downstream `TypeError`, a rendered `undefined`,
or a sheet left ready over garbage. Deliberately a behaviour change on malformed
replies only; nothing on the wire moves.
mobile-task-item-state-operations.ts 17
mobile-task-item-detail-operations.ts 8
mobile-task-item-comment-operations.ts 7
mobile-task-list-operations.ts 6
Two rules decide every schema, and both are stated in
task-provider-entity-reply-schema.ts:
1. A member is required only where a tasks consumer reads it with no guard.
Everything reached through `?.`, `??` or a `typeof` test stays optional,
because a reply without it rendered the same fallback then and now.
2. No member is required that the site's own recorded `normal` reply lacks. The
corpus is the only evidence of what a host really sends at each site, and
requiring a member absent from that control would turn a good reply into an
incompatible one.
Rule 2 holds two schemas at the container: `github.prFileContents`, whose
recorded reply is `{ oldContent, newContent, truncated }` where
`getPRFileContents` returns `{ original, modified, ... }`, and `gitlab.todos`,
whose recorded row is not a `GitLabTodo` and whose `normal` partition therefore
records main crashing in `actionName.replace`. Both still gain their container,
which is what names a reply that is not an object or not a list. Correcting
those two scenarios is the follow-up that unlocks narrowing the rows.
Nine writes share one envelope reader and five comment writes share another:
`ok === false` and `error` are one host convention across them, and no input
would make two of them want different answers. The acceptance, the name and the
recorded family stay per operation. Three readers are reused rather than
re-declared — the session domain's boolean confirmation for `setPRFileViewed`
and `resolveReviewThread`, and its salvaged-member combinators throughout.
Three call-site shape tests the reader now answers for are deleted: both
`Array.isArray(payload)` guards on the checks read and the
`typeof count === 'number'` fallback on the item count. `GitHubPRFileContents`
is widened to optional members, which is what the reader can promise, and
`buildGitHubPrFileDiffPreview` takes the widened sides — `splitContentLines`
already treated a falsy side as no content, so no runtime behaviour moves.
The tasks source-parity hashes are refreshed: hook, statement, declaration and
render-token counts are unchanged, the render-token hash does not move at all,
and `semantics` is a pure deletion of ten lines.
Inventory: 137 unchecked readers over 30 files becomes 99 over 26.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): repin the RPC recording corpus and re-record the tasks reply deltas
`baseline` moves to
|
||
|
|
4b87bc718e |
refactor(agent-launch): redefine the agent.launch contract (#20999)
* refactor(agent-launch): redefine the agent.launch contract
`agent.launch` has no clients yet, so the contract is redefined in place
rather than versioned.
- params require `operation.id`, pinned to the shipped operation-id mint so
the host can read the embedded timestamp back. No caller-supplied
fingerprint: the host derives its own.
- the result carries `disposition` ('created' | 'replayed', the same
vocabulary `RuntimeCreateAgentSessionResult` already uses) and a single
top-level `warning` instead of one on the terminal arm only.
- the prompt receipt becomes an outcome enum, so a receipt can under-claim
instead of reporting a bare `delivered: false`.
- the dead `customization` field is deleted, and the mode-reason union and
receipt are declared once in shared with main re-exporting.
- `clientMutationId` joins the reserved create fields, with a test pinning
the list to the create schema in both directions.
Contract only; no behaviour change and no ledger wiring.
* docs(agent-launch): stop calling the stripped set "agent fields"
`clientMutationId` joined AGENT_LAUNCH_RESERVED_CREATE_FIELDS, so three
comments describing the stripped set as agent fields now teach the wrong
model — including a SAFETY rationale, where a reader is trusting it most.
The rationale's claim is unchanged and still sound: deleting keys from a
parsed object leaves the rest the parsed shape.
* refactor(agent-launch): make the attempt id the launch's only idempotency key
Review follow-ups on the contract redefinition.
`operation: { id }` becomes a flat `clientOperationId`, spelled the way
`terminal.createAgentSession` and the structured mutation envelope already
spell the same concept, and admitted by the shipped
`parseAgentSessionOperationTimestamp` rather than a second copy of its
pattern — so `agent-session-host-authority` keeps the regex private.
The handler now dedupes on that id instead of the create payload's
`clientMutationId`. That field is optional, so keying on it left any launch
that omitted one with no idempotency at all, while the required attempt id
did nothing. Reserving `clientMutationId` is still right, but for the reason
the comments now give: `createManagedWorktree` never reads it, so a copy left
in the forwarded payload is inert while still reading as a guarantee. The
previous rationale — that it was a second live dedupe key — was not true.
`messageId` moves onto the prompt receipt's `journaled` arm so a producer
cannot report the text as committed without saying where, and `rpcCallerKey`
picks up the `terminal.create` call site it was lifted from instead of
shipping with no callers.
* docs(agent-launch): record why disposition is two-valued only for now
The ledger admits attempts whose outcome was never recorded, and neither
`created` nor `replayed` can say "I cannot tell you" — a caller handed
`created` for an unresolved attempt starts a second agent. Noted at the type
rather than in review, so whoever wires the ledger reads it where they edit.
* fix(agent-launch): keep contract within implemented guarantees
|
||
|
|
85d1ffc072 |
fix: accept enterprise managed GitHub owner logins (#20450)
Unify owner validation across project pickers and repository overrides. Preserve EMU usernames in API and auth-status branch-prefix resolution, with regression coverage. Co-authored-by: Neil <neil@stably.ai> |
||
|
|
97aa5ff19b |
fix(mobile): open native chat when a new worktree launches a default agent (#19850)
* refactor(agent-launch): make the launch-mode decision surface-neutral
`decideWorkerStartMode` was the only shared answer to "structured chat session
or terminal agent?", but it lived in an orchestration-named module and spoke
orchestration's vocabulary, so the other launch surfaces could not call it.
Move the decision to `main/agent-launch/agent-launch-mode` unchanged and leave
`orchestration-worker-start-mode` as the adapter that supplies the noun.
A worker is not a special kind of launch; it is the same launch with a dispatch
attached. Naming the receipt's subject is the only thing orchestration actually
contributed, so that is the only thing the adapter keeps: "worker" in both
sentences, plus the `--terminal` wording, which reads as nonsense anywhere a
`--terminal` flag does not exist. Both are pinned, because they are asserted.
No behavior change. The receipts are byte-identical for every reachable case,
proven by running the new pin against both implementations.
Also pins the wording, which nothing was holding. The existing suites assert
`toContain` fragments ('terminal agent', 'cannot create') and the CLI suite
asserts a receipt handed to it by a mock rather than one this code produced;
all six files stayed green against a deliberately corrupted vocabulary. A
dispatch receipt is the only place a structured-to-terminal downgrade explains
itself, so the whole sentence is the contract, not a fragment of it.
* feat(agent-launch): add the launch intent and the one executor that runs it
The sequencing around the launch decision was duplicated per surface, and the
duplicate is where the bug lives. A new worktree was created agent-first, so
its startup terminal WAS the agent and the structured branch below it could
never be reached — every new-worktree launch was a PTY regardless of the user's
default. Orchestration fixed that for itself in #19431; mobile and the CLI
still have it.
`executeAgentLaunch` inverts the order once, for everyone. When the preference
is structured the worktree is created with NO startup agent, the executing host
is then asked whether it can host a session for the workspace that now exists,
and only then is a surface created. The host verdict cannot be hoisted above
creation: `agentSession.createSupport` only answers for a workspace it can
resolve, which is why the decision stays in two halves.
Agent-first creation is deliberately preserved for PTY launches — it is what
sequences the agent's startup command behind the setup runner, so wait-for-setup
comes for free there.
What actually differs per surface is only how a surface is built (an
orchestration worker's session takes a dispatch hold and a mailbox a plain
launch must not take), so that is injected as a factory rather than branched on.
The intent also strips the reserved agent fields from a migrated create payload:
a caller moving off `worktree.create` passes its existing params, and a stale
`startupAgent` in there would re-create the very path this replaces.
Tests assert order and arguments, not just the resulting mode. Reintroducing
agent-first creation reddens 4 of 11.
* feat(agent-launch): expose the launch executor as the agent.launch RPC
Adds `agent.launch` — one host-side method that decides structured-vs-terminal and
creates the surface — wired to the real runtime factories: `createManagedWorktree`
for the workspace, forking on `startupAgent` exactly as the orchestration worker
path does; `createStructuredAgentSessionForWorktree` for a chat session; and
`createTerminal` for a PTY agent. Allowlisted for mobile, which is the surface the
routing gap was reported on.
`worktree.create` is untouched. Its `startupAgent` keeps meaning "spawn a PTY agent"
verbatim, because it answers with `agentTerminalHandle` only on that path: a host
that quietly routed it to a structured session would hand every older client a
response with no handle and no error. All new behaviour sits behind
`agent.launch.v1`, which the host now advertises and a remote client must negotiate,
so a client that does not gets today's behaviour unchanged.
* feat(mobile): route workspace creates through agent.launch
Picking an agent on the mobile create sheet always produced a terminal, even
when the user's default was native chat, because all three create paths put
`startupAgent` on `worktree.create`. That means "create the worktree
agent-first", so its startup terminal IS the agent and the structured branch
below it is unreachable — while the same phone's in-workspace "+" button opened
a chat.
The blank, branch and new-branch creates now send the same payload through
`agent.launch` and let the host settle the surface. `worktree.create` is
untouched, and a host that does not advertise `agent.launch.v1` (read from the
existing `status.get` probe) keeps today's path exactly.
Work-item creates stay on `worktree.create`: they pre-fill the issue/PR URL as
an unsent `startupDraft`, which a structured session cannot hold yet, so routing
them would submit the URL as a first turn.
* fix(agent-launch): drop the deleted draft-prompt blocker from the reason map
main removed the draft-prompt blocker in #19681 (a structured session now holds
an unsent draft), so the exhaustive Record no longer typechecks.
* chore(agent-launch): carry a SAFETY rationale on the agent placement cast
The type-assertion gate landed after this branch's base, so the new file's
copy of the worker-start cast is now a changed-code finding.
* chore(agent-launch): carry agent.launch through main's RPC typing and casting gates
The typed-method contract, the generated params catalog and the
`assertionStyle: never` casting scan all landed after this branch's base.
- AGENT_LAUNCH_METHODS kept an `RpcMethod[]` annotation, which widened its
method name to `string` and broke assignability; every sibling infers instead.
- `agent.launch` binds a schema under src/main, so it joins the catalog's
RPC_METHODS_WITHOUT_SHARED_PARAMS and the parity gate's hand-listed twin.
- The now-typed methods make most test casts unnecessary; the few that remain
carry the line-specific SAFETY rationale the casting gate requires.
* test(mobile): supply the agent-launch fixture the create-submit recording needs
The golden RPC recordings landed upstream while this branch was out, so they
first met agent.launch here. Three things had to happen, and only one of them is
a fixture bump.
1. workspace-settings-mounts.ts mounts useNewWorkspaceCreateSubmit against a
fixture model that throws on any member it was not given. This PR added a
required getAgentLaunchSupport, so the submit aborted with "Missing model
fixture" before it ever issued the create, and three cleanup checkpoints
vanished. That read like a product regression and was not one. Supplying the
member restores the recording byte-for-byte; it is pinned false for the same
reason the cutover probe is, so the baseline stays on worktree.create.
2. Editing that adapter moves adapterSha256 for the twelve settings goldens it
mounts. Their recordings are unchanged - header only, by design: the digest
is per-golden so editing a module fails exactly the goldens that mounted it.
3. Five goldens changed behaviourally, and both changes are this PR's:
the capability probe now reports agentLaunch, and a create whose reply
carries no worktree returns "Failed to create workspace" instead of throwing
a TypeError off an unguarded result.worktree read. The launch route needs
that guard, since a receipt can arrive without a worktreeId.
* refactor(mobile): decode the launch receipt instead of asserting its shape
The changed-code quality gate refuses type assertions, and the eight it flagged
were worth removing rather than suppressing.
The production one was the point. readAgentLaunchCreateOutcome asserted the RPC
payload into Partial<AgentLaunchResult> and then runtime-checked it anyway, so
the assertion bought nothing and claimed a contract the host had not proven. It
now narrows with `in` and validates each hop, which is the same nullability
question readCreateResult already answers on the sibling path - a launch receipt
can legitimately arrive without a worktreeId. AgentLaunchCreateOutcome ties
worktreeId to the shared contract so a change there fails this reader's
typecheck rather than passing a differently-typed field through.
The test fakes claimed a whole RpcClient via `as unknown as RpcClient` while
implementing one member. They now build a typed literal, matching the pattern in
use-mobile-structured-agent-options.test.ts. The read sites cast params and then
read one field; they now assert the payload with toMatchObject, which removes
the cast and pins more of the shape than the cast did.
Also pins the warning passthrough, which nothing covered: a terminal launch that
seats the workspace but cannot start the pty reports why, and the absent, blank,
non-string and structured-surface cases report nothing. Writing that test caught
a real drop I had introduced in the reader.
* ci(mobile): re-run Mobile Checks when a shared capability changes
Mobile Checks is path-filtered to mobile/**, but mobile imports the negotiated
capability names straight from src/shared/protocol-version.ts and records the
whole capability read verbatim in its goldens. So a capability added desktop-side
rewrites a mobile fixture while never triggering the suite that would catch it.
That is what happened here: #19849 introduced agent.launch.v1 and Mobile Checks
never ran on it. Verified at the run level rather than by check name - the
window-free check-runs API on
|
||
|
|
b8d4cde09f |
refactor(mobile): send six screen-mounted call sites through typed RpcOperations (step 4, wave 3) (#20919)
* test(mobile): record six screen-mounted call sites before migrating them Five new mount adapters and six scenarios, recorded against the pinned baseline's product code so the goldens are main's behaviour, not the refactor's. Each site is a screen the recorder could not previously mount: - `home.host-accounts` mounts `fetchMobileHomeAccounts`, whose decoder is re-exported through `AccountUsage.tsx`. That module loads under the mount loader, so the inventory's "no recording can load it" was already stale. - `notifications.display-test-screen` mounts the settings push probe and presses its button by reading the handler back off the rendered inert `Pressable`. - `aiVault.history-screen` mounts the history panel, which is where the last `worktree.ps` lives. Split in two: the base stops once the worktree list has seeded the scopes, because a reply partition there changes the scopePaths the downstream `aiVault.listSessions` carries, and a matrix variant cannot assert params it moved. The full chain is a second scenario, driven as a pilot only. - `tasks.route-repo-list` mounts the tasks screen-root hook and calls its own `ensureLoaded`, which is the only thing that fires `repo.list`. - `linear.select-workspace-picker` calls the render helper the tasks surface calls and invokes the `onSelect` on the element it returns. The picker draws inside `BottomDrawer`, whose reanimated timing driver and gesture builder the recorder would have to impersonate for a row to exist; the closure is the same either way, and the workspace a selection carries comes from the scenario. Five substitute members are added, each with the recording that reads it: `react-native-safe-area-context.useSafeAreaInsets` and `expo-router.useLocalSearchParams` for `tasks.route-repo-list`, and `react-native.TextInput`, `.SectionList` and `.RefreshControl` for `aiVault.history-screen` once its list renders. `useLocalSearchParams` answers one pinned route for the same reason the window size is pinned: a screen's own address is not a device reading, and the one screen that reads it sends `repo.list`, which takes no params. Touching the substitute table moves `recorderSha256`, so all 641 existing goldens are re-recorded. Recorded from a detached worktree at the pinned baseline with this branch's recorder laid over it: every pre-existing golden is header-only, verified by resolving both sides through the value pool — 641 header-only, 0 body, 0 deleted, one distinct `recorderSha256`, `baseline` and `lockfileSha256` across all of them. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the linear workspace picker's model fixture `mobile/tsconfig.json` covers the recorder, and the fixture's setters were written with the argument the product happens to pass rather than the `SetStateAction` the model declares. Typing them moves `adapterSha256` on the two goldens recorded through this module, so they are re-recorded here rather than in the refactor commit, which must move none. Re-recorded at the pinned baseline: `linear-select-workspace` and its reply matrix, header-only, bodies unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): send six screen-mounted call sites through typed RpcOperations Nine references off the raw request port, across six files. Every one is proven against the goldens recorded in the previous commit from the pinned baseline's product code: this commit moves no file under mobile/rpc-foundation/goldens. Reused rather than redefined: - `worktree.ps` in the history panel sends through `worktreeCatalogRead`. Same question, same acceptance — a refused list leaves the screen on what it holds. - `repo.list` in the tasks screen-root hook sends through `newTabRepoListRead`. Its policy raises the host's message and its reader takes `repos` off the payload while preserving the property-read exception a null result used to throw at the cast, which is what this call site did by hand. Its name still says new-tab; a third consumer does not make renaming it this bucket's business. Four operations are new, each because no existing reader on the method takes this consumer's input: - `files.read-directory-or-skip` and `files.legacy-explorer-list-or-skip` for the explorer. Both skip, because neither refusal is the operation's to decide: the readDir refusal code selects the legacy fallback and the list refusal supplies the message. The existing `files.list-or-skip` reads the `files` member alone, and the explorer also needs `truncated` for the "Showing first 5000" note. - `accounts.home-snapshot-or-skip` for the Home card, decoded by `decodeAccountsSnapshot` at the call site as before. - `notifications.test-push-or-skip` for the settings probe, whose `forbidden` and `method_not_found` refusals mean "try the next desktop". - `linear.select-workspace-or-skip` for the filter sheet. Two behaviours are preserved rather than repaired, both recorded: - The workspace switch never read its reply. `.then(() => loadLinearContext())` runs on a refusal exactly as on a success, so only a transport rejection reaches the error copy. Interpreting the operation here would surface a refused switch for the first time; that is a product change with its own re-record. - `app/terminal-settings.tsx` still reads `ms` off the reply envelope instead of off its result, so the value is always undefined. It did not migrate, and the inventory now carries the defect as its own note. Four mutants are added, one per new family that admits a state-only one: the Home snapshot, the push test result and the tasks repo list each decoded one level above the envelope, and the workspace switch with its context reload dropped. `aiVault.history-screen` gets none and says why in the suite: everything `worktree.ps` publishes also moves the `scopePaths` the next scripted completion asserts, so a mutant aborts the sequence instead of diverging from it. Its evidence is the reply matrix at that request. The tasks source-parity ratchet moves with the family it guards: hook, statement, declaration, render and style counts are unchanged, and the semantic source is a pure deletion of four lines — two `rpc:` call signatures and the two method literals they carried. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): matrix the six new screen families' replies One golden per scripted reply, eleven partitions each, recorded at the pinned baseline alongside the pilots. Seven sites: `accounts.list`, `notifications.testPush`, `repo.list`, `linear.selectWorkspace`, and all three of the history screen's — `worktree.ps` and the two `status.get` reads its scan chains off the worktree list. The history matrix is also that family's defect evidence in place of a mutant: every partition at `worktree.ps` changes the `scopePaths` the downstream `aiVault.listSessions` carries, and the sender args are recorded with it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): correct three operation and mutant comments Comment-only, no product behaviour and no golden movement. - `worktreeCatalogRead` says two readers; there are three. Names the third (the agent-history panel's `scopePaths` seed) and drops the stale count from the module header, which described call sites rather than the two operations. - `newTabRepoListRead`'s census counted the two operations over `repo.list`, not its own two callers, and claimed both read a workspace's connection id. The tasks route keeps the whole list for its repo pickers. The split from `nativeChatRepoListRead` stays where it belongs: acceptance. - The `aiVault.history-screen` mutant note pointed at the reply matrix as the accepted-vs-refused oracle. Decoding `matrix-aivault.history-screen-worktree.ps-1.json` through the value pool shows `normal`'s projected state is identical to all seven non-crashing partitions (spinner, two labels, zero rows). The real oracles are the next request's `scopePaths` (`["/repo/feature"]` vs `[]`) and the crash channel the three `inner-*` partitions land in. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): give the second files.list reader its real reason Comment-only, no product behaviour and no golden movement. `legacyFileListRead` claimed "the member reader rejects this consumer's input". Nothing rejects: `rpcUncheckedMemberReader` returns the member, and reusing it here would simply drop `truncated`. The reason the explorer declares its own operation is the other direction. Widening `files.list-or-skip` to a payload reader would split the `workspace-files` variant it shares with `nativeChatFileSearchRead` over `files.searchPaths`, whose only caller feeds both through one `extractPaths` in `use-mobile-native-chat-file-search.ts`, so the member read would move into that hook rather than disappear. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile): indent the six scenario entries spliced during the merge The conflict on `pilot-scenarios.json` was resolved by id rather than by hunk, splicing this branch's six entries into main's text at the array's close. The splice started at the entry's `{` instead of at its line, so those six lines lost their indentation. oxfmt's only change is those six lines; the parsed document is identical, and the recording suite still matches all 667 goldens, so no scenario digest depends on the raw text. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the merged goldens once at the pin One record for the whole merged tree, at the unchanged baseline |
||
|
|
2dfdbc8657 |
refactor(mobile): send the task provider, detail and board domains through typed RpcOperations (#20685)
* test(mobile): record main's task provider item, detail and board RPC behaviour 35 scenarios over 22 of the 25 files left in src/tasks/, recorded from main so the step-4 migration of the provider half has a frozen answer to compare against. Every one of the 70 references this branch will migrate reaches a recorded wire here, which is the check the workspace-creation half added after it lost three sites to fixtures that short-circuited before the call. Scenario params are observed, not written: a generator drove each adapter with nothing answered, read the projected sender calls back, and emitted the completion steps from them, so no `params` in the manifest is a guess about what the screen sends. Five adapter modules, split the way the screens are: one item's reads, the list and composer, the item mutations, the board's reads and the board's row mutations. `mountModelHook` holds the mount/dispatch/project boilerplate these twenty-two hooks share, so each adapter is only its fixture, its actions and its projection. Two fixture modules hold the task items and the project rows, shared so the same pull request looks the same to the comment hook, the merge hook and the checks hook — which is what makes their recordings comparable. `baseline` moves from |
||
|
|
36ef93a64f |
refactor(mobile): migrate the small domains onto RpcOperation (step 4) (#20705)
* refactor(mobile): pin each RPC golden to its own mount adapter, not every domain's `recorderSha256` covered the whole recorder directory, mount adapters included, so a domain PR that adds its adapter module moved the header of all 153 goldens. #20568 did exactly that and its merge with main conflicted on that one line in 153 files; every future domain PR would collide with every other in flight the same way. Split the directory at a real seam instead of a filename convention: `adapters/` holds one module per domain, registered in `adapters/mounted-operation-modules.ts`, and `recorderSha256` now covers the engine only. A new `adapterSha256` covers the source of the module that mounts each operation a golden's scenarios drive, read off the same `mounts` calls that build the table the recording runs against, so the pin cannot name a file the runner did not use. Adding a domain's module now re-digests nothing already recorded; editing one fails exactly the goldens mounted through it. `adapter-seam.test.ts` keeps the split from drifting: an engine file inside `adapters/`, an adapter defined in an engine file, a register entry naming the wrong file, and an adapter importing a sibling each fail. The five adapters that were inline in `pilot-mount-adapters.ts` move into their own modules, which leaves that file as the registry and nothing else. `GOLDEN_FORMAT_VERSION` goes to 5 for the new header field; the goldens re-record in the next commit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens under the split recorder/adapter digest Header-only. Every changed line is `recorderSha256` (the engine digest no longer covers `adapters/`), the new `adapterSha256`, or `goldenFormatVersion` 4 -> 5; `baseline` is unchanged and recording ran against the same pinned product tree. git diff -U0 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \ | grep -vE '^(\+\+\+|---)' \ | grep -vE '^[+-] "(recorderSha256|adapterSha256|goldenFormatVersion)":' | wc -l 0 The seven `adapterSha256` values partition the 153 goldens by the module each was recorded through: 58 settings, 37 hosted review, 21 source control, 11 new-tab agents, 9 file inventory, 9 tasks, 8 workspace settings. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): stop pinning goldens to recorder inputs no recording can read The adapter split left three per-domain edits still moving all 153 headers: the mutant table, the per-family mutant registry beside it, and the probe-hole witness. None can change a recording -- the loader consults a mutant only when a mutant test asks for one, and no suite but the two recording drivers writes a golden -- so pinning them claimed a provenance the goldens do not have and charged every domain a full re-record for it. `mutants/` now holds the table, the registry, the reference states, the mutant suites and the probe-hole witness, and `recorderSha256` skips it. What makes that sound is that no recording can reach it: `operationModuleLoader` takes a resolved mutation spec instead of importing a table by name, so nothing on the recording path names `mutants/` at all. `mutants/mutant-seam.test.ts` checks exactly that, and fails if an engine file names the directory or anything outside imports from it. `recorderSha256` also pins only the suites in `recording-drivers.ts`, which `scripts/rpc-recording.mts` records from, so the two cannot drift. A suite that reads goldens, or writes one to a scratch directory, is no longer provenance for a recorded file. `OPERATION_EXPOSURES` went the other way, because it does change what a recording loads: withhold the resume-metadata exposure and exactly four goldens fail. Each domain module now declares its own exposures and gets its own loader, so `adapterSha256` pins the ones that reached each golden. Two assertions in the digest boundary test were vacuous: `join(root, '.')` normalises back to `root` and hit `recorderSha256`'s per-root cache, so the prose-is-ignored claim never recomputed anything. Each call now spells the root differently. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens under the mutant and driver exclusions Header-only, and no format bump: the header shape is unchanged. `recorderSha256` moves on all 153 because the engine set shrank, and `adapterSha256` moves on the 58 settings goldens because that module now carries its own exposure declaration. git diff -U0 HEAD~1 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \ | grep -vE '^(\+\+\+|---)' \ | grep -vE '^[+-] "(recorderSha256|adapterSha256)":' | wc -l 0 Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): restore the preferences actions the merge resolution dropped #20568 added `resume` and `trust` actions to the `settings.task-preferences` adapter while it still lived in `pilot-mount-adapters.ts`. This branch had already moved that adapter into `adapters/task-mount-adapters.ts`, so resolving the `pilot-mount-adapters.ts` conflict in favour of the registry merge silently discarded them and `tw-task-preferences-resume-write` failed to record at all ("Missing or completed request: ui.set#1"). Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens at main's tip after the merge All 208 goldens, header-only. `baseline` moves from |
||
|
|
231e805b1e |
fix(lint): enable anti-slop/no-shape-in-symbol-names (#20785)
Flip `anti-slop/no-shape-in-symbol-names` from "off" to "error" and clear
every violation under src, config, tests and mobile.
What the rule bans
------------------
The case-insensitive substring "shape" in any JS/TS identifier: variables,
functions, parameters, types, type parameters, class members, private names,
object-literal keys and JSX identifiers. The one exemption is a statically
accessed member read owned by another value (`zodObject.shape` is fine), so
third-party APIs stay readable without a suppression.
"Shape" names a value's structure rather than its domain role. `UserShape`,
`validateArgShape` and `errorShape` all tell you the symbol is "an object
with some fields" -- which is already what a type says -- while saying
nothing about what the value is for or who owns it. The rule forces the
name to carry the domain instead.
Violations fixed
----------------
689 violations across 109 files at baseline (verified by re-running the
audit against the pre-change tree with the rule set to "error").
Fix pattern
-----------
Rename for the domain role, not the structure:
-type FieldShape = 'list' | 'map' | 'whole'
-const FIELD_SHAPES = { ... } satisfies Record<keyof Observation, FieldShape>
+type FieldEncoding = 'list' | 'map' | 'whole'
+const FIELD_ENCODINGS = { ... } satisfies Record<keyof Observation, FieldEncoding>
-function assertGitPushTargetShape(target: unknown): void
+function assertValidGitPushTarget(target: unknown): void
-function describeReadDirPathShape(p: string): ReadDirPathKind
+function classifyReadDirPath(p: string): ReadDirPathKind
Predicates became statements about the value (`isDeltaShapedProviderFrameKind`
-> `isDeltaProviderFrameKind`, `isDeleteShapedDiscardEntry` ->
`discardDeletesEntryFile`, `isSkillsCliAgentKeyShaped` ->
`isUsableSkillsCliAgentKey`). Type aliases dropped the suffix where the
remaining name was already unambiguous (`GhGraphqlErrorShape` ->
`GhGraphqlError`).
No wire-visible name was renamed: no IPC or RPC channel, stream opcode,
request/response param, persisted field, or i18n key. The `--shape=symlink|copy`
CLI flag read by .github/workflows/skill-update-roundtrip.yml is unchanged --
only the local variable holding it was renamed.
Exemptions
----------
They are file-scoped entries in config/oxlint-anti-slop.json, not inline
`oxlint-disable` comments. An inline directive naming an anti-slop rule reads
back as an UNUSED directive under the root lint scan, which does not load this
plugin -- the changed-code quality gate counts that warning, so the comment form
cannot be used for a rule that lives only in this config.
* src/renderer/src/components/browser-pane/annotate/**:
in the screenshot annotator a "shape" is the drawn geometry -- pen, arrow,
rect, ellipse, highlight. That is a genuine domain noun, and it pervades
every symbol in the module.
* repo-icon.tsx, repo-header-project-actions.tsx, mobile MobileRepoIcon.tsx:
lucide exports the icon component as `Shapes`. The name is theirs, and the
matching REPO_LUCIDE_ICONS key is the persisted icon name shared with the
desktop picker -- renaming it would orphan saved repo icons.
* src/shared/onboarding-state-types.ts, src/shared/constants.ts:
`shapedSidebar` is a persisted onboarding-checklist field and a telemetry
enum member; renaming it would orphan saved state.
* src/shared/rpc-contract/rpc-send-params.ts: matching zod's own literal `shape`
property is what selects the ZodObject branch of the conditional type.
No exemption was added merely to avoid a rename. Eight symbols initially
suppressed as "a cross-module refactor outside this change" were proven to have
zero non-TypeScript references repo-wide and renamed instead.
Zod's `ZodRawShape` needed no exemption at all: `Readonly<Record<string,
z.ZodType>>` is its definition, so repo-update-params.ts and
ui-update-value-tolerance-params.ts spell it out instead. Likewise
telemetry-event-classification.ts now reads `.shape` through an `in` narrowing,
which also retires two pre-existing type assertions; three more assertions the
rename had dragged onto changed lines (two `JSON.parse` sites, one node:sqlite
row read) became annotations and an explicit row mapping.
Verified
--------
* Audit reports zero violations; confirmed the rule genuinely fires by
planting a probe violation.
* node config/scripts/run-typecheck-projects-in-parallel.mjs exits 0.
* Vitest over src/shared, src/main/github/project-view, the annotate module,
the repo-icon components and the Chromium SameSite electron spec: all green.
* All 66 removed "shape" identifiers grepped repo-wide across every file type;
none survive.
* node config/scripts/generate-rpc-params-catalog.mjs --check exits 0.
* node --check on every changed .mjs; oxfmt clean on all changed files.
* `pnpm run check:code-quality:changed` reports 0 findings.
Not machine-verified: the 3 mobile/ files (its Vitest run cannot resolve
`expo/tsconfig.base.json` in this worktree), and the WSL- and Playwright-gated
specs. All are rename- or comment-only hunks, read in full.
|
||
|
|
fc525c355d |
refactor(mobile): send the task workspace-creation domain through typed RpcOperations (#20568)
* test(mobile): record main's task workspace-creation RPC behaviour before migrating it 28 scenarios over nine task senders, recorded from main so the step-4 migration of the workspace-creation half of src/tasks/ has a frozen answer to compare against. Four senders mount as plain exported functions; three are model-chained hooks mounted the way the settings adapters mount theirs. The 153 existing goldens change header-only (`baseline`, `recorderSha256`): any new scenario re-digests the recorder, and the pinned baseline had drifted from main because the source-control migration landed. Content is byte-identical on all 153 — verified field-by-field against HEAD. `operation-module-loader.ts` now shares src/transport/rpc-delivery-ambiguity.ts with mounted modules instead of evaluating a second copy. The mark is a WeakSet keyed on the rejection object, so the copy the loader built had an empty registry and every delivery-unknown rejection read as a definite failure inside the operation under test — worktree.create's whole replay path was unreachable. With one registry, `tw-create-retry-ambiguous-after-drop` records the create still pending at the reconnect wait and abandoning at exactly 20000 ms, while the unstamped-create scenario records the same rejection surfacing at 0 ms. No existing golden moves: no other mounted module consumes the mark. `task-preferences-optimistic` is re-anchored above the send rather than across it, so migrating this file does not have to move the anchor. It still kills, and for the same reason: the preset the screen shows no longer follows the tap. Scenarios deliberately pin the empty-message refusals (`*-refused-empty-message`, `*-empty-message`), because a refusal with no message falls back to the screen's copy while a transport error with no message does not, and the two paths are easy to collapse when a call site moves behind an acceptance policy. Goldens: 153 -> 201, 2.9M -> 3.7M. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): format the recording manifest and re-digest the goldens `oxfmt --check` from mobile/ collapses a one-element `sites` array in each new scenario. The JSON value is unchanged — verified by comparing both files parsed and key-sorted — but the manifest is inside `recorderSha256`, so all 201 goldens carry a new digest. Every other field, header and observation alike, is byte-identical. Re-recorded in a separate worktree at the previous commit so the goldens stay attributable to main's product source rather than to the migration that follows. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): separate the goldens from the migration, and re-digest The previous commit accidentally carried the product migration alongside the manifest format, which both broke the commit that is supposed to prove parity and left the suite red: the digest was recorded without a comment move that a lint fix had made inside the adapter, so all 201 goldens failed their `recorderSha256` header. This backs the product half straight out again — the next commit re-applies it byte-for-byte — and re-records from the pinned baseline in a separate worktree carrying this branch's recorder, per the procedure in the recording README. Every field except `recorderSha256` is byte-identical to the previous commit's goldens on all 201 files, so no observation moved in either direction. The suite is green here with main's product source, which is what makes the next commit's "no golden changed" claim mean something. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): send the task workspace-creation domain through typed RpcOperations 12 of src/tasks/'s 37 raw-port files now send through a declared operation instead of the raw request port: 36 references to 0, leaving 25 files and 73 references for the provider item/detail/mutation half. No golden moved — `git show --stat` on this commit touches nothing under mobile/rpc-foundation/, which is the parity claim. Twenty-two operations over twenty methods, in four modules named for what they send: workspace create (create, PR/MR base resolution, create-time capabilities), workspace source (SSH connect/state, agent detection, repo hooks, sparse presets, ref search), task runtime (status, ui.get/ui.set, preflight, Linear status, settings.update) and the Smart picker's provider reads. Two methods carry two policies each, and both pairs are named. `status.get`: the Tasks screen cannot hydrate without it and surfaces the host's message, while create-time capability probing degrades to "no capabilities" and creates anyway — so one throws on refusal and one skips. `ui.set`: two sites await it, one is fire-and-forget and never interprets the reply at all. Both pairs share one reader, so no method has two readers. No new acceptance policy. worktree.create keeps its delivery-unknown contract. `request` returns the transport promise itself, so the retry loop catches the object the transport marked; two new tests assert `toBe(marked)` in one direction and that an unmarked rejection stays unmarked in the other, because a mark added on the way out would replay a create the host never received. `tw-create-retry-ambiguous-after-drop` records the create still pending at the reconnect wait and abandoning at exactly 20000 ms. Three sites still read the raw refusal envelope before interpreting, because the code or the message decides the route and no acceptance policy carries either through: the create retry needs the message for `isRetryableWorktreeCreateConflict`, and the paste lookup needs `method_not_found` to retire the slug probe host-wide. Both are documented at the site. The hydration barrier keeps raw requests inside its `Promise.all`. main's group rejects as soon as one leg rejects; `startRpcOperation` + `interpretAtRpcBarrier` would wait for the slowest peer and let a later policy surface a different error. Interpretation stays after the `stale` guard, where it was. `WorkspaceCreateParams` is now `RpcSendParams<'worktree.create'>` rather than `Record<string, unknown>`, which types the builder and the operation together; every field the three builders already sent typechecks against the host schema unchanged. `RpcSendArguments` now also makes params optional for a method whose params type has no required field, because `preflight.check` is such a method and main sent it none — requiring `{}` would have put a new object on the wire. The Mobile Tasks source-parity hashes move for the same reason bound settings requests moved them: the method string and the envelope read leave the screen. The signature diff is evidence rather than a re-pin — `semantics` is a pure deletion of 22 `rpc:` call signatures and 22 method literals with nothing added, statement/declaration/render/style counts are unchanged, and render tokens, styles and declarations are byte-identical. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): split the task workspace adapters at the sender/hook seam The single adapter file reached 344 lines against mobile's 300-line limit. CI lints every file, so this is red there even though the changed-code gate does not report it. Split along the seam the recording README already draws: exported async senders that take a client and need no React host, and the drawer's three model-chained hooks. No adapter body changed. Both files are inside `recorderSha256`, so all 201 goldens carry a new digest. Every other field is byte-identical, verified file by file. Re-recorded from the pinned baseline in a separate worktree carrying this branch's recorder, so the goldens stay attributable to main's product source. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): give the one-key unchecked reader a name Four readers were the same three lines: read one property off the reply, wrap it unchecked. `rpcUncheckedMemberReader` is the one-key sibling of the existing `rpcUncheckedPayloadReader`, so the annotation and the closure go away at each site. The pilot's `commitCompareEntriesReader` is converted too, so the helper has no longhand twin left to copy from. No behaviour change: the helper composes the same `rpcReadUnchecked` over `rpcPayloadMember`, including the property-read exception on a null result. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): record the local arm of workspace agent detection `preflight.detectAgents` was the one migrated operation with no recorded coverage: the ssh adapter hardcoded `connectionId: 'ssh-1'`, so the detection effect's ternary only ever took the remote arm and the local call site could be repointed at another method without a golden noticing. The adapter now takes the connectionId as a parameter and registers twice; `tasks.workspace-ssh-local` mounts the same hook with no connection, which is the only difference the effect branches on. Recorded at the pinned baseline with this branch's recorder laid over it, so the new golden is main's behaviour and the migrated code has to reproduce it — it does. Goldens: two added (`tw-workspace-ssh-local-agents` and its reply matrix). The other 201 changed on `recorderSha256` only, because the adapter edit moves the recorder digest every golden pins. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): drive workspace create and the Linear list to a recorded wire Two operations passed a policy swap unnoticed, both because no golden reached their acceptance branch. `worktree.create`: the create hook's fixture resolved setup to a prompt, so all three settings.task-workspace scenarios stopped before the request and the only consumer that hands a refusal to interpret was never recorded. The adapter now takes the setup resolution as a parameter and registers a second family that resolves it, so createWorkspace runs to the wire. Two scenarios: a Linear item that creates directly, and a GitHub pull request that resolves its base first, which also puts this hook's built params — start point, generated display name, agent launch fields — in a golden for the first time. The existing prompt family is untouched, so its recordings still pin that branch. `linear.listIssues`: it appeared only in a non-base scenario, and the matrix reads the family base, so the family had no partition for it. The base now lists assigned issues after searching. Goldens: five added. Five moved beyond the digest, all derived from the smart-search base that gained the list leg. The other 198 changed on `recorderSha256` only. Recorded at the pinned baseline with this branch's recorder laid over it, so every new golden is main's behaviour. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): drop the unreachable unmount branches from the task adapters Nothing dispatches `unmount` to these three adapters: the only producer is `lifecycleSchedules`, driven from a hardcoded five-id list that names no task-workspace family, and it pushes a `remount` right after, which these adapters would throw on. The branch read as lifecycle coverage that was never wired up. `dispose: hook.unmount` already tears the mount down. Goldens re-recorded at the pinned baseline because the recorder digest moved; `recorderSha256` is the only line that changed in all 208. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the goldens at main's post-squash baseline Recorded from a detached checkout of |
||
|
|
b0070e3720 |
refactor(mobile): migrate settings reads to RpcOperation (#20499)
* refactor(mobile): migrate settings reads to RpcOperation Replay the settings slice on the landed RPC foundation after rebasing onto main. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refresh task parity snapshots after main rebase Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): correct rebased declaration parity hash Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): account for main task declaration Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): preserve raw RPC rejection timing Return the transport promise directly and interpret replies separately so sibling Promise.all rejection order cannot change. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): refresh parity hashes after timing fix Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): use operation interpreter after raw request * test(mobile): refresh settings migration parity hashes Refresh hook and statement parity hashes for the two task declarations whose settings reads now use RpcOperation request and interpretation. Changed declarations: - useMobileTasksRuntimeHydration: settings.get replaced by settingsRead request/interpret. - useMobileTasksWorkspaceCreateActions: settings.get response handling replaced by settingsRead request/interpret. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
df375cdd8a | perf(mobile): reuse Linear issue ordering when grouping list and board (#20314) | ||
|
|
7e9ade7c74 |
perf(mobile): reuse Linear grouping between list and board (#20431)
* perf(mobile): reuse Linear grouping between list and board * test(mobile): realign parity oracle and ratchet with current main Rebasing onto main surfaced two breakages that the earlier ratchet-only fix could not have caught, because it was computed against a base main had already superseded: - The parity oracle called compareLinearIssues, which #20249 deleted in favour of sortLinearIssues. Rewrote the oracle to use sortLinearIssues, matching what the production memo now calls, and dropped the stale mock override. - Regenerated EXPECTED_SCREEN_HOOKS and EXPECTED_STATEMENTS from an observed run on the rebased tree. Arity assertions (350 hooks, 417 statements) unchanged. mobile/src/tasks: 37 files, 295 tests pass. |
||
|
|
a045af3618 | perf(mobile): precompute Linear issue sort keys (#20249) | ||
|
|
701dc2211c | perf(mobile): precompute task sort keys and reuse repository collation (#20233) | ||
|
|
9a56797486 |
fix(mobile): surface host create warnings and terminal-create errors (#20125)
* fix(mobile): surface host create warnings and terminal-create errors
A workspace created from the phone could land on "No tabs in this session"
with a bare red "Failed to create terminal" and no way to tell why. Two
independent drops hid the host's own explanation:
- createWorktreeWithNameRetry returned only {worktreeId, name}, discarding
worktree.create's `warning`, and hostNewWorktreeSessionRoute built the
session route with only `name` + `created=1`. The session screen has always
had the banner (MobileSessionContentRow + createWarningState) -- only the
tasks create path ever fed it, so the New Workspace path could never report
a startup terminal that failed to spawn.
- handleCreateTerminal collapsed every failure to the literal
'Failed to create terminal', throwing away response.error.message.
Both now propagate, so the daemon's pty-allocation hint ("Your system cannot
allocate any more pty devices.") reaches the phone instead of dying in the
main process. Behaviour is otherwise unchanged: a blank warning is still
omitted from the route, and a host that gives no reason still reads
'Failed to create terminal'.
* test(mobile): refresh route parity baselines
---------
Co-authored-by: Merge Sim <sim@local>
|
||
|
|
8f15f217a2 |
Preserve user-set workspace names across branch changes (#17448)
* fix(worktrees): preserve user workspace names across branch changes * test(worktrees): cover pinned rename metadata * fix(workspaces): address display-name review edge cases * fix(workspaces): keep automatic names fresh across refreshes * fix(workspaces): preserve legacy CLI labels * fix(workspaces): preserve display-name provenance across hosts * fix(workspaces): honor legacy display-name provenance * fix(workspaces): fence display-name refresh races * fix(workspaces): accept peer renames from provenance-less hosts The old-host preserve fence kept a pinned local label on every refresh, which also suppressed a legitimate rename another client persisted through the same host until app restart. Narrow it to labels the host re-derived itself (branch short name, or path basename when detached); any other changed label in a mode-less response is explicit meta a peer wrote there. Stale prior-label responses stay covered by the downstream staleness fence, in-flight writes by the pending fence. * refactor(workspaces): unify display-name pin derivation Three call sites (renderer optimistic update, local IPC updateMeta handler, remote worktree.set handler) each restated the same formula; a future edit to one would silently skew provenance between paths. |
||
|
|
a651e81843 |
refactor(agents): remove dead hook IPC and derive shared agent defaults (#16089)
* refactor(agent-hooks): drop the unused per-agent hook status IPC surface No renderer, CLI, or mobile caller invoked window.api.agentHooks.*Status; main already reads install status through MANAGED_AGENT_HOOK_STATUS_READERS. The 14 handlers had also drifted (kimiStatus existed in main/preload but not in AgentHooksApi or the web stub). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(tui-agent-config): default launchCmd and expectedProcess to detectCmd 32 of 36 entries repeated the binary name three times. Entries are now authored in a source form where both default to detectCmd and resolved once at module load, so TUI_AGENT_CONFIG keeps its exact shape for consumers (verified equal to the previous table). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(mobile): derive the agent order, labels, and picker from src/shared The mobile mirror (and its regex-over-desktop-source parity test) predates mobile importing runtime values from src/shared, which it now does in a dozen modules. Only the favicon-domain map stays mobile-local because desktop's lives in the renderer catalog next to bundled ?url imports. The parity test now imports the real registries and also checks label parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(web): align preload surface after hook IPC removal --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
81b4d88bd6 |
refactor(mobile): split tasks route into focused modules (#17438)
* refactor(mobile): split tasks route into focused modules * fix(mobile): repair tasks refactor module boundaries * chore(mobile): document intentional render resets * fix(mobile): remove stale lint suppressions from tasks split * test(mobile): keep parity checks stable with doctor suppressions * test(mobile): follow tasks module split * test(mobile): follow project routing module split |
||
|
|
8cf7c6926e |
fix(mobile): create-worktree sheet dies after picking a source and loses the picked PR (#16917)
* fix(mobile): keep the create form on screen through drawer swaps and survive reconnects The create-worktree flow could reach a state where the shared modal host was mounted with no sheet in it: a full-screen transparent window that swallows every tap with no way out. Frame analysis of the reported recording and a live simulator repro both land on the same state - the form sheet laid out at the right frame with progress=1, backdrop painting, sheet not painted. - Keep the form sheet mounted through every drawer transition, so the host Modal is never on screen without a sheet, and drop the render-read pin ref. - Re-assert a pinned sheet's enter transform when it takes the window back from a fill picker; nothing re-applied it before. - Key the form session on hostId, not on the RpcClient object: useHostClient swaps that object on every reconnect, which silently remounted the form and threw away the picked source. - Run the pasted-item lookup concurrently with the provider fan-out instead of after it (measured 2631ms -> 1480ms for a typed PR number). * fix(mobile): remount the sheet view on window hand-back so a rebuilt native view repaints On-device confirmation showed the committed hand-back re-assert never reaches the native view: progress already sits at 1 and translateY at 0, so withTiming produces no style delta, and the dead screen stayed reachable (1/25 on the committed build; 1/9 with a sub-pixel value nudge, which lands on the stale native binding when the view was rebuilt with a new tag). Remounting the sheet's Animated.View on an epoch keyed to the hand-back mounts a fresh native view with the style computed from the current shared values - progress is already 1, so it paints in place with no visible animation. 0 dead in 50 attempts on the remount build under the same churn condition that reproduced the dead screen on base. LANE-REPORT.md carries the full confirmation evidence and limits. * chore: drop the stray lane report from the repo root It is a working artifact, not source, and the root directory guard blocks any new top-level entry. * test(mobile): assert the sheet subtree rebuild directly, not through a test-only prop The hand-back test proved the remount by reading an epoch-keyed nativeID that existed only for it — production markup shaped by a test, and an assertion a future refactor could satisfy without rebuilding anything. Count mounts of the sheet's content instead, which is the property the fix actually depends on, and drop the nativeID. Also stop typing test renderers as 'ReactTestRenderer | null'. The static analysis job installs no mobile/node_modules, so that type is unresolvable there and the union trips no-redundant-type-constituents on every added line. The hand-back re-assert is not dead code as the old comment implied: the drawer swap hands back at 166ms, before the 180ms enter animation ends. * fix(mobile): keep the create form when a render is thrown away The session key was built from counters mutated during render. A blurred screen suspends this subtree (react-native-screens freezes via react-freeze), so React runs the component and then discards that render — but the counter bumps survive it. The next committed render then produced a new key and remounted the form, throwing away the picked source for a host switch or a close that never committed. Hold the open epoch in state, which React discards with the render that set it, and put the host in the key directly instead of counting host changes. |
||
|
|
81ae98e10d |
fix(mobile): honor host worktree create retention (#16342)
* fix(mobile): honor host worktree create retention * fix(mobile): cover malformed worktree retention policy * fix(mobile): fail closed on malformed retention policy * fix(mobile): fail closed on missing dedupe ttl |
||
|
|
30bf2647fc |
fix(mobile): replay a delivery-ambiguous worktree.create instead of failing it (#15472)
* fix(mobile): replay a delivery-ambiguous worktree.create instead of failing it A socket close or response timeout rejects an in-flight worktree.create as delivery-unknown: the frame reached the wire, so the host may already have built the worktree. The client only replayed connection-migration cutovers, so every other ambiguity surfaced as a create failure for a create that may well have succeeded. Replay on the same clientMutationId — which the host already dedupes — after waiting for the transport to come back. * fix(mobile): bound the ambiguous worktree.create replay by the host's dedupe window The replay was bounded only by a retry count, but what makes a replay reconcile instead of building a second worktree is wall clock: the host drops a settled create's dedupe record 60s after it resolves, and past that the replay is just a fresh create that the host's suffix loop happily duplicates — for a folder workspace, into a second workspace with the very same name and no collision check at all. Two paths ran past that window: - The request-timeout path. A silently dropped response frame leaves the socket alive, so nothing rejects until WORKTREE_CREATE_TIMEOUT_MS — ten minutes, with no bound at all on when the host actually resolved. This was previously the path that replayed *soonest*, short-circuiting the reconnect wait because the transport still looked healthy. Invert it: every path that reports a real drop has already left 'connected' by the time the rejection surfaces, so still being 'connected' identifies the timeout and is now refused. - The reported-drop path. Worst-case detection is a full liveness idle period plus the missed-probe budget before the client even learns the socket is dead, and the old 20s wait on top of that overran the record. Derive the wait from the watchdog constants and the TTL instead of hardcoding it, and anchor a single deadline at the first ambiguity so a second wait gets the remainder rather than restarting. The TTL now has one definition shared by both processes, so the client asserts its budget against the host's real window instead of a copied literal. * fix(mobile): end the reconnect wait on a revoked pairing, and pin the wait's behavior waitForRpcClientReconnected resolves only on 'connected' or the timeout, but an 'auth-failed' client never reaches 'connected' — so a create interrupted by a revoked pairing sat out the full wait before surfacing the error it already had. Treat auth-failed as a terminal answer on both the fast path and the listener. The helper also shipped with no tests of its own: its already-connected fast path, its timeout path, and the synchronous-notification-during-subscribe teardown were only ever exercised indirectly through the retry suite, and neither RpcClient implementation notifies synchronously, so that branch had no coverage at all. Add a direct suite covering all of them, asserting listener and timer teardown rather than just the resolved value. Also give the fake-timer tests an explicit timeout. advanceTimersByTimeAsync yields through real macrotasks between ticks while vitest's own budget runs on real time, so on a loaded runner the default 5s is reachable — observed once as a spurious timeout in this suite. * fix(mobile): bound the ambiguous replay in wall clock, not timer time The replay window was derived from the liveness watchdog's own budget (idle + missed probes x probe timeout). That is a bound on how long the watchdog takes to *fire*, not on how much wall clock passed. iOS and Android suspend JS timers while the app is backgrounded, so across a background cycle the socket dies silently and the pending create rejects delivery-unknown minutes later with the timer-derived ceiling still reading ~44s. The replay then lands well past the host's 60s dedupe record and the suffix loop builds a SECOND worktree - for a folder workspace, one with the very same name and no collision check at all. Anchor the deadline on the watchdog's lastInboundAt instead: a wall-clock stamp of a frame that really arrived, so it stays honest across a suspension. Fall back to the send time when the transport can't vouch for one (relay sessions run with idleProbeMs: null), which errs toward refusing the replay. Also restore the delivery-unknown discrimination test that the still-connected guard had made vacuous, pin the still-connected guard itself against a live inbound stamp, and pin the deadline against being re-read from a fresher replacement session. |
||
|
|
ab9d1a29a9 |
fix(worktree): never reissue a generated workspace name (#14350)
* fix(worktree): never reissue a generated workspace name
Generated workspace names were deduped only against currently-live
worktrees, so deleting a workspace returned its name to the pool. A later
workspace could draw the same name, land on the same directory path, and
inherit the previous occupant's agent conversation history — coding-agent
CLIs key their prompt history and transcripts by cwd.
Names are now retired permanently per repo. The registry is written in
main with the name Git actually used (the create loop can advance past a
requested name on collision), and seeded once per run from workspace
directories and surviving agent transcript buckets so already-spent names
are excluded from the start. Suggestions degrade to -2, -3 variants
instead of recycling, and those variants retire too.
User-typed names are untouched: retirement filters suggestions only.
* fix(mobile): honor retired workspace names, on one shared implementation
Mobile hand-duplicated the desktop name-suggestion algorithm and deduped
only against live workspaces, so a phone could still be offered a name
whose deleted workspace left agent conversation state behind at that path.
Both platforms now call one shared selector in src/shared, so the two can
no longer drift. The host publishes retired names as an optional field on
the existing worktree.list response, and mobile fetches them per selected
repo while the create sheet is open — mirroring the desktop hook.
Mobile never calls worktree.list for its catalog (it uses worktree.ps,
which carries rows only), so this is a targeted request rather than a
change to the catalog or its cache. Hosts predating the field omit it and
mobile falls back to live-only dedupe, which is the pre-change behavior.
* fix(worktree): close retirement consistency gaps
* test(worktree): cover retirement runtime contracts
* fix(worktree): retire generated collision names
* fix(worktree): enforce retired names at creation
* refactor(ai-vault): extract the Claude project-dir encoder
The bucket-name encoder and its scope-boundary check were private to the
session scanner, so a second consumer had to reimplement them — and got the
per-character encoding wrong. Move both to a shared module with direct tests.
* fix(worktree): make the retirement seed scan actually match buckets
The bucket encoder collapsed runs of non-alphanumerics while the real one
emits a dash per character, so every dot-path bucket missed and the Windows
default workspace root (C:\...) matched nothing at all. Reuse the shared
encoder and its boundary check, which also stops a repo absorbing a sibling
whose path merely shares its prefix.
Also:
- Derive the workspace leaf by stripping the known encoded parent instead of
guessing from trailing dash segments, which retired the parent directory's
name whenever a workspace was named numerically.
- Reuse isAutoGeneratedCreatureBranchName so the -10 and -100 tiers retire.
- Drop the .codex/sessions root: Codex keeps the cwd inside the transcript
rather than in a directory name, so the scan could only ever see a year
folder. Reading transcript contents is not a trade this feature justifies,
so the gap is documented instead.
- Honor CLAUDE_CONFIG_DIR, which relocates the bucket root.
- Delete the unused retirableLeafName export.
Tests write buckets with the real per-character encoding against a fake home,
covering POSIX, dot-directory, Windows drive and WSL UNC roots; all three
platform cases fail against the previous encoder.
* fix(worktree): retire only generated names, keyed by cwd namespace
Two problems in the host-side registry.
Retirement fired for every create, including names the user typed. The
creature pool contains ordinary words — orca, runner, sole, molly, oscar — so
typing a retired 'nautilus' silently produced directory and branch
'nautilus-2' and burned the name for good. Creates now carry an explicit
nameWasGenerated flag; both the skip and the retire are gated on it, and it
defaults to false so CLI and automation callers are unaffected.
The registry was keyed by repo id, but both readers already discarded the id
and unioned by the cwd collision key, because the collision this prevents is
on the path. Keying by that namespace directly fixes several things at once:
entries no longer orphan when a repo is removed, remove/re-add no longer loses
every retirement for an unchanged path, the missing removeProject prune is
moot, and the backfill promise no longer merges into only the first repo id it
saw. The feature is unreleased, so no migration is needed.
Also:
- Memoize the collision key. It runs computeWorktreePath, which for a WSL repo
is a blocking execFileSync('wsl.exe') whose failure path is uncached, and
the previous code recomputed it once per repo on every create and every
listRetiredNames call.
- Drop retiredNamesByRepo from the worktree list result. It had no readers and
leaked onto 'orca worktree list --json', and its awaited backfill sat on CLI
selector resolution. The dedicated listRetiredNames RPC keeps its consumers.
- Make the three RuntimeStore methods required. RuntimeStore is file-private
with two constructors, so the 'older embedders' the optionality protected do
not exist, and the optional chain silently returned no retirements.
- Revert the unrelated forceDeleteBranch rewrite, and make room under the
file's line budget by extracting the create-args mapping instead.
* fix(worktree): send name provenance and stop gating Create on the fetch
Desktop and mobile now mark a create as generated-name only when the user
typed nothing and the composer fell back to the suggestion, so the host knows
which names it may retire.
Remove the retired-names loading gate from every create path. The host already
skips retired candidates before doing any git work, so the client gate bought
nothing while it could disable Create for the length of a full mobile
reconnect ladder (the wait had no timeout) and blank the desktop button
between queued creates. The suggestion still waits; the button never does.
Also make the web client call worktree.listRetiredNames instead of hardcoding
an empty list — the method is registered and mobile-allowlisted, so the
comment claiming no wire call existed was wrong — and filter the mobile
response to strings so a malformed row cannot throw during normalization.
* fix(worktree): key retirement by repo id and prune it with the repo
Reverts the collision-key storage key. It was a function of workspaceDir,
nestWorkspaces, worktreeBasePath and repo.path, so toggling any one of those
orphaned every retirement for every affected repo at once — trading a rare
churn (remove/re-add) for a common one. The read path already unions by cwd
namespace at query time, so cross-repo sharing never depended on the storage
key.
Instead, address the growth and orphaning directly:
- Drop the registry in removeProject, and in removeProjectForHost once the last
host's copy of the repo id is gone, alongside the sparse-preset deletes that
already follow this convention.
- Bound each repo's registry. The cap sits far above the 552-name pool because
evicting inside it would reissue a name whose agent state is still on disk;
only -2/-3 tier accumulation can ever reach it.
- Carry retirements through profile transfer, re-keyed to the destination repo
id and dropped from the source, mirroring sparsePresetsByRepo.
Separately, fix the backfill merge: the scan promise is cached per cwd
namespace, but it closed over the first repo id that triggered it, so a second
repo in the same namespace received nothing. The scan stays shared; the merge
moves out of the cached promise and runs for whichever repo asked.
Local repos re-seed on re-add through that backfill. SSH repos do not — the
scan cannot see the execution host — which is now stated in the module.
* docs(worktree): spell out why the retirement bound sits above the pool
Names the trap directly: the neighbouring 50/200 bounds cap histories, so
lowering this one to match them would silently start reissuing names whose
agent state is still on disk. Also states that oldest-first eviction is a
deliberate least-bad choice rather than a neutral one.
* fix(worktree): send name provenance from the web runtime client
This client hand-enumerates worktree.create params, so the new optional field
was silently dropped and typecheck could not see it. On web and paired-desktop
the host therefore never received it: generated names were never retired, and
the host-side skip that backstops a stale suggestion was disabled too. The same
client does fetch retired names for suggestions, so it was filtering against a
registry nothing ever wrote to.
The test asserts both directions, and fails without the fix.
* fix(worktree): retire names that took more than one collision suffix
isAutoGeneratedCreatureBranchName strips exactly one trailing -N, which is
right for auto-rename eligibility but wrong here. Once the pool is spent the
suggester emits nautilus-2, and a collision on that yields nautilus-2-3 —
which a single strip leaves as nautilus-2, not a pool name, so retirement
no-opped at exactly the tier where every base name is already gone. Strip
repeated suffixes locally rather than moving the auto-rename predicate.
* perf(worktree): keep the retirement backfill off the blocking WSL probe
The backfill runs on composer repo-select, not just at create time, and it
derived the probe path synchronously — which for a WSL repo with a mirrored
workspace dir reaches getWslHome and its blocking execFileSync('wsl.exe').
A stopped distro froze the main process for up to 5s on composer open.
Adds an async twin of computeWorktreePath and uses it for the probe. Resolving
the home there also warms the shared cache, so later sync callers are free.
Also stops memoizing the collision key when the WSL home is still unresolved:
only the success path is cached upstream, so caching the fallback namespace
would strand the repo there for the rest of the session.
* fix(worktree): hold retired names across a refresh instead of blanking
refreshKey changes on every workspace-list mutation, so create-multiple
refetches after each create and the hook returned an empty list until the
refetch landed — precisely the window in which resetForNextCreate clears the
name field and a fresh suggestion is drawn. Keep the previous answer while
revalidating and reset only when the repo changes; a failed refresh keeps what
was already loaded rather than un-retiring everything.
Also makes the returned array referentially stable, so the suggestion memo
downstream stops rerunning on every refetch.
* refactor(worktree): put the retired-name cache rules on one implementation
The desktop and mobile hooks that fetch retired names had already drifted
four ways. The transports genuinely differ (IPC vs RPC), but the caching
rules must not, and mobile's copy reset to [] on any error -- which
un-retires every name for the rest of the sheet session, the one outcome
retirement exists to prevent.
Moves the rules into src/shared/worktree/retired-name-cache: response
normalization, the never-leak-across-repos rule, and the hold-previous-on-
failure rule. Pure, no React, because src/shared is on the main process's
import graph. Each platform keeps its own transport and effect.
Mobile moves up to desktop's behavior: it now holds the previous answer
through a failed refresh, and refetches when the workspace list changes
instead of never refetching after mount.
Also drops the unused `loading` return. Neither platform consumed it; its
only consumer was the Create-button gate reviewed out earlier, and removing
it makes that regression unexpressible.
* fix(worktree): import shared types from their real modules
Main dropped the src/shared/types barrel, so the retirement module's import
resolved locally but not against the PR's merge base.
* refactor(worktree): bound the retirement registry by tier compaction, not eviction
Retirement is a correctness guarantee — a spent name's directory may still hold
agent conversation state keyed by that cwd — so the 2000-entry cap was the wrong
shape: reaching it handed a name back. At the owner's measured rate (~6.6 pool
names retired per day in one repo) the cap was ~9 months out.
Names come from a fixed 552-entry pool and the suggester only reaches tier N+1
once every tier-N name is taken, so a completed tier is exactly a set that no
longer needs listing. A row is now a watermark plus the names above it: reads
answer at-or-below the watermark with no lookup, and compaction drops the 552
entries the watermark now covers. Bounded at one pool per repo forever, with no
eviction and nothing un-retired.
Tiers can complete out of order (a create-time collision can spend `nautilus-2`
while tier 1 is open), so compaction loops and higher-tier names simply wait.
The RPC result carries the watermark beside the names as a new field; a client
predating it reads the names only and under-retires the compacted tiers, which
degrades to the pre-retirement behavior rather than breaking.
* fix(worktree): preserve generated name retirement across failures
|
||
|
|
266b5ae8f5 |
fix(mobile): match desktop project and run target picker (#14457)
* fix(mobile): disambiguate repository locations * fix(mobile): preserve explicit repository ownership * test(mobile): use explicit renderer type * refactor(mobile): match desktop project targets |
||
|
|
77f23b013f |
refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as a re-export barrel so the import sites did not have to change. This removes the barrel: every consumer now imports from the module that actually declares the type, and `src/shared/types.ts` is deleted. Barrels hide where a type lives, make every consumer look like it depends on the whole domain, and let an unrelated edit invalidate a module that ~2,000 files transitively import. 2,323 import declarations across 2,321 files. Rewritten mechanically: each specifier was resolved to an absolute path via the TypeScript AST and recomputed, rather than string-substituted, so alias forms (`@/../../shared/ types`) and per-specifier `type` modifiers survive. Four cases the mechanical pass had to handle, each found by a gate rather than by reading the diff: - Modules inside `src/shared` import the barrel as `./types`, not `shared/types`. A pre-filter on the latter string skipped 176 of them and left imports dangling at a deleted file, which surfaced as confusing `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>` errors rather than "module not found". - The barrel RENAMED one type on the way through (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name in the owning module has to be re-aliased at each consumer. - Three test files put `;(globalThis as ...)` on the line after the import. TypeScript parses that `;` as the import statement's terminator, so replacing through `statement.getEnd()` deletes it and breaks ASI. The rewrite now stops at the module specifier. - A file that already imported directly from a module got a SECOND import from it, because the barrel re-exported those same names — which trips `import/no-duplicates` under `--deny-warnings`. A post-pass merges declarations sharing a specifier and type-only-ness; the `import type` plus `import` pair from one module is left alone, since that form is allowed. Splitting one barrel import into several genuinely adds lines, which pushed `terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character import must wrap, and neither local type collapses onto one line (101 and 116 characters). Rather than contort a type declaration to fit a line budget, `collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` — they are pure structural operations on the layout tree and independent of PTY ownership. `visible-worktrees.ts` similarly loses its own mini-barrel re-export of `isDefaultBranchWorkspace`, with the four real consumers repointed at the declaring module. No `max-lines` bypass added. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches); the full `pnpm lint` green, not just bare oxlint — the narrower local check is what let the duplicate imports reach CI; max-lines ratchet OK at 344. |
||
|
|
583ab1601b |
refactor(shared): group worktree, github, and linear modules into folders (#14437)
`src/shared` is a flat directory of ~1,150 entries. The worktree, github, and
linear domains accounted for 71 of them, so finding the module you wanted meant
scanning a wall of same-prefixed filenames.
Move each domain into its own folder and drop the now-redundant prefix:
src/shared/github-pr-types.ts -> src/shared/github/pull-request-types.ts
src/shared/worktree-id.ts -> src/shared/worktree/id.ts
src/shared/linear-links.ts -> src/shared/linear/links.ts
This follows the existing `network/` and `new-workspace/` convention in the
same directory, which also drop the prefix inside the folder.
Whole clusters move, including tests. Foldering only part of a domain would be
worse than flat: a reader would have to check both `github/` and the flat
directory, and `github-auth-types.ts` / `github-project-types.ts` are type
modules that belong with the rest. No files with these prefixes remain flat.
Import specifiers were rewritten by resolving each one to an absolute path and
recomputing it, not by string substitution, so the `@/../../shared/...` alias
forms are handled correctly. 501 specifiers across 298 files.
Two things `tsc` cannot catch, handled explicitly:
- `github-project-types.ts` carries its own `max-lines` bypass, so its baseline
entry is REPOINTED to the new path rather than pruned. Pruning would drop the
bypass and then flag the new path as a fresh violation. Ratchet stays at 345.
- `mobile/` is outside `pnpm typecheck` and cannot be typechecked here
(`mobile/node_modules` is empty). Instead every relative specifier in the repo
was resolved against the filesystem: 174 unresolved before this change and 174
after — identical, so nothing broke in mobile either.
The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT
rewritten; they are a snapshot of an older release and still reference the old
paths.
Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches).
|
||
|
|
9796f7dc5f | refactor(mobile): use shared GitHub Project sorting (#13459) |