Skip to content

feat(init): add --dry-run flag to preview changes without writing - #1032

Merged
aeppling merged 8 commits into
rtk-ai:developfrom
hed0rah:feat/init-dry-run
May 9, 2026
Merged

aeppling merged 8 commits into
rtk-ai:developfrom
hed0rah:feat/init-dry-run

Conversation

@hed0rah

@hed0rah hed0rah commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds --dry-run to rtk init so users can preview exactly what files would be created or modified before committing to a global install
  • All write operations (hook files, settings.json patches, RTK.md, .clinerules, .windsurfrules, etc.) are guarded — output shows [dry-run] would write: <abs_path> instead of touching the filesystem
  • Combines with --verbose (-v) to also print the full file contents that would be written, making it useful for auditing or CI validation

Test plan

  • rtk init -g --dry-run — prints paths, no files created, verify with rtk init --show still showing "not installed"
  • rtk init -g --dry-run -v — same as above but also prints file contents inline
  • rtk init -g then rtk init -g --dry-run — "already up to date" shown for existing files instead of silently skipping
  • rtk init --agent cline --dry-run — shows abs path to .clinerules in current dir
  • rtk init --agent windsurf --dry-run — same for .windsurfrules
  • rtk init -g --gemini --dry-run — shows hook file + settings.json patch
  • Verify no files/directories are created under any flag combination with --dry-run

@CLAassistant

CLAassistant commented Apr 5, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@hed0rah

hed0rah commented Apr 5, 2026

Copy link
Copy Markdown
Contributor Author

suppressed misleading success messages like "Gemini CLI hook installed (global)." and "Restart Gemini CLI. Test with: git status" that were printing even in dry-run, and printed: [dry-run] Nothing written. instead

@aeppling aeppling self-assigned this Apr 7, 2026
@hed0rah
hed0rah force-pushed the feat/init-dry-run branch from 65f5fd1 to 3fa0323 Compare April 15, 2026 05:38
@aeppling

aeppling commented Apr 18, 2026 •

Copy link
Copy Markdown
Contributor

Hey @hed0rah , thanks for the contribution and keeping this up to date.

Few things to be check :

Merge conflicts with develop

This PR is based on a develop version that used hook scripts (rtk-rewrite.sh). Develop has since moved to binary commands (rtk hook claude/rtk hook cursor). Key functions from this PR no longer exist on develop:

  • ensure_hook_installed(), prepare_hook_paths() → removed (no scripts to install)
  • patch_settings_json(hook_path) → replaced by patch_settings_json_command(hook_command)
  • write_if_changed now uses atomic_write() instead of fs::write()
  • install_cursor_hooks simplified (no script creation, just hooks.json patching)
  • New on develop: migrate_old_hook_script() (cleans up legacy scripts)

Resolution: rebase on current develop, drop the removed functions, and thread dry_run into the new ones (patch_settings_json_command, migrate_old_hook_script, install_cursor_hooks).

Bugs to fix

Please check following issues to ensure a clean feature implementation:

  1. --uninstall --dry-run actually deletes files: uninstall() never receives dry_run. Add the parameter and guard all fs::remove_file / fs::write calls.

  2. Success messages print in dry-run mode: these functions print "installed"/"configured" messages without checking dry_run:

  • run_default_mode() (step 4 + PatchResult match)
  • run_hook_only_mode() (same pattern)
  • run_codex_mode_with_paths() (also missing [dry-run] Nothing written. footer)
  • run_copilot() (same , no guard, no footer)
  • install_cursor_hooks() (same)

Fix: wrap success blocks in if !dry_run { ... }.

  1. prompt_telemetry_consent() runs in dry-run: it prompts interactively and writes to config.toml. Guard with if !dry_run.

  2. KiloCode and Antigravity modes ignore --dry-run: run_kilocode_mode() and run_antigravity_mode() don't accept dry_run, so rtk init --agent kilocode --dry-run writes files anyway.

  3. After rebase: guard integrity::store_hash in run_gemini(): develop has this call outside any dry-run guard. Once you add dry_run to run_gemini, wrap it in if !dry_run.

Missing tests

No tests exercise dry_run=true. At minimum add tests verifying:

  • write_if_changed(..., dry_run=true) does not create the file
  • run_codex_mode_with_paths(..., dry_run=true) does not create RTK.md / AGENTS.md

