* refactor(providers): delete the SSH git and filesystem providers and relay-only wiring
* refactor(ssh): drop relay-protocol exports only the deleted stream readers used
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* refactor(ipc): filesystem and repo handlers are local or runtime only
* test(ipc): drop two casts in the rewritten listing and import tests
* fix(providers): keep the default error name so IPC callers see the old text
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
* Keep repository detection off the main IPC thread
Share repository discovery decisions across synchronous compatibility callers and the existing asynchronous Git runner. Await discovery in project registration, nested imports, and folder upgrades, and recheck folder ownership after discovery completes.
* Make repository detection fixtures work on native Windows
* Await asynchronous Git checks in folder upgrade fixtures
* Prevent duplicate project registration after async discovery
* Check authoritative registration identity before renderer convergence
* Narrow repository responsiveness to detection and registration
* Keep one local project identity after concurrent registration
* Run registration SSH coverage in the existing Docker lane
* Await Git discovery when scanning and importing nested projects
* test(git): cover classifier failure stages and literal path identity
* test(git): use the current process host package
* test(repos): use current process host in registration fixture
* feat(agent-launch): accept a create-folder-workspace target
agent.launch can now create a folder workspace and start its agent there,
the same operation it already runs for a new git worktree. The host
creates the folder workspace (metadata only, no startup terminal), resolves
it the way an existing target is resolved, then settles the mode and builds
the surface as for any existing folder workspace.
Hosts advertise agent.launch.create-folder-workspace.v1 so a client offers
the target only to a host that accepts it.
* fix(agent-launch): authorize, classify and navigate folder creates like their create method
A create-folder-workspace launch could do three things its two-call
equivalent never did:
- A phone could create folder workspaces. The mobile allowlist admits
agent.launch but not folderWorkspace.create, and is checked by method
name only. A create target now needs the authorization of the create
method it stands for, read from the same allowlist, and a phone is
refused with agent_launch_target_forbidden before admission, so no
ledger row and no create. create-worktree maps to worktree.create,
which phones already may call, so nothing changes for it.
- A create refused before anything was stored (group gone, folder
missing, SSH host unreachable) replayed as
agent_session_operation_unknown, and a retry under the same operation
id was refused until the row expired. runtime.createFolderWorkspace now
raises those refusals as FolderWorkspaceCreateRefusedError, with the
same message text, and the launch records it as failed for any target.
A failure after the create still replays unknown.
- A paired caller's folder create activated the new chat on the host
window and did not select the caller's own tab. Only create-worktree,
whose host activation runs setup, keeps the host navigation now; a
folder create navigates like an existing workspace.
Tests cover the phone refusal, replay of a success, a refused create
settling failed and replaying that failure, an after-create failure
staying unknown, an SSH connection reaching the agent's view mode, paired
navigation, and an older host refusing the target at parse.
* fix(agent-launch): replay a refused folder create with its whole path
A folder create refused before anything was stored is recorded as the
launch's failure, and a retry under the same operation id answers from
that record. The record went through the 128-character cap every launch
failure code gets, and folder refusals carry the folder's absolute path,
so a retry could show the user a path cut short: a different folder.
The refusal is now recorded whole when it fits a path as long as Linux's
PATH_MAX after its code. Past that, which no folder a stat accepts
reaches, only the code before the path is kept, so the replay degrades to
the bare code instead of a partial path.
Also: the controller test asserts the exact refusal message, the
create-folder-workspace capability comment says a phone needs its own
gate, and the forbidden-target code says why it is not `forbidden`.
* test(folder-workspace): type the controller test's store stub without an assertion
The changed-code quality gate refuses the `as never` cast on the stub
store. It is now a RuntimeStore whose unreached members throw.
* test(agent-launch): run the folder-create launch tests in the Node runtime project
The operationId replay tests open the real SQLite record store, so the
file belongs with the other ledger-backed agent.launch tests.
* test(agent-launch): pin that a paired folder create's terminal stays off the host window
The folder-create test now asserts surfaceOwner: false on the terminal a
paired caller's launch creates, rather than matching any options. The
caller-selection header now says only an in-process worktree create keeps
worktree.create's navigation.
* test(agent-launch): drop the per-connection view-mode expectation main removed
* perf: bound wildcard segment work in nested repository scans
* perf: bound the ** path walk in nested repository scans, not just one segment
The wildcard-segment fix left the larger blowup in place. A short .gitignore line
made only of `**` segments still costs exponential work in the outer path walk:
`**/**/.../z` at 24 segments and 73 characters, against an eight-segment
candidate, takes about 49 million recursive calls and ~150 ms here — larger than
the 25 ms single-segment case this branch removes. Rules are inherited down the
tree and re-checked for every directory, so that price is per directory, the
runtime's 15s scan timeout fires, and the user silently gets a short repo list.
Two independent bounds, both kept:
- `**` spans zero or more segments, so `**/**` accepts exactly what `**` accepts.
Parsing now collapses a run of them to one segment, taking the reported shape
from 49M recursive calls to 26 matcher steps.
- The walk memoizes on (pattern index, candidate index), so no other arrangement
of `**` can reintroduce the blowup. A rule holding at most one `**` is already
linear and skips the table, because allocating it costs more than the walk it
would save on the shapes real ignore files contain.
The budget suite's process-CPU ceiling is replaced by a matcher step counter read
through `readNestedRepoGlobMatchSteps`, so an algorithmic regression fails the
suite rather than passing on a fast machine. Each bound has its own budget case,
and each fails when only the other is applied.
The equivalence oracle now also covers multi-segment and anchored patterns
including `**`, checked against the pre-change per-segment expression walking
uncollapsed segments. Code-unit and metacharacter cases are unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* perf: skip folder-scope construction for separate repository imports
* refactor(project-groups): share one mode flag between scope skip and root guard
Also cover the separate-import path with real repo paths, which the throwing
getter test no longer exercises.
---------
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
#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.
110 files carried an eslint/oxlint-disable max-lines directive but are
already under the default max-lines budget (300 .ts / 400 .tsx / 600 .mjs
/ 800 test), so the suppression is dead. Removing it restores real
max-lines coverage on these files with zero behavior change.
Each removed directive had max-lines as its only rule; verified via a
full oxlint run (0 max-lines violations, 0 new errors). Diff is pure
deletions (200 lines, 0 additions) — no code touched.
Co-authored-by: Orca <help@stably.ai>
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules
Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.
Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):
error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse (19: copy-then-reverse -> toReversed)
warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type (aliasing footgun guard)
- typescript/no-unsafe-function-type (bans bare Function type)
- unicorn/prefer-array-flat-map (map().flat() -> flatMap())
- unicorn/prefer-regexp-test (.match() in bool ctx -> .test())
mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.
Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.
* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse
mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).
Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>