Expand numeric-leading Linear identifier coverage in the current parser and workspace source flow while rejecting numeric-only team keys in bare inputs and URLs. Main already implements the production behavior through #23423.
Repair two stale localization test assumptions found by full CI. All changes are regression tests.
Validation: 369 focused tests across 46 suites, local typechecking/lint/formatting, and full final-head CI passed.
Co-authored-by: Ken Fukuyama <kenfdev@gmail.com>
* feat(secrets): warn in Settings when a credential is stored unencrypted
When no OS keyring is usable, the MiniMax stores write the credential as a
plaintext envelope and say so with a console.warn nobody reads. The users this
affects are exactly the ones who never see a main-process log, so in practice
they were told nothing (#21827).
Report it where the credential is managed instead. Each store gains a
protection reader, the status IPC carries it, and Settings renders a warning
next to the credential it applies to.
Keyed on the stored bytes, not isEncryptionAvailable(): a credential saved
before a keyring existed stays plaintext until it is saved again, so reporting
current capability would call it protected while the file says otherwise. The
readers parse the envelope kind without decrypting, so opening Settings cannot
provoke a keychain prompt.
The console.warn stays. It carries no secret material, and it is still the only
signal on a headless host with no Settings window.
* feat(secrets): extend the unsealed-credential warning to every affected store
The speech key, Linear tokens, Jira tokens and the Bitbucket credential have
the same plaintext fallback the MiniMax stores do, and the same console-only
warning nobody reads.
Add a shared `readCredentialFileProtection` for the four stores that write bare
ciphertext with no envelope, classifying with the same printable-UTF-8 test
`readStoredCredentialToken` already uses — so the reporter cannot drift into
disagreeing with the reader about the same bytes.
Linear and Jira report across every stored workspace/site rather than the
active one: sealing is a host-wide property, so a second workspace stored while
the keyring was missing is exposed even when the active one is sealed. Both
fields are optional, so an older remote host that omits them reads as unknown
rather than as sealed. Bitbucket reports null for env-supplied auth, where Orca
stores nothing and has no claim to make.
Also fixes the credential-connection test double, whose identity-function
`encryptString` wrote a readable token — faithful enough for a round-trip
assertion, but it made the suite assert that a sealed credential was exposed.
* chore(i18n): extract the unsealed-credential notice strings
CI's localization-extraction gate requires every translate() key to exist in
the primary catalog. Inserted in place rather than re-sorting the file, which
is not fully sorted and would have produced a 17k-line diff.
* test(web): pin the null protection fields on the desktop-only MiniMax bridge
The web bridge reports no protection because it stores nothing; the shape
assertions had to move with it.
Audit sweep over `src/relay`, `src/preload` and `src/shared` (1,087 test files
reviewed). 101 case declarations removed across 40 files, 6 test files deleted
outright, 1,143 lines gone. Executed-case count falls further, since several
removals were `it.each` tables.
Dominant patterns, by frequency:
- Self-comparisons that cannot fail: `expect(f(x)).toBe(f(x))`,
`JSON.parse(JSON.stringify(literal))` deep-equalling the literal for a type
with no codec, and `normalizeKeyToken(t) === normalizeKeyToken(t)` presented as
proof of memoization.
- Object literals asserting their own fields back, where the guarantee comes from
the type annotation and the runtime assertion cannot fail.
- Copied inventories: constants compared to their own initializers, and a
function returning a copy of an exported constant checked against that
constant's literal contents.
- Duplicate invocations of a contract owned at a stronger boundary, including
provider-local replays of a shared helper.
- Table rows varying a field production never reads, so every row runs one path.
- Names promising more than the input exercises: a "Windows launch" case in a
module with no platform input, and a case whose named branch is never entered.
Two production symbols go with them, each a test-only export whose sole caller
was a deleted case:
- `getGitHubProjectRefInputByteLength` — a one-line forward to
`getClipboardTextByteLength`. The real bound
(`GITHUB_PROJECT_REF_INPUT_MAX_BYTES`) and its guard stay.
- `GRAB_STYLE_PROPERTIES` — an intended shared source of truth that nothing ever
consulted; the property set is hand-enumerated at three independent sites.
One case was deliberately restored and strengthened rather than dropped. The
relay integration suite is the only place the real `SshChannelMultiplexer` is
wired to `RelayDispatcher`, so it reaches transport behavior the handler suites
cannot (they use `createMockDispatcher`). Its `fs.writeFile` roundtrip is the one
case producing a void result, and `JSON.stringify` drops an absent `result`
member — a shape no other surviving case exercises. Restored with an assertion
pinning what the client actually observes: `null`, not `undefined`. That
assertion failed on first run, so the fact was previously unasserted anywhere.
One deletion was reverted mid-audit. A case asserting that optional fields stay
invisible to "old attach and ready decoders" builds those decoders from `z.object`
schemas declared in the test file, so it demonstrates zod's unknown-key stripping
rather than anything shipped. It is nonetheless the only forward-compatibility
coverage these envelopes have, and `reliability-gates.jsonc:6232` names it as
evidence verbatim, so it stays. Note that `check-reliability-gates.mjs` passed
both with and without it: the script resolves manifest paths and commands, and
does not check that a named assertion still corresponds to a live case.
Kept deliberately: everything a reliability gate cites as evidence; the three
`registers all expected handlers` RPC manifests (a dropped registration is a
silent wire break no type checker catches, and one carries the STA-4571
`pty.ackData` ratchet); the `child-process` direct-import ratchet; and
prototype-spy cases paired with a `.repeat(10_000)` input, which assert a real
memory bound rather than merely forbidding a technique.
Verified: `pnpm test src/shared src/relay src/preload` (1073 files, 11996
passed, 1 pre-existing `it.fails`, 131 skipped), `pnpm tc` after clearing
`.tsbuildinfo`, `check-reliability-gates.mjs` (140 gates),
`check:code-quality:changed` (0 new findings).
* Avoid Linear read re-fetches when workspace scope is unchanged
Derive a stable scope signature that captures only the connected state
and workspace identity, ignoring volatile metadata like displayName.
Use this in dependency tracking so Linear searches don't re-run on
status updates that don't affect which issues can be queried.
* Expand workspace scope to detect credential and org changes
Cache invalidation key now includes credentialRevision and organizationUrlKey for
both workspace and viewer, ensuring Linear reads re-fetch when credentials rotate or
organizations are renamed — fields that affect what read operations return.
* Include activeWorkspaceId in workspace scope signature
URL lookup falls back to the active workspace even when all workspaces
are selected, so activeWorkspaceId must be part of the scope signature
to ensure reads are keyed correctly.
Fixes STA-5076.
list-issues capped at 50 by default and hard-clamped at 250, with hasMore buried
under result.meta and no stderr warning for --json, so a page that stopped early
read as a complete answer. Omitting --limit now walks Linear's pages until they
run out (meta.limit is null), and --limit <n> is the only cap, paging past
Linear's 250-per-request maximum to reach it. result.truncated sits next to
result.issues and is set only when a cap actually held results back; human output
prints "truncated: showing N".
The read still has to fit the CLI's 60s RPC budget, so a 20s wall-clock deadline
and a 200-page ceiling stop the walk early and report truncated with a
continuation cursor rather than failing the command.
Also:
- issued --cursor values bind the resolved workspace, so call -> nextCursor ->
call works without --workspace; raw Linear cursors still need one and now carry
nextSteps
- issued cursors whose payload smuggles back `all` or an empty workspace are
rejected at decode, since either would widen the read past the bound workspace
- JSON issue rows carry priorityLabel (none/urgent/high/medium/low), matching
orca linear priority set
- truncated and priorityLabel are optional on the wire, so a host that predates
either is not read as "complete"; readers fall back to meta.hasMore
- the truncation line prints the rows actually rendered, so a remote result with
no meta.returned cannot print "showing undefined"
#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.
`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).