Minor

  • Use atomic_write() (develop's version) instead of fs::write() in write_if_changed
  • patch_settings_json_command returns PatchResult::Patched in dry-run which is misleading, consider documenting or adding a WouldPatch variant

Rebased onto current develop (binary-command era). Threads dry_run
through patch_settings_json_command, migrate_old_hook_script,
install_cursor_hooks, run_kilocode_mode, run_antigravity_mode,
run_gemini, run_copilot, and uninstall.

Fixes from PR rtk-ai#1032 review:
- --uninstall --dry-run no longer deletes files (uninstall() now takes
  dry_run, every fs::remove_file / fs::write / atomic_write guarded)
- Success messages ("installed", "configured", "Restart ...") gated on
  !dry_run in run_default_mode, run_hook_only_mode, run_codex_mode,
  run_copilot, install_cursor_hooks, run_gemini
- prompt_telemetry_consent() skipped in dry-run
- integrity::store_hash() in run_gemini guarded
- KiloCode and Antigravity modes now accept dry_run
- PatchResult::WouldPatch variant added for patch_settings_json_command
- [dry-run] Nothing written. footer printed by every sub-mode
- write_if_changed uses atomic_write (not fs::write)

Added integrity::hash_path_for() public wrapper so dry-run can check
sidecar existence without the destructive remove_hash.

Tests: write_if_changed(dry_run=true) creates nothing;
run_codex_mode_with_paths(dry_run=true) creates neither RTK.md nor
AGENTS.md. 1596 tests pass, clippy clean.
@hed0rah
hed0rah force-pushed the feat/init-dry-run branch from 3fa0323 to 21a069a Compare April 19, 2026 05:10
@hed0rah

hed0rah commented Apr 19, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current develop and addressed all review feedback. Force-pushed the branch. History is replaced since the old PR commits predate the hook-script to binary-command refactor.

Addressed

  • Rebase: dropped removed functions (ensure_hook_installed, prepare_hook_paths, old patch_settings_json). Threaded dry_run into the new ones: patch_settings_json_command, migrate_old_hook_script, install_cursor_hooks.
  • --uninstall --dry-run deleted files: uninstall() now takes dry_run, every fs::remove_file / fs::write / atomic_write guarded. Added integrity::hash_path_for() so we can report intent without triggering the destructive remove_hash.
  • Stray success messages: wrapped in if !dry_run { ... } for run_default_mode, run_hook_only_mode, run_codex_mode_with_paths, run_copilot, install_cursor_hooks, run_gemini.
  • prompt_telemetry_consent() ran in dry-run: now guarded at the call site.
  • KiloCode + Antigravity ignored --dry-run: both accept dry_run and thread it through.
  • integrity::store_hash in run_gemini: guarded.
  • PatchResult::WouldPatch variant added; patch_settings_json_command returns it in dry-run.
  • write_if_changed now uses atomic_write instead of fs::write.
  • [dry-run] Nothing written. footer printed by every sub-mode.

Tests

  • test_write_if_changed_dry_run_does_not_create_file
  • test_write_if_changed_dry_run_does_not_modify_existing_file
  • test_run_codex_mode_dry_run_writes_nothing
  • 1596 tests passing, cargo clippy --all-targets clean.

Also manually smoke-tested: rtk init --dry-run, --uninstall --dry-run, --cursor --dry-run, --codex --dry-run, --copilot --dry-run all leave the filesystem untouched.

Ready for re-review.

@hed0rah

hed0rah commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

When --dry-run was added, the signatures of run_default_mode, run_hook_only_mode, run_claude_md_mode, and uninstall each gained a trailing dry_run: bool parameter, but the test calls in src/hooks/init.rs (lines 4233, 4255, 4256, 4272-73, 4285, 4292, 4309, 4327) were never updated.
Added a trailing , false to each call. Local cargo clippy --all-targets now compiles cleanly

…removals

The two remove_file calls flagged by semgrep already existed pre-PR.
Wrapping them in if dry_run { print } else { remove_file } shifted
their context enough that --baseline-commit re-attributed them as new.
The rule's own message says deletion is expected in hooks/init cleanup.

@aeppling aeppling left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: PR #1032 feat(init): add --dry-run flag

Thanks for the rebase and V2 fixes, the filesystem guard coverage is thorough. I built the branch (8b4a146), ran 23 manual tests across all init/uninstall modes, and the full test suite (1609 pass). Every install and uninstall dry-run correctly prevents writes.

One bug and two blocking items need to be addressed before merge, plus one architectural change request.

Bug to fix (blocking)

Double [dry-run] Nothing written. footer with --agent cursor

Repro: rtk init -g --agent cursor --auto-patch --dry-run

Output:

[dry-run] would create RTK.md: ~/.claude/RTK.md
[dry-run] would patch settings.json: ~/.claude/settings.json
[dry-run] would create global filters template: ~/.config/rtk/filters.toml

[dry-run] Nothing written.          <-- first
[dry-run] would patch Cursor hooks.json: ~/.cursor/hooks.json

[dry-run] Nothing written.          <-- second (duplicate)

Cause: run_default_mode() prints its own footer, then run() calls install_cursor_hooks() which prints another.

Fix: Move print_dry_run_footer() out of sub-modes and into run() as the single exit point, after all mode+cursor work completes.

Documentation (blocking)

CONTRIBUTING.md requires documentation for new features. Please add a short section to docs/guide/getting-started/quick-start.md covering --dry-run: what it does, how to use it, and its interaction with -v for content preview.

Architecture: Replace parameter threading with InitContext struct (blocking)

The PR threads dry_run: bool through 36 function signatures, the same 36 that already carry verbose: u8. This works but doesn't scale: PR #1493 (--only/--skip) would add more parameters to these same 36 signatures, and there are 10+ open PRs adding agent modes that each need to thread all params.

RTK already uses this pattern, see RunOptions in src/core/runner.rs:18:

#[derive(Default)]
pub struct RunOptions<'a> {
    pub tee_label: Option<&'a str>,
    pub filter_stdout_only: bool,
    pub skip_filter_on_failure: bool,
    pub no_trailing_newline: bool,
}

Apply the same pattern to init:

#[derive(Clone, Copy, Default)]
pub struct InitContext {
    pub verbose: u8,
    pub dry_run: bool,
}

Every signature changes from:

fn write_if_changed(path: &Path, content: &str, name: &str, verbose: u8, dry_run: bool) -> Result<bool>
fn run_default_mode(global: bool, patch_mode: PatchMode, verbose: u8, install_opencode: bool, dry_run: bool) -> Result<()>
fn patch_cursor_hooks_json(path: &Path, verbose: u8, dry_run: bool) -> Result<bool>

to:

fn write_if_changed(path: &Path, content: &str, name: &str, ctx: InitContext) -> Result<bool>
fn run_default_mode(global: bool, patch_mode: PatchMode, install_opencode: bool, ctx: InitContext) -> Result<()>
fn patch_cursor_hooks_json(path: &Path, ctx: InitContext) -> Result<bool>

Construction at call site:

let ctx = InitContext { verbose: cli.verbose, dry_run };
hooks::init::run(global, install_claude, ..., ctx)?;

Why this matters now:

  • verbose: u8 already pollutes 36 signatures, this PR doubles the problem
  • #[derive(Copy, Clone, Default)] means zero allocation, passable by value
  • Adding future flags (--only, --skip, --quiet) is one struct field instead of 36+ signature changes
  • Every open PR adding an agent mode benefits immediately, they just accept ctx: InitContext
  • This is a mechanical refactor (find-replace), no logic changes

