Skip to content

(MOT-3873) feat(shell): str_replace update op + coder::context workspace snapshot#409

Closed
ytallo wants to merge 1 commit into
mainfrom
feat/coder-str-replace-context
Closed

(MOT-3873) feat(shell): str_replace update op + coder::context workspace snapshot#409
ytallo wants to merge 1 commit into
mainfrom
feat/coder-str-replace-context

Conversation

@ytallo

@ytallo ytallo commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
  • coder::update-file gains { op: "str_replace", old_str, new_str,
    replace_all? }: exact-literal replacement desugared onto the existing
    content-op machinery (escaped-literal regex + $$-escaped replacement).
    Without replace_all the op requires exactly one occurrence and fails
    C210 (nothing written) on 0 or N matches with prescriptive recovery;
    replace_all replaces every occurrence (>=1 required).
  • new coder::context: one-call workspace snapshot (allowed roots,
    platform, bounded git branch/status/log, AGENTS.md / CLAUDE.md
    instruction files capped at 16 KiB) for seeding coding sessions;
    git state degrades to null on non-repo roots or missing git binary.
  • wire-schema goldens regenerated; catalog grows to 10 functions.

Refs MOT-3873

Summary by CodeRabbit

  • New Features

    • Added a new workspace context command that returns your current root, available paths, platform info, optional Git status, and relevant instruction-file contents.
    • Added support for literal string replacement in file updates, making simple text edits more direct and reliable.
  • Bug Fixes

    • Improved safety for string replacements so ambiguous matches fail cleanly without changing files.
    • Truncates large instruction files and Git output to keep responses manageable.

- coder::update-file gains { op: "str_replace", old_str, new_str,
  replace_all? }: exact-literal replacement desugared onto the existing
  content-op machinery (escaped-literal regex + $$-escaped replacement).
  Without replace_all the op requires exactly one occurrence and fails
  C210 (nothing written) on 0 or N matches with prescriptive recovery;
  replace_all replaces every occurrence (>=1 required).
- new coder::context: one-call workspace snapshot (allowed roots,
  platform, bounded git branch/status/log, AGENTS.md / CLAUDE.md
  instruction files capped at 16 KiB) for seeding coding sessions;
  git state degrades to null on non-repo roots or missing git binary.
- wire-schema goldens regenerated; catalog grows to 10 functions.
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jul 6, 2026 11:41am
workers-tech-spec Ready Ready Preview, Comment Jul 6, 2026 11:41am

Request Review

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 32 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new coder::context one-call function returning workspace snapshot data (platform, allowed roots, optional git state, instruction files), registers it in the function catalog, and extends coder::update-file with a new str_replace literal-match operation, including schema and test updates for both.

Changes

coder::context endpoint

Layer / File(s) Summary
Context data model and handler
shell/src/code/functions/context.rs
Defines ContextInput/ContextOutput and supporting structs, implements handle, and adds git_lines, git_context, and read_instruction_files helpers with timeouts, truncation, and error tolerance.
Function registration and catalog wiring
shell/src/code/functions/mod.rs
Adds context module, CONTEXT_ID/CONTEXT_DESC, catalog entry, and register_context registrar scoping the resolver via fs_scope.
Golden schema and integration tests
shell/tests/golden/schemas/coder.context.json, shell/tests/code_context.rs, shell/tests/code_golden_schemas.rs
Adds the coder.context JSON schema and integration tests covering git repo, non-repo directory, and oversized instruction file scenarios; updates catalog-count assertions from 9 to 10 functions.

update-file str_replace operation

Layer / File(s) Summary
StrReplace implementation and docs
shell/src/code/functions/update_file.rs
Updates docs and reworks apply_regex_ops to unify regex replace and literal str_replace handling via CountRule, adds str_replace_mismatch for C210 error messages.
Line-op unreachable-arm updates
shell/src/code/functions/update_file.rs
Updates record_line_op_events, apply_line_ops, anchor, validate_line_ops, cover, and the apply_ops test helper to handle StrReplace consistently with Replace in line-op contexts.
Tests and schema
shell/tests/code_update_ops.rs, shell/tests/golden/schemas/coder.update-file.json, shell/tests/code_golden_schemas.rs
Adds unit and e2e tests for single/ambiguous replacement cases, extends the JSON schema with the str_replace op variant, and updates the golden round-trip example expectations.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant register_context
  participant handle
  participant PathResolver
  participant GitCLI

  Caller->>register_context: coder::context request
  register_context->>PathResolver: scope_root/scope_grants(fs_scope)
  register_context->>handle: handle(resolver, cfg, req)
  handle->>PathResolver: resolve primary_root, base_paths
  handle->>GitCLI: git rev-parse/status/log (2s timeout)
  GitCLI-->>handle: branch, status, commits or failure
  handle->>PathResolver: resolve AGENTS.md/CLAUDE.md
  handle-->>register_context: ContextOutput
  register_context-->>Caller: response
