Skip to content

feat(tools): add selectable edit formats - #820

Merged
matthewyjiang merged 10 commits into
mainfrom
feat/selectable-edit-tools
Aug 8, 2026
Merged

feat(tools): add selectable edit formats#820
matthewyjiang merged 10 commits into
mainfrom
feat/selectable-edit-tools

Conversation

@matthewyjiang

@matthewyjiang matthewyjiang commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

Rho previously exposed only the hash-line edit tool. This PR lets users select edit, apply_patch, or edit_file with [behavior].edit_tool; Rho exposes only the selected schema after restart.

  • Add production apply_patch and exact-string edit_file implementations with shared mutation output.
  • Use one canonical edit-format model across config, SDK registration, presentation, hooks, and the picker.
  • Split patch planning, content matching, transactions, and SDK edit adapters into focused modules.
  • Route the preference through config loading, /config, prompts, diagnostics, hooks, policies, rewind metadata, and tool presentation.
  • Reject unsafe paths before authorization and protect edits from stale files, symlink substitution, clobbers, and concurrent rollback damage.
  • Preserve line endings and file permissions, and use platform no-replace installation for file creation and moves.
  • Document each edit format and the restart requirement.

Validation

python3 scripts/validate.py full
cargo test -p rho-agent-tools apply_patch::tests -j 12
cargo check -p rho-agent-tools --all-features --locked --target x86_64-pc-windows-gnu -j 12
cargo run -p rho-tui-pty --bin rho-pty-scenario -- --bin target/debug/rho --artifacts /tmp/rho-edit-pr-pty-artifacts open_config_picker
cargo test -p rho-agent-tools --all-features --locked -j 12
python3 scripts/check_architecture.py
cargo fmt --all
git diff --check
  • Focused apply_patch transaction suite: 18 passed
  • rho-coding-agent --lib: 1607 passed, 2 ignored
  • PTY open_config_picker: passed
  • Windows GNU cross-check: passed

Docs TUI proof plate

  • Ran bash scripts/check_docs_ui_demo.sh --check (through python3 scripts/validate.py full)

Test gate

  • Followed rho-test-selection (failure mode, owner layer, gap).
  • Each new test names a distinct failure mode (user-visible or contract bug).
  • Each new test has one owner layer (pure unit / SDK contract / PTY / OS).
  • No existing test already covers that failure mode at a better layer.
  • Interactive TUI behavior uses a PTY scenario by default; new crates/rho/src/tui unit tests cover pure formatting and mapping logic.
  • Cases share one test function per rule (tables), not twin functions per literal.
  • Asserts use structured values; string .contains checks cover model-facing tool protocol output rather than UI copy.
  • No locks on help text, statusline chrome, labels, or other copy.
  • No wall-clock sleep used for synchronization; no known-flaky timing races.
  • Nearby weaker or duplicate tests were removed or merged when practical.

New tests

Failure mode Owner layer Why existing coverage is not enough
Malformed patch documents or partial hunk projection produce an invalid plan Pure unit The patch parser and streamed projection did not exist.
Adds, moves, deletes, or rollback clobber another writer, follow a symlink, change line endings, or lose Unix mode OS Existing hash-line edit tests did not own the new mutation paths.
Failed replacement, restoration, staging, or hard-link cleanup leaves untracked files or parent directories OS Existing rollback tests did not inject failures after filesystem mutation began.
Concurrent content prevents removal of a directory created by the transaction OS Existing concurrency coverage checked files, not owned directory effects.
SDK preparation accepts an unsafe path or exposes more than one edit schema SDK contract The SDK previously registered only the hash-line edit tool.
Config loading or saving loses the selected edit format Pure unit The behavior key and enum did not exist.
A user cannot select the edit format from /config or see the restart requirement PTY The existing picker scenario did not cover the new Tools submenu.
Alternate edit tools render the wrong paths, progress, or result metadata Pure unit Presenter mapping handled only the prior edit tool.

PTY exception (if any)

N/A

Next-major debt

  • Followed rho-next-major-debt (ideal shape considered; compromise intentional).
  • Each compromise has a greppable NEXT_MAJOR(<surface>): <cleanup> marker on the API.
  • Preferred end state is named in the marker; helpers cover every arm until major.
  • Host-facing docs updated when external callers must match the awkward shape.

Markers added

Marker Preferred end state
NEXT_MAJOR(rho-tools): remove the EditToolKind alias and use EditFormat directly Remove the 1.x compatibility alias and expose only EditFormat.

Summary by CodeRabbit

  • New Features

    • Added selectable editing modes: Hashline, Apply Patch, and String Replace.
    • Added patch-based editing for creating, updating, deleting, and moving files.
    • Added exact text replacement with single-match or replace-all options.
    • Added previews with affected files, line changes, diffs, and truncation indicators.
    • Added TUI and configuration controls; changes apply after restart.
  • Bug Fixes

    • Improved protection against unsafe paths, symlinks, conflicts, and concurrent modifications.
  • Documentation

    • Updated tool, configuration, search, and editing guidance.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@matthewyjiang, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 31 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 66de9ae8-6868-4f6d-89b6-2542afd59646

📥 Commits

Reviewing files that changed from the base of the PR and between e80a2b4 and 1b2044c.

📒 Files selected for processing (20)
  • crates/rho-tools/src/apply_patch/apply.rs
  • crates/rho-tools/src/apply_patch/planning.rs
  • crates/rho-tools/src/apply_patch/transaction.rs
  • crates/rho-tools/src/edit_format.rs
  • crates/rho-tools/src/file_mutation.rs
  • crates/rho-tools/src/file_mutation/install.rs
  • crates/rho-tools/src/lib.rs
  • crates/rho-tools/src/sdk_adapter/edit.rs
  • crates/rho-tools/src/sdk_adapter/mutation.rs
  • crates/rho-tools/src/sdk_adapter/registry.rs
  • crates/rho-tools/src/str_replace/content.rs
  • crates/rho-tools/src/str_replace/mod.rs
  • crates/rho-tools/src/str_replace_tests.rs
  • crates/rho/src/agent/agent_tests.rs
  • crates/rho/src/agent/definition.rs
  • crates/rho/src/app/interactive_presenter_apply_patch.rs
  • crates/rho/src/app/interactive_presenter_format.rs
  • crates/rho/src/app/interactive_presenter_tests.rs
  • crates/rho/src/tools/sdk_registry_tests.rs
  • scripts/crate_publish_prep.py
📝 Walkthrough

Walkthrough

The PR adds configurable hashline, apply_patch, and str_replace edit tools. It adds transactional patching, shared mutation safety, SDK wiring, configuration support, format-aware previews, tests, documentation, and publish-preparation updates.

Changes

Configurable edit-tool platform

Layer / File(s) Summary
Edit-tool configuration and registration
crates/rho-tools/src/edit_format.rs, crates/rho-tools/src/sdk_adapter/*, crates/rho/src/config*, crates/rho/src/tools/*, crates/rho/src/tui/*
Adds canonical edit-format handling. The selected format is serialized, loaded, shown in the TUI, passed to SDK registration, and used to register one model-facing edit tool.
Shared mutation and string replacement
crates/rho-tools/src/file_mutation.rs, crates/rho-tools/src/edit_file.rs, crates/rho-tools/src/hashline/execute.rs, crates/rho-tools/src/edit_file_tests.rs
Adds locked rewrites, path identity checks, newline preservation, atomic creation, exact string replacement, rollback-aware failures, and tests.
Patch parsing and transactional application
crates/rho-tools/src/apply_patch/*, crates/rho-tools/src/apply_patch_tests.rs
Adds patch parsing, context matching, proposed diff projection, path planning, add/delete/update/move operations, transactional commits, rollback, and filesystem validation.
SDK adapter execution and validation
crates/rho-tools/src/sdk_adapter/*, crates/rho-tools/src/sdk_adapter_tests.rs, crates/rho-tools/src/sdk_support.rs
Adds preparation and execution for all edit formats. The adapters manage authorization, path validation, progress, mutation observation, output metadata, and invalid-argument mapping.
Edit presentation and usage guidance
crates/rho/src/app/interactive_presenter*, crates/rho/src/prompt.rs, docs/*, crates/rho-tools/README.md
Adds format-aware classification, structured patch previews, path extraction, prompt guidance, tool descriptions, configuration guidance, and edit-format documentation.

Publish preparation

Layer / File(s) Summary
Transitive dependency path patches
scripts/crate_publish_prep.py, scripts/tests/test_crate_publish_prep.py, docs/development.md
The publish-preparation policy now selects the full internal dependency closure when a direct internal dependency is unpublished. Tests cover unpublished and fully published dependency graphs.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Config
  participant SDKRegistry
  participant SelectedEditTool
  participant Workspace
  participant Presenter
  Config->>SDKRegistry: provide selected edit format
  SDKRegistry->>SelectedEditTool: register one edit adapter
  SelectedEditTool->>Workspace: validate and mutate files
  Workspace-->>SelectedEditTool: return diffs and snapshots
  SelectedEditTool-->>Presenter: emit mutation metadata
  Presenter->>Presenter: render format-specific edit card
Loading

Poem

A rabbit checks each patch in line,
Hashline, patch, and strings align.
Locks protect the files they touch,
Diffs and snapshots show just enough.
“Hop!” says Bun, “the tools now rhyme!”

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: adding selectable edit formats.
Description check ✅ Passed The description covers the required summary, validation, TUI proof, test gate, and next-major debt sections with relevant details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 8

🧹 Nitpick comments (9)
crates/rho-tools/src/apply_patch_tests.rs (1)

274-331: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for out-of-order and overlapping chunks.

compute_replacements raises "patch chunks overlap or apply out of order" from three separate guards. No test reaches that error. A model that emits chunks in the wrong order hits this path often, so the message and the fail-closed behavior are user-visible. Add one case with two update chunks whose contexts appear in reverse file order, and assert both the error message and that the file is unchanged.

As per coding guidelines: "Prefer behavior or integration tests for user-visible logic and unit tests for focused pure logic."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/apply_patch_tests.rs` around lines 274 - 331, Add an
integration-style test near the existing apply_hunks tests using a single file
with two update chunks whose contexts are supplied in reverse file order. Assert
that apply_hunks returns the exact “patch chunks overlap or apply out of order”
error and verify the target file retains its original contents, covering
fail-closed behavior for compute_replacements.

Source: Coding guidelines

crates/rho-tools/src/apply_patch/seek_sequence_tests.rs (1)

11-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the no-match case and the Unicode fallback.

The test covers the trailing-whitespace fallback and the EOF anchor. Two branches stay untested: the None result when no window matches, and the normalise fallback that maps smart quotes and Unicode dashes. The normalise branch decides whether a model's typographic drift silently rewrites a file, so it deserves a case. The table style already used in edit_file_tests.rs fits here.

As per coding guidelines: "Use the rho-test-selection skill when adding, expanding, reviewing, or deleting tests, including its failure-mode, ownership, tier, determinism, table-driven, and assertion guidance."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/apply_patch/seek_sequence_tests.rs` around lines 11 -
34, The seek_sequence tests need coverage for both unmatched input and Unicode
normalization fallback. Expand matches_normalized_context_and_eof_position or
add a table-driven test using the existing lines and seek_sequence helpers,
asserting None when no window matches and asserting the expected position when
smart quotes or Unicode dashes normalize to the source text; follow the table
style established by edit_file_tests.rs.

Source: Coding guidelines

crates/rho-tools/src/hashline/execute.rs (1)

200-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Label the two adjacent content arguments at every rewrite_locked_file call site. rewrite_locked_file(file, display_path, original, updated) takes two adjacent &str content parameters. A swap compiles and silently reverts the file instead of updating it. The coding guidelines require an inline parameter-name comment for this case.

  • crates/rho-tools/src/hashline/execute.rs#L200-L205: annotate &file.original as /*original*/ and &file.outcome.text as /*updated*/.
  • crates/rho-tools/src/edit_file.rs#L145-L145: annotate &original as /*original*/ and &updated as /*updated*/.
  • crates/rho-tools/src/apply_patch/apply.rs#L668-L668: annotate &live as /*original*/ and &updated as /*updated*/.

As per coding guidelines: "comment unavoidable opaque positional arguments with the exact parameter name."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/hashline/execute.rs` around lines 200 - 205, Annotate
the adjacent content arguments at every rewrite_locked_file call site with exact
parameter-name comments: in crates/rho-tools/src/hashline/execute.rs:200-205,
mark &file.original as /*original*/ and &file.outcome.text as /*updated*/; in
crates/rho-tools/src/edit_file.rs:145-145, mark &original as /*original*/ and
&updated as /*updated*/; and in
crates/rho-tools/src/apply_patch/apply.rs:668-668, mark &live as /*original*/
and &updated as /*updated*/.

Source: Coding guidelines

crates/rho-tools/src/edit_file_tests.rs (1)

83-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for replace_all.

The tests cover the single-match, ambiguous-match, and CRLF paths. No test exercises replace_all = true. That branch changes both validate_match_count and the multi-span rewrite in replace_spans, and multi-span offset mapping is the most intricate logic in this file. Add one case that replaces two occurrences and asserts the resulting file content and the reported occurrence count.

As per coding guidelines: "Prefer behavior or integration tests for user-visible logic and unit tests for focused pure logic."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/edit_file_tests.rs` around lines 83 - 129, Add an
integration-style async test alongside rejects_ambiguous_match_without_mutation
that calls edit_file_content with replace_all set to true, replaces two
occurrences in the file, and verifies both the final file content and the
reported occurrence count in the result. Exercise the multi-span rewrite through
the public edit_file_content path rather than testing validate_match_count or
replace_spans directly.

Source: Coding guidelines

crates/rho-tools/src/apply_patch/apply.rs (1)

1-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Split apply.rs into focused modules.

The file is 930 lines and holds six separable concerns: the FileChange plan model, hunk planning and reads, path validation, content derivation and chunk matching, commit and platform identity checks, and rollback. The coding guidelines require extraction here.

Suggested split: plan.rs for plan_hunk, check_path_conflicts, and the read helpers; paths.rs for validate_patch_path and reject_symlink_entry; content.rs for derive_new_contents, compute_replacements, and apply_replacements; commit.rs for commit_changes, rewrite_current, remove_current, and the identity checks; rollback.rs for rollback_applied, rollback_one, and rollback_move.

As per coding guidelines: "In Rust, avoid large files and extract separable behavior into focused modules while keeping tests and invariant documentation close to the implementation."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/apply_patch/apply.rs` around lines 1 - 30, Split the
monolithic apply implementation into focused sibling modules while preserving
behavior and keeping related tests and invariant documentation with their
implementations: move the FileChange plan model, plan_hunk,
check_path_conflicts, and read helpers to plan.rs; validate_patch_path and
reject_symlink_entry to paths.rs; derive_new_contents, compute_replacements, and
apply_replacements to content.rs; commit_changes, rewrite_current,
remove_current, and platform identity checks to commit.rs; and rollback_applied,
rollback_one, and rollback_move to rollback.rs. Update apply.rs and module
imports/re-exports so the existing patch application flow remains unchanged.

Source: Coding guidelines

crates/rho-tools/src/file_mutation.rs (1)

89-100: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

File::flush does not persist data to disk.

The Write implementation for std::fs::File has a no-op flush. The current code reports success before the OS writes the data. If durability after a tool edit matters, call sync_data instead.

♻️ Proposed change
     file.write_all(contents.as_bytes())
         .map_err(|error| ToolError::Message(format!("could not write {display_path}: {error}")))?;
-    file.flush()
+    file.sync_data()
         .map_err(|error| ToolError::Message(format!("could not write {display_path}: {error}")))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/file_mutation.rs` around lines 89 - 100, Update the
final persistence step in rewrite to call File::sync_data instead of
File::flush, while preserving the existing error mapping and display_path
context for failures.
crates/rho-tools/src/sdk_support.rs (1)

123-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve non-validation error kinds.

map_invalid_app_error maps every AppToolError variant to InvalidArguments. If a caller ever passes AppToolError::Cancelled or AppToolError::Io, the error kind becomes wrong and cancellation stops being reported as cancellation. map_invalid_edit_args in crates/rho-tools/src/sdk_adapter.rs at Line 922 already handles this by matching Message and delegating other variants to map_app_error. Use the same shape here, and consider replacing the duplicate helper in sdk_adapter.rs with this one.

♻️ Proposed change
 pub(crate) fn map_invalid_app_error(error: AppToolError) -> ToolError {
-    ToolError::new(ToolErrorKind::InvalidArguments, error.to_string())
+    match error {
+        AppToolError::Message(message) => ToolError::new(ToolErrorKind::InvalidArguments, message),
+        other => map_app_error(other),
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/sdk_support.rs` around lines 123 - 125, Update
map_invalid_app_error to match AppToolError variants, preserving
InvalidArguments only for validation/message errors and delegating
non-validation variants such as Cancelled and Io to map_app_error. Align the
behavior with map_invalid_edit_args in sdk_adapter.rs, and reuse this helper
there if that removes the duplicate mapping logic.
crates/rho-tools/src/sdk_adapter.rs (1)

581-587: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the boolean argument at the call site.

path_set.collect takes two positional booleans. Line 585 annotates require_existing, and Line 593 annotates both. Line 586 passes mutates_source_entry, which does not match the parameter name reject_symlink_leaf. Add the parameter-name comment so all call sites read the same way.

♻️ Proposed change
-                    mutates_source_entry,
+                    /*reject_symlink_leaf*/ mutates_source_entry,

As per coding guidelines: "comment unavoidable opaque positional arguments with the exact parameter name."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/sdk_adapter.rs` around lines 581 - 587, Add the exact
parameter-name comment `reject_symlink_leaf` before the `mutates_source_entry`
argument in the `path_set.collect` call within the surrounding hunk-processing
function, matching the annotations used by the other call sites.

Source: Coding guidelines

crates/rho/src/tools/sdk_registry_tests.rs (1)

73-95: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the selected edit schema for each configuration.

The aggregated name list passes if every configuration exposes all three edit tools. This misses the requirement that Rho exposes only the selected edit schema.

Keep names separate per EditTool. Assert that each set contains its selected tool and excludes the other two edit-tool names.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho/src/tools/sdk_registry_tests.rs` around lines 73 - 95, Update the
test loop around AppToolSet::unfiltered_names so names remain separate for each
EditTool configuration instead of being aggregated. For every configuration,
assert the selected edit tool name is present and the other two edit-tool names
are absent, while preserving the existing advisor registration setup.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/rho-tools/src/apply_patch/apply.rs`:
- Around line 786-788: Distinguish restore-failure errors in the rewrite flow:
have rewrite_locked_file return a marker when restoring the truncated file
fails, propagate it through rewrite_current, and map that marker to
ApplyFailure::after_mutation at the rewrite_current call site. Keep ordinary
pre-write failures mapped to ApplyFailure::before_mutation so commit_changes and
rollback_applied retain their existing behavior.
- Around line 444-456: Update derive_new_contents so line-ending conversion is
limited to lines affected by apply_replacements, preserving the original endings
of untouched lines in mixed-ending files. Avoid applying replace('\n',
line_ending) to the complete joined output, while retaining the existing
trailing-newline behavior.

In `@crates/rho-tools/src/apply_patch/parser.rs`:
- Around line 334-339: Update the empty-line check in the patch parser to test
the untrimmed line content, so only genuinely empty lines enter this branch.
Allow whitespace-only lines to fall through to the existing strip_prefix(' ')
context handling, preserving their indentation in old_lines and new_lines.

In `@crates/rho-tools/src/edit_file.rs`:
- Around line 126-145: Update edit_file_content_locked to perform the existing
symlink/identity validation used by apply_patch immediately after
lock_for_rewrite and before reading or modifying the file. Reject the operation
unless the locked path is the live regular file addressed by the request,
preventing symlink targets outside the request from being edited.

In `@crates/rho-tools/src/sdk_adapter.rs`:
- Around line 639-645: Update the request-level cache handling in collect so
cached entries retain whether they were validated with reject_symlink_leaf and
require_existing. When a later call requires stricter validation, re-run
reject_symlink_entry and the require_existing resolution before returning;
preserve the fast path only when the cached validation satisfies the current
request.
- Around line 518-535: Update EditFileTool::prepare to resolve args.path with
workspace.resolve_for_write instead of resolve_for_read, preserving the existing
path-error mapping and write/read capability requests so the target matches
execute_prepared_string_edit mutation behavior.

In `@crates/rho/src/prompt.rs`:
- Around line 94-107: Update the prompt-building logic around the
tools.iter().any check to emit edit guidance when any of “edit”, “apply_patch”,
or “edit_file” is selected. Keep the existing grep, hashline TAG, locator, and
PUT-specific guidance exclusively under the “edit” branch, and add separate
generic guidance cases for “apply_patch” and “edit_file” that direct their use
instead of shell or Python rewrites.

In `@docs/configuration.md`:
- Line 37: Update the settings timing description near the `edit_tool` row to
say it applies on the next Rho startup instead of the next session, keeping the
existing timing for `enable_subagents` unchanged.

---

Nitpick comments:
In `@crates/rho-tools/src/apply_patch_tests.rs`:
- Around line 274-331: Add an integration-style test near the existing
apply_hunks tests using a single file with two update chunks whose contexts are
supplied in reverse file order. Assert that apply_hunks returns the exact “patch
chunks overlap or apply out of order” error and verify the target file retains
its original contents, covering fail-closed behavior for compute_replacements.

In `@crates/rho-tools/src/apply_patch/apply.rs`:
- Around line 1-30: Split the monolithic apply implementation into focused
sibling modules while preserving behavior and keeping related tests and
invariant documentation with their implementations: move the FileChange plan
model, plan_hunk, check_path_conflicts, and read helpers to plan.rs;
validate_patch_path and reject_symlink_entry to paths.rs; derive_new_contents,
compute_replacements, and apply_replacements to content.rs; commit_changes,
rewrite_current, remove_current, and platform identity checks to commit.rs; and
rollback_applied, rollback_one, and rollback_move to rollback.rs. Update
apply.rs and module imports/re-exports so the existing patch application flow
remains unchanged.

In `@crates/rho-tools/src/apply_patch/seek_sequence_tests.rs`:
- Around line 11-34: The seek_sequence tests need coverage for both unmatched
input and Unicode normalization fallback. Expand
matches_normalized_context_and_eof_position or add a table-driven test using the
existing lines and seek_sequence helpers, asserting None when no window matches
and asserting the expected position when smart quotes or Unicode dashes
normalize to the source text; follow the table style established by
edit_file_tests.rs.

In `@crates/rho-tools/src/edit_file_tests.rs`:
- Around line 83-129: Add an integration-style async test alongside
rejects_ambiguous_match_without_mutation that calls edit_file_content with
replace_all set to true, replaces two occurrences in the file, and verifies both
the final file content and the reported occurrence count in the result. Exercise
the multi-span rewrite through the public edit_file_content path rather than
testing validate_match_count or replace_spans directly.

In `@crates/rho-tools/src/file_mutation.rs`:
- Around line 89-100: Update the final persistence step in rewrite to call
File::sync_data instead of File::flush, while preserving the existing error
mapping and display_path context for failures.

In `@crates/rho-tools/src/hashline/execute.rs`:
- Around line 200-205: Annotate the adjacent content arguments at every
rewrite_locked_file call site with exact parameter-name comments: in
crates/rho-tools/src/hashline/execute.rs:200-205, mark &file.original as
/*original*/ and &file.outcome.text as /*updated*/; in
crates/rho-tools/src/edit_file.rs:145-145, mark &original as /*original*/ and
&updated as /*updated*/; and in
crates/rho-tools/src/apply_patch/apply.rs:668-668, mark &live as /*original*/
and &updated as /*updated*/.

In `@crates/rho-tools/src/sdk_adapter.rs`:
- Around line 581-587: Add the exact parameter-name comment
`reject_symlink_leaf` before the `mutates_source_entry` argument in the
`path_set.collect` call within the surrounding hunk-processing function,
matching the annotations used by the other call sites.

In `@crates/rho-tools/src/sdk_support.rs`:
- Around line 123-125: Update map_invalid_app_error to match AppToolError
variants, preserving InvalidArguments only for validation/message errors and
delegating non-validation variants such as Cancelled and Io to map_app_error.
Align the behavior with map_invalid_edit_args in sdk_adapter.rs, and reuse this
helper there if that removes the duplicate mapping logic.

In `@crates/rho/src/tools/sdk_registry_tests.rs`:
- Around line 73-95: Update the test loop around AppToolSet::unfiltered_names so
names remain separate for each EditTool configuration instead of being
aggregated. For every configuration, assert the selected edit tool name is
present and the other two edit-tool names are absent, while preserving the
existing advisor registration setup.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bdb49f04-497b-487e-843d-75ca37d5d648

📥 Commits

Reviewing files that changed from the base of the PR and between c43ba0e and 31aae58.

📒 Files selected for processing (45)
  • crates/rho-tools/README.md
  • crates/rho-tools/src/apply_patch/apply.rs
  • crates/rho-tools/src/apply_patch/mod.rs
  • crates/rho-tools/src/apply_patch/parser.rs
  • crates/rho-tools/src/apply_patch/proposed_diff.rs
  • crates/rho-tools/src/apply_patch/seek_sequence.rs
  • crates/rho-tools/src/apply_patch/seek_sequence_tests.rs
  • crates/rho-tools/src/apply_patch_tests.rs
  • crates/rho-tools/src/edit_file.rs
  • crates/rho-tools/src/edit_file_tests.rs
  • crates/rho-tools/src/file_mutation.rs
  • crates/rho-tools/src/grep.rs
  • crates/rho-tools/src/hashline/execute.rs
  • crates/rho-tools/src/lib.rs
  • crates/rho-tools/src/sdk_adapter.rs
  • crates/rho-tools/src/sdk_adapter_tests.rs
  • crates/rho-tools/src/sdk_support.rs
  • crates/rho-tools/src/write_file.rs
  • crates/rho-tui-pty/src/scenarios/config.rs
  • crates/rho/src/agent/definition.rs
  • crates/rho/src/app/interactive_presenter.rs
  • crates/rho/src/app/interactive_presenter_format.rs
  • crates/rho/src/app/interactive_presenter_results.rs
  • crates/rho/src/app/interactive_presenter_tests.rs
  • crates/rho/src/builtin_skills/rho-config/SKILL.md
  • crates/rho/src/config.rs
  • crates/rho/src/config_format.rs
  • crates/rho/src/config_load.rs
  • crates/rho/src/config_load_tests.rs
  • crates/rho/src/diagnostics.rs
  • crates/rho/src/diagnostics_tests.rs
  • crates/rho/src/prompt.rs
  • crates/rho/src/tools/coding.rs
  • crates/rho/src/tools/mod.rs
  • crates/rho/src/tools/sdk_registry.rs
  • crates/rho/src/tools/sdk_registry_tests.rs
  • crates/rho/src/tui/config_actions.rs
  • crates/rho/src/tui/config_picker.rs
  • docs/configuration.md
  • docs/configuration/full-example.md
  • docs/hooks.md
  • docs/tools-workspace.md
  • docs/tools-workspace/documents-and-images.md
  • docs/tools-workspace/edit-format.md
  • docs/tools-workspace/search.md

Comment thread crates/rho-tools/src/apply_patch/apply.rs Outdated
Comment thread crates/rho-tools/src/apply_patch/apply.rs Outdated
Comment thread crates/rho-tools/src/apply_patch/parser.rs Outdated
Comment thread crates/rho-tools/src/edit_file.rs Outdated
Comment thread crates/rho-tools/src/sdk_adapter.rs Outdated
Comment thread crates/rho-tools/src/sdk_adapter.rs Outdated
Comment thread crates/rho/src/prompt.rs
Comment thread docs/configuration.md Outdated
Package verify resolves published rho-agent-tools 0.12.6 from crates.io,
which lacks EditFormat and apply_patch. Bump the workspace tools cut so
publish prep path-patches the unpublished surface used by the agent.
When only one leaf crate is unpublished, path-patching that leaf alone
loads a second rho-sdk through its path edges and breaks type identity.
Patch the full internal dependency closure whenever any direct internal
dep is missing from crates.io.
Make each edit format expose its own model-facing name so presenters and
prompts classify by name alone. Rename the string-replace surface to
StrReplace, align agent capability aliases, reuse mutation_output for
apply_patch, and require locked path identity on hashline commit and
rollback.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 8, 2026
Pad the high-volume stdout flood with keep_alive control frames so the
stream mapper does no journal work per line, and raise the deadlock
timeout above the artifact finish-join budget. Assistant flood lines
were racing macOS CI disk load and looking like pipe deadlocks.
Mirror apply_patch layout with a str_replace/ module, content helper,
and sibling tests. Rename the public content entry point to
str_replace_content and keep legacy edit_file tool-name aliases.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🧹 Nitpick comments (9)
crates/rho/src/app/interactive_presenter_format.rs (1)

255-306: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Extract the apply-patch renderer into a focused module.

apply_patch_card adds a separate presentation policy to an already large module. Move this renderer and its focused tests into a cohesive private module.

As per coding guidelines, “In Rust, avoid large files and extract separable behavior into focused modules while keeping tests and invariant documentation close to the implementation.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho/src/app/interactive_presenter_format.rs` around lines 255 - 306,
Move apply_patch_card and its focused tests from the large presenter module into
a cohesive private module dedicated to apply-patch rendering. Preserve the
existing parsing, path formatting, change mapping, statistics, truncation, and
empty-diff behavior, and update the caller to use the extracted renderer while
keeping related tests and invariant documentation alongside it.

Source: Coding guidelines

crates/rho-tools/src/edit_file.rs (1)

79-87: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Comment the positional boolean argument at the call site.

edit_file_content takes replace_all as a bare positional bool. The tests in edit_file_tests.rs already annotate this argument as /*replace_all*/. Apply the same annotation here for consistency with the coding guidelines.

♻️ Proposed change
             &args.old_string,
             &args.new_string,
-            args.replace_all,
+            /*replace_all*/ args.replace_all,
             ctx.max_output_bytes,

As per coding guidelines: "Make Rust call sites self-documenting by preferring enums, named methods, builders, or newtypes over ambiguous boolean or Option parameters; comment unavoidable opaque positional arguments with the exact parameter name."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/edit_file.rs` around lines 79 - 87, Add the exact
`/*replace_all*/` inline annotation to the `args.replace_all` argument in the
`edit_file_content` call, matching the existing test call sites and leaving the
surrounding arguments unchanged.

Source: Coding guidelines

crates/rho-tools/src/apply_patch/apply.rs (1)

39-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Annotate the two None fault arguments.

The call passes two positional None values for rewrite_fault and create_fault. The parameter identity is not visible at the call site. Add the parameter-name comments used elsewhere in this crate.

♻️ Proposed change
     apply_hunks_inner(
         hunks,
         resolve_path,
         display_path,
         max_output_bytes,
-        None,
-        None,
+        /*rewrite_fault*/ None,
+        /*create_fault*/ None,
     )

As per coding guidelines: "comment unavoidable opaque positional arguments with the exact parameter name."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/apply_patch/apply.rs` around lines 39 - 46, Annotate
both positional None arguments in the apply_hunks_inner call with comments
identifying them exactly as rewrite_fault and create_fault, matching the
parameter-name comment style used elsewhere in the crate.

Source: Coding guidelines

crates/rho-tools/src/apply_patch/transaction.rs (1)

496-501: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Opaque positional arguments at three new call sites. The crate already comments positional arguments with their parameter name, for example /*original*/ and /*updated*/. Three new call sites pass ambiguous bool, Option, and same-typed string arguments without that annotation.

  • crates/rho-tools/src/apply_patch/transaction.rs#L496-L501: annotate new_content as /*expected*/, old_content as /*updated*/, and &None as /*rewrite_fault*/ in the rollback rewrite_current call.
  • crates/rho-tools/src/apply_patch/apply.rs#L39-L46: annotate the two trailing None arguments as /*rewrite_fault*/ and /*create_fault*/.
  • crates/rho-tools/src/edit_file.rs#L79-L87: annotate args.replace_all as /*replace_all*/, matching the tests.

As per coding guidelines: "comment unavoidable opaque positional arguments with the exact parameter name."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/apply_patch/transaction.rs` around lines 496 - 501,
Annotate the opaque positional arguments at all three call sites with their
exact parameter names: in
crates/rho-tools/src/apply_patch/transaction.rs:496-501, mark new_content as
/*expected*/, old_content as /*updated*/, and &None as /*rewrite_fault*/ in
rewrite_current; in crates/rho-tools/src/apply_patch/apply.rs:39-46, mark the
trailing None arguments as /*rewrite_fault*/ and /*create_fault*/; in
crates/rho-tools/src/edit_file.rs:79-87, mark args.replace_all as
/*replace_all*/.

Source: Coding guidelines

crates/rho-tools/src/file_mutation.rs (2)

86-92: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Use symlink_metadata in the Unix identity check for parity with Windows.

The Unix same_file_identity calls std::fs::metadata(path), which follows symlinks. The Windows implementation opens the live path with FILE_FLAG_OPEN_REPARSE_POINT, which does not follow reparse points. Today the asymmetry is masked because locked_path_identity_matches rejects non-regular entries at lines 78-80 before it calls this helper. If a second caller invokes same_file_identity without that guard, the Unix path will compare against the symlink target and report a match for a substituted path.

Make the helper safe on its own.

🛡️ Proposed change
 #[cfg(unix)]
 fn same_file_identity(file: &std::fs::File, path: &Path) -> std::io::Result<bool> {
     use std::os::unix::fs::MetadataExt;
     let left = file.metadata()?;
-    let right = std::fs::metadata(path)?;
+    let right = std::fs::symlink_metadata(path)?;
     Ok(left.dev() == right.dev() && left.ino() == right.ino())
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/file_mutation.rs` around lines 86 - 92, Update the Unix
same_file_identity helper to obtain the path metadata with
std::fs::symlink_metadata instead of std::fs::metadata, so identity comparison
does not follow symlinks and matches the Windows behavior independently of
callers such as locked_path_identity_matches.

553-711: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract the platform install backends into a focused submodule.

file_mutation.rs now holds locking, path-identity checks, rewrite and restore, newline normalization, atomic creation, parent creation, staging, and four platform-specific install backends in one file of over 700 lines. The install backends at lines 553-711 are a clean seam: they depend only on InstallOutcome, AtomicInstallMethod, and AtomicCreateFaultInjector. Moving them to file_mutation/install.rs shortens the file and collects every unsafe FFI block in one reviewable place.

As per coding guidelines: "In Rust, avoid large files and extract separable behavior into focused modules while keeping tests and invariant documentation close to the implementation."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/file_mutation.rs` around lines 553 - 711, Extract
InstallOutcome, install_no_replace, install_platform_no_replace, and
install_with_hard_link into a focused file_mutation/install.rs submodule,
preserving their existing visibility, cfg gates, platform-specific behavior, and
unsafe FFI implementations. Update file_mutation.rs to declare and use the
submodule, passing the existing AtomicInstallMethod and
AtomicCreateFaultInjector dependencies without changing installation semantics
or moving unrelated logic.

Source: Coding guidelines

scripts/crate_publish_prep.py (1)

257-259: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Do not let an unknown package_name silently produce an empty closure.

adjacency is keyed only by INTERNAL_PACKAGE_NAMES. Line 259 uses adjacency.get(package_name, ()), so a package_name outside that set yields an empty closure and select_path_patches returns zero patches. That happens after line 305 already determined that patches are required, so the publish dry-run would proceed unpatched and could pass while the real publish fails. The failure is silent.

internal_dependencies_for raises RuntimeError for an unknown package at line 143. Match that behavior here so the mismatch fails loudly.

🛡️ Proposed change
     ordered: list[InternalDependency] = []
     seen: set[str] = set()
-    stack = list(reversed(adjacency.get(package_name, ())))
+    if package_name not in adjacency:
+        raise RuntimeError(
+            f"package {package_name!r} is not an internal released package"
+        )
+    stack = list(reversed(adjacency[package_name]))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/crate_publish_prep.py` around lines 257 - 259, Update the
dependency-closure logic around `select_path_patches` to validate `package_name`
against the supported internal package names before reading `adjacency`. Raise
`RuntimeError` for unknown packages, matching `internal_dependencies_for`,
instead of allowing `adjacency.get(package_name, ())` to return an empty
closure.
crates/rho-tools/src/edit_format.rs (2)

87-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the tool-name lookups from ALL so a new variant cannot drift.

is_edit_tool_name and from_tool_name each hold a hand-written name list that duplicates tool_name(). EditFormat is #[non_exhaustive] and expected to grow. If someone adds a variant, tool_name() and detail() fail to compile, but these two functions compile and silently return false and None for the new tool name.

Derive both from ALL and keep only the legacy alias as an explicit special case.

♻️ Proposed refactor
     /// Whether `name` is a model-facing built-in edit tool name.
     ///
     /// Includes the legacy `edit_file` name so older transcripts and agent
     /// frontmatter still classify as edit.
     pub fn is_edit_tool_name(name: &str) -> bool {
-        matches!(name, "edit" | "apply_patch" | "str_replace" | "edit_file")
+        Self::from_tool_name(name).is_some()
     }
 
     /// Resolves a model-facing edit tool name.
     ///
     /// Names are unique per format. The legacy model-facing name `edit_file`
     /// still maps to [`Self::StrReplace`].
     pub fn from_tool_name(name: &str) -> Option<Self> {
-        match name {
-            "edit" => Some(Self::Hashline),
-            "apply_patch" => Some(Self::ApplyPatch),
-            "str_replace" | "edit_file" => Some(Self::StrReplace),
-            _ => None,
-        }
+        Self::ALL
+            .iter()
+            .copied()
+            .find(|format| format.tool_name() == name)
+            // Legacy model-facing name retained for older transcripts.
+            .or_else(|| (name == "edit_file").then_some(Self::StrReplace))
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/edit_format.rs` around lines 87 - 106, Update
is_edit_tool_name and from_tool_name to derive current tool-name matching and
resolution by iterating over EditFormat::ALL and comparing each variant’s
tool_name(), preserving the existing mapping behavior. Keep "edit_file" as the
only explicit legacy alias mapping to Self::StrReplace, so newly added variants
cannot be omitted.

108-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the one-use EditFormat::build_sdk_tool wrapper.

EditFormat::build_sdk_tool only forwards to build_edit_sdk_tool. Keep tool construction with sdk_adapter, which already owns the edit tool wiring and call path; use EditFormat only for tool identity such as tool_name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/edit_format.rs` around lines 108 - 114, Remove the
one-use EditFormat::build_sdk_tool method and update its callers to construct
the edit tool through sdk_adapter, which owns the existing build_edit_sdk_tool
wiring. Preserve EditFormat only for tool identity, including tool_name, and
eliminate the now-unnecessary forwarding wrapper.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/rho-tools/src/apply_patch/planning.rs`:
- Around line 66-80: Update the Hunk::Add branch to inspect the requested target
entry itself, including dangling symlinks, before relying on read_optional.
Reject any existing symlink leaf with the existing “file already exists”
ToolError, then retain the current read_optional check for regular files and
other existing entries before constructing FileChange::Add.

In `@crates/rho-tools/src/file_mutation.rs`:
- Around line 527-549: Update stage_file after file.flush() to call
file.sync_data() and propagate errors using the same ToolError and staged-file
context as the existing write and flush failures. Keep the permission
preservation and subsequent installation flow unchanged, matching rewrite’s
durability guarantee.
- Around line 610-617: Update the renameat2 fallback error matching in the
install flow to also treat EOPNOTSUPP and ENOTSUP as unsupported-operation
cases. Preserve the existing ENOSYS and EINVAL handling, and continue invoking
install_no_replace with AtomicInstallMethod::HardLink for all supported fallback
errors.

In `@crates/rho-tools/src/sdk_adapter/mutation.rs`:
- Around line 30-37: Update the match handling in the mutation result flow so
the (Ok(_), Err(capture_error)) case returns an error explicitly stating that
the mutation succeeded but capturing the resulting workspace state failed, while
preserving the existing direct operation-error handling and combined-error
context.

---

Nitpick comments:
In `@crates/rho-tools/src/apply_patch/apply.rs`:
- Around line 39-46: Annotate both positional None arguments in the
apply_hunks_inner call with comments identifying them exactly as rewrite_fault
and create_fault, matching the parameter-name comment style used elsewhere in
the crate.

In `@crates/rho-tools/src/apply_patch/transaction.rs`:
- Around line 496-501: Annotate the opaque positional arguments at all three
call sites with their exact parameter names: in
crates/rho-tools/src/apply_patch/transaction.rs:496-501, mark new_content as
/*expected*/, old_content as /*updated*/, and &None as /*rewrite_fault*/ in
rewrite_current; in crates/rho-tools/src/apply_patch/apply.rs:39-46, mark the
trailing None arguments as /*rewrite_fault*/ and /*create_fault*/; in
crates/rho-tools/src/edit_file.rs:79-87, mark args.replace_all as
/*replace_all*/.

In `@crates/rho-tools/src/edit_file.rs`:
- Around line 79-87: Add the exact `/*replace_all*/` inline annotation to the
`args.replace_all` argument in the `edit_file_content` call, matching the
existing test call sites and leaving the surrounding arguments unchanged.

In `@crates/rho-tools/src/edit_format.rs`:
- Around line 87-106: Update is_edit_tool_name and from_tool_name to derive
current tool-name matching and resolution by iterating over EditFormat::ALL and
comparing each variant’s tool_name(), preserving the existing mapping behavior.
Keep "edit_file" as the only explicit legacy alias mapping to Self::StrReplace,
so newly added variants cannot be omitted.
- Around line 108-114: Remove the one-use EditFormat::build_sdk_tool method and
update its callers to construct the edit tool through sdk_adapter, which owns
the existing build_edit_sdk_tool wiring. Preserve EditFormat only for tool
identity, including tool_name, and eliminate the now-unnecessary forwarding
wrapper.

In `@crates/rho-tools/src/file_mutation.rs`:
- Around line 86-92: Update the Unix same_file_identity helper to obtain the
path metadata with std::fs::symlink_metadata instead of std::fs::metadata, so
identity comparison does not follow symlinks and matches the Windows behavior
independently of callers such as locked_path_identity_matches.
- Around line 553-711: Extract InstallOutcome, install_no_replace,
install_platform_no_replace, and install_with_hard_link into a focused
file_mutation/install.rs submodule, preserving their existing visibility, cfg
gates, platform-specific behavior, and unsafe FFI implementations. Update
file_mutation.rs to declare and use the submodule, passing the existing
AtomicInstallMethod and AtomicCreateFaultInjector dependencies without changing
installation semantics or moving unrelated logic.

In `@crates/rho/src/app/interactive_presenter_format.rs`:
- Around line 255-306: Move apply_patch_card and its focused tests from the
large presenter module into a cohesive private module dedicated to apply-patch
rendering. Preserve the existing parsing, path formatting, change mapping,
statistics, truncation, and empty-diff behavior, and update the caller to use
the extracted renderer while keeping related tests and invariant documentation
alongside it.

In `@scripts/crate_publish_prep.py`:
- Around line 257-259: Update the dependency-closure logic around
`select_path_patches` to validate `package_name` against the supported internal
package names before reading `adjacency`. Raise `RuntimeError` for unknown
packages, matching `internal_dependencies_for`, instead of allowing
`adjacency.get(package_name, ())` to return an empty closure.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ec7473e9-542a-4825-b1f0-c37524b9e878

📥 Commits

Reviewing files that changed from the base of the PR and between 31aae58 and e80a2b4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (53)
  • .release-please-manifest.json
  • crates/rho-tools/Cargo.toml
  • crates/rho-tools/README.md
  • crates/rho-tools/src/apply_patch/apply.rs
  • crates/rho-tools/src/apply_patch/content.rs
  • crates/rho-tools/src/apply_patch/mod.rs
  • crates/rho-tools/src/apply_patch/model.rs
  • crates/rho-tools/src/apply_patch/parser.rs
  • crates/rho-tools/src/apply_patch/planning.rs
  • crates/rho-tools/src/apply_patch/seek_sequence_tests.rs
  • crates/rho-tools/src/apply_patch/transaction.rs
  • crates/rho-tools/src/apply_patch_tests.rs
  • crates/rho-tools/src/edit_file.rs
  • crates/rho-tools/src/edit_file_tests.rs
  • crates/rho-tools/src/edit_format.rs
  • crates/rho-tools/src/file_mutation.rs
  • crates/rho-tools/src/hashline/execute.rs
  • crates/rho-tools/src/lib.rs
  • crates/rho-tools/src/sdk_adapter.rs
  • crates/rho-tools/src/sdk_adapter/edit.rs
  • crates/rho-tools/src/sdk_adapter/mutation.rs
  • crates/rho-tools/src/sdk_adapter/registry.rs
  • crates/rho-tools/src/sdk_adapter_tests.rs
  • crates/rho-tools/src/sdk_support.rs
  • crates/rho-tui-pty/src/scenarios/config.rs
  • crates/rho/Cargo.toml
  • crates/rho/src/agent/agent_tests.rs
  • crates/rho/src/agent/definition.rs
  • crates/rho/src/app/interactive_presenter.rs
  • crates/rho/src/app/interactive_presenter_format.rs
  • crates/rho/src/app/interactive_presenter_results.rs
  • crates/rho/src/app/interactive_presenter_tests.rs
  • crates/rho/src/builtin_skills/rho-config/SKILL.md
  • crates/rho/src/claude_runtime/session_process_tests.rs
  • crates/rho/src/config.rs
  • crates/rho/src/config_load_tests.rs
  • crates/rho/src/diagnostics_tests.rs
  • crates/rho/src/hooks/catalog.rs
  • crates/rho/src/hooks/config_tests.rs
  • crates/rho/src/hooks/matcher_tests.rs
  • crates/rho/src/prompt.rs
  • crates/rho/src/tools/coding.rs
  • crates/rho/src/tools/mod.rs
  • crates/rho/src/tools/sdk_registry_tests.rs
  • crates/rho/src/tui/config_picker.rs
  • docs/configuration.md
  • docs/configuration/full-example.md
  • docs/development.md
  • docs/hooks.md
  • docs/tools-workspace.md
  • docs/tools-workspace/edit-format.md
  • scripts/crate_publish_prep.py
  • scripts/tests/test_crate_publish_prep.py
🚧 Files skipped from review as they are similar to previous changes (13)
  • crates/rho-tui-pty/src/scenarios/config.rs
  • docs/hooks.md
  • crates/rho-tools/README.md
  • crates/rho/src/diagnostics_tests.rs
  • docs/configuration/full-example.md
  • crates/rho-tools/src/apply_patch/seek_sequence_tests.rs
  • crates/rho-tools/src/sdk_support.rs
  • crates/rho/src/tools/sdk_registry_tests.rs
  • crates/rho-tools/src/hashline/execute.rs
  • crates/rho/src/prompt.rs
  • crates/rho-tools/src/apply_patch/mod.rs
  • crates/rho/src/tui/config_picker.rs
  • crates/rho-tools/src/sdk_adapter_tests.rs

Comment thread crates/rho-tools/src/apply_patch/planning.rs
Comment thread crates/rho-tools/src/file_mutation.rs
Comment thread crates/rho-tools/src/file_mutation.rs Outdated
Comment on lines +610 to +617
let error = std::io::Error::last_os_error();
if !matches!(
error.raw_os_error(),
Some(libc::ENOSYS) | Some(libc::EINVAL)
) {
return InstallOutcome::NotInstalled(error);
}
install_no_replace(staged, target, AtomicInstallMethod::HardLink, fault)

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.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🌐 Web query:

renameat2 RENAME_NOREPLACE overlayfs EOPNOTSUPP fallback

💡 Result:

When using the renameat2(2) system call with the RENAME_NOREPLACE flag, the operation requires explicit support from the underlying filesystem [1][2]. If a filesystem does not support this flag, the kernel returns EOPNOTSUPP [3][4]. Overlayfs does not inherently support the RENAME_NOREPLACE flag, and there is no automatic kernel-level "fallback" mechanism that translates a failed RENAME_NOREPLACE attempt into an equivalent operation [1][2][5]. Applications that require this functionality on overlayfs must implement their own user-space fallback logic [5]. To implement a reliable fallback, an application should typically: 1. Attempt the renameat2(2) operation with RENAME_NOREPLACE. 2. If the operation fails with EOPNOTSUPP, catch the error. 3. Perform a manual check (e.g., using stat(2) or access(2)) to determine if the destination path already exists. 4. If the destination does not exist, proceed with a standard rename(2) call. 5. If the destination exists, handle the conflict appropriately (e.g., return EEXIST) to mimic the behavior of RENAME_NOREPLACE [1][2]. Note that because overlayfs is a union filesystem, performing manual checks followed by a rename is not atomic, which can introduce race conditions if other processes are modifying the directory concurrently [1][6]. Application developers should be aware of these potential races when designing fallback strategies [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant source section without executing repository code.
if [ -f crates/rho-tools/src/file_mutation.rs ]; then
  wc -l crates/rho-tools/src/file_mutation.rs
  sed -n '540,705p' crates/rho-tools/src/file_mutation.rs | cat -n
else
  echo "crates/rho-tools/src/file_mutation.rs not found"
  fd -a file_mutation.rs .
fi

# Check the exact raw_os_error match expression and Linux target fallback references.
rg -n "matches!\\(|raw_os_error\\(|EOPNOTSUPP|ENOTSUP|R(?:E\\{||ENAME|NAME_NOREPLACE|ENAME_NOREPLACE)|install_platform_no_replace|install_no_replace" crates/rho-tools/src/file_mutation.rs || true

# Inspect crate target defaults / cfg flags related to file_mutation.
if [ -f crates/rho-tools/Cargo.toml ]; then
  sed -n '1,220p' crates/rho-tools/Cargo.toml | cat -n
fi

# Read-only static verifier: identify exact matches on raw_os_error and whether the fallback
# includes unsupported-operation errors for Unix/Linux syscall failures.
python3 - <<'PY'
from pathlib import Path
p = Path("crates/rho-tools/src/file_mutation.rs")
if not p.exists():
    raise SystemExit("missing source")
s = p.read_text()
needle = "error.raw_os_error()"
idx = s.find(needle)
print("needle_offset", idx)
if idx >= 0:
    snippet = s[max(0, idx-250): idx+350]
    print("snippet:")
    print(snippet)
    for err in ["EOPNOTSUPP", "ENOTSUP", "ENOSYS", "EINVAL"]:
        print(f"{err}_in_snippet", err in snippet)
PY

Repository: matthewyjiang/rho

Length of output: 12812


🌐 Web query:

RUSTUP_TARGET linux-gnu libc ENOTSUP EOPNOTSUPP values

💡 Result:

On Linux targets, including those used by rustup (such as x86_64-unknown-linux-gnu), ENOTSUP and EOPNOTSUPP represent the same error condition and have identical integer values [1][2][3]. Specifically, both constants are defined as 95 in the Rust libc crate [1][4][5]. This equivalence often appears in technical contexts because certain system calls, notably copy_file_range, may return EOPNOTSUPP on older Linux kernels or specific file systems (like NFS) when the operation is not supported [6][7]. When rustup or other Rust tools encounter this error, it is frequently a result of the underlying operating system or file system lacking support for the specific system call being attempted, rather than a configuration error within rustup itself [6][7]. In the Rust standard library and ecosystem, these error codes are treated as synonymous on Linux platforms [2][3]. Consequently, many libraries map both to the same error category, such as ErrorKind::Unsupported, to provide a consistent interface across different environments [2][8].

Citations:


Extend the renameat2 fallback to unsupported-operation errors.

renameat2 with RENAME_NOREPLACE can fail with EOPNOTSUPP/ENOTSUP when the filesystem does not support this flag, such as overlayfs. The fallback only handles ENOSYS and EINVAL, so file creation reports NotInstalled instead of using the hard-link fallback.

Add the unsupported-operation errors to the fallback set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/rho-tools/src/file_mutation.rs` around lines 610 - 617, Update the
renameat2 fallback error matching in the install flow to also treat EOPNOTSUPP
and ENOTSUP as unsupported-operation cases. Preserve the existing ENOSYS and
EINVAL handling, and continue invoking install_no_replace with
AtomicInstallMethod::HardLink for all supported fallback errors.

Comment thread crates/rho-tools/src/sdk_adapter/mutation.rs
Keep the 1.x edit_file tool-name alias for transcripts and agent
frontmatter, and mark it NEXT_MAJOR so 2.0 drops it in favor of
str_replace only.
Reject dangling symlink add targets, fsync staged creates, and clarify
post-mutation capture failures. Extract install/apply-patch helpers and
drive edit tool-name resolution from EditFormat::ALL.
@matthewyjiang
matthewyjiang merged commit 5db37d0 into main Aug 8, 2026
13 checks passed
@matthewyjiang
matthewyjiang deleted the feat/selectable-edit-tools branch August 8, 2026 09:18
@github-actions github-actions Bot mentioned this pull request Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant