* feat: add Claude Code skills for PR and issue triage
Two skills that power structured audits directly in Claude Code sessions.
They replace ad-hoc manual reviews with a repeatable, documented workflow.
**`/pr-triage`** — Pull request audit
Scans open PRs, runs deep review on selected ones, and posts formatted
review comments to GitHub. Covers code quality, test coverage, security
surface, and alignment with CLAUDE.md conventions. Useful before every
merge and for contributor onboarding.
**`/issue-triage`** — Issue audit
Audits open issues, auto-categorizes (bug/feature/doc/question), detects
duplicates, cross-references related PRs, and estimates risk/priority.
Posts structured comments so maintainers have a clear action queue.
Both skills integrate with the `gh` CLI and can be invoked with args:
/pr-triage 42 57 — deep review of specific PRs
/pr-triage all — full audit of all open PRs
/issue-triage — audit-only mode (no comments posted)
/issue-triage all en — deep analysis, English output
These reduce the overhead of code review and issue management to a single
slash command, keeping context inside the Claude Code session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: fix version references 0.24.0 → 0.25.0 and module count (pre-existing)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: curl size guard (#297) + exclude_commands config (#243)
**#297 — curl JSON inflation guard**
- `rtk curl` no longer replaces small JSON responses with a type schema
that is larger than the original payload
- Size guard in `filter_curl_output()`: only return schema when
`schema.len() <= original.len()`
- Inspired by PR #324 (@rursache)
**#243 — exclude_commands config**
- Add `[hooks] exclude_commands = ["curl"]` in config.toml to prevent
the auto-rewrite hook from rewriting specific commands
- Survives `rtk init -g` re-runs (config.toml is user-owned)
- Implemented in `rewrite_cmd::run()` via `HooksConfig` struct
- Threaded through `rewrite_command()` → `rewrite_compound()` → `rewrite_segment()`
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: add #297 and #243 to CHANGELOG + exclude_commands in README
* docs: fix version references 0.24.0 → 0.25.0 across all docs
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add anonymous telemetry ping (1/day, opt-out)
Fire-and-forget daily ping to track active installs for BPI metrics.
- SHA-256 device hash (hostname:user), no PII
- Opt-out: RTK_TELEMETRY_DISABLED=1 or config.toml [telemetry] enabled=false
- Token injected at compile time via option_env!, not in source
- 23h throttle via marker file, 2s timeout, non-blocking thread
* ci: inject RTK_TELEMETRY_TOKEN at build time in release workflow
All 3 build jobs (binary, DEB, RPM) now receive the token from
GitHub secrets so option_env!() compiles it into release binaries.
* refactor: externalize telemetry URL via option_env!(RTK_TELEMETRY_URL)
URL and token are both injected at compile time — no hardcoded
server address in source code. Builds without the env var simply
skip telemetry silently.
* feat: add `rtk rewrite` command — single source of truth for hook rewrites
Implements `rtk rewrite <cmd>` as the canonical rewrite engine for all
LLM hook integrations (Claude Code, Gemini CLI, future tools).
- Add `rewrite_prefixes` field to `RtkRule` in discover/registry.rs
- Add public `rewrite_command()` with compound command support (&&, ||, ;, |)
- Add `rewrite_segment()`, `rewrite_compound()`, `strip_word_prefix()` helpers
- Handle already-rtk commands (exit 0, identical output)
- Handle unsupported/ignored commands (exit 1, no output)
- Add 20 unit tests covering all branches and edge cases
- Create `src/rewrite_cmd.rs` thin CLI wrapper
- Register `Commands::Rewrite` in main.rs
- Simplify `.claude/hooks/rtk-rewrite.sh` from 357 → 60 lines
Hooks no longer need duplicate mapping logic — a single
`REWRITTEN=$(rtk rewrite "$CMD") || exit 0` handles everything.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: fix version references and module count for 0.22.2
- README.md, CLAUDE.md, ARCHITECTURE.md: 0.20.1 → 0.22.2
- ARCHITECTURE.md: module count 48 → 51 (added rewrite_cmd + 2 from master)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci: update hook coverage check to verify registry (not hook script)
Since PR #241, the hook delegates to `rtk rewrite` — command mappings
live in src/discover/registry.rs, not the bash hook script.
Update the "Verify hook coverage" CI step to:
- Check that the hook calls `rtk rewrite` (new architecture)
- Check that registry.rs has rewrite_prefixes for all Python/Go commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: support single `&` background operator in compound rewrites
Per feedback from @xDelph: AI agents increasingly use `cmd1 & cmd2`
for parallel execution. This commit adds support alongside existing
`&&`, `||`, `;`, and `|` operators.
Changes:
- rewrite_compound: add match arm for single `&` (after `&&` check)
- rewrite_command: add `" & "` to has_compound detection
- init: show "installed/updated" vs "already up to date" so users
know whether rtk init changed the hook on re-run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add Python/Go commands to rewrite registry
Add `ruff`, `pytest`, `pip`, `go`, and `golangci-lint` to both
PATTERNS and RULES in registry.rs so the CI coverage check passes
and `rtk rewrite` correctly identifies these commands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: update version reference to 0.23.0 in README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: update version reference to 0.23.0 in CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: update version and module count to 0.23.0/52 in ARCHITECTURE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address P0 review feedback on rtk-rewrite PR
P0.1 - Replace 218-line bash hook with 58-line thin delegating hook.
All rewrite logic now lives exclusively in `rtk rewrite` (Rust registry).
`rtk init` installs the thin hook via include_str!.
P0.2 - Fix `head -20 file` crash at runtime.
Generic prefix replacement produced `rtk read -20 file` (invalid clap args).
Now translates `head -N file` → `rtk read file --max-lines N` and skips
unsupported head flags (e.g. -c) by returning exit 1.
P0.3 - Add version guard in hook for rtk < 0.23.0.
Prints a warning to stderr instead of silently doing nothing.
Also adds missing registry entries vs old hook:
- gh release, cargo install
- docker run/exec/build, kubectl describe/apply
- tree, diff
474 tests pass, 0 clippy warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: cargo fmt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(discover): extract rules/patterns into rules.rs
Per aeppling review: registry.rs was too large (~1500 lines).
Extract all static data into src/discover/rules.rs:
- RtkRule struct (with pub fields)
- PATTERNS const array
- RULES const array
- IGNORED_PREFIXES and IGNORED_EXACT const arrays
registry.rs now contains only logic + tests.
rules.rs is the single place to add a new command mapping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: update version reference to 0.24.0 in README
* docs: update version references to 0.24.0 in CLAUDE.md and ARCHITECTURE.md
* fix(discover): add aws and psql to rewrite registry
rtk aws and rtk psql modules exist since PR #216 but were missing
from the registry rules — rewrite was silently skipping them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(discover): exhaustive regression tests for rewrite registry
Cover all command categories added in PR #241 and missing from
the previous test suite:
- aws / psql (PR #216 modules, gap detected during non-reg run)
- Python: ruff, pytest, python -m pytest, pip, uv pip
- Go: go test/build/vet, golangci-lint
- JS/TS: vitest, pnpm vitest, prisma, prettier, pnpm list
- Compound operators: || and ; rewrites, 4-segment chains,
mixed supported/unsupported segments, all-unsupported → None
- sudo prefix rewrite, env var prefix rewrite
- find with native flags
- Registry invariants: PATTERNS/RULES aligned, all rules valid,
all patterns are valid regex
Before: 559 tests. After: 607 tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(migration): document breaking change + detect outdated hook in rtk config
CHANGELOG [Unreleased]:
- Migration required: rtk init --global needed after upgrade
- Documents upgrade path and explains no immediate breakage
rtk init --show / rtk config now detects old hook (inline if-else)
vs new thin delegator (rtk rewrite) and prints actionable warning:
⚠️ Hook: ~/.claude/hooks/rtk-rewrite.sh (outdated — inline logic, not thin delegator)
→ Run `rtk init --global` to upgrade to the single source of truth hook
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: update hook references for thin delegator (0.24.0)
- ARCHITECTURE.md: fix hook description (shell script → thin delegator)
- INSTALL.md: add upgrade path for old hook users (pre-0.24 breaking change)
- SECURITY.md: add registry.rs + hook files to Tier 1 critical files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(arch): fix module count 52→55, add aws/psql/rewrite to module map
Adds aws_cmd, psql_cmd (PR #216) and rewrite_cmd (this PR) to the
Complete Module Map table. Updates total from 52 to 55 to match main.rs.
Fixes CI validate-docs.sh module count mismatch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci: fix hook coverage check to search rules.rs instead of registry.rs
After the PR #241 refactor, rewrite_prefixes constants moved from
registry.rs to rules.rs. Update grep to search the whole discover/
directory so it finds them in the right file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs caused `rtk playwright test` to always fail with EXIT: 1:
1. **Wrong argument order**: `--reporter=json` was inserted before the
subcommand (e.g. `playwright --reporter=json test`), but Playwright
requires flags after the subcommand (`playwright test --reporter=json`).
2. **Float duration deserialization**: `PlaywrightStats.duration` was typed
as `u64`, but real Playwright output emits a float (e.g. `3519.703...`),
causing serde_json to fail with a type error.
3. **Wrong suite structure**: Suites contain `specs` (not `tests`). Each
spec has `ok: bool` and `tests` (per-browser executions). The old struct
used `tests: Vec<PlaywrightTest>` directly in suites, which never matched
real output and always failed JSON parsing.
Fixes:
- Move `--reporter=json` to after the first arg (the subcommand)
- Change `duration: u64` to `duration: f64` in `PlaywrightStats`
- Rewrite `PlaywrightSuite` → `specs: Vec<PlaywrightSpec>`
- Add `PlaywrightSpec` with `ok: bool` and `tests: Vec<PlaywrightExecution>`
- Add `PlaywrightExecution` with `status` and `results: Vec<PlaywrightAttempt>`
- Update `PlaywrightAttempt` to use `errors: Vec<PlaywrightError>` (array)
- Add tests covering float duration, real suite structure, and failure details
Co-authored-by: devkeruse <devkeruse@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: passthrough fallback when Clap parse fails
When RTK cannot parse a command (e.g. `rtk git -C /path status`),
instead of exiting with error code 2, it now falls back to running
the raw command directly. This keeps developer workflows unbroken.
- Replace Cli::parse() with try_parse() + run_fallback()
- Add parse_failures SQLite table for failure analytics
- Add `rtk gain --failures` / `-F` to view failure log
- Fallback preserves stdin/stdout/stderr via Stdio::inherit()
- --help and --version still work normally
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: guard RTK meta-commands from fallback to raw execution
When Clap fails to parse a meta-command like `rtk gain --badtypo`,
show the Clap error directly instead of trying to execute `gain`
as a binary from $PATH. Adds RTK_META_COMMANDS constant listing
gain, discover, learn, init, config, proxy, hook-audit, cc-economics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: move timer start before command execution in fallback
TimedExecution::start() was called after the command finished,
so all fallback commands showed ~0ms in rtk gain --history.
Now the timer captures actual command runtime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add cleanup_old() call to record_parse_failure()
The parse_failures table was never cleaned up because only record()
called cleanup_old(). Now parse failures also trigger 90-day retention
cleanup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: strip ANSI codes from Clap error before SQLite storage
Clap errors may contain terminal color codes. Strip them with
utils::strip_ansi() before storing in the parse_failures table
to avoid garbled output and wasted space.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: smoke test cargo test assertion and missing skip_test calls
- cargo test check now matches RTK's filtered output format ("passed")
in addition to raw "test result:" and "FAILURES"
- Fix undefined `skip` calls to use existing `skip_test` function
with proper (name, reason) arguments
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: fmt upstream files after rebase
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update version refs to 0.23.0 and module count to 51
Upstream v0.23.0 release bumped Cargo.toml version and added mypy_cmd
module. Update docs to match for CI validation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
run_push, run_pull, run_fetch, run_stash (pop/apply/drop/push and
default), and run_worktree all printed FAILED messages on error but
returned Ok(()) instead of propagating git's exit code.
This breaks CI/CD pipelines and shell scripts that rely on exit code
semantics (e.g. `git push || handle_error`).
Fix: call std::process::exit(output.status.code().unwrap_or(1)) in
each failure branch, matching the pattern already used in run_log,
run_diff, run_add, and run_branch.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends the existing git global options support to accept boolean flags
that the hook already strips for subcommand matching but Clap was rejecting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
git -C <path> and other global options were rejected because Clap
expected a subcommand immediately after `git`. Add these options to
the Git command variant and thread them through all git subcommand
handlers via a git_cmd() helper that prepends global args before
the subcommand.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(gain): add per-project token savings with -p flag
Record project_path (cwd) in tracking database and add filtered query
methods. `rtk gain -p` shows savings scoped to the current project
directory instead of global aggregates.
- tracking.rs: Add project_path column with auto-migration, index,
and filtered variants for all query methods (summary, daily, weekly,
monthly, recent)
- gain.rs: Add resolve_project_scope(), shorten_path(), scope-aware
header, pass project filter to all queries and exports
- main.rs: Add --project/-p flag to Gain command
Backward-compatible: existing rows get empty project_path, unfiltered
queries delegate to filtered(None) which returns all data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(tracking): use GLOB instead of LIKE for project path filtering
Address reviewer feedback on PR #128:
1. Replace SQL LIKE with GLOB in all project-scoped queries to prevent
`_` and `%` characters in path names from being interpreted as
wildcards (e.g., `my_project` matching `myXproject`). GLOB uses `*`
for wildcard matching which is safer for file system paths.
2. Guard the startup `UPDATE commands SET project_path = ''` migration
with an `EXISTS` check so it only runs when NULL rows actually exist,
avoiding a no-op UPDATE on every startup after the first migration.
3. Add `DEFAULT ''` to the ALTER TABLE migration so new installs never
create NULL project_path values.
4. Add 3 new unit tests for project_filter_params GLOB behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add mypy command with grouped error output (80% token reduction)
Add rtk mypy module that parses mypy type checker output, groups errors
by file and error code, and produces compact summaries. Includes
discovery registry pattern, hook rewrites, and 11 new tests (324 total).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review - remove specs/ and clean CLAUDE.md
Remove process docs (spec, plan, tasks, research, checklists) from
specs/001-mypy-cmd/ and strip "Active Technologies" / "Recent Changes"
sections from CLAUDE.md per PR #109 review feedback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review - deduplicate mypy handler, delegate from lint_cmd
Make lint_cmd.rs delegate to mypy_cmd::filter_mypy_output() instead of
maintaining a separate, less capable implementation. The mypy_cmd version
handles column numbers, note continuations, file-less errors, and shows
full error details. Removes 145 lines of duplicate code + 2 redundant tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Robert Mohid <rmohid@Roberts-Mini.ht.home>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
`go test -json` emits build failures as `build-output`/`build-fail`
actions with `ImportPath` (not `Package`/`Test`), followed by a
package-level `fail` with a `FailedBuild` field. The JSON parser
only counted failures when `event.test.is_some()`, so build errors
were silently dropped from the summary — showing "891 passed" when
a package failed to compile.
Now parses `build-output`, `build-fail`, and `FailedBuild` fields.
Build failures count toward the total and display with `[build failed]`
plus the compiler error lines.
Before:
✓ Go test: 891 passed in 74 packages
After:
Go test: 891 passed, 1 failed in 74 packages
═══════════════════════════════════════
📦 stream [build failed]
handler_test.go:64:9: undefined: undefinedVariable
Co-authored-by: Ricardo Liberato <ricardo@liberato.pt>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The hook was rewriting all `docker compose` commands to `rtk docker
compose`, but rtk only supports ps, logs, and build. Unsupported
subcommands (up, down, exec, restart...) and compose-level flags
(-f, --project-name) caused hard failures.
Now matches the approach used by the regular docker branch: only
rewrite when the subcommand is known to be supported.
Fixes#244
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The shell keyword "fi" was listed as a bare prefix in IGNORED_PREFIXES,
causing classify_command("find ...") to return Ignored because
"find".starts_with("fi") is true. This prevented find commands from
being rewritten by rtk rewrite and the hook system.
Move "fi" and "done" from IGNORED_PREFIXES to IGNORED_EXACT so they
only match as exact standalone keywords, not as prefixes of other
commands.
Fixes#170, #204, #236 (partially — find classification was the root
blocker for rtk rewrite).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The current x86_64-unknown-linux-gnu release binary dynamically links against glibc, which causes failures on systems with an older glibc version:
rtk: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found (required by rtk)
Replace the x86_64-unknown-linux-gnu build target with x86_64-unknown-linux-musl, producing a fully statically-linked binary with no libc dependency.
Fixesrtk-ai/rtk#265 - binary fails on systems with old glibc.
Remove `methode-aristote/app` project name from the JS/TS tooling
section. Project-specific CLAUDE.md should not reference personal
projects or contributor-specific context.
Follow-up to #242.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Project CLAUDE.md is loaded for every contributor using Claude Code.
It should contain project documentation, not personal preferences.
Removed:
- Hardcoded home directory path (/Users/florianbruniaux/...)
- Sibling project references (ccboard, cc-economics)
- French language preference and communication style
These belong in the author's personal ~/.claude/CLAUDE.md, not in
the repo where they get imposed on every fork and contributor.
Three bugs fixed:
1. JSON struct mismatch: The parser expected `suites[].tests[]` but
Playwright's JSON reporter outputs `suites[].specs[].tests[]`.
Added PlaywrightSpec layer, use `spec.ok` for failure detection,
and `errors: Vec<PlaywrightError>` (array, not Option).
2. Float duration: Playwright emits `duration` as f64, not u64.
Changed struct field type to avoid deserialization failures.
3. pyenv shim collision: `which playwright` can resolve to a pyenv
shim (Python's playwright) which lacks a `test` subcommand.
Bypass `which` entirely and resolve through the detected package
manager (npx/pnpm/yarn).
Also strips user-supplied `--reporter` flags to avoid conflicts with
the forced `--reporter=json`.
rtk grep previously returned exit 0 for all failure modes (no match,
invalid regex, missing file) where rg returns non-zero. This broke
shell workflows relying on exit code semantics.
Now propagates rg exit codes: 1 (no match), 2 (error) and surfaces
stderr for error cases so users see why the command failed.
Fixes#162
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
`rtk git branch <name>` fell into the listing code path which injected
`-a --no-color`, turning the creation into a pattern-filtered listing
(silent no-op with exit 0). Detect positional args without list-mode
flags and route them to direct passthrough so branch creation, including
`git branch <name> <start-point>`, works correctly.
Adds regression integration tests for both cases.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Two bugs when passing grep-idiom arguments to rtk grep:
1. Pattern alternation `\|` (BRE syntax) returns 0 results because rg
uses PCRE-style `|`, not BRE `\|`. Users coming from grep/awk muscle
memory naturally write `rtk grep "fn foo\|fn bar" src/` and get
empty output with no error.
Fix: translate `\|` → `|` in pattern before passing to rg.
2. `-r`/`--recursive` flag (grep idiom) silently breaks the search.
In rg, `-r` means `--replace` and requires an argument — passing it
alone causes rg to exit with an error. The `.or_else(|_| ...)` fallback
only catches IO errors (command not found), so the grep fallback never
triggers; stdout is empty and rtk reports "0 for '<pattern>'".
Fix: filter out `-r`/`--recursive` from extra_args before building
the rg command (rg is recursive by default, flag is a no-op).
Adds two regression tests documenting both behaviours.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
rtk lint was always returning exit code 0 regardless of the underlying
linter's exit status, causing false-green CI behavior when lint should fail.
Uses !output.status.success() pattern consistent with all other RTK modules,
which correctly handles both non-zero exit codes and signal termination.
Fixes#185
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds a new `wc` subcommand that compresses wc output by:
- Stripping alignment padding: ` 30 96 978 file.py` → `30L 96W 978B`
- Removing common path prefixes in multi-file output
- Returning bare numbers for single-flag modes (-l, -w, -c, -m)
- Showing Σ totals instead of verbose "total" lines
Includes 15 unit tests covering single/multi-file, stdin, all flag
modes, and common prefix detection.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
view_run() only passed the run ID to `gh run view`, silently
dropping all subsequent args. This made it impossible to inspect
CI failure logs when the rtk-rewrite hook was active.
Add should_passthrough_run_view() to detect flags that produce
output incompatible with the summary filter, and pass through
to gh directly when found.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>