remotePathFromUrl now rejects a URL query string or fragment, so a mistyped ssh://host/tmp/a?draft no longer silently operates on /tmp/a; a literal ?/# in a remote filename must be percent-encoded (%3F/%23).
ssh:// transfers now require a sh/bash/zsh login shell. fish can't parse the POSIX transfer snippets and csh/tcsh apply ! history expansion to the command line, so they're refused (use the ssh tool). Host-shell detection no longer misclassifies fish/csh/tcsh as sh (basename allowlist over endsWith), and HOST_INFO_VERSION is bumped to re-probe caches that stored the old classification.
Upstream main 52b8fb156 changed normalizeGeneratedTitle to reconcile a generated title's casing against the user's message instead of forcing Title Case, and updated the tiny-text unit tests but not the title-generator/role-thinking integration suites. The rebase surfaced their stale Title-Case expectations as CI failures. Update the 6 expectations to the reconciled output; no production code change.
The rebase split the ssh:// entries: Added merged into the new [Unreleased] but the Fixed bullets landed under the released [16.1.23] section (where main's release script had moved the old [Unreleased] Fixed anchor). Moved the ssh:// Fixed bullets back under [Unreleased]; main's 16.1.23 and other released sections are verbatim.
dirname is an operand-only POSIX utility (no options), so 'dirname -- "$t"' is non-portable on macOS/BSD (the -- is treated as an operand). The temp path is always absolute, so the end-of-options guard is unnecessary; drop it. mkdir -p -- stays (mkdir conforms to the Utility Syntax Guidelines and supports --).
The >4 MiB JS fallback re-introduced regex-dialect divergence by size. Oversized line-mode virtual resources are now searched in line-boundary chunks (each <= NATIVE_GREP_MAX_FILE_BYTES) through native grep, with matched line numbers offset by each chunk's start, so RE2 dialect parity holds for all line-mode sizes. A single line larger than the cap (un-grepable) is JS-tested individually. Multiline keeps the JS fallback, since chunk boundaries would drop cross-line matches. Test now proves (?i)NEEDLE matches a >4 MiB resource.
decodeOr fails open, so a bad escape (ssh://prod%ZZ, ssh://user%ZZ@prod) passed the canonical authority check (both sides equally fail-open) and reached OpenSSH literally. resolveTarget now validates the encoded authority parts (url.username, url.hostname) with a throwing decodeURIComponent up front and rejects malformed escapes, matching the path decoder's fail-closed behavior. Valid percent-encoded aliases (alice%40prod) still pass.
- search: native grep silently skips files above NATIVE_GREP_MAX_FILE_BYTES (4 MiB), so the RE2 virtual path dropped matches for large virtual resources (history://, big artifacts). searchVirtualResources now falls back to a JS RegExp matcher for oversized content (the pre-RE2 behavior) while keeping native parity for normal sizes; buildVirtualMatches still rebuilds context/ranges.
- ssh write: a trailing-slash target (ssh://h/dir/) is now rejected before staging, so the mkdir -p parent-creation no longer leaves a directory behind on a refused write.
- search: the native virtual probe capped matched-line detection at INTERNAL_TOTAL_CAP before range filtering, so a ranged virtual selector (ssh://h/log:5000-5100) over a file with >2000 earlier matches returned nothing. The probe now uses the line-count bound for ranged resources so range filtering sees every hit.
- ssh write: writeRemoteFile staged into a temp beside the destination before creating parents, so writing a new nested path failed with 'No such file or directory'. It now mkdir -p's the parent before staging, matching local write, keeping the directory/special-file refusal checks.
An empty password marker (ssh://user:@prod, ssh://:@prod) leaves url.password === '' so the truthy password check skipped it. Added a canonical-authority backstop after the explicit guards: reject any authority whose decoded rawHost differs from the canonical [user@]host[:port] WHATWG parsed. This catches empty-password (and any future stray/empty marker) while every valid authority — including percent-encoded reserved-char aliases (alice%40prod, %5Bprod%3A2222%5D) — reconstructs to exactly rawHost.
searchVirtualResources matched with JS RegExp, so an ssh:// (or other virtual) search diverged from local search for RE2-valid but JS-invalid patterns (e.g. (?i)x, [[:digit:]]) — throwing 'Invalid regex' or returning different matches even when local grep had already validated the pattern in a mixed scope. It now detects matched line numbers with native grep (the same RE2 matcher local search uses) and rebuilds the existing forward-only, range-trimmed context windows via buildVirtualMatches, so virtual/remote and local search share one dialect. Removed the now-dead JS-regex helpers (probeRegexDialect, compileVirtualRegex, searchVirtualResource{Lines,Multiline}, findLineIndex).
resolveTarget now rejects a non-empty password (ssh://user:pass@host, ssh://:pw@host) since ssh:// uses key/agent auth (BatchMode), and a literal empty username (ssh://@host) that previously fell through to the bare-host fallback and could match a configured host. Both use the same decoded-authority comparison as the empty-port guard, so a percent-encoded alias like %40prod (decodes to @prod) is still matched.
search's generic glob-char guard saw the [ ] of an IPv6 authority (ssh://[::1]/etc/hosts) and threw 'Glob patterns are not supported' before the SSH handler could strip the brackets. The check now runs only on the path portion for ssh:// URLs, so IPv6 literals resolve while a glob in the remote path (ssh://host/p*) still rejects.
resolve() ran readRemoteFile (head) before any classification, so a FIFO with no writer blocked until the 30s timeout and a device like /dev/zero streamed the whole probe. It now stats first: directory -> listing, special file (other) -> fast refusal, regular file -> read. missing/stat-failure still falls through to the read so the original remote stderr surfaces.
The override-rejection compared the still-encoded url.hostname, so a user/port override on a reserved-char alias (configured alice@prod addressed as ssh://bob@alice%40prod/) slipped through to the opaque target (bob@alice@prod). The guard now also compares the decoded bare host; the error names the decoded host while suggesting the correct encoded URL.
The override path stored url.username/url.hostname verbatim, so ssh://user%40corp@host/ reached OpenSSH as user%40corp@host (wrong user). Decode both before building the SSHConnectionTarget. Decoding happens before buildSshTarget's leading-dash guard, so an encoded %2DoProxyCommand now decodes to -oProxyCommand and is rejected.
The empty-port guard compared decoded url.rawHost against the percent-encoded url.hostname/username, so ssh://prod%2Dblue:/ and ssh://u%2Dname@prod:/ slipped through. Decode both sides before comparing; a %3A alias (decoded host already ends in ':') reconstructs to 'prod::' and is still left alone.
The rebase auto-merged the ssh:// Added/Fixed bullets into the released [16.1.20] section (where main's release script had moved the old [Unreleased] content they were anchored to). Moved them back under [Unreleased]; main's 16.1.20/21/22 sections are untouched.
An empty port (ssh://host:/path) parses with url.port === "" and slipped past the malformed-authority guard as "no port", silently using the default/configured target. resolveTarget now rejects it (raw authority retains the trailing colon; percent-encoded aliases like prod%3A keep %3A in hostname and are unaffected).
- reject explicit ssh:// port 0 before connecting (Codex P2)
- keep a path-less ssh://host:port authority port out of selector peeling
- restore ResolveContext on ProtocolHandler.complete (symmetry with resolve/write)
- clarify search/read selector-parity docs + add read-side regression
- ssh handler complete() suggests the configured hosts; bare `read ssh://` resolves to an immutable host index (markdown links per host)
- thread a minimal cwd through the completion pipeline (provider basePath -> getInternalUrlSuggestions -> router.complete -> handler.complete) so project-scoped ssh.json hosts resolve like the cwd-aware bare-read path
- scoped to configured SSH-capability hosts; opaque ~/.ssh/config aliases stay usable at ssh://alias/path but are not enumerated
- local handler complete() signature updated to match (behavior preserved; the router never threaded context to it)
- `read ssh://host/dir` lists a remote directory one level deep; `ssh://host/` lists the remote root
- add statRemotePath + listRemoteDir; resolve reads first and classifies on error (directory -> one-level listing, dirs-first, dotfiles included)
- directory resources carry isDirectory + immutable and expose no sourcePath
- search refuses a virtual (no-sourcePath) directory resource instead of grepping the listing text
- writeRemoteFile refuses a directory destination and cleans up its temp on that path
- buildSshTarget rejects destinations beginning with "-" (SSH argument-injection / local RCE guard)
- gate ssh:// read/search/write at the exec approval tier; substring scan covers search's pre-expansion delimited paths and write's hashline-wrapped paths
- validate the entire materialized buffer as UTF-8 instead of only the first 8 KiB prefix
- write peels read selectors (raw/conflicts) so it targets the same file read does, and rejects line-range/malformed selectors instead of silently stripping them
- write to a uniquely named remote temp; document symlink-replacement on write as a v1 limit
- Updated `normalizeGeneratedTitle` to reconcile model-generated titles against the user's input instead of forcing title-case.
- Added logic to restore distinctive proper-noun casing (e.g., `TinyVMM`) and flatten model-generated camelCase artifacts (e.g., `dAemon`) that do not appear in the user's message.
- Ensured model-cased proper nouns that are not in the source message (e.g., `GitHub`) are preserved.
Fixed a non-deterministic gc-cli archive test: archive-me and keep-recent
shared ageDays:90, so their mtimes tied within a millisecond on fast CI and
the retainNewestGlobal:1 'keep newest' pick fell back to readdir order,
archiving the wrong session. Give keep-recent ageDays:60 (still cold-eligible,
unambiguously newer).
- Integrated progress-lexicon analysis to identify and warn on low-information lexical stalls during reasoning.
- Enhanced loop stream processing to strip summarizer titles before performing analytical checks.
- Introduced CONCRETE_ANCHOR pattern and windowed state management to improve detection of novel reasoning references.
- Refactored the test suite to use a standardized feed function for chunked streams and added coverage for specific stall scenarios.
- Added instructions for writing sections as cohesive multi-line blocks when performing edit operations.
- Clarified that block operations require multi-line sections to avoid falling back to standard editing behavior.
- Added tree-sitter markdown support to resolve headings into full sections in `pi-ast`.
- Enabled block operations (`SWAP.BLK`, `DEL.BLK`, `INS.BLK.POST`) on markdown headings so they encompass the entire section, including nested deeper headings.
- Updated system prompt to guide agents in using structured markdown heading edits for plans.
- Fixed `plan-mode-guard` to correctly resolve local protocol options for subagents.
The createAgentSession default-role resolution ran before extension
factories registered their providers, so a default role pointing at an
extension-provided model (e.g. an openai-compat plugin's
posthog/claude-opus-4-8) returned undefined there. On a fresh launch
(no -c/--resume) the post-extension fallback went straight to
pickDefaultAvailableModel and replaced the user's configured default
with the first bundled provider default that had auth — commonly
openai/gpt-5.5 when OPENAI_API_KEY was set.
The fallback now retries resolveModelRoleValue against the
post-extension allowed-model set before pickDefaultAvailableModel, and
re-applies the role's explicit thinking selector / model host
preconnect.
Fixes#3569
Detected whether the OMP host already owns an inheritable Windows console before resolving stdio MCP spawn flags.
Skipped CREATE_NO_WINDOW for console-attached MCP wrapper chains so cmd.exe and PowerShell grandchildren reuse the existing terminal instead of allocating visible conhost windows.
Fixes#3567
- Removed multiple test files and cases that relied on brittle source string matching for validation.
- Updated project architecture documentation to explicitly prohibit source-grep style testing patterns.
- Eliminated legacy reproduction tests for issues that reached project maturity.
- Added support for `--quiet` (`-q`) and `--line-regexp` (`-x`) to the `grep` builtin.
- Enabled short-circuiting behavior for `-q` to suppress output and return early on the first match.
- Configured exit status logic to prioritize successful matches over error states when using `-q`.
- Added integration tests to verify correct exit status codes and line anchoring behavior.