Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ Resolution order: `--config <path>` → `$RA_CONFIG` → `./ra.toml` →
| `gh` | Runs native GitHub CLI (`gh`) with argv-safe arguments; ACP hosts use the terminal reverse-call with shell quoting. This path does not use RTK. |
| `jq` | Runs jq filters against inline JSON or a JSON file with argv-safe stdin and a bounded JSON envelope. |
| `mergiraf` | Runs mergiraf merge, solve, and languages actions with argv-safe arguments and bounded JSON output. |
| `sd` | Runs sd regex/literal find-replace across explicit file paths with argv-safe arguments and bounded JSON output. |
| `comby` | Runs comby structural check, diff, and rewrite actions with argv-safe arguments and bounded JSON output. |
| `mise` | Runs mise tasks/tests with argv-safe arguments and bounded JSON output. |
| `just` | Runs just recipes with argv-safe arguments and bounded JSON output. |
| `wrkflw` | Runs wrkflw local GitHub Actions validation/execution with argv-safe arguments and bounded JSON output. |
Expand Down Expand Up @@ -231,13 +233,13 @@ native because it is structured code search rather than a plain shell
command. `bash` remains the fallback for project scripts, tests, and
one-off command pipelines.

Native `git` / `gh` / `jq` / `mergiraf` prioritize argv safety over RTK rewriting. If a
Native `git` / `gh` / `jq` / `mergiraf` / `sd` / `comby` prioritize argv safety over RTK rewriting. If a
high-volume native CLI command needs RTK output compression, run it
through `bash` instead so the existing RTK rewrite path can apply.
`jq` requires the system `jq` binary on `PATH`; missing jq returns
structured install guidance instead of an opaque spawn error.
`mergiraf` requires the system `mergiraf` binary on `PATH`; missing
mergiraf returns structured install guidance. Native `mise` / `just` /
`mergiraf`, `sd`, and `comby` require their system binaries on `PATH`;
missing binaries return structured install guidance. Native `mise` / `just` /
`wrkflw` are intended for test-first task and workflow loops such as
`mise run test`, `just test`, and local GitHub Actions validation. They
run local binaries with argv-safe arguments, optional `cwd`/timeout
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/add-sd-comby-tools/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-02
125 changes: 125 additions & 0 deletions openspec/changes/add-sd-comby-tools/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Design

## Context

Ra already has native tool wrappers for common CLIs. `git` and `gh` preserve
argv boundaries, while `jq`, task workflow tools, webfetch, tmux, and openspec
return bounded JSON envelopes with structured missing-binary and timeout
handling. The `sd` and `comby` tools should follow the JSON-envelope pattern
because they can fail independently, emit large diffs, and need deterministic
fake-binary integration tests.

The user-facing need is split across two rewriting classes:

- `sd`: fast regex or literal replacement across explicit file path arguments.
- `comby`: structural template check, diff, and rewrite actions across a
directory using Comby's `:[hole]` syntax.

## Goals / Non-Goals

**Goals:**

- Provide default built-in tools named `sd` and `comby`.
- Preserve exact allow-list semantics with `[tools].builtin = ["sd"]` and
`[tools].builtin = ["comby"]`.
- Spawn both host binaries through `Command::arg` without shell interpolation.
- Validate unsafe or unsupported request shapes before spawning a binary.
- Return bounded JSON envelopes for success, non-zero exits, invalid requests,
timeouts, and missing binaries.
- Cover behavior with fake-binary integration tests that do not require real
`sd` or `comby` installs.

**Non-Goals:**

- Do not vendor, install, or version-probe `sd` or `comby`.
- Do not parse or reinterpret `sd`/`comby` stdout and stderr beyond bounding
the returned envelope.
- Do not support stdin mode for either tool.
- Do not change `edit`, `apply_patch`, `ast_grep`, or `bash` semantics.

## Decisions

### Use dedicated tool modules

Add `src/tools/sd.rs` and `src/tools/comby.rs` rather than folding the tools
into the generic task workflow wrapper. The schemas and validation rules are
tool-specific: `sd` requires `find`, `replace`, and non-empty `paths`; `comby`
has action-dependent requirements and CLI mappings.

Alternative considered: expose both as generic `args` wrappers. That would
preserve argv boundaries but would not give the model discoverable fields or
pre-spawn validation.

### Keep execution local and transparent

Both tools resolve `cwd` against the session cwd, validate that it is a
directory, find the binary with `which`, then spawn the host command locally
with stdin set to null. The returned command metadata records `program`,
`args`, and `cwd`. Ra does not use ACP terminal wrapping for these tools so
argv boundaries and bounded JSON envelopes remain consistent.

Alternative considered: route through the host terminal like `git`/`gh`. That
would weaken deterministic envelope handling and expose these rewrite tools to
shell rendering.

### Map `sd` directly to its CLI

`sd` maps to:

```text
sd [--fixed-strings] [extra_args...] -- <find> <replace> <paths...>
```

`paths` must be non-empty because the agent tool call has no interactive stdin
stream to rewrite. `extra_args` is appended before the find/replace positionals
so callers can pass flags such as `--flags i` without shell strings. Ra inserts
`--` before `find`/`replace` so leading-dash patterns and replacements are not
interpreted as sd flags.

Alternative considered: support empty `paths` as stdin mode. That would make
tool calls hang or do nothing in unattended agent contexts, so validation
rejects it before spawning `sd`.

### Map `comby` to explicit actions

`comby` uses `CombyAction`:

- `rewrite`: `comby <match> <rewrite> [extensions...] [-d directory]
[-matcher matcher] [-include-files regex] [-exclude-files regex] -in-place
[extra_args...]`
- `check`: `comby <match> "" [extensions...] [-d directory] [-matcher matcher]
[-include-files regex] [-exclude-files regex] -match-only [extra_args...]`
- `diff`: `comby <match> <rewrite> [extensions...] [-d directory]
[-matcher matcher] [-include-files regex] [-exclude-files regex] -diff
[extra_args...]`

`rewrite` and `diff` require `rewrite_template`; `check` does not. `rewrite`
defaults to in-place mutation as requested. `check` and `diff` do not pass
`-in-place`, preserving dry-run behavior.

Alternative considered: a `dry_run` boolean on a single rewrite action. The
enum keeps the wire shape clearer for the model and matches existing
enum-action tool patterns.

### Bound output with PRD defaults

`sd` defaults to a 30,000 ms timeout and 32,768 byte result envelope.
`comby` defaults to a 60,000 ms timeout and 65,536 byte result envelope.
Both trim stderr first to a fixed safety budget, then clip stdout as needed to
preserve valid JSON and set `truncated: true`.

Alternative considered: no default timeout. These commands can traverse many
files, so bounded defaults are safer for unattended agent runs.

## Risks / Trade-offs

- Missing host binaries -> Return structured install guidance and avoid
opaque spawn errors.
- `extra_args` can still request surprising CLI behavior -> Keep argv-safe
execution, document that it is an advanced escape hatch, and rely on tool
allow-lists/hooks for policy.
- `comby` CLI flags may vary across versions -> Do not version-probe in this
change; surface non-zero exits transparently in the envelope.
- Very small `max_output_bytes` values may still exceed the requested budget
because valid JSON must be preserved -> Match existing envelope behavior and
document the budget as best-effort.
43 changes: 43 additions & 0 deletions openspec/changes/add-sd-comby-tools/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Add Native sd and comby Tools

## Why

Ra agents need safer first-class options for bulk text rewriting and structural
code rewriting. Today those workflows fall back to shell-assembled `sed` or
`comby` commands, which hides argv shape from the tool catalog, makes quoting
fragile, and provides no consistent result envelope or missing-binary guidance.

## What Changes

- Add a native built-in `sd` tool for regex or literal find/replace across
explicit file path arguments.
- Add a native built-in `comby` tool for structural code check, diff, and
in-place rewrite actions.
- Execute both tools through argv-safe process spawning with no shell
interpolation and no stdin mode.
- Return bounded JSON envelopes with command metadata, exit status, stdout,
stderr, truncation state, validation errors, timeout errors, and structured
missing-binary guidance.
- Register both tools in the default built-in catalog and preserve exact
`[tools].builtin` allow-list behavior using the tool names `sd` and `comby`.
- Add fake-binary integration tests and document both tools in `spec/tools.md`.

## Capabilities

### New Capabilities

### Modified Capabilities

- `tools`: add native `sd` and `comby` built-in tool contracts to the existing
tool catalog requirements.

## Impact

- Affected code: `src/tools/`, tool registration in `src/tools/mod.rs`, and
focused integration tests under `tests/`.
- Affected docs: `spec/tools.md` and any tool catalog comments that enumerate
built-ins.
- Runtime dependencies: `sd` and `comby` must be discoverable on `PATH`; Ra
does not install, vendor, or version-probe either binary.
- No breaking changes. Existing `edit`, `apply_patch`, and `bash` semantics are
unchanged.
133 changes: 133 additions & 0 deletions openspec/changes/add-sd-comby-tools/specs/tools/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Tools Delta

## ADDED Requirements

### Requirement: Native sd and comby Tool Catalog

Ra SHALL include `sd` and `comby` in the default built-in catalog when
`[tools].builtin` is empty.

#### Scenario: Empty allow-list exposes sd and comby

- **WHEN** Ra builds the default built-in tool catalog with an empty
`[tools].builtin` allow-list
- **THEN** the catalog includes `sd` and `comby`

#### Scenario: Non-empty allow-list can select sd

- **WHEN** Ra builds the default built-in tool catalog with `[tools].builtin`
containing only `sd`
- **THEN** the catalog contains `sd` and omits unspecified tools

#### Scenario: Non-empty allow-list can select comby

- **WHEN** Ra builds the default built-in tool catalog with `[tools].builtin`
containing only `comby`
- **THEN** the catalog contains `comby` and omits unspecified tools

### Requirement: sd Rewrite Execution

Ra SHALL provide an `sd` tool that runs the host `sd` binary for regex or
literal find/replace using argv-safe process spawning and explicit path
arguments.

#### Scenario: sd regex replacement preserves argv boundaries

- **WHEN** the caller provides `find`, `replace`, and one or more `paths`
- **THEN** Ra invokes `sd` with `find`, `replace`, and each path as separate
argv entries

#### Scenario: sd string mode is passed as a flag

- **WHEN** the caller sets `string_mode: true`
- **THEN** Ra invokes `sd` with `--fixed-strings` before the find and replace
positionals

#### Scenario: sd leading-dash positionals are protected

- **WHEN** the caller provides `find` or `replace` values beginning with `-`
- **THEN** Ra inserts `--` before the find and replace positionals

#### Scenario: sd extra args are argv-safe

- **WHEN** the caller provides `extra_args`
- **THEN** Ra passes each extra argument as a separate argv entry without shell
interpolation

#### Scenario: sd requires explicit paths

- **WHEN** the caller provides an empty `paths` array
- **THEN** Ra returns `error.kind: "invalid_request"` before invoking `sd`

### Requirement: comby Structural Rewrite Execution

Ra SHALL provide a `comby` tool that runs the host `comby` binary for
structural check, diff, and rewrite actions using argv-safe process spawning.

#### Scenario: comby rewrite mutates in place

- **WHEN** the caller sets `action: "rewrite"` with a rewrite template
- **THEN** Ra invokes `comby` with `-in-place`

#### Scenario: comby check does not require rewrite template

- **WHEN** the caller sets `action: "check"` without `rewrite_template`
- **THEN** Ra invokes `comby` with an empty rewrite positional and
`-match-only`

#### Scenario: comby diff requires rewrite template

- **WHEN** the caller sets `action: "diff"` without `rewrite_template`
- **THEN** Ra returns `error.kind: "invalid_request"` before invoking `comby`

#### Scenario: comby rewrite requires rewrite template

- **WHEN** the caller sets `action: "rewrite"` without `rewrite_template`
- **THEN** Ra returns `error.kind: "invalid_request"` before invoking `comby`

#### Scenario: comby filters map to argv

- **WHEN** the caller provides extensions, directory, matcher, include_files,
exclude_files, or extra_args
- **THEN** Ra passes each requested value as separate argv entries without
shell interpolation

### Requirement: sd and comby Result Envelopes

Ra SHALL return bounded, valid JSON envelopes for `sd` and `comby` execution
results, including failures.

#### Scenario: successful rewrite returns structured output

- **WHEN** `sd` or `comby` exits with status 0
- **THEN** Ra returns JSON with `ok: true`, the tool name, command metadata,
`exit_code: 0`, stdout, stderr, and `truncated`

#### Scenario: non-zero exit is structured

- **WHEN** `sd` or `comby` exits with a non-zero status
- **THEN** Ra returns JSON with `ok: false`, the exit code, stdout, stderr, and
`error.kind: "command_failed"`

#### Scenario: missing sd binary is structured

- **WHEN** `sd` is not found on `PATH`
- **THEN** Ra returns JSON with `ok: false`, no exit code,
`error.kind: "missing_sd"`, and installation guidance

#### Scenario: missing comby binary is structured

- **WHEN** `comby` is not found on `PATH`
- **THEN** Ra returns JSON with `ok: false`, no exit code,
`error.kind: "missing_comby"`, and installation guidance

#### Scenario: timeout is structured

- **WHEN** `sd` or `comby` exceeds `timeout_ms`
- **THEN** Ra returns JSON with `ok: false`, no exit code, and
`error.kind: "timeout"`

#### Scenario: output is bounded

- **WHEN** stdout or stderr exceeds `max_output_bytes`
- **THEN** Ra returns valid JSON with `truncated: true`
23 changes: 23 additions & 0 deletions openspec/changes/add-sd-comby-tools/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Tasks

## 1. Tool Contracts

- [x] 1.1 Define the `sd` input schema, CLI argv mapping, validation rules, defaults, and result envelope.
- [x] 1.2 Define the `comby` input schema, enum actions, CLI argv mapping, validation rules, defaults, and result envelope.

## 2. Implementation

- [x] 2.1 Add `SdTool` under `src/tools/` using argv-safe process spawning, explicit paths, cwd resolution, timeout handling, output budgeting, and missing-binary guidance.
- [x] 2.2 Add `CombyTool` under `src/tools/` using argv-safe process spawning, action-dependent validation, cwd resolution, timeout handling, output budgeting, and missing-binary guidance.
- [x] 2.3 Register `sd` and `comby` in `tools::default_builtins` and preserve exact allow-list behavior by tool name.

## 3. Documentation

- [x] 3.1 Document `sd` in `spec/tools.md` with schema, examples, output envelope, and error cases.
- [x] 3.2 Document `comby` in `spec/tools.md` with schema, action mappings, examples, output envelope, and error cases.

## 4. Tests

- [x] 4.1 Add fake-binary integration tests for `sd` covering argv mapping, string mode, path validation, missing binary, non-zero exit, truncation, and catalog registration.
- [x] 4.2 Add fake-binary integration tests for `comby` covering rewrite/check/diff argv mapping, validation, missing binary, non-zero exit, truncation, and catalog registration.
- [x] 4.3 Run formatting, focused tool tests, OpenSpec validation, and the full Rust test suite when feasible.
4 changes: 3 additions & 1 deletion spec/ra.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ api_key_env = "ANTHROPIC_API_KEY"
# gh — run native GitHub CLI with argv-safe arguments
# jq — run jq filters against inline JSON or a JSON file
# mergiraf — syntax-aware merge conflict resolution
# sd — regex/literal find-replace across explicit file paths
# comby — structural code check/diff/rewrite
# grep — structured text search
# glob — structured file discovery
# ls — structured directory listing
Expand All @@ -57,7 +59,7 @@ api_key_env = "ANTHROPIC_API_KEY"
# tmux_wait — block until a tmux event, hook expression, program result, or sleep timeout resolves
# An empty list (or omitted section) ships every built-in tool.
[tools]
# builtin = ["read", "write", "edit", "bash", "ast_grep", "git", "gh", "jq", "mergiraf", "grep", "glob", "ls", "fuzzy", "apply_patch", "webfetch_fetch", "webfetch_crawl", "openspec", "tmux_run", "tmux_send", "tmux_capture", "tmux_kill", "tmux_listen", "tmux_wait"]
# builtin = ["read", "write", "edit", "bash", "ast_grep", "git", "gh", "jq", "mergiraf", "sd", "comby", "grep", "glob", "ls", "fuzzy", "apply_patch", "webfetch_fetch", "webfetch_crawl", "openspec", "tmux_run", "tmux_send", "tmux_capture", "tmux_kill", "tmux_listen", "tmux_wait"]
builtin = []

# ─── Skills (Claude Code / agentskills.io) ──────────────────────────
Expand Down
Loading
Loading