Minor (non-blocking)

  • --show --dry-run: Silently ignores --dry-run. Consider #[arg(conflicts_with = "show")] on dry_run.
  • Missing tests: No test for uninstall dry-run path (the #1 bug from V1), no test for default mode dry-run. The 3 existing tests cover the primitive, but the most destructive paths deserve regression tests.

hed0rah added 2 commits April 26, 2026 17:53
Addresses three review items from PR rtk-ai#1032:

- Bundle verbose+dry_run into a Clone+Copy InitContext struct (mirrors
  RunOptions in src/core/runner.rs). Collapses 25+ function signatures
  that already carried both fields and makes future flags one struct
  field instead of N signature changes.
- Emit "[dry-run] Nothing written." exactly once from the top-level
  run() and uninstall() exit points instead of from every sub-mode.
  Fixes the double footer when --agent cursor combined with default
  mode.
- Reject --show with --dry-run via clap conflicts_with rather than
  silently ignoring --dry-run.
- Add regression tests for run_default_mode and uninstall dry-run paths
  using the existing with_claude_dir_override scaffolding.
Adds a "Preview without writing" subsection under Step 1 covering the
--dry-run flag, -v interaction for content preview, that telemetry
consent is skipped, and the --show conflict. Required by CONTRIBUTING.md
section 4 (new features need documentation).
@hed0rah

hed0rah commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author

@aeppling thanks for the thorough review. Pushed two commits addressing all five items.

Blocking (refactor): 98fe82d introduces InitContext { verbose, dry_run } with #[derive(Clone, Copy, Default)] mirroring RunOptions at src/core/runner.rs:17. Collapses 25 helper signatures plus the public run/uninstall/run_gemini/run_copilot/run_kilocode_mode/run_antigravity_mode entry points. Construction at the CLI seam in src/main.rs. Tests (16 sites) updated to InitContext::default() or struct literal.

Blocking (bug): Same commit. Removed all print_dry_run_footer() calls from sub-modes reached via run() (run_default_mode, run_hook_only_mode, run_claude_md_mode, run_opencode_only_mode, run_codex_mode_with_paths, run_windsurf_mode, run_cline_mode, install_cursor_hooks). Added a single call before Ok(()) in run() and uninstall(). Verified against the original repro: rtk init -g --agent cursor --auto-patch --dry-run now prints exactly one footer.

Blocking (docs): 2f185a4 adds a "Preview without writing" subsection to docs/guide/getting-started/quick-start.md under Step 1.

Minor (conflicts_with): Same code commit. #[arg(long = "dry-run", conflicts_with = "show")] on the dry_run flag. clap now rejects --show --dry-run with a clear error.

Minor (missing tests): Same code commit. Added test_run_default_mode_dry_run_writes_nothing and test_uninstall_dry_run_preserves_artifacts using the existing with_claude_dir_override helper. Both pass.

Full gate: cargo fmt && cargo clippy --all-targets && cargo test --all reports 1612 passed, 0 failed. Smoke-tested the release build against ~/.claude with an mtime snapshot diff and saw zero unintended writes.

@aeppling

aeppling commented May 9, 2026

Copy link
Copy Markdown
Contributor

LGTM , thanks for contributing to RTK !

@aeppling
aeppling merged commit 172ec54 into rtk-ai:develop May 9, 2026
11 checks passed
@aeppling aeppling mentioned this pull request May 9, 2026
ryanmurf added a commit to murphytek/rtk that referenced this pull request May 30, 2026
* fix(uninstall): uninstall removes --claude-md artifacts on Windows

  Core fix (src/init.rs — uninstall()):
  - Added step 3b after existing @RTK.md removal: checks for <!-- rtk-instructions marker in CLAUDE.md and removes the block
  using the existing remove_rtk_block() helper
  - Handles both artifacts in a single pass (covers edge case where both @RTK.md and the instructions block exist, e.g. after
  running both rtk init -g and rtk init -g --claude-md)
  - If CLAUDE.md becomes empty after cleanup, deletes the file

  Hardening:
  - Extracted RTK_BLOCK_START and RTK_BLOCK_END constants — all marker matching across uninstall(), upsert_rtk_block(),
  remove_rtk_block(), patch_claude_md(), and show_config() now uses these instead of duplicated string literals
  - When uninstall finds nothing to remove, prints which paths were checked to help users diagnose path issues
  - resolve_claude_dir() error message now shows %USERPROFILE% on Windows instead of $HOME

Signed-off-by: Adrien Eppling <adrien.eppling@supinfo.com>

* fix(init-uninstall): trailing newline, test refactor, changelog

Signed-off-by: aesoft <43991222+aeppling@users.noreply.github.com>

* fix(pnpm): install don't take a list of packages

Signed-off-by: Nicolas Le Cam <niko.lecam@gmail.com>

* fix(ls): add LC_ALL=C and fallback to raw on unrecognized locale

- Force LC_ALL=C so ls always outputs English month names regardless of system locale
- When no lines are parsed (e.g., non-English locale where regex fails to match),
  fall back to raw output instead of returning '(empty)'
- This prevents silent data loss for users in zh_CN/ja/ko/etc. locales
- Fixes rtk-ai#1276

* chore(master): release 0.37.0

* fix(install): reject archive with path traversal before extraction (rtk-ai#1250)

The installer previously ran `tar -xzf` on the downloaded archive with no
pre-extraction verification. A malicious mirror could ship a tarball with
`../` components or absolute paths and write files anywhere on the user's
filesystem (CWE-22).

Add a pre-extraction check that lists archive contents with `tar -tzf`
and rejects any entry whose name starts with `/` or contains a `..` path
component. The check is POSIX-compliant and adds negligible overhead for
the single-binary RTK release tarball.

Covered by scripts/test-install.sh, which exercises one safe archive
and four crafted malicious archives (leading `..`, absolute path,
mid-path `..`, trailing `..`) plus a regression guard that ensures the
check remains in install.sh.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(discover): skip head/tail rewrite when multiple files are passed (rtk-ai#1362)

`head -N file1 file2 ...` was rewritten to
`rtk read file1 file2 ... --max-lines N`. The capture regex was greedy
(`(.+)$`), so every file argument was folded into a single string. `rtk
read` then silently mis-handled the shape and fell through to the system
`read` builtin (`/usr/bin/read`) which rejects paths starting with `/` as
"not a valid identifier".

Tighten each head/tail regex to a single non-whitespace file argument
(`\S+$`). When >1 file is provided the regex misses, `rewrite_line_range`
returns `None`, and the command falls through to the native `head` / `tail`
binary — which already handles multi-file input with the expected
`==> name <==` banners. Same class as the cat multi-file fix in rtk-ai#989.

Added six regression tests covering the `-N`, `--lines=N`, `-n N`, and
`--lines N` variants for both `head` and `tail`.

* fix(ls): distinguish empty dir from unparseable locale content

- Move . and .. detection before date parsing (is_dotdir) for non-English locale compatibility
- Add dotdirs counter to distinguish empty dir (only . and ..) from real content that failed to parse
- Fix test_compact_empty to use real ls -la output (includes . and ..)
- Add test_compact_empty_chinese_locale for Chinese locale empty dir case
- Closes regression where fallback falsely triggered on empty directories

* refactor(ls): remove unused parse_failed variable

* chore(master): release 0.37.1

* feat(init): add --dry-run flag to preview changes without writing

Rebased onto current develop (binary-command era). Threads dry_run
through patch_settings_json_command, migrate_old_hook_script,
install_cursor_hooks, run_kilocode_mode, run_antigravity_mode,
run_gemini, run_copilot, and uninstall.

Fixes from PR rtk-ai#1032 review:
- --uninstall --dry-run no longer deletes files (uninstall() now takes
  dry_run, every fs::remove_file / fs::write / atomic_write guarded)
- Success messages ("installed", "configured", "Restart ...") gated on
  !dry_run in run_default_mode, run_hook_only_mode, run_codex_mode,
  run_copilot, install_cursor_hooks, run_gemini
- prompt_telemetry_consent() skipped in dry-run
- integrity::store_hash() in run_gemini guarded
- KiloCode and Antigravity modes now accept dry_run
- PatchResult::WouldPatch variant added for patch_settings_json_command
- [dry-run] Nothing written. footer printed by every sub-mode
- write_if_changed uses atomic_write (not fs::write)

Added integrity::hash_path_for() public wrapper so dry-run can check
sidecar existence without the destructive remove_hash.

Tests: write_if_changed(dry_run=true) creates nothing;
run_codex_mode_with_paths(dry_run=true) creates neither RTK.md nor
AGENTS.md. 1596 tests pass, clippy clean.

* chore(master): release 0.37.2

* fix(tests): update init.rs test calls for dry_run parameter

* feat(cicd): add auto next release parser

Create Next Release PR and set up the description with fix / feat PRs and issues to be closed

* fix(ls): LC_ALL=C + fallback to raw on unrecognized locale

- Force LC_ALL=C so ls always outputs English month names
- When zero lines parsed but directory has content, fallback to raw output
- Add is_dotdir() to distinguish empty dirs (only . and ..) from unparseable content
- Fix empty directory regression for both English and non-English locales
- Closes rtk-ai#1276

* chore(init): suppress semgrep filesystem-deletion on dry-run-wrapped removals

The two remove_file calls flagged by semgrep already existed pre-PR.
Wrapping them in if dry_run { print } else { remove_file } shifted
their context enough that --baseline-commit re-attributed them as new.
The rule's own message says deletion is expected in hooks/init cleanup.

* test(dotnet): try to lessen test flakiness

Fix seems counter intuitive but it helps a lot on my machine.
Before, I had 234 errors (23%) out of 1,000 tests run; now, I only have one (0.1%)

It's not totaly fixed but it reduce error a lot and avoids having one almost every time I test a PR.

Another way to completly fix it would be to use std::fs::set_times on old.trx to set each time in the past, but it requires unstable fs_set_times feature, so I'm not sure you'll agree with that

Signed-off-by: Nicolas Le Cam <niko.lecam@gmail.com>

* fix(grep): adjust the command to fall through if the output would already be as small as possible

* refactor(init): introduce InitContext and centralize dry-run footer

Addresses three review items from PR rtk-ai#1032:

- Bundle verbose+dry_run into a Clone+Copy InitContext struct (mirrors
  RunOptions in src/core/runner.rs). Collapses 25+ function signatures
  that already carried both fields and makes future flags one struct
  field instead of N signature changes.
- Emit "[dry-run] Nothing written." exactly once from the top-level
  run() and uninstall() exit points instead of from every sub-mode.
  Fixes the double footer when --agent cursor combined with default
  mode.
- Reject --show with --dry-run via clap conflicts_with rather than
  silently ignoring --dry-run.
- Add regression tests for run_default_mode and uninstall dry-run paths
  using the existing with_claude_dir_override scaffolding.

* docs(init): document --dry-run in quick-start guide

Adds a "Preview without writing" subsection under Step 1 covering the
--dry-run flag, -v interaction for content preview, that telemetry
consent is skipped, and the --show conflict. Required by CONTRIBUTING.md
section 4 (new features need documentation).

* fix(stream): route to respective fd

* fix(benchmark): capture all fd for stream cmd benchmark

* fix(benchmark): benchmark capture all fd only stream

* refactor(git): Fix stash status detection for all cases

Consolidate stash default handlers to ensure "no local changes"
is properly detected across all stash operations, not just the
default push case.

Signed-off-by: Nicolas Le Cam <niko.lecam@gmail.com>

* fix(stream): add semgrep flag for sh tests

* hotfix(cicd): add git app token for release please

* Update cd.yml

* hotfix(cicd): authorize dispatch for release please + git app for bump

* hotfix(cicd): git app token for artefact

* chore(master): release 0.38.0

* fix(git): surface in-progress state in compact `rtk git status`

Default `rtk git status` runs `git status --porcelain -b` to build its
compact view, but porcelain v1 omits the state header git prints when a
rebase, merge, cherry-pick, revert, bisect, am, or sparse checkout is in
progress. That header is correctness-critical — hiding "You are currently
editing a commit while rebasing..." leaves the user thinking the repo is
clean when it isn't.

`run_status` already captures plain `git status` output as `raw_output`
for tracking, so this fix adds `extract_state_header` to pull the state
block out of it and prepends the block to the compact output. Returns
`None` when nothing is in progress so clean repos are unchanged.

Covered states: interactive / regular rebase, merge-with-unmerged-paths,
"still merging after conflicts fixed", cherry-pick, revert, bisect, am,
sparse checkout. Preserves the directive hints git prints alongside
(`git rebase --continue`, `git commit --amend`, `git bisect reset`,
etc.), while still filtering generic `(use "git add")` / `(use "git
restore")` noise.

Design principle: Correctness vs Token Savings — never hide information
that changes the user's understanding of repo state.

* fix(git): compact in-progress status state

* fix(git): address review feedback on status state surfacing

- Drop redundant LANG=C env (LC_ALL=C is sufficient).
- Replace nested if/else chains in detect_status_state and
  extract_state_hint with const slices + iter for readability.
- Drop the 2-space indent prefix on hint lines — adds nothing for
  machine consumption.

* fix(git): drop state-hint extraction in compact status

KuSh: the in-progress state line ("rebase in progress", "merge in
progress. unresolved conflicts", etc.) is enough — LLMs know which git
commands resolve each state, so the per-state hint list was noise.

Removes STATE_HINTS and extract_state_hint, and reduces extract_state_header
to a short walk that returns the first detected state summary or None.
Existing per-state tests now assert the exact compact summary.

* chore: fix clippy warnings

- **Path Centralization:** Hardcoded directory and file paths across all hook logic () are replaced with dedicated, exported constants in . This prevents magic strings and simplifies maintenance when system directories change.
- **Code Cleanup:** Move all code only used by tests behind cfg(test) attribute
- **Refactoring:** Apply Clippy fixes and address remaining warnings

Signed-off-by: Nicolas Le Cam <niko.lecam@gmail.com>

* build(deps): bump rustls-webpki from 0.103.9 to 0.103.13

Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.9 to 0.103.13.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](rustls/webpki@v/0.103.9...v/0.103.13)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(json): use char boundary when truncating long string values

* fix(json): expand char boundary truncation test

* test(json): assert truncated value byte length for multibyte strings

* feat(cicd): target develop branch

* fix(curl): JSON passthrough + IsTerminal gate to prevent invalid JSON output

`rtk curl` was unconditionally truncating any response >=500 bytes when a
tee hint was available, inserting the literal `... (N bytes total)` marker
mid-stream. For JSON bodies this produces invalid, unparseable output and
silently breaks every downstream `jq`, `python -m json.tool`, or agent
pipeline that consumes `rtk curl`.

Two changes in `filter_curl_output`:

1. JSON heuristic passthrough — if the trimmed body starts/ends with
   matching JSON brackets, return it unchanged. The body is already
   buffered in memory so the check is essentially free.

2. `is_terminal()` gate — only truncate non-JSON output when stdout is
   a TTY. Pipes and shell redirects (`> file`, `| jq`) now receive the
   full body, matching what `curl -o file` already does and closing
   the silent-data-loss path described in rtk-ai#1282.

The tee file is still written for inspection, but the hint line is
suppressed in the passthrough cases so it never leaks into pipes.

Tests: existing 5 cases updated to pass `is_tty=true` (preserve TTY-mode
assertions); 4 new cases cover JSON object/array passthrough at
>500 bytes and pipe-mode (non-TTY) full-body delivery.

Verified end-to-end against the public repro from the issue:
`rtk curl -s https://jsonplaceholder.typicode.com/posts | python3 -c
'import sys,json; print(len(json.load(sys.stdin)))'` now returns 100.

Closes rtk-ai#1536
Refs rtk-ai#1282

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(curl): gate force_tee_hint, extend JSON heuristic, avoid full-body alloc

Follow-up to the initial rtk-ai#1536 fix after a deeper review pass.

- force_tee_hint() is no longer called on the passthrough path. Previously
  every non-TTY pipe invocation (curl | jq, curl | python -m json.tool —
  the exact scenarios this PR fixes) was still writing a tee log file as a
  side effect, which is wasteful: the tee file's purpose is to give the
  LLM a recovery path when output is truncated, and we are no longer
  truncating in those cases.

- looks_like_json now also matches bare top-level JSON strings (e.g.
  /api/token endpoints returning a long quoted token). Previously a
  >500-byte bare JSON string on a TTY would still get truncated mid-stream
  and produce invalid output.

- FilterResult.content is now Cow<'_, str> so the passthrough path no
  longer copies the whole body. For multi-MB curl responses piped through
  rtk this eliminates a full-size allocation.

- Exit code from curl is propagated via Ok(result.exit_code) instead of
  the previously hardcoded Ok(0). On the success path the value is 0
  anyway, but the inconsistency with the early-exit branch above made the
  intent unclear.

Tests: 9 -> 11 (added bare-JSON-string passthrough and a Cow::Borrowed
assertion verifying the passthrough paths don't allocate). cargo test
--all: 1693 passed, 6 ignored.

Verified end-to-end against jsonplaceholder.typicode.com/posts (~27 KB,
100 entries, parseable) and a randomly-generated 100-entry / 52 KB / 1450-line
JSON served from a local http.server (rtk output byte-identical to source
modulo the trailing newline added by println!).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ls): handle device files (block, char, pipe, socket) in ls filter

Character devices (c), block devices (b), pipes (p), and sockets (s)
were silently dropped by compact_ls because only regular files (-),
symlinks (l), and directories (d) were handled. This caused
`rtk ls /dev/ttyACM*` to return "(empty)".

Closes rtk-ai#844

* fix(cicd): match ":" for body prefix to catch

Co-authored-by: Nicolas Le Cam <niko.lecam@gmail.com>

* fix(cicd): match allowed repo list in pr bodies

for next release body auto write

create a regex with pipes of list of allowed repo to be matched

added allowed_repos in cicd env

* feat(gradlew): add Gradle/gradlew support with streaming filters

Adds rtk gradlew command for build, test, lint, and dependency
operations on Gradle projects. Filters task progress noise, preserves
build scan URLs, test failures, lint violations, and compiler warnings.

Recognises ./gradlew, gradlew, gradlew.bat, and gradle invocations.
Surfaces unit-test report paths and shows a progress indicator for
long-running tasks.

Targets 75% savings (90% on test, 80% on build).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(hooks): add Pi coding agent integration

* refactor: handling of uninstallation

* test(cli): add init parse coverage for --pi, --agent pi, and --uninstall --pi

* fix(gradlew): satisfy semgrep dynamic-command-execution rule

Extract a new_gradle_command() helper that uses string literals in
every Command::new() branch. The .semgrep.yml dynamic-command-execution
rule rejects Command::new(var) — semgrep needs to statically audit
the executable set.

Same runtime behaviour: prefer ./gradlew (or gradlew.bat on Windows),
fall back to gradle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(gradlew): align with shared runner pattern

Replaces custom run_streaming / run_batch / run_passthrough / ProgressIndicator
with the shared runner helpers used by cargo and other commands:

- Build → runner::run_streamed with a BuildLineFilter implementing StreamFilter
- Test/Lint/Connected/Deps → runner::run_filtered with the existing filter_*
  closures (filter_test, filter_connected, filter_lint, filter_dependencies)
- Other / verbose flags → runner::run_passthrough(tool, args, verbose)

Benefits inherited from runner.rs: ChildGuard zombie prevention, 10 MiB output
cap, broken pipe handling, proper Result<i32> exit-code propagation.

run() now returns Result<i32> like cargo_cmd / golangci_cmd. main.rs updated
to forward the exit code with `?` directly (no manual `0` wrapper).

All Command::new() invocations remain string literals (semgrep
dynamic-command-execution rule).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(master): release 0.39.0

* fix(gradlew): use resolved_command for system gradle fallback

new_gradle_command() now uses resolved_command("gradle") for the two
fallback branches (Windows + Unix when no local wrapper is present),
matching how cargo / golangci-lint / etc. resolve system binaries.

Local wrappers (./gradlew, gradlew.bat) stay as string literals — they
are relative paths, not on PATH, and semgrep's dynamic-command-execution
rule needs literals here.

Net effect: PATHEXT-aware resolution on Windows (.CMD/.BAT shims work),
no behavioural change on Unix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: rm rtk awareness injection

* chore: docs cleanup

* Update telemetry documentation link to use 'master' branch

* fix(dotnet): move build/test/restore status line to the bottom

The `ok` / `fail` verdict line was emitted at the top of the filtered
output, before errors and warnings. Consumers reading the tail of the
stream — `| tail -N`, IDE log followers, agent watch/monitor loops,
bounded-context-window readers — saw error noise followed by EOF with
no verdict anywhere near the end, silently breaking any agent loop that
gates the next step on build success.

Reorder `format_build_output`, `format_test_output`, and
`format_restore_output` to emit the body first, then the separator
(only when the body is non-empty), then the verdict header last,
matching native `dotnet` which ends with `Build succeeded.` or
`Build FAILED.`.

Add three regression tests using strict `output.lines().last()` plus a
`tail -5` inclusion assertion so future regressions that append a
trailing context line get caught.

Closes rtk-ai#1574

Signed-off-by: Artiom Tofan <arto@queue-it.com>

* fix(dotnet): 🐛 format build/test/restore output sections

- Separate warnings and errors into distinct sections.
- Ensure status line is emitted last for clarity.
- Maintain consistency with native `dotnet` output behavior.

* fix(dotnet): 🐛 format build/test/restore output summaries

- Improve formatting of output summaries for build, test, and restore commands.
- Ensure status lines are emitted last for better stream consumption.

* fix(dotnet): 🐛 format warnings section in build/test/restore outputs

- Refactor warning formatting to improve consistency across output sections.
- Ensure warnings are displayed correctly in all relevant output formats.

* fix: correct ARCHITECTURE.md path in README links

The file lives at docs/contributing/ARCHITECTURE.md, not at the repo
root — both the nav header and Documentation section pointed to the
wrong location.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(init): make --pi route to Pi-only mode and skip CLAUDE.md injection

* chore: update hooks/pi/rtk.ts to reflect new package name

Co-authored-by: Jean du Plessis <jeandp@gmail.com>

* feat(hooks): add transparent_prefixes config for wrapper commands

Command wrappers like shadowenv (https://github.com/Shopify/shadowenv),
`direnv exec .`, `nix develop --command`, `docker exec <container>`,
`poetry run`, and `bundle exec` sit in front of every command in their
respective project layouts. When rtk is driving a Claude Code / Gemini /
Copilot / opencode hook, these wrappers defeat the rewrite — `rtk
rewrite "shadowenv exec -- git status"` returns exit 1 (no rewrite)
because the registry can't see past the wrapper to the inner `git
status`. Every build tool, test runner, linter, and `git` invocation
in such a layout stays uncompressed.

rtk already solves exactly this shape for shell builtins via
SHELL_PREFIX_BUILTINS (`noglob`, `command`, `builtin`, `exec`,
`nocorrect`): strip the prefix, recurse on the inner command, re-prepend
the prefix to the rewrite. This commit extends that pattern with a new
[hooks].transparent_prefixes config field so users can register
multi-word wrappers that don't change what runs, only how.

  [hooks]
  transparent_prefixes = ["shadowenv exec --", "direnv exec ."]

With that config, `rtk rewrite "shadowenv exec -- cargo test"` becomes
`shadowenv exec -- rtk cargo test` and exits 3 (ask, matching the
existing ask semantics for inner commands).

Implementation:

- New `rewrite_command_with_prefixes(cmd, excluded, transparent_prefixes)`
  public entry. The original `rewrite_command(cmd, excluded)` is kept as
  a thin back-compat wrapper that passes an empty prefix slice, so the
  ~150 existing unit-test call sites don't need updating.
- `transparent_prefixes: &[String]` threaded through `rewrite_compound`,
  `rewrite_segment`, `rewrite_segment_inner`.
- In `rewrite_segment_inner`, a second strip loop runs after the
  SHELL_PREFIX_BUILTINS loop, with identical strip-recurse-reprepend
  semantics. Matching is whole-word via the existing `strip_word_prefix`
  helper (multi-word prefixes like `"shadowenv exec --"` are handled
  by-value — byte-level `starts_with` plus a space check).
- Recursion is bounded by the existing MAX_PREFIX_DEPTH so pathological
  or self-referential configs cannot stack-overflow.
- Production callers (`hooks/rewrite_cmd.rs`, `hooks/hook_cmd.rs`,
  `main.rs`) now load both `exclude_commands` and `transparent_prefixes`
  from config in the same `Config::load()` call — no new I/O.

Tests (14 new):

- config: 3 TOML roundtrip tests (present, missing, mixed with older
  fields).
- registry: 11 behavior tests — strip/reprepend, unknown inner returns
  None, unmatched passthrough, composed with shell builtin, multiple
  prefixes configured, whole-word matching, empty-rest None, blank-entry
  skip, compound `&&` with prefix on both segments, excluded-inner
  returns None, recursion bounded.

Design principles (from CONTRIBUTING.md):
- Extensibility: extends an existing, documented pattern.
- Zero Overhead: config load already happens on every hook invocation;
  this reads one additional vec. Empty `transparent_prefixes` is the
  default and adds one cheap loop-over-zero-elements on the hot path.
- Correctness vs Token Savings: without this, hook rewrites silently
  miss every command run through a wrapper — exactly the kind of
  correctness gap the principle warns against.

* fix(hooks): compose env and transparent prefixes

* fix(hooks): address transparent prefix review

* fix(hooks): address transparent prefix review comments

* fix: minor code cleanup, avoid duplicating logic

* fix: new rewite_command test call after rebase

* fix: don't inject -json for go test -bench runs

When -bench is present, benchmark output is already compact and useful.
Injecting -json causes the filter to discard benchmark results since they
don't produce pass/fail events the JSON parser expects.

Fixes rtk-ai#1609

* refactor(warn): src/core/utils.rs has an unused error warning at compilation

* fix(cicd): pr-target clean msg + git app token

inform user how to solve CI pr-target failing check

* fix(git): resolve status completeness conflicts

* fix(hooks): make Cursor preToolUse rewrites work and stay visible

Three related issues prevented RTK from working with the Cursor
preToolUse hook on Windows. Each one alone produced the same
user-visible symptom (`Output: {}` and the original command running
unmodified), so they're fixed together:

1. Cursor preToolUse only enforces allow/deny — `permission: "ask"` is
   accepted by the schema but ignored at runtime, so `updated_input`
   rewrites could silently be dropped. Always return `allow` for
   rewritten commands (deny rules still take precedence and are
   evaluated before this point).

2. Cursor's preToolUse panel renders the JSON returned by the hook.
   Without a `continue: true` field the panel collapses to
   `Output: {}` even when the rewrite ran successfully, which makes
   the hook look broken from the user's perspective. Every other
   Cursor hook (`afterShellExecution`, `beforeSubmitPrompt`, `stop`,
   ...) returns `continue: true`; mirror that shape here so the panel
   surfaces the actual `permission` and `updated_input` payload.

3. Cursor on Windows prepends one or two UTF-8 BOMs (`EF BB BF`,
   sometimes doubled) to the JSON it pipes into the hook process.
   serde_json refuses to parse BOM-prefixed input, so `run_cursor`
   bailed out into the "no command" branch and returned `{}` for
   every invocation. Strip leading BOMs before parsing. This was the
   root cause of the long-standing "RTK silently no-ops in Cursor"
   reports — the rewrite path was never reached.

Tests cover the flat allow output, `continue: true` on both single
and compound (`cd ... && git status`) rewrites, the BOM-strip on
double-BOM payloads matching what Cursor actually sends, and assert
that the legacy hookSpecificOutput envelope is not emitted.

Verified end to end on Windows by tracing real Cursor stdin payloads:
the hook now returns `{"continue":true,"permission":"allow",
"updated_input":{"command":"rtk git status"}}` for `git status` and
the panel renders it instead of `Output: {}`.

* fix: resolve merge conflict artifacts in init.rs

* chore: fix clippy 0.1.95 warnings

* chore(ci): deny warnings and make clippy pass mandatory in ci

* feat(hermes): add rtk integration

Signed-off-by: Kayphoon <109347466+Kayphoon@users.noreply.github.com>

* style(hooks): format BOM helper assertion

* feat(init): remove --pi flag, canonicalize Pi install to --agent pi
chore: sync the codebase after mergew

* fix(security): replace insecure tmp, lock git workflow perm

* fix(security): pin workflow actions to SHA, clean up tempfile on failure

* Update pr-target-check.yml

* fix(cicd): set release-please target-branch to master [skip ci]

* chore(master): release 0.40.0

* fix(docker): forward --tail flag in compose logs

Previously `rtk docker compose logs --tail=20 web` would fall through to
the passthrough handler because clap had no `--tail` field on the `Logs`
subcommand, resulting in 0% token savings. Additionally, `run_compose_logs`
always fetched 100 lines regardless of user input.

- Add `#[arg(long, default_value_t = 100)] tail: u32` to `ComposeCommands::Logs`
- Thread the value through to the docker invocation
- Default unchanged (100 lines), so bare `rtk docker compose logs` behaves as before

Ports the core fix from rtk-ai#580, retargeted onto current develop layout
(`src/cmds/cloud/container.rs`). Relates to rtk-ai#578.

Co-authored-by: Mihir Dash <137862945+slice-mihird@users.noreply.github.com>

* fix(filters): address adversarial test-suite findings on aggressive filtering

Several filters dropped or rewrote information an agent needs, producing
output that was misleading rather than merely compressed. This fixes the
clear-cut cases surfaced by the TheDecipherist/rtk-test suite:

- ls: stop hiding the `.env` file (removed `.env`/`env` from NOISE_DIRS).
- git status: restore the explicit "HEAD detached at <sha>" line instead of
  the opaque "* HEAD (no branch)".
- log/docker logs: recognise CRITICAL/FATAL/ALERT/EMERGENCY/SEVERE (and
  NOTICE) as severities so those lines are no longer silently filtered;
  expose `log` as a `rtk pipe` filter and accept a positional filter name.
- wc: forward stdin to the child so `cat file | rtk wc` counts the pipe
  instead of reporting 0 (new RunOptions::inherit_stdin).
- jest/vitest: include skipped/pending test count in the compact summary.
- pytest: surface xfailed/xpassed counts and list XFAIL/XPASS entries with
  their reasons (adds `-rxX`).
- ruff/eslint: list individual violations with file:line:col, not just
  rule/file group counts.
- pnpm/npm list: render the actual package list instead of the false-positive
  "All packages up-to-date" when there's no upgrade info.
- git add: stay silent on a no-op instead of printing an ambiguous "ok".
- git stash: pass git's own message through instead of collapsing to "ok".
- docker ps: keep the Status column (health/restart info) and list
  stopped/exited containers; docker images: show all images with full names;
  docker compose ps: use `-a` so crashed services stay visible.

All 1870 unit tests pass; cargo fmt + clippy clean.

* chore(filters): remove filter-level annotations and restore compose logs tail arg

* fix(filters): add test for aggressive filter batch fix

* fix(git): stream push output via FilterMode::Streaming (rtk-ai#963)

## Problem (rtk-ai#963)

`rtk git push` reportedly times out: users see
`bash tool terminated command after exceeding timeout 30000 ms` while
plain `git push` to the same remote completes fine. P1-critical because
every Claude Code git push goes through rtk.

## Root cause

`run_push` used `cmd.stdin(Stdio::inherit()).output()`. `Command::output()`
captures both stdout and stderr until the child exits. Git push prints
its progress (`Counting objects` / `Compressing objects` / `Writing
objects`) to stderr and may prompt for SSH passphrases or HTTPS
credentials. With stderr captured, Claude Code's bash tool saw zero
output for 30+ seconds and killed the command — exactly the 30000 ms
message in the issue.

## Fix

Rewrite `run_push` on top of the streaming infrastructure that already
exists for this exact purpose (`stream::run_streaming` +
`FilterMode::Streaming`, added in 0.37.0).

Add a counterpart to `BlockStreamFilter<H: BlockHandler>` in
`src/core/stream.rs`: `LineStreamFilter<H: LineHandler>`. Where
`BlockStreamFilter` defaults to DROP and emits only collected blocks,
`LineStreamFilter` defaults to KEEP and lets handlers opt into dropping
noise. Trait surface mirrors `BlockHandler`:

- `should_skip(&mut self, line: &str) -> bool` — default false
- `observe_line(&mut self, line: &str)` — default no-op
- `format_summary(&self, exit_code, raw) -> Option<String>`

This lets future streaming commands reuse the line-oriented pattern.

`GitPushLineHandler` then becomes a tiny `LineHandler` impl:

- `should_skip` drops the high-volume progress phases (Enumerating /
  Counting / Compressing / Writing objects, Delta compression, Total)
  and blank lines.
- `observe_line` captures the up-to-date sentinel and the first ref
  update target (e.g. `master`) for the summary.
- `format_summary` emits `ok <ref>` / `ok (up-to-date)` / `ok` on
  success; nothing on failure (raw error lines already flowed through).

Stdin is inherited (`StdinMode::Inherit`) so SSH passphrase and HTTPS
credential prompts still reach the user. Tracking now records the real
raw output and the filtered output.

## Test plan

- [x] `cargo fmt --all -- --check`
- [x] `cargo clippy --all-targets -- -D warnings` — clean
- [x] `cargo test --all` — 1880 passed, 0 failed, 6 ignored
- Six unit tests cover the push handler: progress-prefix drop,
  up-to-date summary, remote message passthrough, no-summary-on-failure,
  first-ref-wins, and token-savings (>=60% on a representative payload).
- Four unit tests cover the new `LineStreamFilter` trait:
  default-keep-all, skip-drops-matching, summary-propagates-exit-code,
  observe-only-called-for-kept-lines.

## Notes

- Behaviour change: users now see git's native output line-by-line
  (with progress phases stripped) plus a final `ok <ref>` summary,
  instead of just the compact summary. This matches plain `git push`
  more closely and is what the issue reporter expects.
- No regression for other filters: `run_pull`, `run_fetch`, `run_clone`
  are untouched; only `run_push` is modified.

Closes rtk-ai#963

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---
_Vibe Coded by Ousama Ben Younes_
_Developed With Ora Studio (Claude Code)_

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(hints): add tail hints for tee & hints + address reviews

- fix signal truncation gaps + kush reviews
- added a new tee and hints function to give an hint with tail (avoid re-ingest of the head)
- extracted patterns in constants

* fix: re-add env python as noisy dir

* fix: '...' ascii to unicode, remove some comments

* fix(kubectl): compact get pods and services aliases

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(filters): split docker ps/-a paths, cap ruff violations at 50

- docker ps reverts to plain (no -a augmentation)
- docker ps -a / compose ps -a wired to the running/stopped split filter
- ruff Violations: capped at 50 + force_tee_tail_hint recovery
- tests for the ruff and pytest xfail caps

* fix(init): honor dry-run for Pi install and uninstall paths

* fix(tee): safe truncation caps and compose-ps tee content fix

Replace all bare magic offset literals (e.g. 21 for cap=20) with named
constants (MAX_XXX + 1) so offsets stay in sync if caps change.
Fix compose_ps to tee pre-formatted lines instead of raw tab-separated
input, so tail -n +N gives the agent readable content.
All 13 affected modules updated: container, dotnet, gh, glab, go, lint,
pnpm, pytest, rubocop, cargo, aws. 1884 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(rust): multi-line blocks used with tail hint

* docs(cmds): truncations hints and recovery guidelines

* test(hooks/init): add red test for copilot-instructions preservation

Adds a TDD red test that asserts pre-existing user content in
.github/copilot-instructions.md must survive 'rtk init --copilot'.
Currently fails because run_copilot() uses write_if_changed() which
truncates the file.

Refs rtk-ai#1964

* fix(hooks/init): preserve user content in copilot-instructions.md

run_copilot() previously called write_if_changed() which truncated any
pre-existing .github/copilot-instructions.md, destroying user-authored
Copilot rules.

This routes copilot-instructions.md through upsert_rtk_block() (the
same idempotent marker-block helper already used for CLAUDE.md and
AGENTS.md). The COPILOT_INSTRUCTIONS constant is wrapped in
<!-- rtk-instructions v2 --> ... <!-- /rtk-instructions --> markers
so subsequent inits replace only RTK-owned content; the rest of the
file is preserved verbatim. Malformed pre-existing blocks (opening
marker without closing) are refused with a remediation message
rather than silently rewritten.

Fixes rtk-ai#1964, rtk-ai#1891

* test(hooks/init): cover idempotency, stale-block, dry-run, fresh, malformed paths

Adds five additional tests around copilot-instructions.md upsert:
- idempotent re-init (no duplicated blocks, identical content)
- stale RTK block in-place update with surrounding user content preserved
- dry-run never creates or modifies the file
- fresh install creates file with RTK marker block when no file exists
- malformed pre-existing block (opening marker without closing) is
  refused with a clear error rather than silently rewritten

Refs rtk-ai#1964

* refactor(hooks/init): extract run_copilot_at for parallel-safe tests

run_copilot(ctx) is now a thin wrapper around run_copilot_at(base, ctx);
tests pass a TempDir path so they no longer mutate process-global cwd.
This eliminates flakes when cargo test runs in parallel (the default).

The public API is unchanged; only the internal seam moves.

Refs rtk-ai#1964

* docs(readme): fix license references to match LICENSE (Apache-2.0)

The README badge and footer claimed MIT, but the LICENSE file is
Apache License 2.0. Updates both references so the README matches
the authoritative LICENSE file.

Closes rtk-ai#1996

Co-Authored-By: Claude <noreply@anthropic.com>

* refacto(truncations): Set global CAPS for truncation

Following tee and hint refacto
Add global CAP constant to be inherited , to enable easier global configuration later

* refactor(hooks/init): share write_rtk_block, unify malformed handling

Address review feedback on rtk-ai#1976:

- Extract write_rtk_block() shared dispatcher: eliminates the duplicated
  4-arm RtkBlockUpsert match between run_claude_md_mode and the former
  upsert_copilot_instructions (now inlined). Both callers stay under the
  ~60-line guideline.
- Unify malformed handling: both paths now bail!() with a diagnostic and
  the exact recovery command. CLAUDE.md previously warned and exited 0,
  silently skipping the OpenCode plugin step; behaviour is now consistent.
- Reorder run_copilot_at: upsert copilot-instructions.md BEFORE writing
  the hook config so a malformed file aborts the install without leaving
  a stale .github/hooks/rtk-rewrite.json on disk.

Regression coverage:
- test_claude_md_mode_refuses_malformed_block mirrors the existing
  copilot malformed test against the shared dispatcher contract.
- test_copilot_init_malformed_leaves_no_hook_on_disk pins the new write
  order so a future re-order regression is caught.

cargo fmt / clippy --all-targets / test --bin rtk: clean (1909 passed).

* fix(truncate): global caps reduce (avoid underflow and 0 results)

- Avoid underflow + 0 results caused by overwrite of global constant CAPS
- Soon user will be able to apply global configuration, this will change global CAPS and should not cause any underflow or 0 results, if overwrite sub >= CAPS -> use CAPS.

* fix: honor explicit -n N limit for git log on merge commits

When user runs 'git log -1 --format='%H' HEAD' where HEAD is a merge
commit, rtk was adding --no-merges which filtered out the merge commit
itself and returned the second parent instead. This made 'git log -1'
return wrong SHAs for merge commits.

Fix: don't add --no-merges when user explicitly passes -n N or
--max-count=N. When a user specifies an exact count they expect exactly
that many commits, not filtered results. Also skip --no-merges if user
already passed --merges or --no-merges explicitly.

Fixes rtk-ai#2009.

* chore: regroup agent init tests by agent section

* fix(git): drop -uall from compact status so output never exceeds raw

The compact `git status` path ran `git status --porcelain -b -uall`. The
`-uall` flag expands fully-untracked directories into every file, while raw
`git status` collapses them (e.g. `node_modules/`). This made rtk output
larger than raw — measured ~29x on a 200-file untracked dir (5500B vs 191B) —
violating RTK's compress-or-match-raw invariant and inflating tokens.

Remove `-uall` so untracked directories collapse exactly like raw. This keeps
rtk-ai#991's actual fix intact: all modified/staged/renamed/conflict paths are still
shown with no grouped summaries or `... +N more` overflow markers (`-uall`
never affected those lines). Untracked files in partially-tracked dirs and any
paths git itself expands are still preserved by the formatter.

Measured (rtk vs raw git status):
- node_modules/ (200 files): 5500B (-2780%) -> 25B (+87% savings)
- normal (8 mod + 2 untracked): +71% -> +76%
- 17 modified (rtk-ai#991 case): unchanged at +58%, all 17 shown, 0 overflow markers

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Update git.rs

* docs(git): sync status README with --porcelain -b (drop -uall)

* chore(master): release 0.41.0

* feat: address pr suggestions

* chore: sync with pi api

* chore: minor cleanup

* chore(master): release 0.42.0

* fix(clippy): factor mvn version-cache triple into type alias

Pre-existing clippy::type_complexity on the probe_mvn_version CACHE static
(deny-warnings in CI's `cargo clippy --all-targets` gate). Extract the
(u32,u32,u32) into a VersionTriple alias per clippy's own suggestion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Adrien Eppling <adrien.eppling@supinfo.com>
Signed-off-by: aesoft <43991222+aeppling@users.noreply.github.com>
Signed-off-by: Nicolas Le Cam <niko.lecam@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Artiom Tofan <arto@queue-it.com>
Signed-off-by: Kayphoon <109347466+Kayphoon@users.noreply.github.com>
Co-authored-by: aesoft <43991222+aeppling@users.noreply.github.com>
Co-authored-by: Nicolas Le Cam <niko.lecam@gmail.com>
Co-authored-by: Lumin Cui <vincenthcui@qq.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ousama Ben Younes <benyounes.ousama@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: hed0rah <18272116+hed0rah@users.noreply.github.com>
Co-authored-by: Joshua <joshua.french@hey.com>
Co-authored-by: rtk-release-bot[bot] <280461666+rtk-release-bot[bot]@users.noreply.github.com>
Co-authored-by: Victor Sumner <victor.sumner@shopify.com>
Co-authored-by: Victor Sumner <vsumner@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: swithek <52840391+swithek@users.noreply.github.com>
Co-authored-by: patrick <patrick@rtk-ai.app>
Co-authored-by: Maxence Bombeeck <maxence.bombeeck@supinfo.com>
Co-authored-by: Adrien Eppling <adrien.eppling@supinfo.com>
Co-authored-by: Kevin <kevin.herembourg@gmail.com>
Co-authored-by: gitbluf <gh+git@gitbluf.xyz>
Co-authored-by: gitbluf <git+gh@gitbluf.xyz>
Co-authored-by: SADIK KUZU <sadikkuzu@hotmail.com>
Co-authored-by: Artiom Tofan <arto@queue-it.com>
Co-authored-by: Sam Severance <swseverance@gmail.com>
Co-authored-by: Marko Petrovic <22802784+gitbluf@users.noreply.github.com>
Co-authored-by: Jean du Plessis <jeandp@gmail.com>
Co-authored-by: Yaniv Michael Kaul <yaniv.kaul@scylladb.com>
Co-authored-by: xdm67x <m-ozkan@outlook.fr>
Co-authored-by: em0t <10153971+em0t@users.noreply.github.com>
Co-authored-by: kamilkaczmareksolutions <kamil.kaczmarek.us@gmail.com>
Co-authored-by: patrick szymkowiak <52030887+pszymkowiak@users.noreply.github.com>
Co-authored-by: Kayphoon <109347466+Kayphoon@users.noreply.github.com>
Co-authored-by: Mihir Dash <137862945+slice-mihird@users.noreply.github.com>
Co-authored-by: pagarsky <opaharskyi@chartbeat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: 李冠辰 <liguanchen@xiaomi.com>
Co-authored-by: Tylere Zimmerman <tylere.zimmerman@delinea.com>
Co-authored-by: okwn <root@okwn.cc>
Co-authored-by: tron-coord <coord@murphytek.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants