Skip to content

Safely handle action inputs - #187

Merged
dtolnay merged 2 commits into
masterfrom
pwn
Oct 9, 2026
Merged

dtolnay merged 2 commits into
masterfrom
pwn

Conversation

@dtolnay

@dtolnay dtolnay commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Closes #179.

@dtolnay
dtolnay merged commit e2a55d2 into master Oct 9, 2026
32 checks passed
@dtolnay
dtolnay deleted the pwn branch October 9, 2026 01:48
rapp992 pushed a commit to Felag-Engineering/relay that referenced this pull request Oct 9, 2026
Upstream moved the v1 tag to e2a55d2 (dtolnay/rust-toolchain#187,
"Safely handle action inputs": multi-line-safe GITHUB_OUTPUT writes and
argv-array rustup flags instead of string-built command lines). zizmor's
online ref-version-mismatch audit now fails every PR's lint step because
our six SHA pins, commented '# v1', point at the old head 7e38f4b. The
new commit descends from the old pin and changes only action.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
(cherry picked from commit 0a3a00a)
bryanfawcett added a commit to mzizi-dev/mzizi that referenced this pull request Oct 9, 2026
…able branch

workflow audit (zizmor 1.30.1) failed on #111 and #112 with four high
impostor-commit findings, all on dtolnay/rust-toolchain@89b1218 (# stable),
used in ci.yml (three jobs) and supply-chain.yml. The action's stable branch
is regenerated by force-push: git ls-remote shows stable at 686976e (2026-10-08)
and 89b1218 (a 2026-10-01 'toolchain: stable' commit) is no longer in its
history, so the pinned commit has no history in the repository. The failure is
on staging's workflows, not in either pull request.

Pinned instead to e2a55d2, the head of master and the v1 tag, which master's
history keeps reachable, with toolchain: stable passed explicitly: master's
action.yml differs from stable's only in toolchain being required (no default).
git diff 89b1218 686976e shows the newer action only hardens its input handling
(dtolnay/rust-toolchain#187).

Measured: zizmor 1.30.1 --offline over .github/workflows: no findings
(1 ignored, 8 suppressed). The online impostor-commit audit needs the
workflow token and runs in CI. prettier 3.9.4 --check passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1aSrvXhc8FYbBWpr8yFqx
rapp992 added a commit to Felag-Engineering/relay that referenced this pull request Oct 9, 2026
Upstream moved the v1 tag to e2a55d2 (dtolnay/rust-toolchain#187,
"Safely handle action inputs": multi-line-safe GITHUB_OUTPUT writes and
argv-array rustup flags instead of string-built command lines). zizmor's
online ref-version-mismatch audit now fails every PR's lint step because
our six SHA pins, commented '# v1', point at the old head 7e38f4b. The
new commit descends from the old pin and changes only action.yml.


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

Co-authored-by: Claude <noreply@anthropic.com>
rapp992 added a commit to Felag-Engineering/relay that referenced this pull request Oct 9, 2026
…re-issued call never double-dispatches (#957)

* relay: adopt Go 1.26.9 for twelve new stdlib advisories

The pull-request govulncheck gate now fails every Go PR on twelve newly
published standard-library advisories reached from the Relay's code:
GO-2026-6599/6600 (html/template), 6603/6605/6609/6610/6611/6612/
6613/6617 (net/http), 6607 (crypto/tls) and 6608 (net/textproto,
mime/multipart). Go 1.26.9 changes each of those packages (e.g. the
new 200-range cap on Range headers, GODEBUG httpservecontentmaxranges).

Every go directive moves 1.26.8 -> 1.26.9: relay/go.mod and the five
tools/* modules, so tools keep building with the module's toolchain.
No dependency changes; go.sum files are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
(cherry picked from commit e9c94f3)

* relay: bump golang.org/x/net to v0.60.0 for four HTTP/2 server advisories

After the Go 1.26.9 bump the gate still reports GO-2026-6603, 6611,
6612 and 6617 (HTTP/2 server: Trailer memory exhaustion, repeated
initial-window changes, double flow-control refund, HPACK encoder race).
The Relay reaches them through google.golang.org/grpc's use of
golang.org/x/net/http2; x/net v0.60.0 changes exactly those server paths
(deferred SETTINGS_HEADER_TABLE_SIZE application to the encoder,
initial-window handling). Its go directive is 1.26.0, so nothing else
moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
(cherry picked from commit bb84eab)

* ci: repin dtolnay/rust-toolchain to the current v1 (e2a55d2)

Upstream moved the v1 tag to e2a55d2 (dtolnay/rust-toolchain#187,
"Safely handle action inputs": multi-line-safe GITHUB_OUTPUT writes and
argv-array rustup flags instead of string-built command lines). zizmor's
online ref-version-mismatch audit now fails every PR's lint step because
our six SHA pins, commented '# v1', point at the old head 7e38f4b. The
new commit descends from the old pin and changes only action.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
(cherry picked from commit 0a3a00a)

* feat(mcp): dispatch returns a Job handle once Tasks are persisted; a re-issued call never double-dispatches

run_operation and raw_exec return the Job ID with job_status=running as soon as every Task is
persisted (capability Service.Start), with wait_seconds (0-300) as the opt-in blocking form; an
Account-scoped idempotency_key answers a re-issued call with the existing Job and never
dispatches again. No spec claims promoted (execution.md is legacy).

Closes #895

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek

---------

Co-authored-by: Claude <noreply@anthropic.com>
rapp992 added a commit to Felag-Engineering/relay that referenced this pull request Oct 9, 2026
… under concurrent fan-out (#954)

* relay: adopt Go 1.26.9 for twelve new stdlib advisories

The pull-request govulncheck gate now fails every Go PR on twelve newly
published standard-library advisories reached from the Relay's code:
GO-2026-6599/6600 (html/template), 6603/6605/6609/6610/6611/6612/
6613/6617 (net/http), 6607 (crypto/tls) and 6608 (net/textproto,
mime/multipart). Go 1.26.9 changes each of those packages (e.g. the
new 200-range cap on Range headers, GODEBUG httpservecontentmaxranges).

Every go directive moves 1.26.8 -> 1.26.9: relay/go.mod and the five
tools/* modules, so tools keep building with the module's toolchain.
No dependency changes; go.sum files are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
(cherry picked from commit e9c94f3)

* relay: bump golang.org/x/net to v0.60.0 for four HTTP/2 server advisories

After the Go 1.26.9 bump the gate still reports GO-2026-6603, 6611,
6612 and 6617 (HTTP/2 server: Trailer memory exhaustion, repeated
initial-window changes, double flow-control refund, HPACK encoder race).
The Relay reaches them through google.golang.org/grpc's use of
golang.org/x/net/http2; x/net v0.60.0 changes exactly those server paths
(deferred SETTINGS_HEADER_TABLE_SIZE application to the encoder,
initial-window handling). Its go directive is 1.26.0, so nothing else
moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
(cherry picked from commit bb84eab)

* relay(capability): re-check a Job's cancel immediately before each Task send

Each dispatch worker's cancel check now runs inside the Router, serialised per
Task ID, immediately before the TaskRequest is written to the Tunnel; a
withheld Task resolves cancelled and never sent (MarkCancelledBeforeSend), and
the Reconciler re-reads a Task's state before any resend. No spec claims (the
execution spec is not migrated).

Closes #943

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek

* ci: repin dtolnay/rust-toolchain to the current v1 (e2a55d2)

Upstream moved the v1 tag to e2a55d2 (dtolnay/rust-toolchain#187,
"Safely handle action inputs": multi-line-safe GITHUB_OUTPUT writes and
argv-array rustup flags instead of string-built command lines). zizmor's
online ref-version-mismatch audit now fails every PR's lint step because
our six SHA pins, commented '# v1', point at the old head 7e38f4b. The
new commit descends from the old pin and changes only action.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
(cherry picked from commit 0a3a00a)

---------

Co-authored-by: Claude <noreply@anthropic.com>
rapp992 added a commit to Felag-Engineering/relay that referenced this pull request Oct 9, 2026
…nly by clippy, fuzzable crates (#962)

* ci: repin dtolnay/rust-toolchain to the current v1 (e2a55d2)

Upstream moved the v1 tag to e2a55d2 (dtolnay/rust-toolchain#187,
"Safely handle action inputs": multi-line-safe GITHUB_OUTPUT writes and
argv-array rustup flags instead of string-built command lines). zizmor's
online ref-version-mismatch audit now fails every PR's lint step because
our six SHA pins, commented '# v1', point at the old head 7e38f4b. The
new commit descends from the old pin and changes only action.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek
(cherry picked from commit 0a3a00a)

* refactor(daemon): Cargo workspace — relay-policy and relay-ops read-only by clippy, fuzzable crates

relay-exec (sole spawning crate), relay-ops (write-banned) and relay-transport move out of
daemon/src; step-1 guards extended to them, plus spawn, unsafe, ExecRequest and feature censuses.

Closes #889
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017phewNxcFMxpU3ntX2TKek

---------

Co-authored-by: Claude <noreply@anthropic.com>
epompeii added a commit to bencherdev/bencher that referenced this pull request Oct 12, 2026
## Problem

`Lint / Zizmor Online` fails on every pull request (for example run
37884500334 on #1153) with one medium finding, exit 13:

```
warning[ref-version-mismatch]: action's hash pin has mismatched or missing version comment
  --> ./.github/workflows/update_sandbox.yml:18:81
18 |       - uses: dtolnay/rust-toolchain@7e38f4b # v1
   |                                                                                 ^^ points to commit e2a55d2ffb04
```

The upstream `v1` tag of `dtolnay/rust-toolchain` moved to
`e2a55d2ffb04f378e9626c28d38b36d230d1e12f` on 2026-10-09 when
dtolnay/rust-toolchain#187 merged. The job is `continue-on-error: true`,
so it does not block a merge, but it is red on every open pull request.

## Change

Repin `update_sandbox.yml` to the commit `v1` points at today and keep
the `# v1` comment. This follows #987, #1045, and #1088, which tracked
`v1` for this same pin, and `v1` is the only tag upstream publishes. The
hash was resolved from the GitHub refs API.

## Upstream range

`7e38f4b4...e2a55d2f` is three commits, all in `action.yml`, fixing the
command injection reported in dtolnay/rust-toolchain#179:

- Every input and the parsed toolchain move out of inline `${{ }}`
expressions in `run:` scripts and into `env:` variables, quoted at each
use.
- The parsed `toolchain` output is written with a heredoc delimiter
instead of `key=value`.
- The separate step that built `--target`, `--component`, and
`--allow-downgrade` strings is folded into the install step as a bash
array.

This call site passes only `toolchain: stable`, no targets or
components, so it was never exposed, and it still runs `rustup toolchain
install stable --profile minimal --no-self-update --force-non-host`
followed by `rustup default stable --force-non-host`. No other workflow
or composite action uses this action.

## Other moving refs

Every other pin whose comment names a moving ref still matches it today:
`rui314/setup-mold@10ca16bf # v1` (14 sites),
`dawidd6/action-download-artifact@b6e2e706 # v21` (2 sites), and
`superfly/flyctl-actions/setup-flyctl@ed8efb33 # master` (3 sites, the
current `master` head). Nothing else needs a change.

## Verification

zizmor 1.25.2, the version CI pins, from the same digest-pinned image
`zizmor-action` runs
(`ghcr.io/zizmorcore/zizmor:1.25.2@sha256:14ea7f5c...`), with the
action's arguments (`--persona=regular --format=github`, plus
`--no-online-audits` for the offline job):

| Tree | Offline | Online |
| --- | --- | --- |
| `devel` | exit 0 | exit 13, `1 medium` (the finding above) |
| this branch | exit 0 | exit 0, `No findings to report` |

CI on this pull request is the final check.
MejiroRina pushed a commit to seiunx-dev/ci-templates that referenced this pull request Oct 12, 2026
Moves the pin from 02cb101 to e2a55d2 (upstream master, also upstream v1). Upstream changes: inputs are passed through env and quoted instead of being interpolated into run: lines (dtolnay/rust-toolchain#187), and rustup toolchain install retries up to 4 times on release-server checksum failures (#186). No behaviour change for the toolchain / components / targets inputs setup-rust passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UenSDaGVdAotkLspjNTnpu
coseto6125 added a commit to coseto6125/egent-code-plexus that referenced this pull request Oct 21, 2026
…PHP/Python flow, overlay parity, hook cd target (#853)

* chore(deps): tree-sitter 0.27, ast-grep-core 0.51, cargo update, action pins

- tree-sitter 0.26.13 -> 0.27.1 and ast-grep-core 0.45.2 -> 0.51.0 move
  together: ast-grep-core 0.51 requires tree-sitter ^0.27. The vendored
  tree-sitter-nim pin moves from ~0.26 to ~0.27 so one tree-sitter links.
- tree-sitter 0.27 makes QueryMatch::captures private: 48 field reads become
  captures() calls. Node::child_count now returns u32 (tree-sitter#5313):
  15 redundant `as u32` casts go.
- cargo update moves the rest of the semver-compatible tree, including
  rmcp 3.6.0, tokio 1.54.0, toml 1.1.8.
- Held back by upstream pins: cc 1.2.67 (tree-sitter-sequel ~1.2.1),
  psm 0.1.24 (pest =0.1.24), generic-array 0.14.7 (crypto-common =0.14.7).
- Actions: rust-cache v2.9.3, action-actionlint v1.80.4,
  harden-runner v2.22.2, dtolnay/rust-toolchain stable head b725eda
  (includes dtolnay/rust-toolchain#187 input-handling hardening).

* chore(deps): correct count in previous message: 45 captures() call sites, not 48

* test(mcp): pin rmcp 3.6 stdio line cap of 16 MiB

rmcp 3.6.0 (modelcontextprotocol/rust-sdk#1049) drops an incoming stdio
line over 16 MiB and keeps the session open. serve_stdio inherits it.
Keep the new default: no ecp tool takes arguments near that size, and a
peer that never sends a newline can no longer grow the buffer without
bound. The test fails under an unbounded transport (checked with
with_max_line_length(usize::MAX): the server answers id 2).

* fix(deps): review follow-ups for the dependency upgrade

- ecp-mcp: raise the rmcp floor from 3.4 to 3.6. serve_stdio's 16 MiB
  line cap and the test that pins it exist only from rmcp 3.6.0.
- ecp-mcp: the serve_stdio comment states that a request on a dropped
  line gets no reply.
- ecp-mcp test: build the oversized line as text, so the 16 MiB pad is
  allocated once instead of twice.
- ecp-analyzer/Cargo.toml: the svelte ABI comment no longer names
  tree-sitter 0.26.

* fix(registry): bump BUILDER_FINGERPRINT to schema12 for tree-sitter 0.27

tree-sitter 0.27 recovers some files with syntax errors into different
trees than 0.26 did. On the 23-repo parity corpus the only change is
nlohmann json.hpp: one friend operator< that 0.26 parsed as a
declaration is now a function. A binary built from git at the same
crate version would otherwise reuse L2 graphs and parse-cache entries
built by tree-sitter 0.26. schema7 set the precedent for a grammar pin.

* chore(deps): declare rust-version 1.90 for the workspace

tree-sitter 0.27.1 and ast-grep-core 0.51.0 declare rust-version 1.90,
so rustc 1.88/1.89 can no longer build ecp. Declaring it makes
`cargo install egent-code-plexus` on an older rustc fail with a clear
MSRV message instead of a dependency resolution error.

* chore(deps): declare rust-version 1.91, not 1.90

clippy's incompatible_msrv check, run against rust-version 1.90, flags
ecp-mcp/src/schema.rs: `TypeId::of` is stable in a const context since
1.91.0, and INT_TIDS / FLOAT_TIDS call it in a const. The dependencies
need 1.90; the workspace's own code needs 1.91. The schema.rs comment
said 1.85.

* fix(resolution): keep same-name method overloads as named nodes

The uid (kind, path, owner, name) carries no parameter signature, so
`save(int)` and `save(String)` share one. Pass 1 kept the first and
replaced every later one with an empty-name tombstone: no name, no owner,
no HasMethod, no incoming edge. On .sample_repo that was 6595 of 58892
Java Method/Constructor nodes (11.2%), 982 of 7424 C#, 442 of 2871
Kotlin and 323 of 3572 TypeScript. `impact --target audit` listed a
nameless caller and stopped there, because nothing called the tombstone.

Pass 1 now keeps a later Method / Constructor overload as a node with the
first one's name and owner, under `uid::overload(first, ordinal)`. It is
registered by id only (`SymbolTable::register_overload`), so every name
lookup and resolver tier still returns the first declaration and binding
is unchanged. A Calls edge bound to the first now reaches every overload
of the set with reason `call:overload_set` and unchanged confidence: the
argument types that pick one are not parsed. Self-recursion is skipped
per member, so `save(int)` delegating to `save(int, String)` keeps its
edge to the sibling. Other collisions (`#ifdef` redefinitions, field
reassignments) stay tombstones.

class_membership gives each overload its own HasMethod edge. It also
resolved a member by name alone, so a member landed on the first
same-name declaration in the file whatever its owner: two classes'
`__init__`, or a Rust trait's `fn save` ahead of `impl K`. A member now
resolves to the declaration its owner matches.

The query-time overlay mirrors all of it: the same uid numbering, later
overloads kept out of every name tier (base ones found through
`ArchivedZeroCopyGraph::is_later_overload`), constructor choice ignoring
them, and the same Calls fan-out for virtual and clean base targets.

BUILDER_FINGERPRINT moves to schema13 so cached graphs and parse caches
rebuild.

Tests: overload_members.rs covers the 14 mainstream languages (C checks
that an `#ifdef` redefinition stays one node). Five tests that pinned
the collapsed overload (one constructor call, one closure parent, one
static-import target) now expect each overload. Three overlay parity
tests compare the overlay against a full reindex by uid.

* chore(blindspot): retire the method-overload dev-metric bucket

Pass 1 keeps overloads as nodes and records no method-overload
BlindSpot any more, so the bucket is gone from DEV_METRIC_BS_KINDS, the
summary docs and help text, and its exclusion test.

* fix(resolution): link each overload set once instead of fanning out calls

Fanning each call out to every overload multiplied Calls by the set
size. On .sample_repo Calls went from 198868 to 313695 (+58%): C# +211%
(Newtonsoft XUnitAssert.AreEqual alone gained 27717 edges, about 30
overloads times its call sites), Java +96% (guava ImmutableList.of,
13 overloads).

A call now binds the first overload only, as before this branch, with
reason `call`. Instead, the first overload `References` each later one
(reason `overload_set`, confidence 1.0): k - 1 edges per set. Upstream
impact of a later overload reaches the set's callers through the first,
one hop further than with the fan-out. A first overload delegating to a
sibling needs no special case: the link already connects them.

The overlay emits the same links for a dirty file's sets and drops the
base ones sourced in it, as it does for closure references. The tests
that pinned one constructor call per construction keep that contract.

* fix(impact): name an overload set by its first overload

Two same-file overloads share name, file and kind, so --file and --kind
could not split them and `ecp impact save` failed as ambiguous. A later
overload is reached through the first one (calls bind to the first,
which references it), so resolve_candidates skips it as a target and as
a same-name collision, as before the overloads were named. ecp path
shares the resolver.

* fix(inspect): report endLine 1-based like startLine

inspect printed the raw 0-based span row as endLine, so a symbol ended
one line before it started. routes' enclosing-scope check compared the
same raw row against a 1-based line, rejecting a route on its scope's
last line and never matching the single-line Const guard.

* fix(resolution): prefer the caller's own member; bind duplicated trees to the nearest copy

A member lookup took the first same-name declaration in the file, so a
trait or interface declared ahead of the class won `self.save()` (Rust
inherent methods, and the class's own member in the other languages, take
precedence). A call into a source tree that exists twice (android/guava
next to guava) was dropped as ambiguous; when every candidate is the same
file under another root, the copy sharing the deepest directory with the
caller now binds, at confidence 0.6 with reason call:nearest-copy, and a
tie stays unresolved. Builder fingerprint +schema14.

* fix(overlay): replay the qualifier tier for typed-receiver and static calls

The overlay looked a qualified callee (K.save, K::st) up by its literal
name and found nothing, so a dirty file lost every typed-receiver and
class-qualified call the index binds at 0.85. resolve_callee now replays
the index's Tier 2.5: find the qualifier's file (same file, import,
unique global, module stem) and pick the member it declares, preferring
the qualifier's own member. A qualified constructor type resolves its
whole path before the last segment, as the index does.

* test(resolution): one function per line in the Rust own-member fixture

Calls are attributed to the first function on their line, so three
functions on one line made audit, not run, the caller. Drop the
overload_members comment the own-member rule made false.

* fix(overlay): mirror own-member precedence and nearest-copy binding

A dirty file's same-file call with several declarations of the name bound
nothing, where the index binds the caller's own member; an ambiguous
global call dropped every candidate, where the index binds the copy in
the caller's tree. The overlay now passes the caller's owner into
resolve_callee and picks the nearest copy with the index's confidence
and reason. Calls reached through the receiver-typing ladder, which the
overlay does not replay, stay a recorded divergence.

* feat(analyzer): reference enum variants from the code that uses them

No construction, read or pattern of an enum variant had an edge, so
upstream impact on a variant reported dead code. Each function now
References each variant it uses once per reason (enum_variant:use,
enum_variant:pattern), resolved through the qualifier's type so a
variant name shared by two enums binds the right one. Same commit fixes
the enum's Defines edge binding a same-named variant of another enum.

* test(routes): route on its scope's last line keeps the enclosing scope

Red before 8e90271b: the raw 0-based end row put the route one line past
its scope.

* fix(verify-resolver): rank every resolving tier above Unresolved

The dedup rank listed SameFile, ImportScoped and Global only, so
QualifierScoped, TypeOwned, ModuleTree and NearestCopy tied with
Unresolved and the first record of a key won.

* chore: define the nearest-copy constants once; clear clippy findings

heuristics.rs re-exports the ecp-core constants the overlay already
uses, so the index and the overlay cannot drift apart.

* test(overlay): close the nearest-copy divergences that already match

Kotlin, C#, Python, Swift, Dart and Go record the qualified call by its
bare member name, so it reaches the global tier the overlay replays.

* chore(registry): builder fingerprint schema15 for enum variant references

A graph built under schema14 lacks the enum_variant References edges and
must not be reused.

* fix(calls): attribute a call to its enclosing function by row and column

Several functions on one line (minified JS, one-line C or Ruby definitions)
sent every call on that line to the first function: attribution compared
rows only. Containers and pending calls now carry (row, col) positions, and
the two duplicate sweeps share enclosing_targets.

* fix(receiver-types): scope locals and classes by row and column

Local-type and class scopes keyed on rows, so on a line with two functions
the second one's receiver read the first one's locals. Scopes now hold
position ranges. span_area no longer flattens its ordering past column 10000.

* fix(resolver): a same-file pick never guesses between unrelated owners

Tier 1 bound the first same-name callable in the caller's file even when it
belonged to another type, at confidence 1.0. prefer_owned now picks the
caller's own member, then a base's member in heritage order, then a free
function, then the sole owner's member, then the root of one hierarchy;
two or more unrelated owners fall through to the later tiers. Tier 2.5,
Tier 2.75 and MemberOwner::Any share the rule.

* fix(resolver): an untyped receiver does not narrow a same-file pick by the caller's bases

super(E, self).m() reaches Tier 1 as an untyped member call; walking the
caller's heritage bound E.m, which the explicit two-argument form skips.

* feat(graph): keep path aliases and supertypes in graph.bin; overlay fragments carry heritage and variant refs

The overlay re-resolves dirty files without the index's alias table,
supertypes, base names or enum variant uses, so it could not match a
reindex. graph.bin v14 appends the alias and supertype tables; fragment
format V4 adds heritage and variant_refs per symbol. VariantRef, its
reasons and PathAliases move to ecp-core. BUILDER_FINGERPRINT schema16.

* fix(csharp): resolve a type-name receiver as a type, through using aliases

csharp_callee emitted StringAssert.AreEqual(..) as the bare AreEqual, so
three same-file AreEqual owners left the same-file pick nothing to choose
by. An identifier receiver that is neither a local nor a member of an
enclosing type is now a FromReceiver site: the resolver takes it as a
type when the project or an import names one (using Alias = Ns.T
included), else as the bare member, as before.

* feat(overlay): rebuild enum variant refs, heritage-tier calls, Rust use qualifiers and path aliases for dirty files

A dirty file kept its stale enum_variant References and missed calls the
index binds through a base class in another file, a Rust qualifier named
by a use import, and a tsconfig paths alias. The overlay now replays those
tiers in the index's order: variant refs per symbol with stale base edges
masked, the heritage tier at 0.8 before the wildcard pass, the qualifier
tier before the Rust module tree, and the archived alias table in every
candidate walk.

* fix(cpp): type field and untyped receivers, keep owners in macro-heavy class templates, read templated bases

A member call on an untyped receiver went out as a bare name, so it looked
like an implicit-this call. Class template members lost their owner when
macros broke the parse (nlohmann basic_json), out-of-class definitions
took no owner, a field receiver stayed untyped, and heritage dropped
templated or qualified bases and emitted an empty-name class per extra
base.

* fix(parsers): keep the receiver kind of member calls in Swift, Ruby, Java, Kotlin, Dart and Python

An untyped receiver went out as a bare name (Swift, Ruby), a type-name
receiver was dropped (Swift, Kotlin, Dart, Python), and Java new T().m()
and ((A) o).m() lost their type. Each now emits an untyped member site, a
FromReceiver site the resolver may take as a type, or the typed callee.
Swift members of extensions, protocol extensions and enums carry their
owner, and Swift parameters are keyed by their internal name.

* fix(resolver): namespace owners count as free, impl blocks reach their trait, member calls never bind a type

The same-file pick read a C++/C#/Rust namespace owner as an unrelated type,
an ownerless Rust impl member as a free function, and let an untyped
member call fall into the constructor fallback or a free function. Type
receivers of C#, Swift, Kotlin and Dart resolve as types; an untyped
receiver is never narrowed by the caller's type or bases in any tier; a
receiver-typed call never binds a free function. Heritage-bearing kinds
(class, struct, interface, trait, enum) share one enclosing-heritage rule
in ecp-core.

* test(receiver-types): Swift and Ruby untyped receivers are untyped member sites

* fix(resolver): an ownerless Rust impl method is not top level for import binding

* fix(parsers): owners for Rust generic impls and enum members, path-qualified heritage, C++ ns::f() as a namespace function

Members of impl blocks over generic, path-qualified or reference self types
had no owner, so their uids collided; enum members had none in Java,
Kotlin, Dart, PHP and Swift; impl fmt::Display recorded no heritage; an
out-of-line void ns::f() was an ownerless Method.

* fix(resolver): self calls bind their own type and impl block; Python modules stay reachable through untyped receivers

Python self.m() is typed as the enclosing class, as Java this.m() is, so
the untyped-receiver rule no longer strips its own type and bases. A Rust
self.m() in one of several impl blocks for one type binds the caller's
block, and a self type the index does not declare resolves in the
caller's file. A Python untyped api.helper() may still reach another
module's function.

* feat(overlay): share the same-file pick and the receiver-typing ladder with the index

The overlay re-implemented the index's tiers and drifted each time one
changed. prefer_owned, member_call_admits and the receiver-typing ladder
move to ecp-core behind PickFacts / TypeFacts; the index and the overlay
both call them. The overlay now replays type receivers, untyped-receiver
rules, the Kotlin extension fallback and the constructor-fallback gate,
and KNOWN_DIVERGENCES for the ladder is empty.

* fix(overlay): compare archived and owned node kinds through NodeKind

* fix(overlay): bind JS/TS bare calls through the index's lexical scope

Full indexing binds a bare JS/TS call to the innermost named function in
scope before any name tier, and an ambiguous or unreachable one leaves
only the named-import tier. The overlay had no such step; phase 2's
same-file pick then bound one of two same-uid helper twins the index
leaves unresolved. LexicalFunctions moves to ecp-core and both sides
build it over one file's declarations.

* fix(resolver): type a Python from-imported class receiver

`from k import K; K.st()` resolved by the bare member `st`: 0.7 when the
name was unique, and no edge at all once another class declared `st`.
When the from-import's module file declares the receiver as a type
(reexport::python_imported_type), the call now resolves as `K.st`
through the qualifier tier, in the index and the overlay. A submodule
still binds first; an object, a re-imported name or a library name keeps
the bare member. A package attribute class that shadows a submodule now
binds its own member (test renamed from keeps_previous_path).

view.rs also carries the overlay half of the previous commit: the
LexicalFunctions step before the call tiers, and span_of.

CallSite docs: FromReceiver and UntypedMember list every language that
emits them now.

* chore(clippy): clear the branch's -D warnings findings

* fix(php): type $this and self:: in a trait or an enum

ClassContext held classes and interfaces only, so $this->m() inside a
trait or an enum was an untyped member call. Once untyped calls stopped
binding the caller's own type's members by name, every trait-internal
call dropped (Laravel: ValidatesAttributes::requireParameterCount,
ManagesFrequencies::spliceIntoPosition). Traits and enums now name the
receiver as a class does: the trait's own members, the using class's
members and an enum's own methods bind at the qualifier tier.

* fix(php): a trait types $this only for the members it declares

$this in a trait is the class that uses it. Typing every call as
Trait.member lost the calls into the host class (Laravel
HasUniqueStringIds::getIncrementing -> Model::getKeyName, 17 edges): the
trait has no such member and no heritage reaches the host. The trait's
and the enum's own members still bind through the type; any other member
keeps the by-name route it had before.

* perf(overlay): memoize the qualifier tier per file and build name candidates lazily

A dirty-file overlay query on guava (4 dirty files) went from 26 ms to
38 ms over this branch. Memoizing qualified_callee per (file, imports,
callee, target, heritage, Rust caller) and building clean_base only
when a tier reads it brings it to 33 ms with byte-identical output.

* perf(overlay): memoize Tier 3's global pick per file and callee

Tier 3 filters every base node of the callee's name, and a file calls
one common name at many sites: on guava 626 Tier 3 calls had 179
distinct (file, callee) pairs and took 8.7 ms of a 20 ms overlay build.
Keying the pick by every input it reads brings a 4-dirty-file query
from 37.5 ms to 30.8 ms (29 ms before this branch's overlay tiers),
with byte-identical output.

* fix(hook): resolve the repo from a leading cd in the Bash command

A command such as cd "<repo>" && grep -rn X app run from another cwd
gave no graph nudge, because the hook read the envelope cwd only. The
last literal cd before the grep/rg segment now sets the resolution dir
when it names an existing directory. A command without "cd" pays one
substring check.

* fix(flow): PHP static and member calls reach their callee; declarations are not values

PHP scoped calls were evaluated as plain values, so a forward slice
stopped at UploadFile::upload() imported by use. php.rs now models
classes by lower-case FQN, binds use clauses (alias, grouped), and
resolves Class::m, self::, parent::, static:: and $this-> through the
parent chain. Declaration nodes no longer become value nodes, and
structural nodes (namespace, class body, parameter lists, types) raise
no unsupported_semantics boundary. The uncalled sweep runs the queried
file's entry points first.

* fix(routes): Laravel string handlers, group prefixes and per-route middleware

Route::post('/upload', 'FileController@upload') reported an inline
handler with no callers, prefix()->group() and group(base_path(file))
prefixes were dropped (/admin/import/award came out as /award), and no
route carried its middleware.

- 'Ctrl@m', [Ctrl::class, 'm'], invokable controllers and
  Route::controller()->group() get a HandlesRoute edge to the controller
  method: exact FQN under a known namespace (1.0), a repo-unique class
  name otherwise (0.9), nothing when ambiguous.
- Closure groups, option-array groups and chained prefixes join into the
  path; an include group (base_path, __DIR__, withRouting) becomes an
  INCLUDE record that Pass 1.5 applies to the included file's routes.
- Middleware (outer to inner) lives in a sparse route_middleware side
  table and shows as a middleware array in ecp routes.

GRAPH_FORMAT_VERSION 14 -> 15 and BUILDER_FINGERPRINT +schema17, so
existing graph.bin caches rebuild.

* fix(flow): lower the queried file and its imports first; Python methods live on their class

The AST budget is global and spent in corpus order, so on a 514-file
Laravel repo the callee file of a cross-file static call (file #449)
was cut to a bare root and the slice stopped at its use clause. The
queried file now lowers first, then the files its imports name,
breadth-first; the rest keep corpus order and max_steps stays the one
limit.

A Python class body runs in its own scope: methods are attributes of
the class object, not bare module names, so a module-level get() no
longer reaches a method get. self.m() and Box.m() resolve through the
class, with superclasses and overrides, sharing classes.rs with PHP.

* fix(routes): Laravel resource, match and any routes; included files, inherited actions and entry points

- Route::resource / apiResource / resources (only, except, parameters,
  shallow, nested) emit Laravel's action set; match emits one route per
  verb, any one ANY route, and --method keeps ANY rows.
- A route file without a framework import keeps its records when an
  admitted file includes it; a file declaring its own Route stays out.
- Controller actions bind after the supertypes pass: own method, then
  one used trait, then one resolved parent. Bound methods get an
  EntryPoint marker in their own file.
- TOON output joins middleware so the routes table stays tabular.

BUILDER_FINGERPRINT +schema18: RawRouteScope gained needs_include.

* fix(routes): pass named_child a u32 index

* perf(flow): share unchanged frames between state snapshots

Every fork and every sweep entry point deep-cloned all scopes and
objects of the program, and restore deep-compared them. With PHP
methods as functions the client Laravel query spent 6.2 s in 869
invokes. Frames is a chunked persistent vector of Rc scopes: a
snapshot copies one pointer per chunk, a write un-shares one chunk and
one scope, and restore/join visit only frames that differ.

* docs(skill): ecp routes carries middleware and ANY routes

* chore(clippy): clear the branch's -D warnings findings

* fix(hook): only a cd that surely runs in the search's shell moves it

A cd after ||, inside a pipeline or a background job, or ending an && chain
the search is not part of, no longer re-roots the search. The scan stops at
any rg/grep word (rtk proxy rg, git grep), matching extract_from_shell. A
quoted '~/x' is a literal directory, not the home directory. Redirects such
as 2>&1 and &>log no longer split segments.

* fix(overlay): drop a dirty impl's old traits; clear qualified memo after supertypes; real 0.15.0 fixture

- base_supertypes drops a trait that only the dirty file's archived impl
  gave, so impl A -> impl B binds B.m like a reindex (both kept when a
  clean impl also implements A).
- QualifiedCache entries filled in the supertypes pre-pass read
  hierarchy_root before dirty_supertypes was set; clear the memo after set.
- OverlayNames.same_file is a set: every reader only tests membership.
- overlay_view_inputs reads a session written by the released 0.15.0
  (format 3, v0.15.0+schema11) instead of one the new code built.
- Doc and comment fixes: fragment format v4, same-file twin wording.

* fix(routes): Laravel middleware exclusion, controller middleware, include fixes

- dirname levels outside 1..=64 drop the include instead of overflowing.
- withoutMiddleware / excluded_middleware remove middleware at promote time;
  variadic middleware('a','b') records every argument.
- Controller middleware ($this->middleware with only/except, static
  middleware(), parent::__construct rules) appends to the route list.
- A global 'use Route;' import no longer reads as the file's own router.
- A group controller reaches an included file's bare actions.
- A qualified action under a group namespace gets no second prefix.
- Scopes past the include cap emit a laravel-include-scope-cap BlindSpot.
- ANY answers Laravel's verbs only, in fetch lookup and routes --method.
- The same path from two includes keeps the last registration.
BUILDER_FINGERPRINT bumps to schema19; graph.bin layout is unchanged.

* fix(flow): drop each tree after lowering; Python decorators and MRO; PHP namespace aliases

- Trees are no longer held until lowering_order finishes: corpus order
  lowers while parsing, a queried file drives an on-demand BFS, and the PHP
  class map keeps names only (peak RSS was up to 7x the release).
- staticmethod binds no receiver, classmethod binds the class, any other
  decorator leaves the call unresolved with an external_call boundary.
- Inherited members follow C3 MRO; ambiguous hierarchies keep the union.
- A field read walks the parent chain like a method call does.
- A PHP anonymous class method no longer leaks into the outer scope.
- The queried module initialises before the rest of the corpus.
- PHP aliases and class names resolve per namespace block, case-insensitive.
- A method sees functions declared later in the same file.
- pass raises no boundary; .pyi uses Python semantics.

* fix(resolver): no name-only binds for typed receivers; C++ out-of-line owners and scopes

- A receiver typed to a declared class no longer binds to an unrelated
  same-file owner or a free function; the nearest ancestor's member wins,
  else the type ladder decides (languages with extension members excluded).
- C++ out-of-line definitions take their qualifier as owner, so A::reset
  and B::reset no longer form a false overload set.
- Kotlin/Dart: Other().m() types the constructor receiver; other computed
  receivers never bind to the caller's own member.
- C++ lambda and catch parameters get their own scope; field tables are
  keyed by namespace path.
- cpp recovery macro lookahead is memoised (was quadratic before budget).
- extract_variant_refs carries the parent on its stack instead of calling
  Node::parent per identifier.
- Constructors sharing the class name get HasMethod.

* test(class_membership): same-named constructor now gets HasMethod

The pinned test asked to be flipped once a multi-id-per-name lookup
landed; member_node now resolves through first_declaration.

* chore: clippy manual_contains and disallowed_names

* fix(hook): fall back to the envelope cwd whenever the search directory is uncertain

A cd that may not run in the search's shell, an unknowable target (cd -,
$VAR, backticks, ~user, options, bare cd), a cwd-changing builtin other
than cd (pushd, popd, source, eval, ...), a cd inside a subshell, or an
unbalanced quote now yields no override instead of a stale one. A
successful 'cd x || ...' skips its || operand, so 'cd x || exit; rg'
searches x. Leading assignments and '{' groups keep the cd.

* fix(resolver,flow): no guessed ancestor member; Rust-only impl drop; classmethod receiver; C3 memo

- ancestor_member returns a member only when one declaring ancestor
  inherits every other declarer; otherwise the type ladder decides.
- The overlay drops a dirty impl's old trait only for Rust types, so a
  Python class with the same name keeps its heritage.
- self.cm() binds cls to the receiver's class objects, else stays an
  external_call.
- C3 linearization memoises per lookup (a stacked diamond was 2^n).
- Kotlin/Dart constructor inference yields to a same-named local.

* fix(flow): string tokens and interpolation raise no unsupported_semantics

Quote tokens, f-string interpolation, template substitutions and heredoc
parts were 82% of the unsupported_semantics boundaries on ComfyUI and
crowded real boundaries out of the returned set. Their values still merge;
shell_command_expression and dynamic variable names keep their boundary.

* fix(routes): controller middleware only when statically certain, else a BlindSpot

- Static middleware() requires HasMiddleware on the class or an ancestor
  and exactly one top-level array return.
- Constructor middleware is one ordered list; parent::__construct() inlines
  the parent's registrations at the call; a used trait's constructor wins
  over the parent's, as in PHP.
- Closures, nested functions and anonymous classes are not scanned; a
  conditional or non-literal registration, or an out-of-graph parent/trait,
  adds no middleware and records laravel-controller-middleware-dynamic.
- X::class resolves in its own file before dedupe and exclusion.
- Only a global 'use Route;' is the facade; 'use Router;' binds a class.
- A class-constant action in a controller group stays X@__invoke.
- A repeated include scope keeps its last position; within one route file
  the last registration wins between a verb and ANY in fetch lookup.
- Controller middleware merges once instead of sorted inserts (was N^2).

* fix(routes): an unbound Laravel action is reported as unresolved, not inline

Pass 1.95 records a laravel-unbound-action BlindSpot carrying the action
text; ecp routes reports handlerKind unresolved_action with that text
instead of claiming an inline closure (client: 'AuthController@logintest'
to a missing method).

* fix(resolver): a base's member yields to an earlier base's inherited one

prefer_owned took the first declared base that owns the member, so a typed
self.m() in D(B, C) with B(A) bound C.m although Python's MRO reaches A.m.
A base now stands only when no earlier base reaches the member through its
ancestors; otherwise the pick gives no edge. Also documents the controller
middleware record markers on RawRoute::CONTROLLER_MIDDLEWARE.

* fix(hook): tri-state cd target; unknown cwd suppresses graph hits

A cd that may or may not run, an unknowable target, or a cwd-moving
builtin now yields Unknown, and the hook prints no hits instead of
falling back to the envelope cwd. Skipping a successful cd's || operand
tracks { } / ( ) depth, and a cd inside an and-or list that ends in &
stays in the background job.

* fix(routes,receiver-types): codex round 3 findings

- Laravel: a middleware or parent::__construct after a possible
  constructor exit becomes a laravel-controller-middleware-dynamic
  BlindSpot.
- ANY precedence applies only to one file's registrations of the same
  raw URI, in linear time; cross-file order stays a union.
- routes: an unbound action matches its registration by row and column.
- Kotlin: a nested type's property no longer shadows an outer class.
- Kotlin/Dart: a block-scoped local shadows only inside its block.

* fix(hook,php,dart): codex round 4 findings

- hook: a cd in the search's own background job applies; a quoted
  '{' or '(' in a skipped || operand opens no group.
- Laravel: $exit and static::exit are not constructor exits.
- Dart: each declarator of A a = A(), b = a.copy() binds before the
  next initializer.

* fix(hook): a shell comment ends its segment

An unquoted # at a word start runs to the newline, so a ( ; or quote in
the comment no longer shifts subshell depth or segment bounds.

* fix(hook): ${...} is opaque, a heredoc is unknown, # starts a word only after a metacharacter

A # inside ${v:- #} or after a CR is literal, as in bash. A heredoc
body is not modelled, so a command holding one yields Unknown and no
graph hits.

* perf(overlay): look up the qualifier's file stem before the member scan

The named-file tier scanned every declaration of the member per
qualified call (9 us on a common name such as add). The stem index is
built once per overlay build, so the tier is one hash lookup per call.

* fix(hook): a quote or escape inside ${...} makes the split unknown

Also drop a needless borrow that clippy -D warnings rejects in the
Laravel route test.

* fix(hook): four cases that named a directory bash does not search

- A brace inside ${...} makes the split unknown.
- A cd or cwd builtin behind a prefix word (! cd, time cd) is unknown.
- cd .. drops the last component of the logical path, as bash -L does.
- A relative cd under an inherited or assigned CDPATH is unknown.

* fix(hook): six more shapes that named a directory bash does not search

Unknown for: a logical and a physical path that disagree, a glob in the
cd argument, a line continuation, set -P / -o physical, and a cd or
source behind a prefix word (! . file). CDPATH exempts only a first
component of . or .., so cd .codex consults it.

* feat(hook): name the cd target repo in the hits header; join line continuations

Hits a leading cd moved to another repo now open with 'ecp graph hits in
<dir>:', so the agent reads their paths against that repo and a wrapper
no longer re-derives the directory. A backslash-newline outside single
quotes joins its lines before the split, as in bash, so 'cd x && \'
over two lines keeps its hits. After a prefix word only the command
word is checked, so 'time git diff -- .' is not a source.

* fix(hook): join line continuations before extracting the search pattern

'cd repo && \' then 'grep -rn X .' on the next line resolved the cd but
found no grep word, so the hook printed nothing.

* fix(hook): a comment's trailing backslash joins nothing; skip time options

A '# old && \' comment line no longer swallows the next line's cd, and
'time -p . env.sh' is checked as a source.

* fix(hook): a cd the search may not follow gives Unknown

Codex round 13 found three shapes where the hook named the wrong
directory: a search in a subshell that cds ((cd x && rg) | head), a cd
after && whose list ends before the search (false && cd x && true; rg),
and a newline after && that split the list (false &&\ncd x; rg).

The same reasoning covers a cd in a { } group or compound command that
closes before the search (a function body, a skipped operand, a group in
a pipeline) and a cd in a loop. A cd after && now counts only when every
operator up to the search is && or |.

* fix(hook): else branches, command substitutions and rg lookups

Codex round 14: a search in an else branch kept the cd of the then
branch; m=$(cd x && rg ...) hid its cd behind the assignment; and
'command -v rg && cd x && rg Foo' stopped at the looked-up name, both for
the cd target and for the pattern. runs_search now decides which rg /
grep word runs a search, and both scans share it. A cd before else /
elif, inside a case, or inside a backtick substitution gives Unknown.

* style(hook): if let for the single compound opener arm

* test(update): write the fake ecp from a child process

test_run_skill_refresh_hung_child_times_out failed with 'Text file busy':
another test thread forked while this process held the script open for
writing, and the forked child kept the write fd until its own exec. A
separate sh process now writes the script, so no test thread ever holds
that fd.

* fix(hook): |& is one pipe; git -C grep gives Unknown

Codex round 15: 'cd x && git status |& head' split |& into a pipe and a
background job, so the cd looked like another job's and the next rg got
Unchanged. And 'git -C dir grep' searches dir while the hook took the
envelope cwd (or an earlier cd). |& is now one Sep::Pipe, and a git grep
behind -C / --git-dir / --work-tree gives Unknown.

* fix(hook): --color values and rg --files are not the search term

Codex round 16: 'rg --color never ... HookInput' extracted 'never', and
'rg --files -g ... | xargs grep HookInput' stopped at the listing rg and
extracted 'xargs'. Value-taking long options join flags_with_values, and
runs_search skips an rg that lists files, for both the term and the cd
target.

* fix(hook): the argument of cd grep is not the search

Codex round 17: 'cd grep && rg -n main src' stopped at the directory
name grep, so the cd never counted and the hook took the envelope cwd.
runs_search now skips an rg / grep word that a cd or pushd names.

* fix(hook): a & inside a group does not end the outer list

Codex round 18: 'cd x && (git status & git diff & wait); rg Foo' took
the first & inside the subshell as the end of the cd's and-or list, so
the cd looked like a background job and the search got Unchanged. A
list now ends only at a ; or & no deeper than the segment it starts at;
group_marks runs once per segment.

* fix(hook): background jobs by nesting level, compound commands included

Codex round 19: 'cd x && { rg a; rg b; } & wait' put the search in the
cd's own background job but compared inner list ends, so the cd was
dropped; and 'cd x && if c; then d; fi & rg' ended the cd's list at the
if's ; because depth counted only braces and parens, so the background
cd reached the foreground search.

other_job now walks from the cd to the search, closing one list level
at each ; and reporting another job at the first & that ends a list
around the cd. Depth counts if / case / loops through compound_marks,
and command_words computes the prefix scan once per segment.

* fix(hook): rg --version is not a search; a case with a cd gives Unknown

Codex round 20: 'rg --version && cd x && rg Foo' stopped at the version
query, so the cd never counted and the term became the cd path. And a
case pattern's ) (Linux) make ;;) reads as a subshell close, which
cancelled the case's depth and let a backgrounded cd reach the
foreground search.

runs_search now rejects --version / -V / --help, and --files /
--type-list for rg, reading separators glued to a word (grep -V;). A
command with a case and a cd before the search gives Unknown.

* fix(hook): a } before a redirect closes its group

Codex round 21: 'cd x && { git status; }>/dev/null 2>&1 & rg Foo' left
the group open, because group_marks closed a } only before whitespace.
The depth stayed at 1, the background & no longer ended the cd's list,
and the foreground search got Dir(x). A } before > < & | ; ) now closes.

* fix(hook): shell_words drops redirections with their targets

Codex round 22: 'cd x>/dev/null && rg Foo' kept '>/dev/null' in the cd
argument, so the directory looked missing and the hook took the envelope
cwd; and 'if c; then d; fi>/dev/null &' hid the fi, so the depth stayed
at 1 and the backgrounded cd reached the foreground search.

shell_words now ends a word at an unquoted < or >, drops the redirection
with its target and a leading fd number or &, and leaves <( / >( alone.
Every scan that reads words (cd arguments, compound closers, the search
term) sees the command the way the program does.

* fix(hook): the term and the cd target read the same search segment

Codex round 24: in a multi-line script, extract_from_shell read the whole
command as one word list, so 'rg --files' on a later line made
runs_search skip the first rg and the term came from the second search,
while cd_target_with_home resolved the directory of the first. The term
now comes from the first segment that runs a search, the same segment
the cd target reads.

* fix(hook): a search in a function body gives Unknown

Codex round 25: 'scan() { rg Foo; }; cd x && scan' stopped the walk at
the rg in the definition, before the cd, and returned Unchanged though
the search runs where scan is called. A search at or after a function
header (name(), name (), function name) now gives Unknown; the header
must be the command word, so rg 'fn main()' still resolves.

* fix(hook): spaced $( substitutions and redirects before a ~ argument

Codex round 26: 'root=$( cd /main && pwd -P ); rg Foo' skipped the
assignment word that opens the substitution and applied its cd to the
parent shell; and 'cd 2>/dev/null ~/x && rg Foo' read the redirection as
the raw cd argument, so the ~ looked quoted and the cd looked failed.
An assignment holding ( now puts the segment's command in a subshell,
and unquoted_tilde_arg skips redirections before the argument.

* fix(hook): a skipped || operand ends after its compound closes

Codex round 27: 'cd A || if test -d B; then cd B; fi; rg Foo' stopped
skipping at the if's ; because the skip depth counted only braces and
parens, so the fallback cd B looked sure and the search got Dir(B). The
skip depth now counts compound opens and closes too.

* fix(hook): every name a tool lookup lists is no search

Codex round 28: 'command -v rg grep git; cd x && rg Foo' skipped the
first name but took grep, the second, as the search, because
runs_search read only the word before it. It now also reads the
segment's command word: which / type / hash / whereis / cd / pushd, or
command with a v / V flag, makes no rg / grep in that segment a search.

* fix(hook): a search in a loop condition sees the loop's later cd

Codex round 29: 'cd x && until grep -q w Cargo.toml; do cd ..; done'
searched in x, then its parents, but the loop check read only the groups
open before the search segment, which itself opens the until. The check
now counts a loop the search segment opens.

* fix(hook): GIT_WORK_TREE / GIT_DIR git grep; glued -e patterns

Codex round 30: 'GIT_WORK_TREE=/main git grep Foo' searched /main while
the hook took the envelope cwd, since only the -C / --git-dir /
--work-tree options were read; and 'rg --regexp=Foo f' / 'grep -eFoo f'
skipped the pattern word and took the path as the term. A git grep with
GIT_DIR= or GIT_WORK_TREE= set in or before its segment now gives
Unknown, and a pattern glued to -e / --regexp= is the term.

* fix(hook): a cd to a directory missing now gives Unknown

Codex round 31 came back clean but named a blind spot: 'mkdir -p x && cd
x && rg Foo' and 'git worktree add ../wt && cd ../wt && rg Foo' make the
directory before the cd runs, while the hook saw it missing, treated the
cd as failed and returned Unchanged. A missing target now gives Unknown:
the search either never runs or runs in a directory the command made.

* fix(hook): Windows build and a plain cd-target path

CI on windows-latest failed to compile the hook tests: the symlink test
calls std::os::unix::fs::symlink. It is now #[cfg(unix)].

The same run showed the cd target would print as \\?\C:\... on
Windows, since std canonicalize returns a verbatim path. The target and
the test helper now use dunce::canonicalize, as repo_selector and
git_cache already do, and the integration test reads the header from the
parsed JSON so escaped backslashes do not hide it.

* test(hook): the escaped-space cd case is POSIX-only

windows-latest failed test_cd_target_quoted_cjk_path_with_spaces_resolves
on its unquoted case, 'cd C:\...\業主\ 原始碼\... && rg Foo': bash
reads every unquoted backslash there as an escape, so Unknown is the
right answer on Windows. The two quoted cases passed there; the
backslash-escaped case now runs on unix only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant