{"owner":"redis","repo":"redis-rb","hasSkills":true,"totalSkillsCount":5,"totalTokensCount":7091,"categories":["claude-rule","anthropic-skill","root-instruction"],"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md",".claude/skills/add-new-command/scripts/resolve_spec.sh",".claude/skills/add-new-command/SKILL.md",".claude/skills/add-new-command/examples/command-specification-template.md","AGENTS.md"],"skills":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n@AGENTS.md",".claude/skills/add-new-command/scripts/resolve_spec.sh":"#!/usr/bin/env bash\n# Deterministic spec resolver for the add-new-command skill.\n#\n# Takes the raw skill argument (a Redis command name like \"TS.BGET\", or a path\n# to a spec file) and decides — without any LLM reasoning — where the spec is,\n# whether it is filled in or still a stub, and (when the user must act) how to\n# resume. It prints a small structured report the skill body branches on.\n#\n# Run from the repo root. Usage:\n#   bash .claude/skills/add-new-command/scripts/resolve_spec.sh \"<command-name-or-path>\"\n\nset -euo pipefail\n\narg=\"${1:-}\"\narg=\"$(printf '%s' \"$arg\" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')\"\n\nif [ -z \"$arg\" ]; then\n  echo \"RESOLUTION: no_argument\"\n  echo \"Ask the user which Redis command to add (e.g. TS.BGET) or for a path to a spec file.\"\n  exit 0\nfi\n\nemit_spec_block() {\n  echo \"--- BEGIN SPEC ---\"\n  cat \"$1\"\n  echo \"\"\n  echo \"--- END SPEC ---\"\n}\n\n# Classify a found spec file as ready or incomplete, then emit its report.\n# A spec is still a stub if it carries the template's $COMMAND_NAME placeholder.\n#   $1 = source label, $2 = spec file path, $3 = re-run token (command name or path)\nhandle_found() {\n  local source=\"$1\" file=\"$2\" rerun=\"$3\"\n  if grep -qF -- '$COMMAND_NAME' \"$file\"; then\n    echo \"RESOLUTION: incomplete\"\n    echo \"SPEC_FILE: $file\"\n    echo \"RERUN_HINT: /add-new-command ${rerun}\"\n  else\n    echo \"RESOLUTION: ready\"\n    echo \"SOURCE: $source\"\n    echo \"SPEC_FILE: $file\"\n  fi\n  emit_spec_block \"$file\"\n}\n\n# 1. Explicit path to an existing file.\nif [ -f \"$arg\" ]; then\n  handle_found \"explicit_path\" \"$arg\" \"$arg\"\n  exit 0\nfi\n\n# 1b. Path-shaped argument (contains a slash or a .md suffix) that doesn't exist.\n# Don't fall through to command-name handling — that would fabricate a bogus\n# COMMAND / REDIS_IO_URL / TARGET_SPEC_FILE from the path string.\ncase \"$arg\" in\n  */* | *.md)\n    echo \"RESOLUTION: path_not_found\"\n    echo \"SPEC_FILE: $arg\"\n    echo \"The argument looks like a spec file path, but no such file exists.\"\n    echo \"RERUN_HINT: /add-new-command ${arg}\"\n    exit 0\n    ;;\nesac\n\n# 2. Treat as a command name. Uppercase + keep dots for the filename; lowercase for the URL.\nname_upper=\"$(printf '%s' \"$arg\" | tr '[:lower:]' '[:upper:]')\"\nname_lower=\"$(printf '%s' \"$arg\" | tr '[:upper:]' '[:lower:]')\"\nspec_file=\"command_specs/${name_upper}-specification-template.md\"\n\n# 3. Local spec lookup (authoritative; also the resume path).\nif [ -f \"$spec_file\" ]; then\n  handle_found \"local\" \"$spec_file\" \"$name_upper\"\n  exit 0\nfi\n\n# 4. No local spec — point at redis.io and where to save the spec.\necho \"RESOLUTION: missing\"\necho \"COMMAND: ${name_upper}\"\necho \"REDIS_IO_URL: https://redis.io/docs/latest/commands/${name_lower}/\"\necho \"TARGET_SPEC_FILE: ${spec_file}\"\necho \"RERUN_HINT: /add-new-command ${name_upper}\"\n",".claude/skills/add-new-command/SKILL.md":"---\ndescription: >-\n  Adds support for a new Redis command to the gem from a specification. Triggers on requests\n  like \"add support for the TS.BGET command\" or via /add-new-command <COMMAND|path>. Resolves the\n  spec from command_specs/, falling back to official redis.io docs. See .claude/skills/add-new-command/examples/command-specification-template.md.\nargument-hint: \"[command-name | path-to-spec]\"\nallowed-tools: Bash, Read, Write, Edit, WebFetch, Glob, Grep, AskUserQuestion\n---\n\n# Add new Redis command support\n\n`$ARGUMENTS` is either a Redis command **name** (e.g. `TS.BGET`, `JSON.GET`) or a **path** to a\nspecification file. Before doing anything else, resolve it to a filled specification using Step 0.\n\n## Step 0 — Resolve the specification\n\nRun `bash .claude/skills/add-new-command/scripts/resolve_spec.sh \"$ARGUMENTS\"` from the repo root. Branch on its `RESOLUTION:` line.\nThe only way into Step 1 is a `RESOLUTION: ready` from the resolver — never enter it on your own judgment that a spec looks filled.\n\n- **`ready`** — filled spec is in the output. Go to Step 1.\n- **`incomplete`** — spec exists but still has the `$COMMAND_NAME` placeholder. Ask the user to fill in the template, show the `RERUN_HINT` to resume, then STOP.\n- **`missing`** — WebFetch `REDIS_IO_URL`.\n  - Found → write a spec to `TARGET_SPEC_FILE` using `.claude/skills/add-new-command/examples/command-specification-template.md` structure, then use the **AskUserQuestion** tool to present a \"Proceed / Stop\" choice. On Proceed → re-run the resolver on `TARGET_SPEC_FILE` and branch on its `RESOLUTION:` again — only `ready` may enter Step 1; `incomplete` means the written spec is still a stub, so follow the `incomplete` branch. On Stop → end (the saved spec can be edited and re-run later).\n  - Not found → ask the user to either give a spec path, or have you copy `.claude/skills/add-new-command/examples/command-specification-template.md` verbatim to `TARGET_SPEC_FILE` (keep the `$COMMAND_NAME` marker so the resolver flags it `incomplete` until filled) for them to fill; then show the `RERUN_HINT` and STOP.\n- **`path_not_found`** — the argument looks like a file path but no file exists at `SPEC_FILE`. Tell the user, ask for a corrected path (or a command name to resolve instead), then STOP. Do not guess a command name from the path.\n- **`no_argument`** — ask for a command name or spec path, then STOP.\n\n## Execution Instructions\n\n### 1. Preparations\n\n- Go through the guide `specs/adding-commands.md`\n\n### 2. Read and Understand\n\n- Read the ENTIRE specification carefully\n- Go through Command Description and identify command type (string, list, set, etc.)\n- Go through the Command API:\n    - Identify required and optional arguments\n    - Identify how to match Redis command arguments type to Ruby types\n    - Identify return value and possible response types\n- Check relevant Redis-Cli examples, if provided\n- Review the Test Plan\n\n### 3. Execute Tasks in Order\n\n#### a. Navigate to the task\n- Identify the files and action required\n- Read existing related files if modifying\n\n#### b. Implement the command\n- Identify the command API surface object you need to work with.\n- Identify required and optional arguments and their matching types.\n- Add command implementation following `specs/adding-commands.md`\n- Ensure doc string documents are added\n\n#### c. Verify as you go\n- After each file change, check syntax\n- Ensure imports are correct\n- Verify types are properly defined\n\n### 4. Implement Testing Plan\n\nAfter completing implementation tasks:\n\n- Identify matching test file or create new one if needed\n- Implement all test cases as separate test methods\n- Ensure adding version constraint if specified in the specification\n- Ensure tests cover edge cases\n\n### 5. Run tests\n\n- Run newly added test cases\n- Get back to the Implementation stage if any test failed\n\n### 6. Final Verification\n\nBefore completing:\n\n- ✅ All tasks from plan completed\n- ✅ All tests created and passing\n- ✅ Code follows project conventions\n- ✅ Documentation added/updated as needed\n\n## Output Report\n\nProvide summary:\n\n### Specification Source\n- How the spec was resolved (local file / built from redis.io / user-provided path)\n- Path to the spec file used\n\n### Completed Tasks\n- List of all tasks completed\n- Files created (with paths)\n- Files modified (with paths)\n\n### Tests Added\n- Test files created\n- Test cases implemented\n- Test results\n",".claude/skills/add-new-command/examples/command-specification-template.md":"# $COMMAND_NAME command specification\n\n## Supported version\n\nAdd supported Redis version here. For example: Redis >= 6.2.0\n\n## Command description\n\nAdd a description of the command here.\n\n## Command API\n\nSpecify an API for the command in the format that official docs uses. For example:\n\n```\n$COMMAND_NAME $key $member [NX|XX] [CH] [INCR]\n```\n\n## Redis-CLI examples\n\nAdd relevant Redis-CLI examples here.\n\n## Test plan\n\nSpecify how you want to test the command in terms of integration testing. For example:\n\n- Test only with required arguments, assert that single value returned\n- Test with required arguments and optional XX modifier, ensure that 1 returned\n- ...\n","AGENTS.md":"## Repository layout\n\nThis repository ships **two gems** from one tree:\n\n- `redis` — the high-level standalone/sentinel client. Source under `lib/`, gemspec at `redis.gemspec`.\n- `redis-clustering` — the cluster client, a thin subclass that depends on `redis`. Source under `cluster/lib/`, gemspec at `cluster/redis-clustering.gemspec`. It is a separate gem on purpose: passing `cluster:` to `Redis.new` raises (see `lib/redis.rb`).\n\nCluster code, tests, and CHANGELOG live under `cluster/`. When making changes that span both, edit both — the cluster gem reuses `lib/redis/commands/**` from the main gem but has its own client class (`cluster/lib/redis/cluster/client.rb`) and transaction adapter.\n\n### `Redis::XXH3` — pure-Ruby local digest computation\n\n`lib/redis/xxh3.rb` provides `Redis::XXH3.hexdigest`, local XXH3-64 digest computation for `SET`'s `IFDEQ`/`IFDNE` and `DELEX`'s `IFDEQ`/`IFDNE` options (Redis 8.4+ CAS/CAD), so a `match-digest` can be built without a round trip to `DIGEST`. It's plain Ruby, always available — no native extension, no build step, no optional flag: an earlier version of this vendored the reference xxHash C library as a native extension, but that made every `gem install redis` depend on `make` being present (a RubyGems property of any gem declaring `s.extensions`, regardless of whether the extension actually compiles anything), so it was replaced with a direct, function-by-function port of the same reference algorithm (https://github.com/Cyan4973/xxHash, v0.8.3 — the version and secret table the server itself uses). Each private method in `lib/redis/xxh3.rb` is named after the upstream C function it mirrors, to keep that mapping checkable; the 128-bit multiply-and-fold XXH3 needs throughout — the main source of complexity in a C port, which has no native 128-bit integer type — is trivial in Ruby thanks to native arbitrary-precision integers. `test/redis/xxh3_test.rb` cross-checks known-answer vectors at every algorithm size-class boundary (0–16, 17–128, 129–240, >240 bytes) against a live server's own `DIGEST`, and `test/redis/digest_consistency_test.rb` drives real `SET`/`DELEX` CAS calls with a locally computed digest end to end.\n\n## Common commands\n\nThe dev workflow runs Redis in Docker containers via `docker-compose.yml` using the prebuilt `redislabs/client-libs-test` image. Topologies are selected by Docker profiles (`standalone`, `sentinel`, `cluster`, `all`). The `makefile` is a thin shim around `docker compose --profile X up -d --wait` and `down -v`, so the historical target names still work:\n\n```sh\n# bring up everything: standalone, replica, 3 sentinels, 6-node cluster\nmake start_all\n\n# run the full suite (all four test groups)\nmake test\n\n# stop everything\nmake stop_all\n\n# one shot: start, test, stop\nmake all\n```\n\n`make test` shells out to `bundle exec rake test`, which runs four `Rake::TestTask` groups defined in `Rakefile`:\n\n```sh\nbundle exec rake test:redis        # lib/redis core\nbundle exec rake test:distributed  # lib/redis/distributed (client-side sharding)\nbundle exec rake test:sentinel     # sentinel-mode tests\nbundle exec rake test:cluster      # cluster gem (loads cluster/lib + cluster/test)\n```\n\nTo run a single test file or method, use Minitest's options via `TESTOPTS`:\n\n```sh\nbundle exec rake test:redis TEST=test/redis/commands_on_strings_test.rb\nBUNDLE_GEMFILE=cluster/Gemfile bundle exec rake test:cluster TEST=cluster/test/commands_on_strings_test.rb TESTOPTS=\"--name=/get/\"\n```\n\nThe cluster group needs `BUNDLE_GEMFILE=cluster/Gemfile` (the root bundle doesn't include `redis-cluster-client`); CI does the same.\n\nOther useful knobs:\n\n- `REDIS_VERSION=8.X.Y make start_all` — pin the image tag (default is set at the top of `makefile`). Tags are published per Redis minor.patch (e.g. `8.0.6`, `8.2.6`, `8.4.3`, `8.6.3`, `8.8.0`); a bare `8.4` tag generally does not exist.\n- `DRIVER=hiredis bundle exec rake test` — run the suite against the `hiredis-client` C-extension driver instead of the pure-Ruby parser (see `test/helper.rb`).\n- `PROTOCOL=2 bundle exec rake test` — run the suite over RESP2 instead of the default RESP3 (`test/helper.rb:11`). Reply-reshaping changes should be verified under both protocols (and ideally both drivers).\n- `REDIS_SOCKET_PATH=...` — override the Unix socket location. The default expects `tmp/redis.sock`, which the standalone container bind-mounts from `./tmp:/sockets`; `test/helper.rb` aborts if it's missing.\n- `bundle exec rubocop` — lint. The Rubocop config is in `.rubocop.yml` (root) and `cluster/.rubocop.yml`.\n- `bin/console` — IRB session with `redis` preloaded.\n\nYou can also drive `docker compose` directly when you want a single profile up:\n\n```sh\ndocker compose --profile standalone up -d --wait\ndocker compose --profile sentinel   up -d --wait\ndocker compose --profile cluster    up -d --wait\ndocker compose --profile all down -v\n```\n\nThe cluster profile's healthcheck waits for `cluster_state:ok` (not just PING) so tests can connect without hitting the historical `InitialSetupError` race. Pre-configured sentinel node directories live under `test/support/sentinel-config/` and are bind-mounted into the sentinel container; the image's entrypoint starts each as a sentinel because their directory names begin with `node-sentinel`.\n\n### macOS / Docker Desktop note\n\nThe compose stack uses `network_mode: host` so sentinel and cluster nodes report `127.0.0.1` addresses the test runner on the host can reach. On Linux this works natively. On macOS, Docker Desktop's \"host networking\" beta must be enabled (Settings → Resources → Network → Enable host networking); without it, the containers are healthy but their ports aren't visible on `127.0.0.1`. AF_UNIX sockets bind-mounted out of the standalone container also don't route through Docker Desktop's VM on macOS, so `test_connecting_to_unix_domain_socket` fails locally but passes on Linux CI.\n\n## Architecture\n\n### Layering\n\n```\nRedis (lib/redis.rb)                ergonomics: keyword DSL, reply reshaping, error translation,\n   ├ Commands (lib/redis/commands)  pub/sub second-socket, pipelined/multi wrappers,\n   ├ Monitor (lock)                 RESP3→RESP2 fallback, HIMPORT fieldset registry\n   └ @client : Redis::Client < RedisClient\n                                    ↓\n              redis-client gem (external, vendored as runtime dep)\n                                    ↓\n              TCP/TLS/Unix socket\n```\n\nThe `Redis` class is the public surface. It delegates all network I/O to `Redis::Client`, which inherits from `RedisClient` (in the `redis-client` gem) and only adds:\n\n1. Error translation: maps `RedisClient::*` exceptions to `Redis::*` via `ERROR_MAPPING` in `lib/redis/client.rb`. Every public method on `Redis::Client` is wrapped in a rescue that calls `Client.translate_error!`.\n2. A default of `protocol: 3` (RESP3) in `lib/redis/client.rb`; callers can pass `protocol: 2`. Servers without RESP3 (Redis < 6.0, or anything replying `NOPROTO`) are detected on connect and `Redis#with_protocol_fallback` (`lib/redis.rb`) transparently rebuilds `@client` for RESP2 — every `@client` access funnels through `Redis#synchronize`, which is what applies the fallback, so pipelines/multi/watch fall back too. Return values are protocol-invariant except GEO coordinates (`Float` under RESP3).\n3. Trivial config delegators (`#host`, `#port`, `#db`, …).\n\nThe full command execution flow is: `Redis#some_command` (defined in `lib/redis/commands/<category>.rb`) builds an array → `Redis#send_command` grabs `@monitor` → `Redis::Client#call_v` rescues + re-raises → `RedisClient#call_v` serializes RESP and reads the reply → optional reshape lambda runs → result returned.\n\n### Commands as a module composition\n\nEvery Redis command category is a module under `lib/redis/commands/` (strings, lists, hashes, sets, sorted_sets, streams, arrays, scripting, transactions, pubsub, etc.). Module-provided command families live under `lib/redis/commands/modules/` (`json.rb` for `JSON.*`, `search.rb` + `search/` for the Query Engine `FT.*`, whose replies reshape into `Search::SearchResult`/`Search::AggregateResult` objects). They are all `include`d into a parent `Redis::Commands` module (`lib/redis/commands.rb`), which is in turn mixed into:\n\n- `Redis` (`lib/redis.rb`)\n- `Redis::PipelinedConnection` (`lib/redis/pipeline.rb`) — used inside `pipelined` and `multi` blocks\n- (Indirectly) `Redis::Cluster` via inheritance from `Redis`\n\nThis is the **single most important pattern in the codebase**. To add a new command, find the matching `commands/<category>.rb` and add a method that calls `send_command([:cmd, ...])` — that method automatically becomes available on every client type. The mixin classes each provide their own `send_command` and `synchronize` so the same `Commands` methods work in direct calls, pipelines, and transactions.\n\nThere is also a catch-all `method_missing` in `lib/redis/commands.rb` that forwards any unknown method as a Redis command — so unwrapped commands \"just work.\"\n\n### Reply reshaping (protocol-aware)\n\nThe top of `lib/redis/commands.rb` defines a family of lambdas — `Boolify`, `BoolifySet`, `Hashify`, `Floatify`, `FloatifyPairs`, `HashifyInfo`, `HashifyStreamEntries`, `HashifyClusterNodes`, … — that reshape raw replies into idiomatic Ruby (Hash, Float, boolean, etc.). They are passed as blocks to `send_command`:\n\n```ruby\ndef incrbyfloat(key, increment)\n  send_command([:incrbyfloat, key, Float(increment)], &Floatify)\nend\n```\n\nSince 6.0 the client negotiates RESP3 by default, so these lambdas are **protocol-aware**: they must accept both the RESP2 wire shape (flat arrays, string-encoded numbers) and the RESP3 one (native maps, doubles, pairs) and converge on the same Ruby value. The pattern is \"detect the already-final shape and pass it through\" — e.g. `Hashify` returns a `Hash` unchanged and `each_slice(2).to_h`'s a flat array; `FloatifyPairs` skips re-mapping when the reply is already `[[member, Float], ...]`. When adding or changing a lambda, keep both branches, and test the command under both protocols (`PROTOCOL=2` / `PROTOCOL=3`, see below).\n\n`Boolify` is the exception: it assumes an integer reply (`value != 0 unless value.nil?`) and would invert a native RESP3 boolean — `Boolify.call(false)` returns `true`. That's unreachable for commands that reply with integers under both protocols (the case for all current uses), but confirm the command's `reply_schema` says `\"type\": \"integer\"` before reaching for it.\n\nWhen adding a command that needs reply transformation, write or reuse one of these lambdas; do not coerce in the command method itself. See `specs/adding-commands.md` for the full catalog and the end-to-end checklist (the `add-new-command` skill automates this flow from a spec file).\n\n### Connection lifecycle (long-lived, lazy, fork-safe)\n\n- `Redis.new` does **not** open a socket — it's lazy. The first command triggers `RedisClient#ensure_connected`, which runs the `connection_prelude` (HELLO/AUTH, SELECT, CLIENT SETINFO, then CLIENT SETNAME and ROLE) and caches the socket.\n- One `Redis` instance owns one socket, guarded by a `Monitor` defined in `lib/redis.rb` and acquired in `Redis#send_command`. The reentrant lock matters for nested `watch { multi { ... } }` patterns; do not \"optimize\" it to `Mutex` without redesigning that API.\n- For concurrent use, wrap with `connection_pool` — the README documents this and it's the only recommended pooling story. The gem itself does not pool.\n- Fork safety is handled inside `redis-client` via `PIDCache`: a forked child detects the inherited socket and reconnects on next use. The `inherit_socket` option in `lib/redis.rb` disables that check; almost no callers should use it.\n\n### Pub/Sub — separate socket, same process\n\n`subscribe` / `psubscribe` / `ssubscribe` open a **second** dedicated socket via `@client.pubsub` (in `lib/redis.rb`) wrapped in `SubscribedClient` (`lib/redis/subscribe.rb`). This keeps the command socket usable from other threads while one thread is blocked in the `next_event` loop. The subscription loop on the calling thread is synchronous — if you want it off the main thread, the caller spawns a `Thread`. There's a separate write-monitor on the subscription socket. Sharded pub/sub (`SSUBSCRIBE`) subscribes one channel at a time to avoid cross-slot errors in cluster mode.\n\n### Pipelines and transactions\n\n`pipelined` and `multi` both yield a `Redis::PipelinedConnection` (or `MultiConnection`) that re-includes `Commands` (`lib/redis/pipeline.rb`). Each command inside the block returns a `Redis::Future` that resolves when the batch flushes. `MultiFuture` (in `lib/redis/pipeline.rb`) splits the `EXEC` reply array back across individual command futures. Inside a `MULTI`, blocking commands degrade to non-blocking — that's intentional, matching Redis server semantics.\n\n### HIMPORT — server session state with client-side recovery (experimental)\n\nThe `HIMPORT` command family (Redis 8.10) is the one place a command's state outlives a single call: fieldsets are **per-connection server session state**, destroyed by reconnect/failover/`RESET`. `Redis#initialize` keeps a registry of prepared schemas (`@himport_fieldsets`) and the `himport_*` overrides in `lib/redis.rb` repair a lost fieldset reactively — on the server's \"no such fieldset\" error, re-prepare from the registry and retry the SET exactly once. Recovery is per-fieldset and lazy (there is no reconnect hook); disable with `himport_auto_prepare: false`, in which case re-preparing is the caller's responsibility (the registry has no public reader). The overrides hold `@monitor` across command + registry mutation — don't split them. The cluster subclass adds reply aggregation on top (prepare/discard fan out to all masters).\n\n### Client identification (CLIENT SETINFO)\n\n`Redis::LibIdentity` (`lib/redis/lib_identity.rb`) reports `lib-name=redis-rb lib-ver=<version>` on every connection by **prepending onto `RedisClient::Config` / `RedisClient::SentinelConfig`** and appending a second `SETINFO` pair to the connection prelude (last-write-wins overrides redis-client's own pair; zero extra round trips). The override is gated by a `redis-rb_v<version>` marker in `driver_info`, so applications using redis-client directly are untouched. Downstream gems extend the name via `Redis.new(driver_info: \"my-gem_v1.0.0\")`; `driver_info: false` disables identification. This couples to redis-client's `connection_prelude` — one of the reasons the gemspec pins redis-client to an exact version; re-audit on every driver bump.\n\n### Cluster gem differences\n\n`cluster/lib/redis/cluster.rb` defines `Redis::Cluster < ::Redis`, so it inherits the full `Commands` surface but swaps `initialize_client` to build a `RedisClient::Cluster` via the `redis-cluster-client` gem. Cluster-specific differences worth knowing:\n\n- `Redis::Cluster::Client` (`cluster/lib/redis/cluster/client.rb`) maintains a **per-node connection pool internally** via `redis-cluster-client`. Do not wrap this in the `connection_pool` gem.\n- `watch` **requires a block with an argument** in `cluster/lib/redis/cluster/client.rb`. The block receives a `Redis::Cluster::TransactionAdapter` that pins commands to the same node/slot. Standalone `Redis#watch` accepts the no-block form; cluster does not.\n- `Redis::Cluster#connection` raises `NotImplementedError` — there's no single \"connection\" to report.\n- Extra error classes: `InitialSetupError`, `OrchestrationCommandNotSupported`, `CommandErrorCollection`, `AmbiguousNodeError`, `TransactionConsistencyError`, `NodeMightBeDown` (defined in `cluster/lib/redis/cluster.rb`).\n\n### Command routings (cluster)\n\nWhich node(s) a command goes to, and how fan-out replies are combined, is the **driver's job, not redis-rb's**. `redis-cluster-client` routes keyed commands by slot and keyless commands by the server's command tips (`request_policy` / `response_policy` from `COMMAND INFO`), with a built-in routing table for the exceptions (`RedisClient::Cluster::Router::RoutingTable`). Do not reimplement that in `Redis::Cluster` by enumerating nodes and calling them one by one — it bypasses the driver's redirection handling, topology refresh and error collection.\n\nWhen the driver's default routing is wrong for a command, fix it through the driver's `command_routings` config option rather than in Ruby: `Redis::Cluster#initialize_client` passes `DEFAULT_COMMAND_ROUTINGS` (`cluster/lib/redis/cluster.rb`), a `{ 'command' => { request_policy:, response_policy: } }` hash the driver validates and merges over its table, and caller-supplied `command_routings:` are merged on top so applications keep the last word. Supported values are the driver's, not the server's full set: `request_policy` `all_shards` / `all_nodes`, and `response_policy` `nil` (one reply per node), `all_succeeded`, `one_succeeded`, `agg_sum` (sums Integer replies only). Anything else, e.g. `agg_min`, is rejected at construction.\n\nOnly when no driver response policy produces the documented standalone return value does `Redis::Cluster` add a **thin reply-shaping override** (`waitaof`, `himport_*`): the driver still does the fan-out and hands back one reply per primary, and the override only collapses that array into the standalone shape. It has to be a method override, not a reply block on the shared command: the driver applies reply blocks **per node**, before it collects the fan-out, so a block never sees the aggregate. Aggregate the way the server's `response_policy` tip says (`agg_min` → per-position minimum, as `waitaof` does, so thresholds keep their single-node meaning; `agg_sum` → sum), not the way a sibling command happens to be handled. Guard it with `reply.first.is_a?(Array)` so a plain reply passes through (a caller who changed the routing, or a later driver that aggregates natively). The missing override does not affect pipelines' *reply shape* — `pipelined`/`multi` route each command to a single node, so the plain standalone shape comes back without any aggregation — but it does not restore the command's *meaning* there: single-node routing inside a pipeline is chosen by the driver (e.g. `any_replica_node_key`), not by which node carried the pipeline's writes, so a fan-out command's per-connection guarantee is still lost. Add a cluster test that issues the command inside `pipelined` to lock in the shape, but don't treat that test as proof the guarantee holds. Prefer contributing the missing policy upstream over growing these overrides. Re-audit `DEFAULT_COMMAND_ROUTINGS` and the overrides on every `redis-cluster-client` bump (the gemspec pins an exact version for this reason).\n\n### `Redis::Distributed` is not Redis Cluster\n\n`lib/redis/distributed.rb` + `lib/redis/hash_ring.rb` implement **client-side consistent-hash sharding** across N independent standalone Redis servers. It is *not* the Redis Cluster protocol — there are no slot maps, no MOVED/ASK redirects, no automatic resharding. Keys are hashed with CRC32 against an MD5-built ring (160 vnodes/server) and dispatched to one underlying `Redis` instance. Multi-key commands raise `CannotDistribute` since co-location isn't guaranteed.\n\nIt is **separately maintained** from `Commands` — `Redis::Distributed` does *not* include the `Commands` mixin; every method is explicitly defined in `distributed.rb` so it can route to the right node via `node_for(key)`. When adding a new Redis command that should be available here, add a corresponding method to `lib/redis/distributed.rb` and a test under `test/distributed/`. The `test:distributed` Rake task runs as part of the default suite, so missing or broken Distributed implementations will fail CI.\n\nIt is supported (recent commits add JSON, arrays (`AR*`), HEXPIRE/HPTTL, HSCAN, etc.) and not deprecated. For new applications needing horizontal scaling, `Redis::Cluster` is generally the better choice because the server enforces consistency; `Redis::Distributed` is the right tool when you have N independent standalone Redises and want memcache-style key distribution.\n\n## Conventions\n\n- Every file starts with `# frozen_string_literal: true`. Keep it when editing or creating files.\n- Commands take symbols for the verb (`[:incr, key]`); strings and arguments are coerced where needed (`Integer(x)`, `Float(x)`, `value.to_s`). Follow the local pattern in `lib/redis/commands/<category>.rb` when adding new methods.\n- Yard-style docstrings (`@param`, `@option`, `@return`, `@example`) are on every public command method. New methods should keep that format.\n- Error mapping is centralized in `Redis::Client::ERROR_MAPPING` (and extended in `Redis::Cluster::Client::ERROR_MAPPING`). If you add a new RedisClient error class to handle, add it there rather than catching in individual command methods.\n- Test files must live under one of `test/redis`, `test/distributed`, `test/sentinel`, `cluster/test`. The Rakefile fails the build if a `*_test.rb` exists outside those groups.\n"},"files":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n@AGENTS.md",".claude/skills/add-new-command/scripts/resolve_spec.sh":"#!/usr/bin/env bash\n# Deterministic spec resolver for the add-new-command skill.\n#\n# Takes the raw skill argument (a Redis command name like \"TS.BGET\", or a path\n# to a spec file) and decides — without any LLM reasoning — where the spec is,\n# whether it is filled in or still a stub, and (when the user must act) how to\n# resume. It prints a small structured report the skill body branches on.\n#\n# Run from the repo root. Usage:\n#   bash .claude/skills/add-new-command/scripts/resolve_spec.sh \"<command-name-or-path>\"\n\nset -euo pipefail\n\narg=\"${1:-}\"\narg=\"$(printf '%s' \"$arg\" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')\"\n\nif [ -z \"$arg\" ]; then\n  echo \"RESOLUTION: no_argument\"\n  echo \"Ask the user which Redis command to add (e.g. TS.BGET) or for a path to a spec file.\"\n  exit 0\nfi\n\nemit_spec_block() {\n  echo \"--- BEGIN SPEC ---\"\n  cat \"$1\"\n  echo \"\"\n  echo \"--- END SPEC ---\"\n}\n\n# Classify a found spec file as ready or incomplete, then emit its report.\n# A spec is still a stub if it carries the template's $COMMAND_NAME placeholder.\n#   $1 = source label, $2 = spec file path, $3 = re-run token (command name or path)\nhandle_found() {\n  local source=\"$1\" file=\"$2\" rerun=\"$3\"\n  if grep -qF -- '$COMMAND_NAME' \"$file\"; then\n    echo \"RESOLUTION: incomplete\"\n    echo \"SPEC_FILE: $file\"\n    echo \"RERUN_HINT: /add-new-command ${rerun}\"\n  else\n    echo \"RESOLUTION: ready\"\n    echo \"SOURCE: $source\"\n    echo \"SPEC_FILE: $file\"\n  fi\n  emit_spec_block \"$file\"\n}\n\n# 1. Explicit path to an existing file.\nif [ -f \"$arg\" ]; then\n  handle_found \"explicit_path\" \"$arg\" \"$arg\"\n  exit 0\nfi\n\n# 1b. Path-shaped argument (contains a slash or a .md suffix) that doesn't exist.\n# Don't fall through to command-name handling — that would fabricate a bogus\n# COMMAND / REDIS_IO_URL / TARGET_SPEC_FILE from the path string.\ncase \"$arg\" in\n  */* | *.md)\n    echo \"RESOLUTION: path_not_found\"\n    echo \"SPEC_FILE: $arg\"\n    echo \"The argument looks like a spec file path, but no such file exists.\"\n    echo \"RERUN_HINT: /add-new-command ${arg}\"\n    exit 0\n    ;;\nesac\n\n# 2. Treat as a command name. Uppercase + keep dots for the filename; lowercase for the URL.\nname_upper=\"$(printf '%s' \"$arg\" | tr '[:lower:]' '[:upper:]')\"\nname_lower=\"$(printf '%s' \"$arg\" | tr '[:upper:]' '[:lower:]')\"\nspec_file=\"command_specs/${name_upper}-specification-template.md\"\n\n# 3. Local spec lookup (authoritative; also the resume path).\nif [ -f \"$spec_file\" ]; then\n  handle_found \"local\" \"$spec_file\" \"$name_upper\"\n  exit 0\nfi\n\n# 4. No local spec — point at redis.io and where to save the spec.\necho \"RESOLUTION: missing\"\necho \"COMMAND: ${name_upper}\"\necho \"REDIS_IO_URL: https://redis.io/docs/latest/commands/${name_lower}/\"\necho \"TARGET_SPEC_FILE: ${spec_file}\"\necho \"RERUN_HINT: /add-new-command ${name_upper}\"\n",".claude/skills/add-new-command/SKILL.md":"---\ndescription: >-\n  Adds support for a new Redis command to the gem from a specification. Triggers on requests\n  like \"add support for the TS.BGET command\" or via /add-new-command <COMMAND|path>. Resolves the\n  spec from command_specs/, falling back to official redis.io docs. See .claude/skills/add-new-command/examples/command-specification-template.md.\nargument-hint: \"[command-name | path-to-spec]\"\nallowed-tools: Bash, Read, Write, Edit, WebFetch, Glob, Grep, AskUserQuestion\n---\n\n# Add new Redis command support\n\n`$ARGUMENTS` is either a Redis command **name** (e.g. `TS.BGET`, `JSON.GET`) or a **path** to a\nspecification file. Before doing anything else, resolve it to a filled specification using Step 0.\n\n## Step 0 — Resolve the specification\n\nRun `bash .claude/skills/add-new-command/scripts/resolve_spec.sh \"$ARGUMENTS\"` from the repo root. Branch on its `RESOLUTION:` line.\nThe only way into Step 1 is a `RESOLUTION: ready` from the resolver — never enter it on your own judgment that a spec looks filled.\n\n- **`ready`** — filled spec is in the output. Go to Step 1.\n- **`incomplete`** — spec exists but still has the `$COMMAND_NAME` placeholder. Ask the user to fill in the template, show the `RERUN_HINT` to resume, then STOP.\n- **`missing`** — WebFetch `REDIS_IO_URL`.\n  - Found → write a spec to `TARGET_SPEC_FILE` using `.claude/skills/add-new-command/examples/command-specification-template.md` structure, then use the **AskUserQuestion** tool to present a \"Proceed / Stop\" choice. On Proceed → re-run the resolver on `TARGET_SPEC_FILE` and branch on its `RESOLUTION:` again — only `ready` may enter Step 1; `incomplete` means the written spec is still a stub, so follow the `incomplete` branch. On Stop → end (the saved spec can be edited and re-run later).\n  - Not found → ask the user to either give a spec path, or have you copy `.claude/skills/add-new-command/examples/command-specification-template.md` verbatim to `TARGET_SPEC_FILE` (keep the `$COMMAND_NAME` marker so the resolver flags it `incomplete` until filled) for them to fill; then show the `RERUN_HINT` and STOP.\n- **`path_not_found`** — the argument looks like a file path but no file exists at `SPEC_FILE`. Tell the user, ask for a corrected path (or a command name to resolve instead), then STOP. Do not guess a command name from the path.\n- **`no_argument`** — ask for a command name or spec path, then STOP.\n\n## Execution Instructions\n\n### 1. Preparations\n\n- Go through the guide `specs/adding-commands.md`\n\n### 2. Read and Understand\n\n- Read the ENTIRE specification carefully\n- Go through Command Description and identify command type (string, list, set, etc.)\n- Go through the Command API:\n    - Identify required and optional arguments\n    - Identify how to match Redis command arguments type to Ruby types\n    - Identify return value and possible response types\n- Check relevant Redis-Cli examples, if provided\n- Review the Test Plan\n\n### 3. Execute Tasks in Order\n\n#### a. Navigate to the task\n- Identify the files and action required\n- Read existing related files if modifying\n\n#### b. Implement the command\n- Identify the command API surface object you need to work with.\n- Identify required and optional arguments and their matching types.\n- Add command implementation following `specs/adding-commands.md`\n- Ensure doc string documents are added\n\n#### c. Verify as you go\n- After each file change, check syntax\n- Ensure imports are correct\n- Verify types are properly defined\n\n### 4. Implement Testing Plan\n\nAfter completing implementation tasks:\n\n- Identify matching test file or create new one if needed\n- Implement all test cases as separate test methods\n- Ensure adding version constraint if specified in the specification\n- Ensure tests cover edge cases\n\n### 5. Run tests\n\n- Run newly added test cases\n- Get back to the Implementation stage if any test failed\n\n### 6. Final Verification\n\nBefore completing:\n\n- ✅ All tasks from plan completed\n- ✅ All tests created and passing\n- ✅ Code follows project conventions\n- ✅ Documentation added/updated as needed\n\n## Output Report\n\nProvide summary:\n\n### Specification Source\n- How the spec was resolved (local file / built from redis.io / user-provided path)\n- Path to the spec file used\n\n### Completed Tasks\n- List of all tasks completed\n- Files created (with paths)\n- Files modified (with paths)\n\n### Tests Added\n- Test files created\n- Test cases implemented\n- Test results\n",".claude/skills/add-new-command/examples/command-specification-template.md":"# $COMMAND_NAME command specification\n\n## Supported version\n\nAdd supported Redis version here. For example: Redis >= 6.2.0\n\n## Command description\n\nAdd a description of the command here.\n\n## Command API\n\nSpecify an API for the command in the format that official docs uses. For example:\n\n```\n$COMMAND_NAME $key $member [NX|XX] [CH] [INCR]\n```\n\n## Redis-CLI examples\n\nAdd relevant Redis-CLI examples here.\n\n## Test plan\n\nSpecify how you want to test the command in terms of integration testing. For example:\n\n- Test only with required arguments, assert that single value returned\n- Test with required arguments and optional XX modifier, ensure that 1 returned\n- ...\n","AGENTS.md":"## Repository layout\n\nThis repository ships **two gems** from one tree:\n\n- `redis` — the high-level standalone/sentinel client. Source under `lib/`, gemspec at `redis.gemspec`.\n- `redis-clustering` — the cluster client, a thin subclass that depends on `redis`. Source under `cluster/lib/`, gemspec at `cluster/redis-clustering.gemspec`. It is a separate gem on purpose: passing `cluster:` to `Redis.new` raises (see `lib/redis.rb`).\n\nCluster code, tests, and CHANGELOG live under `cluster/`. When making changes that span both, edit both — the cluster gem reuses `lib/redis/commands/**` from the main gem but has its own client class (`cluster/lib/redis/cluster/client.rb`) and transaction adapter.\n\n### `Redis::XXH3` — pure-Ruby local digest computation\n\n`lib/redis/xxh3.rb` provides `Redis::XXH3.hexdigest`, local XXH3-64 digest computation for `SET`'s `IFDEQ`/`IFDNE` and `DELEX`'s `IFDEQ`/`IFDNE` options (Redis 8.4+ CAS/CAD), so a `match-digest` can be built without a round trip to `DIGEST`. It's plain Ruby, always available — no native extension, no build step, no optional flag: an earlier version of this vendored the reference xxHash C library as a native extension, but that made every `gem install redis` depend on `make` being present (a RubyGems property of any gem declaring `s.extensions`, regardless of whether the extension actually compiles anything), so it was replaced with a direct, function-by-function port of the same reference algorithm (https://github.com/Cyan4973/xxHash, v0.8.3 — the version and secret table the server itself uses). Each private method in `lib/redis/xxh3.rb` is named after the upstream C function it mirrors, to keep that mapping checkable; the 128-bit multiply-and-fold XXH3 needs throughout — the main source of complexity in a C port, which has no native 128-bit integer type — is trivial in Ruby thanks to native arbitrary-precision integers. `test/redis/xxh3_test.rb` cross-checks known-answer vectors at every algorithm size-class boundary (0–16, 17–128, 129–240, >240 bytes) against a live server's own `DIGEST`, and `test/redis/digest_consistency_test.rb` drives real `SET`/`DELEX` CAS calls with a locally computed digest end to end.\n\n## Common commands\n\nThe dev workflow runs Redis in Docker containers via `docker-compose.yml` using the prebuilt `redislabs/client-libs-test` image. Topologies are selected by Docker profiles (`standalone`, `sentinel`, `cluster`, `all`). The `makefile` is a thin shim around `docker compose --profile X up -d --wait` and `down -v`, so the historical target names still work:\n\n```sh\n# bring up everything: standalone, replica, 3 sentinels, 6-node cluster\nmake start_all\n\n# run the full suite (all four test groups)\nmake test\n\n# stop everything\nmake stop_all\n\n# one shot: start, test, stop\nmake all\n```\n\n`make test` shells out to `bundle exec rake test`, which runs four `Rake::TestTask` groups defined in `Rakefile`:\n\n```sh\nbundle exec rake test:redis        # lib/redis core\nbundle exec rake test:distributed  # lib/redis/distributed (client-side sharding)\nbundle exec rake test:sentinel     # sentinel-mode tests\nbundle exec rake test:cluster      # cluster gem (loads cluster/lib + cluster/test)\n```\n\nTo run a single test file or method, use Minitest's options via `TESTOPTS`:\n\n```sh\nbundle exec rake test:redis TEST=test/redis/commands_on_strings_test.rb\nBUNDLE_GEMFILE=cluster/Gemfile bundle exec rake test:cluster TEST=cluster/test/commands_on_strings_test.rb TESTOPTS=\"--name=/get/\"\n```\n\nThe cluster group needs `BUNDLE_GEMFILE=cluster/Gemfile` (the root bundle doesn't include `redis-cluster-client`); CI does the same.\n\nOther useful knobs:\n\n- `REDIS_VERSION=8.X.Y make start_all` — pin the image tag (default is set at the top of `makefile`). Tags are published per Redis minor.patch (e.g. `8.0.6`, `8.2.6`, `8.4.3`, `8.6.3`, `8.8.0`); a bare `8.4` tag generally does not exist.\n- `DRIVER=hiredis bundle exec rake test` — run the suite against the `hiredis-client` C-extension driver instead of the pure-Ruby parser (see `test/helper.rb`).\n- `PROTOCOL=2 bundle exec rake test` — run the suite over RESP2 instead of the default RESP3 (`test/helper.rb:11`). Reply-reshaping changes should be verified under both protocols (and ideally both drivers).\n- `REDIS_SOCKET_PATH=...` — override the Unix socket location. The default expects `tmp/redis.sock`, which the standalone container bind-mounts from `./tmp:/sockets`; `test/helper.rb` aborts if it's missing.\n- `bundle exec rubocop` — lint. The Rubocop config is in `.rubocop.yml` (root) and `cluster/.rubocop.yml`.\n- `bin/console` — IRB session with `redis` preloaded.\n\nYou can also drive `docker compose` directly when you want a single profile up:\n\n```sh\ndocker compose --profile standalone up -d --wait\ndocker compose --profile sentinel   up -d --wait\ndocker compose --profile cluster    up -d --wait\ndocker compose --profile all down -v\n```\n\nThe cluster profile's healthcheck waits for `cluster_state:ok` (not just PING) so tests can connect without hitting the historical `InitialSetupError` race. Pre-configured sentinel node directories live under `test/support/sentinel-config/` and are bind-mounted into the sentinel container; the image's entrypoint starts each as a sentinel because their directory names begin with `node-sentinel`.\n\n### macOS / Docker Desktop note\n\nThe compose stack uses `network_mode: host` so sentinel and cluster nodes report `127.0.0.1` addresses the test runner on the host can reach. On Linux this works natively. On macOS, Docker Desktop's \"host networking\" beta must be enabled (Settings → Resources → Network → Enable host networking); without it, the containers are healthy but their ports aren't visible on `127.0.0.1`. AF_UNIX sockets bind-mounted out of the standalone container also don't route through Docker Desktop's VM on macOS, so `test_connecting_to_unix_domain_socket` fails locally but passes on Linux CI.\n\n## Architecture\n\n### Layering\n\n```\nRedis (lib/redis.rb)                ergonomics: keyword DSL, reply reshaping, error translation,\n   ├ Commands (lib/redis/commands)  pub/sub second-socket, pipelined/multi wrappers,\n   ├ Monitor (lock)                 RESP3→RESP2 fallback, HIMPORT fieldset registry\n   └ @client : Redis::Client < RedisClient\n                                    ↓\n              redis-client gem (external, vendored as runtime dep)\n                                    ↓\n              TCP/TLS/Unix socket\n```\n\nThe `Redis` class is the public surface. It delegates all network I/O to `Redis::Client`, which inherits from `RedisClient` (in the `redis-client` gem) and only adds:\n\n1. Error translation: maps `RedisClient::*` exceptions to `Redis::*` via `ERROR_MAPPING` in `lib/redis/client.rb`. Every public method on `Redis::Client` is wrapped in a rescue that calls `Client.translate_error!`.\n2. A default of `protocol: 3` (RESP3) in `lib/redis/client.rb`; callers can pass `protocol: 2`. Servers without RESP3 (Redis < 6.0, or anything replying `NOPROTO`) are detected on connect and `Redis#with_protocol_fallback` (`lib/redis.rb`) transparently rebuilds `@client` for RESP2 — every `@client` access funnels through `Redis#synchronize`, which is what applies the fallback, so pipelines/multi/watch fall back too. Return values are protocol-invariant except GEO coordinates (`Float` under RESP3).\n3. Trivial config delegators (`#host`, `#port`, `#db`, …).\n\nThe full command execution flow is: `Redis#some_command` (defined in `lib/redis/commands/<category>.rb`) builds an array → `Redis#send_command` grabs `@monitor` → `Redis::Client#call_v` rescues + re-raises → `RedisClient#call_v` serializes RESP and reads the reply → optional reshape lambda runs → result returned.\n\n### Commands as a module composition\n\nEvery Redis command category is a module under `lib/redis/commands/` (strings, lists, hashes, sets, sorted_sets, streams, arrays, scripting, transactions, pubsub, etc.). Module-provided command families live under `lib/redis/commands/modules/` (`json.rb` for `JSON.*`, `search.rb` + `search/` for the Query Engine `FT.*`, whose replies reshape into `Search::SearchResult`/`Search::AggregateResult` objects). They are all `include`d into a parent `Redis::Commands` module (`lib/redis/commands.rb`), which is in turn mixed into:\n\n- `Redis` (`lib/redis.rb`)\n- `Redis::PipelinedConnection` (`lib/redis/pipeline.rb`) — used inside `pipelined` and `multi` blocks\n- (Indirectly) `Redis::Cluster` via inheritance from `Redis`\n\nThis is the **single most important pattern in the codebase**. To add a new command, find the matching `commands/<category>.rb` and add a method that calls `send_command([:cmd, ...])` — that method automatically becomes available on every client type. The mixin classes each provide their own `send_command` and `synchronize` so the same `Commands` methods work in direct calls, pipelines, and transactions.\n\nThere is also a catch-all `method_missing` in `lib/redis/commands.rb` that forwards any unknown method as a Redis command — so unwrapped commands \"just work.\"\n\n### Reply reshaping (protocol-aware)\n\nThe top of `lib/redis/commands.rb` defines a family of lambdas — `Boolify`, `BoolifySet`, `Hashify`, `Floatify`, `FloatifyPairs`, `HashifyInfo`, `HashifyStreamEntries`, `HashifyClusterNodes`, … — that reshape raw replies into idiomatic Ruby (Hash, Float, boolean, etc.). They are passed as blocks to `send_command`:\n\n```ruby\ndef incrbyfloat(key, increment)\n  send_command([:incrbyfloat, key, Float(increment)], &Floatify)\nend\n```\n\nSince 6.0 the client negotiates RESP3 by default, so these lambdas are **protocol-aware**: they must accept both the RESP2 wire shape (flat arrays, string-encoded numbers) and the RESP3 one (native maps, doubles, pairs) and converge on the same Ruby value. The pattern is \"detect the already-final shape and pass it through\" — e.g. `Hashify` returns a `Hash` unchanged and `each_slice(2).to_h`'s a flat array; `FloatifyPairs` skips re-mapping when the reply is already `[[member, Float], ...]`. When adding or changing a lambda, keep both branches, and test the command under both protocols (`PROTOCOL=2` / `PROTOCOL=3`, see below).\n\n`Boolify` is the exception: it assumes an integer reply (`value != 0 unless value.nil?`) and would invert a native RESP3 boolean — `Boolify.call(false)` returns `true`. That's unreachable for commands that reply with integers under both protocols (the case for all current uses), but confirm the command's `reply_schema` says `\"type\": \"integer\"` before reaching for it.\n\nWhen adding a command that needs reply transformation, write or reuse one of these lambdas; do not coerce in the command method itself. See `specs/adding-commands.md` for the full catalog and the end-to-end checklist (the `add-new-command` skill automates this flow from a spec file).\n\n### Connection lifecycle (long-lived, lazy, fork-safe)\n\n- `Redis.new` does **not** open a socket — it's lazy. The first command triggers `RedisClient#ensure_connected`, which runs the `connection_prelude` (HELLO/AUTH, SELECT, CLIENT SETINFO, then CLIENT SETNAME and ROLE) and caches the socket.\n- One `Redis` instance owns one socket, guarded by a `Monitor` defined in `lib/redis.rb` and acquired in `Redis#send_command`. The reentrant lock matters for nested `watch { multi { ... } }` patterns; do not \"optimize\" it to `Mutex` without redesigning that API.\n- For concurrent use, wrap with `connection_pool` — the README documents this and it's the only recommended pooling story. The gem itself does not pool.\n- Fork safety is handled inside `redis-client` via `PIDCache`: a forked child detects the inherited socket and reconnects on next use. The `inherit_socket` option in `lib/redis.rb` disables that check; almost no callers should use it.\n\n### Pub/Sub — separate socket, same process\n\n`subscribe` / `psubscribe` / `ssubscribe` open a **second** dedicated socket via `@client.pubsub` (in `lib/redis.rb`) wrapped in `SubscribedClient` (`lib/redis/subscribe.rb`). This keeps the command socket usable from other threads while one thread is blocked in the `next_event` loop. The subscription loop on the calling thread is synchronous — if you want it off the main thread, the caller spawns a `Thread`. There's a separate write-monitor on the subscription socket. Sharded pub/sub (`SSUBSCRIBE`) subscribes one channel at a time to avoid cross-slot errors in cluster mode.\n\n### Pipelines and transactions\n\n`pipelined` and `multi` both yield a `Redis::PipelinedConnection` (or `MultiConnection`) that re-includes `Commands` (`lib/redis/pipeline.rb`). Each command inside the block returns a `Redis::Future` that resolves when the batch flushes. `MultiFuture` (in `lib/redis/pipeline.rb`) splits the `EXEC` reply array back across individual command futures. Inside a `MULTI`, blocking commands degrade to non-blocking — that's intentional, matching Redis server semantics.\n\n### HIMPORT — server session state with client-side recovery (experimental)\n\nThe `HIMPORT` command family (Redis 8.10) is the one place a command's state outlives a single call: fieldsets are **per-connection server session state**, destroyed by reconnect/failover/`RESET`. `Redis#initialize` keeps a registry of prepared schemas (`@himport_fieldsets`) and the `himport_*` overrides in `lib/redis.rb` repair a lost fieldset reactively — on the server's \"no such fieldset\" error, re-prepare from the registry and retry the SET exactly once. Recovery is per-fieldset and lazy (there is no reconnect hook); disable with `himport_auto_prepare: false`, in which case re-preparing is the caller's responsibility (the registry has no public reader). The overrides hold `@monitor` across command + registry mutation — don't split them. The cluster subclass adds reply aggregation on top (prepare/discard fan out to all masters).\n\n### Client identification (CLIENT SETINFO)\n\n`Redis::LibIdentity` (`lib/redis/lib_identity.rb`) reports `lib-name=redis-rb lib-ver=<version>` on every connection by **prepending onto `RedisClient::Config` / `RedisClient::SentinelConfig`** and appending a second `SETINFO` pair to the connection prelude (last-write-wins overrides redis-client's own pair; zero extra round trips). The override is gated by a `redis-rb_v<version>` marker in `driver_info`, so applications using redis-client directly are untouched. Downstream gems extend the name via `Redis.new(driver_info: \"my-gem_v1.0.0\")`; `driver_info: false` disables identification. This couples to redis-client's `connection_prelude` — one of the reasons the gemspec pins redis-client to an exact version; re-audit on every driver bump.\n\n### Cluster gem differences\n\n`cluster/lib/redis/cluster.rb` defines `Redis::Cluster < ::Redis`, so it inherits the full `Commands` surface but swaps `initialize_client` to build a `RedisClient::Cluster` via the `redis-cluster-client` gem. Cluster-specific differences worth knowing:\n\n- `Redis::Cluster::Client` (`cluster/lib/redis/cluster/client.rb`) maintains a **per-node connection pool internally** via `redis-cluster-client`. Do not wrap this in the `connection_pool` gem.\n- `watch` **requires a block with an argument** in `cluster/lib/redis/cluster/client.rb`. The block receives a `Redis::Cluster::TransactionAdapter` that pins commands to the same node/slot. Standalone `Redis#watch` accepts the no-block form; cluster does not.\n- `Redis::Cluster#connection` raises `NotImplementedError` — there's no single \"connection\" to report.\n- Extra error classes: `InitialSetupError`, `OrchestrationCommandNotSupported`, `CommandErrorCollection`, `AmbiguousNodeError`, `TransactionConsistencyError`, `NodeMightBeDown` (defined in `cluster/lib/redis/cluster.rb`).\n\n### Command routings (cluster)\n\nWhich node(s) a command goes to, and how fan-out replies are combined, is the **driver's job, not redis-rb's**. `redis-cluster-client` routes keyed commands by slot and keyless commands by the server's command tips (`request_policy` / `response_policy` from `COMMAND INFO`), with a built-in routing table for the exceptions (`RedisClient::Cluster::Router::RoutingTable`). Do not reimplement that in `Redis::Cluster` by enumerating nodes and calling them one by one — it bypasses the driver's redirection handling, topology refresh and error collection.\n\nWhen the driver's default routing is wrong for a command, fix it through the driver's `command_routings` config option rather than in Ruby: `Redis::Cluster#initialize_client` passes `DEFAULT_COMMAND_ROUTINGS` (`cluster/lib/redis/cluster.rb`), a `{ 'command' => { request_policy:, response_policy: } }` hash the driver validates and merges over its table, and caller-supplied `command_routings:` are merged on top so applications keep the last word. Supported values are the driver's, not the server's full set: `request_policy` `all_shards` / `all_nodes`, and `response_policy` `nil` (one reply per node), `all_succeeded`, `one_succeeded`, `agg_sum` (sums Integer replies only). Anything else, e.g. `agg_min`, is rejected at construction.\n\nOnly when no driver response policy produces the documented standalone return value does `Redis::Cluster` add a **thin reply-shaping override** (`waitaof`, `himport_*`): the driver still does the fan-out and hands back one reply per primary, and the override only collapses that array into the standalone shape. It has to be a method override, not a reply block on the shared command: the driver applies reply blocks **per node**, before it collects the fan-out, so a block never sees the aggregate. Aggregate the way the server's `response_policy` tip says (`agg_min` → per-position minimum, as `waitaof` does, so thresholds keep their single-node meaning; `agg_sum` → sum), not the way a sibling command happens to be handled. Guard it with `reply.first.is_a?(Array)` so a plain reply passes through (a caller who changed the routing, or a later driver that aggregates natively). The missing override does not affect pipelines' *reply shape* — `pipelined`/`multi` route each command to a single node, so the plain standalone shape comes back without any aggregation — but it does not restore the command's *meaning* there: single-node routing inside a pipeline is chosen by the driver (e.g. `any_replica_node_key`), not by which node carried the pipeline's writes, so a fan-out command's per-connection guarantee is still lost. Add a cluster test that issues the command inside `pipelined` to lock in the shape, but don't treat that test as proof the guarantee holds. Prefer contributing the missing policy upstream over growing these overrides. Re-audit `DEFAULT_COMMAND_ROUTINGS` and the overrides on every `redis-cluster-client` bump (the gemspec pins an exact version for this reason).\n\n### `Redis::Distributed` is not Redis Cluster\n\n`lib/redis/distributed.rb` + `lib/redis/hash_ring.rb` implement **client-side consistent-hash sharding** across N independent standalone Redis servers. It is *not* the Redis Cluster protocol — there are no slot maps, no MOVED/ASK redirects, no automatic resharding. Keys are hashed with CRC32 against an MD5-built ring (160 vnodes/server) and dispatched to one underlying `Redis` instance. Multi-key commands raise `CannotDistribute` since co-location isn't guaranteed.\n\nIt is **separately maintained** from `Commands` — `Redis::Distributed` does *not* include the `Commands` mixin; every method is explicitly defined in `distributed.rb` so it can route to the right node via `node_for(key)`. When adding a new Redis command that should be available here, add a corresponding method to `lib/redis/distributed.rb` and a test under `test/distributed/`. The `test:distributed` Rake task runs as part of the default suite, so missing or broken Distributed implementations will fail CI.\n\nIt is supported (recent commits add JSON, arrays (`AR*`), HEXPIRE/HPTTL, HSCAN, etc.) and not deprecated. For new applications needing horizontal scaling, `Redis::Cluster` is generally the better choice because the server enforces consistency; `Redis::Distributed` is the right tool when you have N independent standalone Redises and want memcache-style key distribution.\n\n## Conventions\n\n- Every file starts with `# frozen_string_literal: true`. Keep it when editing or creating files.\n- Commands take symbols for the verb (`[:incr, key]`); strings and arguments are coerced where needed (`Integer(x)`, `Float(x)`, `value.to_s`). Follow the local pattern in `lib/redis/commands/<category>.rb` when adding new methods.\n- Yard-style docstrings (`@param`, `@option`, `@return`, `@example`) are on every public command method. New methods should keep that format.\n- Error mapping is centralized in `Redis::Client::ERROR_MAPPING` (and extended in `Redis::Cluster::Client::ERROR_MAPPING`). If you add a new RedisClient error class to handle, add it there rather than catching in individual command methods.\n- Test files must live under one of `test/redis`, `test/distributed`, `test/sentinel`, `cluster/test`. The Rakefile fails the build if a `*_test.rb` exists outside those groups.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/redis/redis-rb/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"## Repository layout\n\nThis repository ships **two gems** from one tree:\n\n- `redis` — the high-level standalone/sentinel client. Source under `lib/`, gemspec at `redis.gemspec`.\n- `redis-clustering` — the cluster client, a thin subclass that depends on `redis`. Source under `cluster/lib/`, gemspec at `cluster/redis-clustering.gemspec`. It is a separate gem on purpose: passing `cluster:` to `Redis.new` raises (see `lib/redis.rb`).\n\nCluster code, tests, and CHANGELOG live under `cluster/`. When making changes that span both, edit both — the cluster gem reuses `lib/redis/commands/**` from the main gem but has its own client class (`cluster/lib/redis/cluster/client.rb`) and transaction adapter.\n\n### `Redis::XXH3` — pure-Ruby local digest computation\n\n`lib/redis/xxh3.rb` provides `Redis::XXH3.hexdigest`, local XXH3-64 digest computation for `SET`'s `IFDEQ`/`IFDNE` and `DELEX`'s `IFDEQ`/`IFDNE` options (Redis 8.4+ CAS/CAD), so a `match-digest` can be built without a round trip to `DIGEST`. It's plain Ruby, always available — no native extension, no build step, no optional flag: an earlier version of this vendored the reference xxHash C library as a native extension, but that made every `gem install redis` depend on `make` being present (a RubyGems property of any gem declaring `s.extensions`, regardless of whether the extension actually compiles anything), so it was replaced with a direct, function-by-function port of the same reference algorithm (https://github.com/Cyan4973/xxHash, v0.8.3 — the version and secret table the server itself uses). Each private method in `lib/redis/xxh3.rb` is named after the upstream C function it mirrors, to keep that mapping checkable; the 128-bit multiply-and-fold XXH3 needs throughout — the main source of complexity in a C port, which has no native 128-bit integer type — is trivial in Ruby thanks to native arbitrary-precision integers. `test/redis/xxh3_test.rb` cross-checks known-answer vectors at every algorithm size-class boundary (0–16, 17–128, 129–240, >240 bytes) against a live server's own `DIGEST`, and `test/redis/digest_consistency_test.rb` drives real `SET`/`DELEX` CAS calls with a locally computed digest end to end.\n\n## Common commands\n\nThe dev workflow runs Redis in Docker containers via `docker-compose.yml` using the prebuilt `redislabs/client-libs-test` image. Topologies are selected by Docker profiles (`standalone`, `sentinel`, `cluster`, `all`). The `makefile` is a thin shim around `docker compose --profile X up -d --wait` and `down -v`, so the historical target names still work:\n\n```sh\n# bring up everything: standalone, replica, 3 sentinels, 6-node cluster\nmake start_all\n\n# run the full suite (all four test groups)\nmake test\n\n# stop everything\nmake stop_all\n\n# one shot: start, test, stop\nmake all\n```\n\n`make test` shells out to `bundle exec rake test`, which runs four `Rake::TestTask` groups defined in `Rakefile`:\n\n```sh\nbundle exec rake test:redis        # lib/redis core\nbundle exec rake test:distributed  # lib/redis/distributed (client-side sharding)\nbundle exec rake test:sentinel     # sentinel-mode tests\nbundle exec rake test:cluster      # cluster gem (loads cluster/lib + cluster/test)\n```\n\nTo run a single test file or method, use Minitest's options via `TESTOPTS`:\n\n```sh\nbundle exec rake test:redis TEST=test/redis/commands_on_strings_test.rb\nBUNDLE_GEMFILE=cluster/Gemfile bundle exec rake test:cluster TEST=cluster/test/commands_on_strings_test.rb TESTOPTS=\"--name=/get/\"\n```\n\nThe cluster group needs `BUNDLE_GEMFILE=cluster/Gemfile` (the root bundle doesn't include `redis-cluster-client`); CI does the same.\n\nOther useful knobs:\n\n- `REDIS_VERSION=8.X.Y make start_all` — pin the image tag (default is set at the top of `makefile`). Tags are published per Redis minor.patch (e.g. `8.0.6`, `8.2.6`, `8.4.3`, `8.6.3`, `8.8.0`); a bare `8.4` tag generally does not exist.\n- `DRIVER=hiredis bundle exec rake test` — run the suite against the `hiredis-client` C-extension driver instead of the pure-Ruby parser (see `test/helper.rb`).\n- `PROTOCOL=2 bundle exec rake test` — run the suite over RESP2 instead of the default RESP3 (`test/helper.rb:11`). Reply-reshaping changes should be verified under both protocols (and ideally both drivers).\n- `REDIS_SOCKET_PATH=...` — override the Unix socket location. The default expects `tmp/redis.sock`, which the standalone container bind-mounts from `./tmp:/sockets`; `test/helper.rb` aborts if it's missing.\n- `bundle exec rubocop` — lint. The Rubocop config is in `.rubocop.yml` (root) and `cluster/.rubocop.yml`.\n- `bin/console` — IRB session with `redis` preloaded.\n\nYou can also drive `docker compose` directly when you want a single profile up:\n\n```sh\ndocker compose --profile standalone up -d --wait\ndocker compose --profile sentinel   up -d --wait\ndocker compose --profile cluster    up -d --wait\ndocker compose --profile all down -v\n```\n\nThe cluster profile's healthcheck waits for `cluster_state:ok` (not just PING) so tests can connect without hitting the historical `InitialSetupError` race. Pre-configured sentinel node directories live under `test/support/sentinel-config/` and are bind-mounted into the sentinel container; the image's entrypoint starts each as a sentinel because their directory names begin with `node-sentinel`.\n\n### macOS / Docker Desktop note\n\nThe compose stack uses `network_mode: host` so sentinel and cluster nodes report `127.0.0.1` addresses the test runner on the host can reach. On Linux this works natively. On macOS, Docker Desktop's \"host networking\" beta must be enabled (Settings → Resources → Network → Enable host networking); without it, the containers are healthy but their ports aren't visible on `127.0.0.1`. AF_UNIX sockets bind-mounted out of the standalone container also don't route through Docker Desktop's VM on macOS, so `test_connecting_to_unix_domain_socket` fails locally but passes on Linux CI.\n\n## Architecture\n\n### Layering\n\n```\nRedis (lib/redis.rb)                ergonomics: keyword DSL, reply reshaping, error translation,\n   ├ Commands (lib/redis/commands)  pub/sub second-socket, pipelined/multi wrappers,\n   ├ Monitor (lock)                 RESP3→RESP2 fallback, HIMPORT fieldset registry\n   └ @client : Redis::Client < RedisClient\n                                    ↓\n              redis-client gem (external, vendored as runtime dep)\n                                    ↓\n              TCP/TLS/Unix socket\n```\n\nThe `Redis` class is the public surface. It delegates all network I/O to `Redis::Client`, which inherits from `RedisClient` (in the `redis-client` gem) and only adds:\n\n1. Error translation: maps `RedisClient::*` exceptions to `Redis::*` via `ERROR_MAPPING` in `lib/redis/client.rb`. Every public method on `Redis::Client` is wrapped in a rescue that calls `Client.translate_error!`.\n2. A default of `protocol: 3` (RESP3) in `lib/redis/client.rb`; callers can pass `protocol: 2`. Servers without RESP3 (Redis < 6.0, or anything replying `NOPROTO`) are detected on connect and `Redis#with_protocol_fallback` (`lib/redis.rb`) transparently rebuilds `@client` for RESP2 — every `@client` access funnels through `Redis#synchronize`, which is what applies the fallback, so pipelines/multi/watch fall back too. Return values are protocol-invariant except GEO coordinates (`Float` under RESP3).\n3. Trivial config delegators (`#host`, `#port`, `#db`, …).\n\nThe full command execution flow is: `Redis#some_command` (defined in `lib/redis/commands/<category>.rb`) builds an array → `Redis#send_command` grabs `@monitor` → `Redis::Client#call_v` rescues + re-raises → `RedisClient#call_v` serializes RESP and reads the reply → optional reshape lambda runs → result returned.\n\n### Commands as a module composition\n\nEvery Redis command category is a module under `lib/redis/commands/` (strings, lists, hashes, sets, sorted_sets, streams, arrays, scripting, transactions, pubsub, etc.). Module-provided command families live under `lib/redis/commands/modules/` (`json.rb` for `JSON.*`, `search.rb` + `search/` for the Query Engine `FT.*`, whose replies reshape into `Search::SearchResult`/`Search::AggregateResult` objects). They are all `include`d into a parent `Redis::Commands` module (`lib/redis/commands.rb`), which is in turn mixed into:\n\n- `Redis` (`lib/redis.rb`)\n- `Redis::PipelinedConnection` (`lib/redis/pipeline.rb`) — used inside `pipelined` and `multi` blocks\n- (Indirectly) `Redis::Cluster` via inheritance from `Redis`\n\nThis is the **single most important pattern in the codebase**. To add a new command, find the matching `commands/<category>.rb` and add a method that calls `send_command([:cmd, ...])` — that method automatically becomes available on every client type. The mixin classes each provide their own `send_command` and `synchronize` so the same `Commands` methods work in direct calls, pipelines, and transactions.\n\nThere is also a catch-all `method_missing` in `lib/redis/commands.rb` that forwards any unknown method as a Redis command — so unwrapped commands \"just work.\"\n\n### Reply reshaping (protocol-aware)\n\nThe top of `lib/redis/commands.rb` defines a family of lambdas — `Boolify`, `BoolifySet`, `Hashify`, `Floatify`, `FloatifyPairs`, `HashifyInfo`, `HashifyStreamEntries`, `HashifyClusterNodes`, … — that reshape raw replies into idiomatic Ruby (Hash, Float, boolean, etc.). They are passed as blocks to `send_command`:\n\n```ruby\ndef incrbyfloat(key, increment)\n  send_command([:incrbyfloat, key, Float(increment)], &Floatify)\nend\n```\n\nSince 6.0 the client negotiates RESP3 by default, so these lambdas are **protocol-aware**: they must accept both the RESP2 wire shape (flat arrays, string-encoded numbers) and the RESP3 one (native maps, doubles, pairs) and converge on the same Ruby value. The pattern is \"detect the already-final shape and pass it through\" — e.g. `Hashify` returns a `Hash` unchanged and `each_slice(2).to_h`'s a flat array; `FloatifyPairs` skips re-mapping when the reply is already `[[member, Float], ...]`. When adding or changing a lambda, keep both branches, and test the command under both protocols (`PROTOCOL=2` / `PROTOCOL=3`, see below).\n\n`Boolify` is the exception: it assumes an integer reply (`value != 0 unless value.nil?`) and would invert a native RESP3 boolean — `Boolify.call(false)` returns `true`. That's unreachable for commands that reply with integers under both protocols (the case for all current uses), but confirm the command's `reply_schema` says `\"type\": \"integer\"` before reaching for it.\n\nWhen adding a command that needs reply transformation, write or reuse one of these lambdas; do not coerce in the command method itself. See `specs/adding-commands.md` for the full catalog and the end-to-end checklist (the `add-new-command` skill automates this flow from a spec file).\n\n### Connection lifecycle (long-lived, lazy, fork-safe)\n\n- `Redis.new` does **not** open a socket — it's lazy. The first command triggers `RedisClient#ensure_connected`, which runs the `connection_prelude` (HELLO/AUTH, SELECT, CLIENT SETINFO, then CLIENT SETNAME and ROLE) and caches the socket.\n- One `Redis` instance owns one socket, guarded by a `Monitor` defined in `lib/redis.rb` and acquired in `Redis#send_command`. The reentrant lock matters for nested `watch { multi { ... } }` patterns; do not \"optimize\" it to `Mutex` without redesigning that API.\n- For concurrent use, wrap with `connection_pool` — the README documents this and it's the only recommended pooling story. The gem itself does not pool.\n- Fork safety is handled inside `redis-client` via `PIDCache`: a forked child detects the inherited socket and reconnects on next use. The `inherit_socket` option in `lib/redis.rb` disables that check; almost no callers should use it.\n\n### Pub/Sub — separate socket, same process\n\n`subscribe` / `psubscribe` / `ssubscribe` open a **second** dedicated socket via `@client.pubsub` (in `lib/redis.rb`) wrapped in `SubscribedClient` (`lib/redis/subscribe.rb`). This keeps the command socket usable from other threads while one thread is blocked in the `next_event` loop. The subscription loop on the calling thread is synchronous — if you want it off the main thread, the caller spawns a `Thread`. There's a separate write-monitor on the subscription socket. Sharded pub/sub (`SSUBSCRIBE`) subscribes one channel at a time to avoid cross-slot errors in cluster mode.\n\n### Pipelines and transactions\n\n`pipelined` and `multi` both yield a `Redis::PipelinedConnection` (or `MultiConnection`) that re-includes `Commands` (`lib/redis/pipeline.rb`). Each command inside the block returns a `Redis::Future` that resolves when the batch flushes. `MultiFuture` (in `lib/redis/pipeline.rb`) splits the `EXEC` reply array back across individual command futures. Inside a `MULTI`, blocking commands degrade to non-blocking — that's intentional, matching Redis server semantics.\n\n### HIMPORT — server session state with client-side recovery (experimental)\n\nThe `HIMPORT` command family (Redis 8.10) is the one place a command's state outlives a single call: fieldsets are **per-connection server session state**, destroyed by reconnect/failover/`RESET`. `Redis#initialize` keeps a registry of prepared schemas (`@himport_fieldsets`) and the `himport_*` overrides in `lib/redis.rb` repair a lost fieldset reactively — on the server's \"no such fieldset\" error, re-prepare from the registry and retry the SET exactly once. Recovery is per-fieldset and lazy (there is no reconnect hook); disable with `himport_auto_prepare: false`, in which case re-preparing is the caller's responsibility (the registry has no public reader). The overrides hold `@monitor` across command + registry mutation — don't split them. The cluster subclass adds reply aggregation on top (prepare/discard fan out to all masters).\n\n### Client identification (CLIENT SETINFO)\n\n`Redis::LibIdentity` (`lib/redis/lib_identity.rb`) reports `lib-name=redis-rb lib-ver=<version>` on every connection by **prepending onto `RedisClient::Config` / `RedisClient::SentinelConfig`** and appending a second `SETINFO` pair to the connection prelude (last-write-wins overrides redis-client's own pair; zero extra round trips). The override is gated by a `redis-rb_v<version>` marker in `driver_info`, so applications using redis-client directly are untouched. Downstream gems extend the name via `Redis.new(driver_info: \"my-gem_v1.0.0\")`; `driver_info: false` disables identification. This couples to redis-client's `connection_prelude` — one of the reasons the gemspec pins redis-client to an exact version; re-audit on every driver bump.\n\n### Cluster gem differences\n\n`cluster/lib/redis/cluster.rb` defines `Redis::Cluster < ::Redis`, so it inherits the full `Commands` surface but swaps `initialize_client` to build a `RedisClient::Cluster` via the `redis-cluster-client` gem. Cluster-specific differences worth knowing:\n\n- `Redis::Cluster::Client` (`cluster/lib/redis/cluster/client.rb`) maintains a **per-node connection pool internally** via `redis-cluster-client`. Do not wrap this in the `connection_pool` gem.\n- `watch` **requires a block with an argument** in `cluster/lib/redis/cluster/client.rb`. The block receives a `Redis::Cluster::TransactionAdapter` that pins commands to the same node/slot. Standalone `Redis#watch` accepts the no-block form; cluster does not.\n- `Redis::Cluster#connection` raises `NotImplementedError` — there's no single \"connection\" to report.\n- Extra error classes: `InitialSetupError`, `OrchestrationCommandNotSupported`, `CommandErrorCollection`, `AmbiguousNodeError`, `TransactionConsistencyError`, `NodeMightBeDown` (defined in `cluster/lib/redis/cluster.rb`).\n\n### Command routings (cluster)\n\nWhich node(s) a command goes to, and how fan-out replies are combined, is the **driver's job, not redis-rb's**. `redis-cluster-client` routes keyed commands by slot and keyless commands by the server's command tips (`request_policy` / `response_policy` from `COMMAND INFO`), with a built-in routing table for the exceptions (`RedisClient::Cluster::Router::RoutingTable`). Do not reimplement that in `Redis::Cluster` by enumerating nodes and calling them one by one — it bypasses the driver's redirection handling, topology refresh and error collection.\n\nWhen the driver's default routing is wrong for a command, fix it through the driver's `command_routings` config option rather than in Ruby: `Redis::Cluster#initialize_client` passes `DEFAULT_COMMAND_ROUTINGS` (`cluster/lib/redis/cluster.rb`), a `{ 'command' => { request_policy:, response_policy: } }` hash the driver validates and merges over its table, and caller-supplied `command_routings:` are merged on top so applications keep the last word. Supported values are the driver's, not the server's full set: `request_policy` `all_shards` / `all_nodes`, and `response_policy` `nil` (one reply per node), `all_succeeded`, `one_succeeded`, `agg_sum` (sums Integer replies only). Anything else, e.g. `agg_min`, is rejected at construction.\n\nOnly when no driver response policy produces the documented standalone return value does `Redis::Cluster` add a **thin reply-shaping override** (`waitaof`, `himport_*`): the driver still does the fan-out and hands back one reply per primary, and the override only collapses that array into the standalone shape. It has to be a method override, not a reply block on the shared command: the driver applies reply blocks **per node**, before it collects the fan-out, so a block never sees the aggregate. Aggregate the way the server's `response_policy` tip says (`agg_min` → per-position minimum, as `waitaof` does, so thresholds keep their single-node meaning; `agg_sum` → sum), not the way a sibling command happens to be handled. Guard it with `reply.first.is_a?(Array)` so a plain reply passes through (a caller who changed the routing, or a later driver that aggregates natively). The missing override does not affect pipelines' *reply shape* — `pipelined`/`multi` route each command to a single node, so the plain standalone shape comes back without any aggregation — but it does not restore the command's *meaning* there: single-node routing inside a pipeline is chosen by the driver (e.g. `any_replica_node_key`), not by which node carried the pipeline's writes, so a fan-out command's per-connection guarantee is still lost. Add a cluster test that issues the command inside `pipelined` to lock in the shape, but don't treat that test as proof the guarantee holds. Prefer contributing the missing policy upstream over growing these overrides. Re-audit `DEFAULT_COMMAND_ROUTINGS` and the overrides on every `redis-cluster-client` bump (the gemspec pins an exact version for this reason).\n\n### `Redis::Distributed` is not Redis Cluster\n\n`lib/redis/distributed.rb` + `lib/redis/hash_ring.rb` implement **client-side consistent-hash sharding** across N independent standalone Redis servers. It is *not* the Redis Cluster protocol — there are no slot maps, no MOVED/ASK redirects, no automatic resharding. Keys are hashed with CRC32 against an MD5-built ring (160 vnodes/server) and dispatched to one underlying `Redis` instance. Multi-key commands raise `CannotDistribute` since co-location isn't guaranteed.\n\nIt is **separately maintained** from `Commands` — `Redis::Distributed` does *not* include the `Commands` mixin; every method is explicitly defined in `distributed.rb` so it can route to the right node via `node_for(key)`. When adding a new Redis command that should be available here, add a corresponding method to `lib/redis/distributed.rb` and a test under `test/distributed/`. The `test:distributed` Rake task runs as part of the default suite, so missing or broken Distributed implementations will fail CI.\n\nIt is supported (recent commits add JSON, arrays (`AR*`), HEXPIRE/HPTTL, HSCAN, etc.) and not deprecated. For new applications needing horizontal scaling, `Redis::Cluster` is generally the better choice because the server enforces consistency; `Redis::Distributed` is the right tool when you have N independent standalone Redises and want memcache-style key distribution.\n\n## Conventions\n\n- Every file starts with `# frozen_string_literal: true`. Keep it when editing or creating files.\n- Commands take symbols for the verb (`[:incr, key]`); strings and arguments are coerced where needed (`Integer(x)`, `Float(x)`, `value.to_s`). Follow the local pattern in `lib/redis/commands/<category>.rb` when adding new methods.\n- Yard-style docstrings (`@param`, `@option`, `@return`, `@example`) are on every public command method. New methods should keep that format.\n- Error mapping is centralized in `Redis::Client::ERROR_MAPPING` (and extended in `Redis::Cluster::Client::ERROR_MAPPING`). If you add a new RedisClient error class to handle, add it there rather than catching in individual command methods.\n- Test files must live under one of `test/redis`, `test/distributed`, `test/sentinel`, `cluster/test`. The Rakefile fails the build if a `*_test.rb` exists outside those groups.\n","isInternal":false,"tokens":5054,"sizeBytes":21081},{"name":"command-specification-template.md","path":".claude/skills/add-new-command/examples/command-specification-template.md","rawUrl":"https://raw.githubusercontent.com/redis/redis-rb/HEAD/.claude/skills/add-new-command/examples/command-specification-template.md","title":"Examples Skill","category":"anthropic-skill","format":"markdown","content":"# $COMMAND_NAME command specification\n\n## Supported version\n\nAdd supported Redis version here. For example: Redis >= 6.2.0\n\n## Command description\n\nAdd a description of the command here.\n\n## Command API\n\nSpecify an API for the command in the format that official docs uses. For example:\n\n```\n$COMMAND_NAME $key $member [NX|XX] [CH] [INCR]\n```\n\n## Redis-CLI examples\n\nAdd relevant Redis-CLI examples here.\n\n## Test plan\n\nSpecify how you want to test the command in terms of integration testing. For example:\n\n- Test only with required arguments, assert that single value returned\n- Test with required arguments and optional XX modifier, ensure that 1 returned\n- ...\n","isInternal":false,"tokens":151,"sizeBytes":665},{"name":"SKILL.md","path":".claude/skills/add-new-command/SKILL.md","rawUrl":"https://raw.githubusercontent.com/redis/redis-rb/HEAD/.claude/skills/add-new-command/SKILL.md","title":"add-new-command","category":"anthropic-skill","format":"markdown","content":"---\ndescription: >-\n  Adds support for a new Redis command to the gem from a specification. Triggers on requests\n  like \"add support for the TS.BGET command\" or via /add-new-command <COMMAND|path>. Resolves the\n  spec from command_specs/, falling back to official redis.io docs. See .claude/skills/add-new-command/examples/command-specification-template.md.\nargument-hint: \"[command-name | path-to-spec]\"\nallowed-tools: Bash, Read, Write, Edit, WebFetch, Glob, Grep, AskUserQuestion\n---\n\n# Add new Redis command support\n\n`$ARGUMENTS` is either a Redis command **name** (e.g. `TS.BGET`, `JSON.GET`) or a **path** to a\nspecification file. Before doing anything else, resolve it to a filled specification using Step 0.\n\n## Step 0 — Resolve the specification\n\nRun `bash .claude/skills/add-new-command/scripts/resolve_spec.sh \"$ARGUMENTS\"` from the repo root. Branch on its `RESOLUTION:` line.\nThe only way into Step 1 is a `RESOLUTION: ready` from the resolver — never enter it on your own judgment that a spec looks filled.\n\n- **`ready`** — filled spec is in the output. Go to Step 1.\n- **`incomplete`** — spec exists but still has the `$COMMAND_NAME` placeholder. Ask the user to fill in the template, show the `RERUN_HINT` to resume, then STOP.\n- **`missing`** — WebFetch `REDIS_IO_URL`.\n  - Found → write a spec to `TARGET_SPEC_FILE` using `.claude/skills/add-new-command/examples/command-specification-template.md` structure, then use the **AskUserQuestion** tool to present a \"Proceed / Stop\" choice. On Proceed → re-run the resolver on `TARGET_SPEC_FILE` and branch on its `RESOLUTION:` again — only `ready` may enter Step 1; `incomplete` means the written spec is still a stub, so follow the `incomplete` branch. On Stop → end (the saved spec can be edited and re-run later).\n  - Not found → ask the user to either give a spec path, or have you copy `.claude/skills/add-new-command/examples/command-specification-template.md` verbatim to `TARGET_SPEC_FILE` (keep the `$COMMAND_NAME` marker so the resolver flags it `incomplete` until filled) for them to fill; then show the `RERUN_HINT` and STOP.\n- **`path_not_found`** — the argument looks like a file path but no file exists at `SPEC_FILE`. Tell the user, ask for a corrected path (or a command name to resolve instead), then STOP. Do not guess a command name from the path.\n- **`no_argument`** — ask for a command name or spec path, then STOP.\n\n## Execution Instructions\n\n### 1. Preparations\n\n- Go through the guide `specs/adding-commands.md`\n\n### 2. Read and Understand\n\n- Read the ENTIRE specification carefully\n- Go through Command Description and identify command type (string, list, set, etc.)\n- Go through the Command API:\n    - Identify required and optional arguments\n    - Identify how to match Redis command arguments type to Ruby types\n    - Identify return value and possible response types\n- Check relevant Redis-Cli examples, if provided\n- Review the Test Plan\n\n### 3. Execute Tasks in Order\n\n#### a. Navigate to the task\n- Identify the files and action required\n- Read existing related files if modifying\n\n#### b. Implement the command\n- Identify the command API surface object you need to work with.\n- Identify required and optional arguments and their matching types.\n- Add command implementation following `specs/adding-commands.md`\n- Ensure doc string documents are added\n\n#### c. Verify as you go\n- After each file change, check syntax\n- Ensure imports are correct\n- Verify types are properly defined\n\n### 4. Implement Testing Plan\n\nAfter completing implementation tasks:\n\n- Identify matching test file or create new one if needed\n- Implement all test cases as separate test methods\n- Ensure adding version constraint if specified in the specification\n- Ensure tests cover edge cases\n\n### 5. Run tests\n\n- Run newly added test cases\n- Get back to the Implementation stage if any test failed\n\n### 6. Final Verification\n\nBefore completing:\n\n- ✅ All tasks from plan completed\n- ✅ All tests created and passing\n- ✅ Code follows project conventions\n- ✅ Documentation added/updated as needed\n\n## Output Report\n\nProvide summary:\n\n### Specification Source\n- How the spec was resolved (local file / built from redis.io / user-provided path)\n- Path to the spec file used\n\n### Completed Tasks\n- List of all tasks completed\n- Files created (with paths)\n- Files modified (with paths)\n\n### Tests Added\n- Test files created\n- Test cases implemented\n- Test results\n","frontmatter":{"description":"Adds support for a new Redis command to the gem from a specification. Triggers on requests like \"add support for the TS.BGET command\" or via /add-new-command <COMMAND|path>. Resolves the spec from command_specs/, falling back to official redis.io docs. See .claude/skills/add-new-command/examples/command-specification-template.md.","argument-hint":"[command-name | path-to-spec]","allowed-tools":"Bash, Read, Write, Edit, WebFetch, Glob, Grep, AskUserQuestion","allowedTools":["Bash,","Read,","Write,","Edit,","WebFetch,","Glob,","Grep,","AskUserQuestion"]},"allowedTools":["Bash,","Read,","Write,","Edit,","WebFetch,","Glob,","Grep,","AskUserQuestion"],"isInternal":false,"tokens":1046,"sizeBytes":4457},{"name":"resolve_spec.sh","path":".claude/skills/add-new-command/scripts/resolve_spec.sh","rawUrl":"https://raw.githubusercontent.com/redis/redis-rb/HEAD/.claude/skills/add-new-command/scripts/resolve_spec.sh","title":"Claude Directive: resolve_spec.sh","category":"claude-rule","format":"markdown","content":"#!/usr/bin/env bash\n# Deterministic spec resolver for the add-new-command skill.\n#\n# Takes the raw skill argument (a Redis command name like \"TS.BGET\", or a path\n# to a spec file) and decides — without any LLM reasoning — where the spec is,\n# whether it is filled in or still a stub, and (when the user must act) how to\n# resume. It prints a small structured report the skill body branches on.\n#\n# Run from the repo root. Usage:\n#   bash .claude/skills/add-new-command/scripts/resolve_spec.sh \"<command-name-or-path>\"\n\nset -euo pipefail\n\narg=\"${1:-}\"\narg=\"$(printf '%s' \"$arg\" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')\"\n\nif [ -z \"$arg\" ]; then\n  echo \"RESOLUTION: no_argument\"\n  echo \"Ask the user which Redis command to add (e.g. TS.BGET) or for a path to a spec file.\"\n  exit 0\nfi\n\nemit_spec_block() {\n  echo \"--- BEGIN SPEC ---\"\n  cat \"$1\"\n  echo \"\"\n  echo \"--- END SPEC ---\"\n}\n\n# Classify a found spec file as ready or incomplete, then emit its report.\n# A spec is still a stub if it carries the template's $COMMAND_NAME placeholder.\n#   $1 = source label, $2 = spec file path, $3 = re-run token (command name or path)\nhandle_found() {\n  local source=\"$1\" file=\"$2\" rerun=\"$3\"\n  if grep -qF -- '$COMMAND_NAME' \"$file\"; then\n    echo \"RESOLUTION: incomplete\"\n    echo \"SPEC_FILE: $file\"\n    echo \"RERUN_HINT: /add-new-command ${rerun}\"\n  else\n    echo \"RESOLUTION: ready\"\n    echo \"SOURCE: $source\"\n    echo \"SPEC_FILE: $file\"\n  fi\n  emit_spec_block \"$file\"\n}\n\n# 1. Explicit path to an existing file.\nif [ -f \"$arg\" ]; then\n  handle_found \"explicit_path\" \"$arg\" \"$arg\"\n  exit 0\nfi\n\n# 1b. Path-shaped argument (contains a slash or a .md suffix) that doesn't exist.\n# Don't fall through to command-name handling — that would fabricate a bogus\n# COMMAND / REDIS_IO_URL / TARGET_SPEC_FILE from the path string.\ncase \"$arg\" in\n  */* | *.md)\n    echo \"RESOLUTION: path_not_found\"\n    echo \"SPEC_FILE: $arg\"\n    echo \"The argument looks like a spec file path, but no such file exists.\"\n    echo \"RERUN_HINT: /add-new-command ${arg}\"\n    exit 0\n    ;;\nesac\n\n# 2. Treat as a command name. Uppercase + keep dots for the filename; lowercase for the URL.\nname_upper=\"$(printf '%s' \"$arg\" | tr '[:lower:]' '[:upper:]')\"\nname_lower=\"$(printf '%s' \"$arg\" | tr '[:upper:]' '[:lower:]')\"\nspec_file=\"command_specs/${name_upper}-specification-template.md\"\n\n# 3. Local spec lookup (authoritative; also the resume path).\nif [ -f \"$spec_file\" ]; then\n  handle_found \"local\" \"$spec_file\" \"$name_upper\"\n  exit 0\nfi\n\n# 4. No local spec — point at redis.io and where to save the spec.\necho \"RESOLUTION: missing\"\necho \"COMMAND: ${name_upper}\"\necho \"REDIS_IO_URL: https://redis.io/docs/latest/commands/${name_lower}/\"\necho \"TARGET_SPEC_FILE: ${spec_file}\"\necho \"RERUN_HINT: /add-new-command ${name_upper}\"\n","isInternal":false,"tokens":809,"sizeBytes":2796},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/redis/redis-rb/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n@AGENTS.md","isInternal":false,"tokens":31,"sizeBytes":127}],"systemPromptSnippet":"<yakaai_skills repo=\"redis/redis-rb\">\n<!-- File: AGENTS.md (Tokens: ~5054 | Category: root-instruction) -->\n## Repository layout\n\nThis repository ships **two gems** from one tree:\n\n- `redis` — the high-level standalone/sentinel client. Source under `lib/`, gemspec at `redis.gemspec`.\n- `redis-clustering` — the cluster client, a thin subclass that depends on `redis`. Source under `cluster/lib/`, gemspec at `cluster/redis-clustering.gemspec`. It is a separate gem on purpose: passing `cluster:` to `Redis.new` raises (see `lib/redis.rb`).\n\nCluster code, tests, and CHANGELOG live under `cluster/`. When making changes that span both, edit both — the cluster gem reuses `lib/redis/commands/**` from the main gem but has its own client class (`cluster/lib/redis/cluster/client.rb`) and transaction adapter.\n\n### `Redis::XXH3` — pure-Ruby local digest computation\n\n`lib/redis/xxh3.rb` provides `Redis::XXH3.hexdigest`, local XXH3-64 digest computation for `SET`'s `IFDEQ`/`IFDNE` and `DELEX`'s `IFDEQ`/`IFDNE` options (Redis 8.4+ CAS/CAD), so a `match-digest` can be built without a round trip to `DIGEST`. It's plain Ruby, always available — no native extension, no build step, no optional flag: an earlier version of this vendored the reference xxHash C library as a native extension, but that made every `gem install redis` depend on `make` being present (a RubyGems property of any gem declaring `s.extensions`, regardless of whether the extension actually compiles anything), so it was replaced with a direct, function-by-function port of the same reference algorithm (https://github.com/Cyan4973/xxHash, v0.8.3 — the version and secret table the server itself uses). Each private method in `lib/redis/xxh3.rb` is named after the upstream C function it mirrors, to keep that mapping checkable; the 128-bit multiply-and-fold XXH3 needs throughout — the main source of complexity in a C port, which has no native 128-bit integer type — is trivial in Ruby thanks to native arbitrary-precision integers. `test/redis/xxh3_test.rb` cross-checks known-answer vectors at every algorithm size-class boundary (0–16, 17–128, 129–240, >240 bytes) against a live server's own `DIGEST`, and `test/redis/digest_consistency_test.rb` drives real `SET`/`DELEX` CAS calls with a locally computed digest end to end.\n\n## Common commands\n\nThe dev workflow runs Redis in Docker containers via `docker-compose.yml` using the prebuilt `redislabs/client-libs-test` image. Topologies are selected by Docker profiles (`standalone`, `sentinel`, `cluster`, `all`). The `makefile` is a thin shim around `docker compose --profile X up -d --wait` and `down -v`, so the historical target names still work:\n\n```sh\n# bring up everything: standalone, replica, 3 sentinels, 6-node cluster\nmake start_all\n\n# run the full suite (all four test groups)\nmake test\n\n# stop everything\nmake stop_all\n\n# one shot: start, test, stop\nmake all\n```\n\n`make test` shells out to `bundle exec rake test`, which runs four `Rake::TestTask` groups defined in `Rakefile`:\n\n```sh\nbundle exec rake test:redis        # lib/redis core\nbundle exec rake test:distributed  # lib/redis/distributed (client-side sharding)\nbundle exec rake test:sentinel     # sentinel-mode tests\nbundle exec rake test:cluster      # cluster gem (loads cluster/lib + cluster/test)\n```\n\nTo run a single test file or method, use Minitest's options via `TESTOPTS`:\n\n```sh\nbundle exec rake test:redis TEST=test/redis/commands_on_strings_test.rb\nBUNDLE_GEMFILE=cluster/Gemfile bundle exec rake test:cluster TEST=cluster/test/commands_on_strings_test.rb TESTOPTS=\"--name=/get/\"\n```\n\nThe cluster group needs `BUNDLE_GEMFILE=cluster/Gemfile` (the root bundle doesn't include `redis-cluster-client`); CI does the same.\n\nOther useful knobs:\n\n- `REDIS_VERSION=8.X.Y make start_all` — pin the image tag (default is set at the top of `makefile`). Tags are published per Redis minor.patch (e.g. `8.0.6`, `8.2.6`, `8.4.3`, `8.6.3`, `8.8.0`); a bare `8.4` tag generally does not exist.\n- `DRIVER=hiredis bundle exec rake test` — run the suite against the `hiredis-client` C-extension driver instead of the pure-Ruby parser (see `test/helper.rb`).\n- `PROTOCOL=2 bundle exec rake test` — run the suite over RESP2 instead of the default RESP3 (`test/helper.rb:11`). Reply-reshaping changes should be verified under both protocols (and ideally both drivers).\n- `REDIS_SOCKET_PATH=...` — override the Unix socket location. The default expects `tmp/redis.sock`, which the standalone container bind-mounts from `./tmp:/sockets`; `test/helper.rb` aborts if it's missing.\n- `bundle exec rubocop` — lint. The Rubocop config is in `.rubocop.yml` (root) and `cluster/.rubocop.yml`.\n- `bin/console` — IRB session with `redis` preloaded.\n\nYou can also drive `docker compose` directly when you want a single profile up:\n\n```sh\ndocker compose --profile standalone up -d --wait\ndocker compose --profile sentinel   up -d --wait\ndocker compose --profile cluster    up -d --wait\ndocker compose --profile all down -v\n```\n\nThe cluster profile's healthcheck waits for `cluster_state:ok` (not just PING) so tests can connect without hitting the historical `InitialSetupError` race. Pre-configured sentinel node directories live under `test/support/sentinel-config/` and are bind-mounted into the sentinel container; the image's entrypoint starts each as a sentinel because their directory names begin with `node-sentinel`.\n\n### macOS / Docker Desktop note\n\nThe compose stack uses `network_mode: host` so sentinel and cluster nodes report `127.0.0.1` addresses the test runner on the host can reach. On Linux this works natively. On macOS, Docker Desktop's \"host networking\" beta must be enabled (Settings → Resources → Network → Enable host networking); without it, the containers are healthy but their ports aren't visible on `127.0.0.1`. AF_UNIX sockets bind-mounted out of the standalone container also don't route through Docker Desktop's VM on macOS, so `test_connecting_to_unix_domain_socket` fails locally but passes on Linux CI.\n\n## Architecture\n\n### Layering\n\n```\nRedis (lib/redis.rb)                ergonomics: keyword DSL, reply reshaping, error translation,\n   ├ Commands (lib/redis/commands)  pub/sub second-socket, pipelined/multi wrappers,\n   ├ Monitor (lock)                 RESP3→RESP2 fallback, HIMPORT fieldset registry\n   └ @client : Redis::Client < RedisClient\n                                    ↓\n              redis-client gem (external, vendored as runtime dep)\n                                    ↓\n              TCP/TLS/Unix socket\n```\n\nThe `Redis` class is the public surface. It delegates all network I/O to `Redis::Client`, which inherits from `RedisClient` (in the `redis-client` gem) and only adds:\n\n1. Error translation: maps `RedisClient::*` exceptions to `Redis::*` via `ERROR_MAPPING` in `lib/redis/client.rb`. Every public method on `Redis::Client` is wrapped in a rescue that calls `Client.translate_error!`.\n2. A default of `protocol: 3` (RESP3) in `lib/redis/client.rb`; callers can pass `protocol: 2`. Servers without RESP3 (Redis < 6.0, or anything replying `NOPROTO`) are detected on connect and `Redis#with_protocol_fallback` (`lib/redis.rb`) transparently rebuilds `@client` for RESP2 — every `@client` access funnels through `Redis#synchronize`, which is what applies the fallback, so pipelines/multi/watch fall back too. Return values are protocol-invariant except GEO coordinates (`Float` under RESP3).\n3. Trivial config delegators (`#host`, `#port`, `#db`, …).\n\nThe full command execution flow is: `Redis#some_command` (defined in `lib/redis/commands/<category>.rb`) builds an array → `Redis#send_command` grabs `@monitor` → `Redis::Client#call_v` rescues + re-raises → `RedisClient#call_v` serializes RESP and reads the reply → optional reshape lambda runs → result returned.\n\n### Commands as a module composition\n\nEvery Redis command category is a module under `lib/redis/commands/` (strings, lists, hashes, sets, sorted_sets, streams, arrays, scripting, transactions, pubsub, etc.). Module-provided command families live under `lib/redis/commands/modules/` (`json.rb` for `JSON.*`, `search.rb` + `search/` for the Query Engine `FT.*`, whose replies reshape into `Search::SearchResult`/`Search::AggregateResult` objects). They are all `include`d into a parent `Redis::Commands` module (`lib/redis/commands.rb`), which is in turn mixed into:\n\n- `Redis` (`lib/redis.rb`)\n- `Redis::PipelinedConnection` (`lib/redis/pipeline.rb`) — used inside `pipelined` and `multi` blocks\n- (Indirectly) `Redis::Cluster` via inheritance from `Redis`\n\nThis is the **single most important pattern in the codebase**. To add a new command, find the matching `commands/<category>.rb` and add a method that calls `send_command([:cmd, ...])` — that method automatically becomes available on every client type. The mixin classes each provide their own `send_command` and `synchronize` so the same `Commands` methods work in direct calls, pipelines, and transactions.\n\nThere is also a catch-all `method_missing` in `lib/redis/commands.rb` that forwards any unknown method as a Redis command — so unwrapped commands \"just work.\"\n\n### Reply reshaping (protocol-aware)\n\nThe top of `lib/redis/commands.rb` defines a family of lambdas — `Boolify`, `BoolifySet`, `Hashify`, `Floatify`, `FloatifyPairs`, `HashifyInfo`, `HashifyStreamEntries`, `HashifyClusterNodes`, … — that reshape raw replies into idiomatic Ruby (Hash, Float, boolean, etc.). They are passed as blocks to `send_command`:\n\n```ruby\ndef incrbyfloat(key, increment)\n  send_command([:incrbyfloat, key, Float(increment)], &Floatify)\nend\n```\n\nSince 6.0 the client negotiates RESP3 by default, so these lambdas are **protocol-aware**: they must accept both the RESP2 wire shape (flat arrays, string-encoded numbers) and the RESP3 one (native maps, doubles, pairs) and converge on the same Ruby value. The pattern is \"detect the already-final shape and pass it through\" — e.g. `Hashify` returns a `Hash` unchanged and `each_slice(2).to_h`'s a flat array; `FloatifyPairs` skips re-mapping when the reply is already `[[member, Float], ...]`. When adding or changing a lambda, keep both branches, and test the command under both protocols (`PROTOCOL=2` / `PROTOCOL=3`, see below).\n\n`Boolify` is the exception: it assumes an integer reply (`value != 0 unless value.nil?`) and would invert a native RESP3 boolean — `Boolify.call(false)` returns `true`. That's unreachable for commands that reply with integers under both protocols (the case for all current uses), but confirm the command's `reply_schema` says `\"type\": \"integer\"` before reaching for it.\n\nWhen adding a command that needs reply transformation, write or reuse one of these lambdas; do not coerce in the command method itself. See `specs/adding-commands.md` for the full catalog and the end-to-end checklist (the `add-new-command` skill automates this flow from a spec file).\n\n### Connection lifecycle (long-lived, lazy, fork-safe)\n\n- `Redis.new` does **not** open a socket — it's lazy. The first command triggers `RedisClient#ensure_connected`, which runs the `connection_prelude` (HELLO/AUTH, SELECT, CLIENT SETINFO, then CLIENT SETNAME and ROLE) and caches the socket.\n- One `Redis` instance owns one socket, guarded by a `Monitor` defined in `lib/redis.rb` and acquired in `Redis#send_command`. The reentrant lock matters for nested `watch { multi { ... } }` patterns; do not \"optimize\" it to `Mutex` without redesigning that API.\n- For concurrent use, wrap with `connection_pool` — the README documents this and it's the only recommended pooling story. The gem itself does not pool.\n- Fork safety is handled inside `redis-client` via `PIDCache`: a forked child detects the inherited socket and reconnects on next use. The `inherit_socket` option in `lib/redis.rb` disables that check; almost no callers should use it.\n\n### Pub/Sub — separate socket, same process\n\n`subscribe` / `psubscribe` / `ssubscribe` open a **second** dedicated socket via `@client.pubsub` (in `lib/redis.rb`) wrapped in `SubscribedClient` (`lib/redis/subscribe.rb`). This keeps the command socket usable from other threads while one thread is blocked in the `next_event` loop. The subscription loop on the calling thread is synchronous — if you want it off the main thread, the caller spawns a `Thread`. There's a separate write-monitor on the subscription socket. Sharded pub/sub (`SSUBSCRIBE`) subscribes one channel at a time to avoid cross-slot errors in cluster mode.\n\n### Pipelines and transactions\n\n`pipelined` and `multi` both yield a `Redis::PipelinedConnection` (or `MultiConnection`) that re-includes `Commands` (`lib/redis/pipeline.rb`). Each command inside the block returns a `Redis::Future` that resolves when the batch flushes. `MultiFuture` (in `lib/redis/pipeline.rb`) splits the `EXEC` reply array back across individual command futures. Inside a `MULTI`, blocking commands degrade to non-blocking — that's intentional, matching Redis server semantics.\n\n### HIMPORT — server session state with client-side recovery (experimental)\n\nThe `HIMPORT` command family (Redis 8.10) is the one place a command's state outlives a single call: fieldsets are **per-connection server session state**, destroyed by reconnect/failover/`RESET`. `Redis#initialize` keeps a registry of prepared schemas (`@himport_fieldsets`) and the `himport_*` overrides in `lib/redis.rb` repair a lost fieldset reactively — on the server's \"no such fieldset\" error, re-prepare from the registry and retry the SET exactly once. Recovery is per-fieldset and lazy (there is no reconnect hook); disable with `himport_auto_prepare: false`, in which case re-preparing is the caller's responsibility (the registry has no public reader). The overrides hold `@monitor` across command + registry mutation — don't split them. The cluster subclass adds reply aggregation on top (prepare/discard fan out to all masters).\n\n### Client identification (CLIENT SETINFO)\n\n`Redis::LibIdentity` (`lib/redis/lib_identity.rb`) reports `lib-name=redis-rb lib-ver=<version>` on every connection by **prepending onto `RedisClient::Config` / `RedisClient::SentinelConfig`** and appending a second `SETINFO` pair to the connection prelude (last-write-wins overrides redis-client's own pair; zero extra round trips). The override is gated by a `redis-rb_v<version>` marker in `driver_info`, so applications using redis-client directly are untouched. Downstream gems extend the name via `Redis.new(driver_info: \"my-gem_v1.0.0\")`; `driver_info: false` disables identification. This couples to redis-client's `connection_prelude` — one of the reasons the gemspec pins redis-client to an exact version; re-audit on every driver bump.\n\n### Cluster gem differences\n\n`cluster/lib/redis/cluster.rb` defines `Redis::Cluster < ::Redis`, so it inherits the full `Commands` surface but swaps `initialize_client` to build a `RedisClient::Cluster` via the `redis-cluster-client` gem. Cluster-specific differences worth knowing:\n\n- `Redis::Cluster::Client` (`cluster/lib/redis/cluster/client.rb`) maintains a **per-node connection pool internally** via `redis-cluster-client`. Do not wrap this in the `connection_pool` gem.\n- `watch` **requires a block with an argument** in `cluster/lib/redis/cluster/client.rb`. The block receives a `Redis::Cluster::TransactionAdapter` that pins commands to the same node/slot. Standalone `Redis#watch` accepts the no-block form; cluster does not.\n- `Redis::Cluster#connection` raises `NotImplementedError` — there's no single \"connection\" to report.\n- Extra error classes: `InitialSetupError`, `OrchestrationCommandNotSupported`, `CommandErrorCollection`, `AmbiguousNodeError`, `TransactionConsistencyError`, `NodeMightBeDown` (defined in `cluster/lib/redis/cluster.rb`).\n\n### Command routings (cluster)\n\nWhich node(s) a command goes to, and how fan-out replies are combined, is the **driver's job, not redis-rb's**. `redis-cluster-client` routes keyed commands by slot and keyless commands by the server's command tips (`request_policy` / `response_policy` from `COMMAND INFO`), with a built-in routing table for the exceptions (`RedisClient::Cluster::Router::RoutingTable`). Do not reimplement that in `Redis::Cluster` by enumerating nodes and calling them one by one — it bypasses the driver's redirection handling, topology refresh and error collection.\n\nWhen the driver's default routing is wrong for a command, fix it through the driver's `command_routings` config option rather than in Ruby: `Redis::Cluster#initialize_client` passes `DEFAULT_COMMAND_ROUTINGS` (`cluster/lib/redis/cluster.rb`), a `{ 'command' => { request_policy:, response_policy: } }` hash the driver validates and merges over its table, and caller-supplied `command_routings:` are merged on top so applications keep the last word. Supported values are the driver's, not the server's full set: `request_policy` `all_shards` / `all_nodes`, and `response_policy` `nil` (one reply per node), `all_succeeded`, `one_succeeded`, `agg_sum` (sums Integer replies only). Anything else, e.g. `agg_min`, is rejected at construction.\n\nOnly when no driver response policy produces the documented standalone return value does `Redis::Cluster` add a **thin reply-shaping override** (`waitaof`, `himport_*`): the driver still does the fan-out and hands back one reply per primary, and the override only collapses that array into the standalone shape. It has to be a method override, not a reply block on the shared command: the driver applies reply blocks **per node**, before it collects the fan-out, so a block never sees the aggregate. Aggregate the way the server's `response_policy` tip says (`agg_min` → per-position minimum, as `waitaof` does, so thresholds keep their single-node meaning; `agg_sum` → sum), not the way a sibling command happens to be handled. Guard it with `reply.first.is_a?(Array)` so a plain reply passes through (a caller who changed the routing, or a later driver that aggregates natively). The missing override does not affect pipelines' *reply shape* — `pipelined`/`multi` route each command to a single node, so the plain standalone shape comes back without any aggregation — but it does not restore the command's *meaning* there: single-node routing inside a pipeline is chosen by the driver (e.g. `any_replica_node_key`), not by which node carried the pipeline's writes, so a fan-out command's per-connection guarantee is still lost. Add a cluster test that issues the command inside `pipelined` to lock in the shape, but don't treat that test as proof the guarantee holds. Prefer contributing the missing policy upstream over growing these overrides. Re-audit `DEFAULT_COMMAND_ROUTINGS` and the overrides on every `redis-cluster-client` bump (the gemspec pins an exact version for this reason).\n\n### `Redis::Distributed` is not Redis Cluster\n\n`lib/redis/distributed.rb` + `lib/redis/hash_ring.rb` implement **client-side consistent-hash sharding** across N independent standalone Redis servers. It is *not* the Redis Cluster protocol — there are no slot maps, no MOVED/ASK redirects, no automatic resharding. Keys are hashed with CRC32 against an MD5-built ring (160 vnodes/server) and dispatched to one underlying `Redis` instance. Multi-key commands raise `CannotDistribute` since co-location isn't guaranteed.\n\nIt is **separately maintained** from `Commands` — `Redis::Distributed` does *not* include the `Commands` mixin; every method is explicitly defined in `distributed.rb` so it can route to the right node via `node_for(key)`. When adding a new Redis command that should be available here, add a corresponding method to `lib/redis/distributed.rb` and a test under `test/distributed/`. The `test:distributed` Rake task runs as part of the default suite, so missing or broken Distributed implementations will fail CI.\n\nIt is supported (recent commits add JSON, arrays (`AR*`), HEXPIRE/HPTTL, HSCAN, etc.) and not deprecated. For new applications needing horizontal scaling, `Redis::Cluster` is generally the better choice because the server enforces consistency; `Redis::Distributed` is the right tool when you have N independent standalone Redises and want memcache-style key distribution.\n\n## Conventions\n\n- Every file starts with `# frozen_string_literal: true`. Keep it when editing or creating files.\n- Commands take symbols for the verb (`[:incr, key]`); strings and arguments are coerced where needed (`Integer(x)`, `Float(x)`, `value.to_s`). Follow the local pattern in `lib/redis/commands/<category>.rb` when adding new methods.\n- Yard-style docstrings (`@param`, `@option`, `@return`, `@example`) are on every public command method. New methods should keep that format.\n- Error mapping is centralized in `Redis::Client::ERROR_MAPPING` (and extended in `Redis::Cluster::Client::ERROR_MAPPING`). If you add a new RedisClient error class to handle, add it there rather than catching in individual command methods.\n- Test files must live under one of `test/redis`, `test/distributed`, `test/sentinel`, `cluster/test`. The Rakefile fails the build if a `*_test.rb` exists outside those groups.\n\n<!-- File: .claude/skills/add-new-command/examples/command-specification-template.md (Tokens: ~151 | Category: anthropic-skill) -->\n# $COMMAND_NAME command specification\n\n## Supported version\n\nAdd supported Redis version here. For example: Redis >= 6.2.0\n\n## Command description\n\nAdd a description of the command here.\n\n## Command API\n\nSpecify an API for the command in the format that official docs uses. For example:\n\n```\n$COMMAND_NAME $key $member [NX|XX] [CH] [INCR]\n```\n\n## Redis-CLI examples\n\nAdd relevant Redis-CLI examples here.\n\n## Test plan\n\nSpecify how you want to test the command in terms of integration testing. For example:\n\n- Test only with required arguments, assert that single value returned\n- Test with required arguments and optional XX modifier, ensure that 1 returned\n- ...\n\n<!-- File: .claude/skills/add-new-command/SKILL.md (Tokens: ~1046 | Category: anthropic-skill) -->\n---\ndescription: >-\n  Adds support for a new Redis command to the gem from a specification. Triggers on requests\n  like \"add support for the TS.BGET command\" or via /add-new-command <COMMAND|path>. Resolves the\n  spec from command_specs/, falling back to official redis.io docs. See .claude/skills/add-new-command/examples/command-specification-template.md.\nargument-hint: \"[command-name | path-to-spec]\"\nallowed-tools: Bash, Read, Write, Edit, WebFetch, Glob, Grep, AskUserQuestion\n---\n\n# Add new Redis command support\n\n`$ARGUMENTS` is either a Redis command **name** (e.g. `TS.BGET`, `JSON.GET`) or a **path** to a\nspecification file. Before doing anything else, resolve it to a filled specification using Step 0.\n\n## Step 0 — Resolve the specification\n\nRun `bash .claude/skills/add-new-command/scripts/resolve_spec.sh \"$ARGUMENTS\"` from the repo root. Branch on its `RESOLUTION:` line.\nThe only way into Step 1 is a `RESOLUTION: ready` from the resolver — never enter it on your own judgment that a spec looks filled.\n\n- **`ready`** — filled spec is in the output. Go to Step 1.\n- **`incomplete`** — spec exists but still has the `$COMMAND_NAME` placeholder. Ask the user to fill in the template, show the `RERUN_HINT` to resume, then STOP.\n- **`missing`** — WebFetch `REDIS_IO_URL`.\n  - Found → write a spec to `TARGET_SPEC_FILE` using `.claude/skills/add-new-command/examples/command-specification-template.md` structure, then use the **AskUserQuestion** tool to present a \"Proceed / Stop\" choice. On Proceed → re-run the resolver on `TARGET_SPEC_FILE` and branch on its `RESOLUTION:` again — only `ready` may enter Step 1; `incomplete` means the written spec is still a stub, so follow the `incomplete` branch. On Stop → end (the saved spec can be edited and re-run later).\n  - Not found → ask the user to either give a spec path, or have you copy `.claude/skills/add-new-command/examples/command-specification-template.md` verbatim to `TARGET_SPEC_FILE` (keep the `$COMMAND_NAME` marker so the resolver flags it `incomplete` until filled) for them to fill; then show the `RERUN_HINT` and STOP.\n- **`path_not_found`** — the argument looks like a file path but no file exists at `SPEC_FILE`. Tell the user, ask for a corrected path (or a command name to resolve instead), then STOP. Do not guess a command name from the path.\n- **`no_argument`** — ask for a command name or spec path, then STOP.\n\n## Execution Instructions\n\n### 1. Preparations\n\n- Go through the guide `specs/adding-commands.md`\n\n### 2. Read and Understand\n\n- Read the ENTIRE specification carefully\n- Go through Command Description and identify command type (string, list, set, etc.)\n- Go through the Command API:\n    - Identify required and optional arguments\n    - Identify how to match Redis command arguments type to Ruby types\n    - Identify return value and possible response types\n- Check relevant Redis-Cli examples, if provided\n- Review the Test Plan\n\n### 3. Execute Tasks in Order\n\n#### a. Navigate to the task\n- Identify the files and action required\n- Read existing related files if modifying\n\n#### b. Implement the command\n- Identify the command API surface object you need to work with.\n- Identify required and optional arguments and their matching types.\n- Add command implementation following `specs/adding-commands.md`\n- Ensure doc string documents are added\n\n#### c. Verify as you go\n- After each file change, check syntax\n- Ensure imports are correct\n- Verify types are properly defined\n\n### 4. Implement Testing Plan\n\nAfter completing implementation tasks:\n\n- Identify matching test file or create new one if needed\n- Implement all test cases as separate test methods\n- Ensure adding version constraint if specified in the specification\n- Ensure tests cover edge cases\n\n### 5. Run tests\n\n- Run newly added test cases\n- Get back to the Implementation stage if any test failed\n\n### 6. Final Verification\n\nBefore completing:\n\n- ✅ All tasks from plan completed\n- ✅ All tests created and passing\n- ✅ Code follows project conventions\n- ✅ Documentation added/updated as needed\n\n## Output Report\n\nProvide summary:\n\n### Specification Source\n- How the spec was resolved (local file / built from redis.io / user-provided path)\n- Path to the spec file used\n\n### Completed Tasks\n- List of all tasks completed\n- Files created (with paths)\n- Files modified (with paths)\n\n### Tests Added\n- Test files created\n- Test cases implemented\n- Test results\n\n<!-- File: .claude/skills/add-new-command/scripts/resolve_spec.sh (Tokens: ~809 | Category: claude-rule) -->\n#!/usr/bin/env bash\n# Deterministic spec resolver for the add-new-command skill.\n#\n# Takes the raw skill argument (a Redis command name like \"TS.BGET\", or a path\n# to a spec file) and decides — without any LLM reasoning — where the spec is,\n# whether it is filled in or still a stub, and (when the user must act) how to\n# resume. It prints a small structured report the skill body branches on.\n#\n# Run from the repo root. Usage:\n#   bash .claude/skills/add-new-command/scripts/resolve_spec.sh \"<command-name-or-path>\"\n\nset -euo pipefail\n\narg=\"${1:-}\"\narg=\"$(printf '%s' \"$arg\" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')\"\n\nif [ -z \"$arg\" ]; then\n  echo \"RESOLUTION: no_argument\"\n  echo \"Ask the user which Redis command to add (e.g. TS.BGET) or for a path to a spec file.\"\n  exit 0\nfi\n\nemit_spec_block() {\n  echo \"--- BEGIN SPEC ---\"\n  cat \"$1\"\n  echo \"\"\n  echo \"--- END SPEC ---\"\n}\n\n# Classify a found spec file as ready or incomplete, then emit its report.\n# A spec is still a stub if it carries the template's $COMMAND_NAME placeholder.\n#   $1 = source label, $2 = spec file path, $3 = re-run token (command name or path)\nhandle_found() {\n  local source=\"$1\" file=\"$2\" rerun=\"$3\"\n  if grep -qF -- '$COMMAND_NAME' \"$file\"; then\n    echo \"RESOLUTION: incomplete\"\n    echo \"SPEC_FILE: $file\"\n    echo \"RERUN_HINT: /add-new-command ${rerun}\"\n  else\n    echo \"RESOLUTION: ready\"\n    echo \"SOURCE: $source\"\n    echo \"SPEC_FILE: $file\"\n  fi\n  emit_spec_block \"$file\"\n}\n\n# 1. Explicit path to an existing file.\nif [ -f \"$arg\" ]; then\n  handle_found \"explicit_path\" \"$arg\" \"$arg\"\n  exit 0\nfi\n\n# 1b. Path-shaped argument (contains a slash or a .md suffix) that doesn't exist.\n# Don't fall through to command-name handling — that would fabricate a bogus\n# COMMAND / REDIS_IO_URL / TARGET_SPEC_FILE from the path string.\ncase \"$arg\" in\n  */* | *.md)\n    echo \"RESOLUTION: path_not_found\"\n    echo \"SPEC_FILE: $arg\"\n    echo \"The argument looks like a spec file path, but no such file exists.\"\n    echo \"RERUN_HINT: /add-new-command ${arg}\"\n    exit 0\n    ;;\nesac\n\n# 2. Treat as a command name. Uppercase + keep dots for the filename; lowercase for the URL.\nname_upper=\"$(printf '%s' \"$arg\" | tr '[:lower:]' '[:upper:]')\"\nname_lower=\"$(printf '%s' \"$arg\" | tr '[:upper:]' '[:lower:]')\"\nspec_file=\"command_specs/${name_upper}-specification-template.md\"\n\n# 3. Local spec lookup (authoritative; also the resume path).\nif [ -f \"$spec_file\" ]; then\n  handle_found \"local\" \"$spec_file\" \"$name_upper\"\n  exit 0\nfi\n\n# 4. No local spec — point at redis.io and where to save the spec.\necho \"RESOLUTION: missing\"\necho \"COMMAND: ${name_upper}\"\necho \"REDIS_IO_URL: https://redis.io/docs/latest/commands/${name_lower}/\"\necho \"TARGET_SPEC_FILE: ${spec_file}\"\necho \"RERUN_HINT: /add-new-command ${name_upper}\"\n\n<!-- File: CLAUDE.md (Tokens: ~31 | Category: claude-rule) -->\n# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n@AGENTS.md\n</yakaai_skills>"}