Loading

Possibly related PRs

  • iii-hq/workers#376: The new coder::context handler scopes its PathResolver via fs_scope, directly using the session-scoped resolver mechanism introduced/adjusted in this PR.

Suggested reviewers: andersonleal, sergiofilhowz

Poem

A rabbit peeked into the repo's den,
Found git status, branches, and commits ten,
With str_replace so precise and neat,
One match required, no ambiguous feat,
Hop hop — context and swaps complete! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the two main changes: str_replace support and the new coder::context snapshot endpoint.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/coder-str-replace-context

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

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
shell/src/code/functions/update_file.rs (1)

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

Correct implementation of literal str_replace via escaped-literal regex.

The regex::escape(old_str) pattern plus new_str.replace('$', "$$") replacement correctly produces verbatim expansion — Captures::expand treats $$ as a literal-$ escape, so a $ in new_str (e.g. ${name}) survives untouched. The CountRule unification cleanly enforces str_replace's "found-and-unique or replace_all" contract alongside Replace's expect_matches, and matches the unit tests (str_replace_dollar_in_new_str_lands_verbatim, str_replace_ambiguous_without_replace_all_is_c210, etc.).

One readability note: this function now handles two fairly different op kinds' setup logic plus the shared match/count-guard machinery in a single ~150-line body. Extracting the per-op-kind setup (the two match arms building (Regex, Cow<str>, CountRule)) into small helper functions (e.g. compile_replace_op, compile_str_replace_op) would reduce the cognitive load of this function without touching behavior.

🤖 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 `@shell/src/code/functions/update_file.rs` around lines 501 - 670, The logic in
apply_regex_ops is correct, but the per-op setup inside the match on UpdateOp is
doing too much in one long function. Extract the two branches that build (Regex,
Cow<str>, CountRule) into small helpers like compile_replace_op and
compile_str_replace_op, then keep the shared replacement/count-guard flow in
apply_regex_ops. This will reduce cognitive load without changing behavior and
makes the CountRule and regex compilation paths easier to follow.
shell/src/code/functions/context.rs (1)

143-165: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sequential git invocations add up to ~6s worst-case latency for a "one-call" snapshot.

branch, status, and recent_commits are fetched with three sequential .awaits, each bounded by a 2s GIT_TIMEOUT. In a degenerate case (e.g., slow FS, large repo) the handler could take up to ~6s even though status and recent_commits don't depend on each other. Since the doc comment frames this as a fast one-call bootstrap primitive, running these concurrently would meaningfully cut worst-case latency.

♻️ Proposed refactor: run status/log concurrently after the repo probe
     let branch = git_lines(root, &["rev-parse", "--abbrev-ref", "HEAD"])
         .await?
         .into_iter()
         .next()?;
-    let mut status = git_lines(root, &["status", "--porcelain"])
-        .await
-        .unwrap_or_default();
+    let (status_res, commits_res) = tokio::join!(
+        git_lines(root, &["status", "--porcelain"]),
+        git_lines(root, &["log", "--oneline", &format!("-{RECENT_COMMITS}")])
+    );
+    let mut status = status_res.unwrap_or_default();
     let status_truncated = status.len() > STATUS_MAX_ENTRIES;
     status.truncate(STATUS_MAX_ENTRIES);
-    // Fails on a repo with no commits yet — an empty history, not an error.
-    let recent_commits = git_lines(root, &["log", "--oneline", &format!("-{RECENT_COMMITS}")])
-        .await
-        .unwrap_or_default();
+    let recent_commits = commits_res.unwrap_or_default();
🤖 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 `@shell/src/code/functions/context.rs` around lines 143 - 165, The git snapshot
in git_context is doing branch, status, and recent_commits fetches sequentially,
which unnecessarily stretches the one-call latency. Keep the repo probe via
git_lines(... "rev-parse" ...) first, then run the independent status and log
calls concurrently (for example by starting both futures after branch is known
and awaiting them together), and preserve the existing truncation/default
handling for STATUS_MAX_ENTRIES and RECENT_COMMITS.
🤖 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 `@shell/src/code/functions/context.rs`:
- Around line 124-141: The git command in git_lines can outlive the timeout
because dropping the output() future does not stop the spawned process; update
the tokio::process::Command setup in git_lines to enable kill_on_drop(true) so
timed-out git invocations are terminated instead of leaving background children
running.

---

Nitpick comments:
In `@shell/src/code/functions/context.rs`:
- Around line 143-165: The git snapshot in git_context is doing branch, status,
and recent_commits fetches sequentially, which unnecessarily stretches the
one-call latency. Keep the repo probe via git_lines(... "rev-parse" ...) first,
then run the independent status and log calls concurrently (for example by
starting both futures after branch is known and awaiting them together), and
preserve the existing truncation/default handling for STATUS_MAX_ENTRIES and
RECENT_COMMITS.

In `@shell/src/code/functions/update_file.rs`:
- Around line 501-670: The logic in apply_regex_ops is correct, but the per-op
setup inside the match on UpdateOp is doing too much in one long function.
Extract the two branches that build (Regex, Cow<str>, CountRule) into small
helpers like compile_replace_op and compile_str_replace_op, then keep the shared
replacement/count-guard flow in apply_regex_ops. This will reduce cognitive load
without changing behavior and makes the CountRule and regex compilation paths
easier to follow.
🪄 Autofix (Beta)

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

Run ID: 41b96137-b7e6-4abd-a061-227b31db6088

📥 Commits

Reviewing files that changed from the base of the PR and between 936ba3b and c9b9941.

📒 Files selected for processing (8)
  • shell/src/code/functions/context.rs
  • shell/src/code/functions/mod.rs
  • shell/src/code/functions/update_file.rs
  • shell/tests/code_context.rs
  • shell/tests/code_golden_schemas.rs
  • shell/tests/code_update_ops.rs
  • shell/tests/golden/schemas/coder.context.json
  • shell/tests/golden/schemas/coder.update-file.json

Comment on lines +124 to +141
async fn git_lines(root: &Path, args: &[&str]) -> Option<Vec<String>> {
let fut = tokio::process::Command::new("git")
.arg("-C")
.arg(root)
.args(args)
.stdin(std::process::Stdio::null())
.output();
let out = tokio::time::timeout(GIT_TIMEOUT, fut).await.ok()?.ok()?;
if !out.status.success() {
return None;
}
Some(
String::from_utf8_lossy(&out.stdout)
.lines()
.map(str::to_string)
.collect(),
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does tokio::process::Command::output() kill the child process on timeout/drop by default, or does it require kill_on_drop(true)?

💡 Result:

By default, tokio::process::Command::output() does not kill the child process if the future is dropped before it completes [1][2][3]. To enable this behavior, you must explicitly call kill_on_drop(true) on the Command object before spawning or running the output command [4][2][3]. In the tokio library, dropping a Child process handle (or the future returned by output()) does not trigger cancellation by default, a design choice intended to mirror the behavior of the standard library's std::process::Command [1][2][3]. If kill_on_drop(true) is set, the destructor of the future returned by output() will attempt to kill the child process if it is still running when the future is dropped [2][3]. Because of the potential for zombie processes and the non-guaranteed nature of automatic reaping, it is generally recommended to explicitly manage the child process lifecycle using child.wait().await or child.kill().await when strict cleanup guarantees are required [2][5][6].

Citations:


Kill timed-out git children in shell/src/code/functions/context.rs:124-131 tokio::time::timeout drops the output() future, but without kill_on_drop(true) the spawned git process can keep running after the deadline. Add kill_on_drop(true) here so timed-out calls don't leave background processes behind.

🤖 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 `@shell/src/code/functions/context.rs` around lines 124 - 141, The git command
in git_lines can outlive the timeout because dropping the output() future does
not stop the spawned process; update the tokio::process::Command setup in
git_lines to enable kill_on_drop(true) so timed-out git invocations are
terminated instead of leaving background children running.

@ytallo

ytallo commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Consolidated into #423.

@ytallo ytallo closed this Jul 6, 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