Enhance tool documentation and add new features

- Added module-level documentation for memory tools (`remember`, `recall`, `forget`) to clarify their purpose.
- Improved documentation in `recall.rs` and `remember.rs` to describe the functionality and flow of memory entry operations.
- Updated `mod.rs` to include descriptions for the tool trait and execution context.
- Enhanced `plan.rs` with detailed comments on plan-mode signaling tools.
- Documented text search tools in `search.rs` to explain their functionality.
- Improved sequential-thinking tool documentation in `seqthink.rs`.
- Added safety filter documentation in `shell_filter` for credential and git operations.
- Enhanced utility tools documentation, including `cd`, `dir_cache_update`, and `todowrite`.
- Improved rendering documentation in view modules (`chat`, `markdown`, `status`, `workflow`) to clarify rendering flows and purposes.
This commit is contained in:
asepharyana
2026-07-12 11:28:39 +07:00
parent 7158d362fd
commit 2efd40ca88
124 changed files with 2379 additions and 19 deletions
+5
View File
@@ -1,8 +1,12 @@
//! Tool implementations for interacting with background bash jobs: `bash_output`
//! and `bash_kill`. Both take a `job_id` produced by `bash` with `run_in_background=true`.
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::Tool;
use super::ToolCtx;
/// Tool: fetch buffered output from a background bash job by `job_id`.
pub struct BashOutput;
impl Tool for BashOutput {
@@ -39,6 +43,7 @@ impl Tool for BashOutput {
}
}
/// Tool: terminate a running background bash job by `job_id`.
pub struct BashKill;
impl Tool for BashKill {
+7
View File
@@ -1,3 +1,5 @@
//! Tool: `delete` — remove a file or empty directory relative to a workspace root.
use std::fs;
use std::path::PathBuf;
use serde_json::{json, Value};
@@ -7,6 +9,7 @@ use super::super::ToolCtx;
use super::super::resolve_path;
use super::helpers::arg_str;
/// Tool: delete a file or empty directory. Refuses non-empty directories.
pub struct Delete;
impl Tool for Delete {
@@ -31,6 +34,10 @@ impl Tool for Delete {
})
}
/// Delete a file or empty directory. Returns success message or errors on failure.
///
/// Flow: resolve path → check existence → check dir/file → remove.
/// Only empty directories are deletable (non-empty returns an error).
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = arg_str(args, "path")?;
let path: PathBuf = resolve_path(&ctx.workspaces, &rel)?;
+10
View File
@@ -1,3 +1,5 @@
//! Tool: `edit` — replace a substring in a file with a new string.
use std::fs;
use std::path::PathBuf;
use serde_json::{json, Value};
@@ -8,6 +10,7 @@ use super::super::resolve_path;
use super::super::check_graduated_checks;
use super::helpers::arg_str;
/// Tool: replace text in a file. Requires the old string to be unique unless `replace_all` is true.
pub struct Edit;
impl Tool for Edit {
@@ -48,6 +51,13 @@ impl Tool for Edit {
})
}
/// Perform the in-file string replacement.
///
/// Flow: validate args → resolve path → read file → count occurrences →
/// replace one or all → write back → report byte delta (+ optional graduated checks).
///
/// Why: requires a non-empty `reason` and a non-empty `old` string to prevent
/// accidental identity edits. Enforces uniqueness unless `replace_all` is set.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = arg_str(args, "path")?;
let old = arg_str(args, "old")?;
+13
View File
@@ -1,7 +1,14 @@
//! Shared helpers for filesystem tools: extracting string arguments from JSON
//! and producing user-friendly "not found" diagnostics.
use std::path::Path;
use serde_json::Value;
use anyhow::{Result, anyhow};
/// Extract a required string argument from a JSON args map.
///
/// Return: the value as `String` if present and a string type; `Err` if missing
/// or of a different JSON type (null, number, boolean, array, object).
pub fn arg_str(args: &Value, name: &str) -> Result<String> {
args.get(name)
.and_then(|v| v.as_str())
@@ -9,6 +16,12 @@ pub fn arg_str(args: &Value, name: &str) -> Result<String> {
.ok_or_else(|| anyhow!("missing required argument: {}", name))
}
/// Produce a user-friendly diagnostic string when a path doesn't resolve or exist.
///
/// Checks whether the resolved path canonically falls inside any workspace root
/// and reports either "path outside workspaces" or "path does not exist" accordingly.
///
/// Return: a one-line description of the resolution failure.
pub fn not_found_help(ctx: &super::super::ToolCtx, path: &Path, rel: &str) -> String {
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let in_ws = ctx.workspaces.iter().any(|w| {
+3
View File
@@ -1,3 +1,6 @@
//! Filesystem tool implementations: read, write, edit, and delete operations
//! on workspace-rooted paths.
pub mod delete;
pub mod edit;
pub mod helpers;
+10
View File
@@ -1,3 +1,5 @@
//! Tool: `read` — display file contents with line numbers.
use std::fs;
use std::path::PathBuf;
use serde_json::{json, Value};
@@ -7,6 +9,7 @@ use super::super::ToolCtx;
use super::super::resolve_path;
use super::helpers::{arg_str, not_found_help};
/// Tool: read a file and display it with line numbers, optionally truncated to `limit` lines.
pub struct Read;
impl Tool for Read {
@@ -35,6 +38,13 @@ impl Tool for Read {
})
}
/// Read and display a file with line numbers.
///
/// Flow: resolve path → if not found, call `not_found_help` for diagnostic →
/// read entire file → enumerate and format lines → optionally truncate by `limit`.
///
/// Return: line-numbered content; `not_found_help` message if the path doesn't
/// exist; a "is a directory" message if the path points at a directory.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = arg_str(args, "path")?;
let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as usize);
+10
View File
@@ -1,3 +1,5 @@
//! Tool: `write` — write content to a file, creating parent directories on demand.
use std::fs;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
@@ -7,6 +9,7 @@ use super::super::resolve_path;
use super::super::check_graduated_checks;
use super::helpers::arg_str;
/// Tool: write content to a file, auto-creating parent directories as needed.
pub struct Write;
impl Tool for Write {
@@ -39,6 +42,13 @@ impl Tool for Write {
})
}
/// Write content to a file, creating parent directories as needed.
///
/// Flow: validate args (non-empty reason) → resolve path → create parent
/// dirs → write file → report byte count (+ optional graduated checks).
///
/// Why: requires a non-empty `reason` to discourage stray writes; parent
/// directories are created silently so the tool works for new paths.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = arg_str(args, "path")?;
let content = arg_str(args, "content")?;
+12
View File
@@ -1,9 +1,12 @@
//! Tool wrapper around `git credential` for store/get/erase operations.
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use std::process::Command;
use super::Tool;
use super::ToolCtx;
/// Tool that shells out to `git credential <op>` to store, retrieve, or erase credentials.
pub struct GitCred;
impl Tool for GitCred {
@@ -29,6 +32,15 @@ impl Tool for GitCred {
})
}
/// Run `git credential <operation>`, forwarding stdin-less invocation to the git binary.
///
/// Flow: extract `operation` arg → spawn `git credential <operation>` → capture output.
///
/// Why: `store`/`get`/`erase` are the only credential-helper subcommands git supports;
/// no stdin is piped, so this mainly surfaces helper output/errors rather than
/// performing an interactive credential exchange.
///
/// Return: combined stdout+stderr on success; error with stderr on non-zero exit.
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let operation = args.get("operation")
.and_then(|v| v.as_str())
+13
View File
@@ -1,9 +1,12 @@
//! Generic tool for running arbitrary git subcommands.
use std::process::Command;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::Tool;
use super::ToolCtx;
/// Tool that runs `git <operation> [args...]` and returns combined stdout/stderr.
pub struct GitOperator;
impl Tool for GitOperator {
@@ -33,6 +36,16 @@ impl Tool for GitOperator {
})
}
/// Run `git <operation> [args...]` and return its combined output.
///
/// Flow: extract `operation` + `args` → spawn `git <operation> <args>` → trim and
/// join stdout/stderr.
///
/// Why: no allowlist here — the model may run any git subcommand; destructive
/// operations are blocked upstream by `shell_filter::git`, not by this tool.
///
/// Return: trimmed combined output on success; error including exit code and
/// stderr on failure.
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let operation = args.get("operation")
.and_then(|v| v.as_str())
+10
View File
@@ -1,9 +1,12 @@
//! Tool for creating git worktrees under the session's worktrees directory.
use std::process::Command;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::Tool;
use super::ToolCtx;
/// Tool that creates a new git worktree (`git worktree add`) from a given base ref.
pub struct GitWorktree;
impl Tool for GitWorktree {
@@ -32,6 +35,13 @@ impl Tool for GitWorktree {
})
}
/// Create the worktree directory and run `git worktree add --checkout <path> <base_ref>`.
///
/// Flow: extract name/base_ref → create worktree dir under `ctx.worktrees_dir` →
/// spawn `git worktree add` → combine stdout/stderr.
///
/// Return: success message with combined output on success; error including exit
/// code and stderr on failure.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let name = args.get("name")
.and_then(|v| v.as_str())
+9
View File
@@ -1,9 +1,12 @@
//! Tool for deleting a persisted memory entry by name.
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::super::Tool;
use super::super::ToolCtx;
use crate::model::memory::Memory;
/// Tool that removes a single memory entry from `ctx.memory_dir` by exact name.
pub struct Forget;
impl Tool for Forget {
@@ -28,6 +31,12 @@ impl Tool for Forget {
})
}
/// Delete the memory file matching `name` from disk.
///
/// Flow: extract `name` → `Memory::remove` → confirmation string.
///
/// Return: confirmation message on success; error if the memory does not exist
/// or the file could not be removed.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let name = args.get("name")
.and_then(|v| v.as_str())
+2
View File
@@ -1,3 +1,5 @@
//! Memory tools: `remember`, `recall`, and `forget` for persisted project memory entries.
pub mod forget;
pub mod recall;
pub mod remember;
+15
View File
@@ -1,9 +1,12 @@
//! Tool for reading a single memory entry or listing the whole memory index.
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::super::Tool;
use super::super::ToolCtx;
use crate::model::memory::Memory;
/// Tool that reads one memory entry by name, or lists all entries when name is omitted.
pub struct Recall;
impl Tool for Recall {
@@ -27,6 +30,12 @@ impl Tool for Recall {
})
}
/// Read a specific memory entry, or fall back to listing all entries.
///
/// Flow: if `name` present and non-empty → `Memory::read` and format as frontmatter
/// + body; otherwise → `list_all`.
///
/// Return: formatted memory content, or the full index listing.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
if let Some(name) = args.get("name").and_then(|v| v.as_str()) {
if name.is_empty() {
@@ -48,6 +57,12 @@ impl Tool for Recall {
}
}
/// List every memory entry in `ctx.memory_dir` as a one-line summary index.
///
/// Flow: `Memory::list` names → for each, try `Memory::read` for kind/description →
/// fall back to bare name if the file can't be parsed.
///
/// Return: `Ok` with the formatted index (never fails; missing dir yields "(no memory entries)").
fn list_all(ctx: &ToolCtx) -> Result<String> {
let names = Memory::list(&ctx.memory_dir);
if names.is_empty() {
+13
View File
@@ -1,9 +1,12 @@
//! Tool for saving a new memory entry to persistent project memory.
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::super::Tool;
use super::super::ToolCtx;
use crate::model::memory::Memory;
/// Tool that writes a new `Memory` entry (name/description/content/kind) to disk.
pub struct Remember;
impl Tool for Remember {
@@ -41,6 +44,16 @@ impl Tool for Remember {
})
}
/// Build a `Memory` from the given args and persist it to `ctx.memory_dir`.
///
/// Flow: extract name/description/content/kind → validate name via `Memory::slugify`
/// → construct `Memory` with `lifecycle: "new"` and current timestamps →
/// `memory.write`.
///
/// Why: name must slugify to a valid filename (alphanumeric + hyphens, 1-80 chars)
/// since it's used directly as the on-disk file identifier.
///
/// Return: confirmation string on success; error if name is invalid or the write fails.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let name = args.get("name")
.and_then(|v| v.as_str())
+39
View File
@@ -1,3 +1,5 @@
//! Tool trait, execution context, and the registry of all 28 built-in tools.
use std::path::PathBuf;
use serde_json::Value;
use anyhow::Result;
@@ -16,6 +18,7 @@ pub mod shell_filter;
pub mod utility;
pub mod workflow;
/// Common interface every agent-invocable tool implements: name, JSON schema, and execution.
pub trait Tool: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
@@ -23,6 +26,7 @@ pub trait Tool: Send + Sync {
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String>;
}
/// A project-defined rule that flags a matching file path or content pattern for review.
#[derive(Debug, Clone)]
pub struct GraduatedCheck {
pub name: String,
@@ -30,6 +34,8 @@ pub struct GraduatedCheck {
pub rule: String,
}
/// Shared execution context passed to every `Tool::run` call: workspace roots, session
/// paths, and cached directory state.
#[derive(Clone)]
pub struct ToolCtx {
pub workspaces: Vec<PathBuf>,
@@ -42,6 +48,12 @@ pub struct ToolCtx {
pub graduated_checks: Vec<GraduatedCheck>,
}
/// Find which graduated checks apply to a given file path/content pair.
///
/// Flow: for each check, match its `pattern` against `path` or its `rule` against
/// `content` (substring match) → collect matching check names.
///
/// Return: names of all matching checks; empty if none match.
pub fn check_graduated_checks(path: &str, content: &str, checks: &[GraduatedCheck]) -> Vec<String> {
let mut matches = Vec::new();
for check in checks {
@@ -53,11 +65,13 @@ pub fn check_graduated_checks(path: &str, content: &str, checks: &[GraduatedChec
}
impl ToolCtx {
/// Start building a `ToolCtx` with `ToolCtxBuilder`'s defaults.
pub fn builder() -> ToolCtxBuilder {
ToolCtxBuilder::default()
}
}
/// Builder for `ToolCtx`; fields default to empty paths and a fresh `DirCache`.
pub struct ToolCtxBuilder {
pub workspaces: Vec<PathBuf>,
pub session_dir: PathBuf,
@@ -85,8 +99,11 @@ impl Default for ToolCtxBuilder {
}
impl ToolCtxBuilder {
/// Set the session directory.
pub fn session_dir(mut self, v: PathBuf) -> Self { self.session_dir = v; self }
/// Set the origin (main process vs. daemon-attached).
pub fn origin(mut self, v: crate::app::state::types::Origin) -> Self { self.origin = v; self }
/// Consume the builder and produce the final `ToolCtx`.
pub fn build(self) -> ToolCtx {
ToolCtx {
workspaces: self.workspaces,
@@ -101,6 +118,10 @@ impl ToolCtxBuilder {
}
}
/// Construct one instance of every built-in tool, in the fixed order exposed to the LLM.
///
/// Return: boxed trait objects for all 28 tools (fs, search, bash, git, memory, plan,
/// workflow, utility).
pub fn all_tools() -> Vec<Box<dyn Tool>> {
vec![
Box::new(super::tool::fs::read::Read),
@@ -131,10 +152,16 @@ pub fn all_tools() -> Vec<Box<dyn Tool>> {
]
}
/// Whether a tool by name can mutate the filesystem or run arbitrary shell commands.
///
/// Why: used by the harness to decide which tool calls need user confirmation/guard checks.
pub fn tool_is_risky(name: &str) -> bool {
matches!(name, "write" | "delete" | "edit" | "bash" | "git_operator")
}
/// Convert a list of tools into the provider-facing `ToolDef` request schema.
///
/// Return: one `ToolDef` per tool, in the same order as `tools`.
pub fn tool_defs(tools: &[Box<dyn Tool>]) -> Vec<crate::dto::provider::request::ToolDef> {
tools
.iter()
@@ -149,6 +176,18 @@ pub fn tool_defs(tools: &[Box<dyn Tool>]) -> Vec<crate::dto::provider::request::
.collect()
}
/// Resolve a tool-supplied relative path to an absolute path within a workspace root,
/// rejecting escapes.
///
/// Flow: parse optional `[N]` workspace-index prefix (defaults to workspace 0) → join
/// remainder onto that workspace root → canonicalize → verify the canonical path
/// still starts with one of `workspaces`.
///
/// Why: canonicalizing and re-checking containment (rather than trusting the join)
/// prevents `../` traversal from escaping the sandboxed workspace roots.
///
/// Return: the canonical absolute path, or an error if the workspace index is invalid
/// or the resolved path falls outside all workspace roots.
pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
let _parts: Vec<&str> = rel.splitn(2, '/').collect();
let (ws_idx, path) = if rel.starts_with('[') {
+11
View File
@@ -1,8 +1,11 @@
//! Plan-mode signaling tools: entering plan mode with a proposal, and confirming readiness.
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::Tool;
use super::ToolCtx;
/// Tool the model calls to present a step-by-step plan and enter plan mode.
pub struct PlanEnter;
impl Tool for PlanEnter {
@@ -31,6 +34,10 @@ impl Tool for PlanEnter {
})
}
/// Validate that both `plan` and `sign_off` are present; the actual plan text is
/// surfaced to the user by the harness rather than returned here.
///
/// Return: fixed acknowledgement string on success; error if either arg is missing.
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let _plan = args.get("plan")
.and_then(|v| v.as_str())
@@ -42,6 +49,7 @@ impl Tool for PlanEnter {
}
}
/// Tool the model calls to confirm it will follow the approved plan before executing it.
pub struct PlanReady;
impl Tool for PlanReady {
@@ -66,6 +74,9 @@ impl Tool for PlanReady {
})
}
/// Validate that a `confirmation` argument was supplied before exiting plan mode.
///
/// Return: fixed "ready to execute" string on success; error if `confirmation` is missing.
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let _confirmation = args.get("confirmation")
.and_then(|v| v.as_str())
+25
View File
@@ -1,3 +1,5 @@
//! Text search tools: `grep` (line matching) and `glob` (filename pattern matching).
use std::fs;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
@@ -7,6 +9,7 @@ use super::Tool;
use super::ToolCtx;
use super::resolve_path;
/// Tool that recursively searches text files under a directory for a literal substring.
pub struct Grep;
impl Tool for Grep {
@@ -35,6 +38,16 @@ impl Tool for Grep {
})
}
/// Recursively walk the resolved directory and collect matching lines.
///
/// Flow: extract `pattern` + `path` → `resolve_path` (workspace-scoped) → bail if
/// missing/not a dir → `ignore::Walk` the tree → for each file, `read_to_string`
/// and substring-match each line → emit `<rel_path>:<line_no>:<text>` rows.
///
/// Why: `ignore::Walk` respects `.gitignore` and skips heavy dirs (e.g. `.git/`)
/// which is what the agent expects when running in real repos.
///
/// Return: "no matches found" if empty, else a header + `path:line:text` rows.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let pattern = args.get("pattern")
.and_then(|v| v.as_str())
@@ -80,6 +93,7 @@ impl Tool for Grep {
}
}
/// Tool that lists files under a directory matching a glob pattern.
pub struct Glob;
impl Tool for Glob {
@@ -108,6 +122,17 @@ impl Tool for Glob {
})
}
/// Walk the resolved directory and collect entries matching the glob pattern.
///
/// Flow: extract pattern + path → `resolve_path` → build a `GlobSet` from the
/// joined absolute pattern → `ignore::Walk` the tree → keep entries that
/// match → sort → join with newlines, appending `/` for directories.
///
/// Why: joining the workspace-relative pattern onto the resolved root lets users
/// supply familiar glob shapes (`**/*.rs`) while the sandbox still controls the
/// boundary.
///
/// Return: sorted newline-joined matches; "no files match" sentinel if empty.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let pat_str = args.get("pattern")
.and_then(|v| v.as_str())
+10
View File
@@ -1,8 +1,12 @@
//! Sequential-thinking tool: a no-side-effect echo that records reasoning steps.
use serde_json::{json, Value};
use anyhow::Result;
use super::Tool;
use super::ToolCtx;
/// Tool that accepts a reasoning step and returns it verbatim, giving the model a
/// structured way to surface its thought chain to the TUI.
pub struct SeqThink;
impl Tool for SeqThink {
@@ -27,6 +31,12 @@ impl Tool for SeqThink {
})
}
/// Return the `thought` string verbatim (or empty if missing).
///
/// Why: there's no I/O or state mutation — the harness surfaces the text in the
/// TUI's reasoning pane.
///
/// Return: the thought text, possibly empty; never an error.
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let thought = args.get("thought").and_then(|v| v.as_str()).unwrap_or("");
Ok(thought.to_string())
+17
View File
@@ -1,3 +1,5 @@
//! Bash-shell execution tool with safety filters and optional timeout.
use std::process::Command;
use std::time::Duration;
use serde_json::{json, Value};
@@ -5,6 +7,8 @@ use anyhow::{Result, anyhow};
use super::Tool;
use super::ToolCtx;
/// Tool that runs `bash -c <command>`, optionally in the background, with safety
/// filters applied before spawning.
pub struct Bash;
impl Tool for Bash {
@@ -41,6 +45,19 @@ impl Tool for Bash {
})
}
/// Run a bash command (foreground or background) with safety filters and a timeout.
///
/// Flow: extract args → run `check_credential_read` then `check_git_destructive`
/// (bail if either rejects) → branch on `run_in_background`: if true, hand off
/// to the bg-bash subsystem and return the job ID; else spawn `bash -c`,
/// poll with `try_wait`, kill on timeout, format combined stdout+stderr.
///
/// Why: the safety filters run unconditionally so background jobs are also gated;
/// the timeout is enforced by polling the child rather than relying on a libc alarm
/// so cleanup stays in Rust.
///
/// Return: exit-code + elapsed-seconds summary line (plus captured output) for
/// foreground runs, or the job ID for background runs.
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let cmd = args.get("command")
.and_then(|v| v.as_str())
+11
View File
@@ -1,5 +1,16 @@
//! Block shell commands that try to read common credential files or secrets.
use anyhow::Result;
/// Reject shell commands whose lowercased form contains any known credential-read pattern.
///
/// Flow: lowercase the command → for each pattern, substring-match → bail with the
/// matching pattern on the first hit.
///
/// Why: catches `cat ~/.ssh/id_rsa`, `grep token= foo.txt`, `.git-credentials`,
/// cloud-CLI credential paths, etc., before the bash tool spawns anything.
///
/// Return: `Ok(())` if no pattern matches; error naming the offending pattern otherwise.
pub fn check_credential_read(cmd: &str) -> Result<()> {
let patterns = [
"cat ~/.ssh",
+11
View File
@@ -1,5 +1,16 @@
//! Block shell commands that perform destructive or hard-to-reverse git operations.
use anyhow::Result;
/// Reject shell commands whose lowercased form contains any known destructive git pattern.
///
/// Flow: lowercase the command → for each pattern, substring-match → bail with the
/// matching pattern on the first hit.
///
/// Why: hard-resets, force-pushes, `clean -fdx`, `filter-branch`, etc. can destroy
/// uncommitted work or rewrite shared history; the bash tool refuses to run them.
///
/// Return: `Ok(())` if no pattern matches; error naming the offending pattern otherwise.
pub fn check_git_destructive(cmd: &str) -> Result<()> {
let patterns = [
"force-push",
+2
View File
@@ -1,2 +1,4 @@
//! Pre-execution safety filters applied to shell commands before they're spawned.
pub mod credentials;
pub mod git;
+13
View File
@@ -1,8 +1,11 @@
//! `cd` tool: verify and resolve a workspace-relative directory path.
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::super::Tool;
use super::super::ToolCtx;
/// Tool that resolves a workspace-relative path and reports whether it exists and is a dir.
pub struct Cd;
impl Tool for Cd {
@@ -27,6 +30,16 @@ impl Tool for Cd {
})
}
/// Resolve `path` against the workspace roots and report its status.
///
/// Flow: extract `path` → `resolve_path` (sandboxed to `ctx.workspaces`) →
/// check `exists()` and `is_dir()` → canonicalize → return canonical path.
///
/// Why: the agent has no persistent cwd between tool calls; "cd" here is purely a
/// verification + canonicalization helper rather than a state change.
///
/// Return: canonical path on success; explicit "does not exist" / "not a directory"
/// message (still `Ok`) so the model can react without treating it as an error.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = args.get("path")
.and_then(|v| v.as_str())
+32
View File
@@ -1,8 +1,19 @@
//! Tool for refreshing the shared workspace directory cache.
//!
//! Flow: resolve the requested path against the workspace roots →
//! non-recursively walk it → spin up a one-shot Tokio runtime (the agent
//! turn runs on a plain `std::thread` with no async context) → write the
//! entries into the shared `dir_cache` behind an async `RwLock`.
//!
//! Why: other tools rely on this cache for faster path resolution, so
//! it must be kept fresh on demand rather than only populated at startup.
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::super::Tool;
use super::super::ToolCtx;
/// Tool that refreshes the shared directory cache for a given path.
pub struct DirCacheUpdate;
impl Tool for DirCacheUpdate {
@@ -27,6 +38,19 @@ impl Tool for DirCacheUpdate {
})
}
/// Resolve `path`, walk its immediate entries, and store them in the shared cache.
///
/// Flow: extract `path` argument → resolve against workspace roots →
/// bail early with a plain message (not an error) if it doesn't exist
/// → `walk_directory` collects direct children → spawn a temporary
/// Tokio runtime to acquire the async `RwLock` write guard and call
/// `cache.set(entries)`.
///
/// Why: uses a fresh one-shot runtime instead of `ctx`'s own executor
/// because this tool can be invoked from a non-async thread.
///
/// Return: a confirmation string with the entry count, or an error if
/// the `path` argument is missing or the temp runtime fails to start.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = args.get("path")
.and_then(|v| v.as_str())
@@ -55,6 +79,14 @@ impl Tool for DirCacheUpdate {
}
}
/// Non-recursively list the immediate entries of `path`.
///
/// Flow: read_dir → flatten Ok entries → collect their paths.
///
/// Why: silently skips unreadable entries (e.g. permission errors)
/// rather than failing the whole cache update.
///
/// Return: paths of direct children; empty vec if `path` can't be read.
fn walk_directory(path: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut result = Vec::new();
if let Ok(entries) = std::fs::read_dir(path) {
+24
View File
@@ -1,9 +1,20 @@
//! Tool for listing the immediate contents of a workspace directory.
//!
//! Flow: resolve the requested path against workspace roots → validate
//! it exists and is a directory → read its direct children with
//! `fs::read_dir`, tagging subdirectories with a trailing `/` → format
//! into a header + newline-joined listing.
//!
//! Why: gives the agent a quick, one-level view of the workspace
//! structure without pulling in the full recursive directory cache.
use std::fs;
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::super::Tool;
use super::super::ToolCtx;
/// Tool that lists the immediate contents of a workspace directory.
pub struct DirList;
impl Tool for DirList {
@@ -28,6 +39,19 @@ impl Tool for DirList {
})
}
/// List the immediate entries of the requested workspace directory.
///
/// Flow: extract `path` argument → resolve against workspace roots →
/// short-circuit with a plain message if the path doesn't exist or
/// isn't a directory → `read_dir` → map each entry to its name
/// (appending `/` for subdirectories) → join into a formatted listing
/// with an entry-count header showing the canonicalized path.
///
/// Why: entries whose metadata fails to read (`e.ok()` filter) are
/// silently skipped rather than aborting the whole listing.
///
/// Return: header + newline-joined entry names, or an error if the
/// `path` argument is missing or `read_dir` fails outright.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let rel = args.get("path")
.and_then(|v| v.as_str())
+2
View File
@@ -1,3 +1,5 @@
//! Small standalone utility tools (cd, dir listing/caching, pong, todowrite).
pub mod cd;
pub mod dir_cache_update;
pub mod dir_list;
+9
View File
@@ -1,8 +1,17 @@
//! Trivial connectivity-check tool.
//!
//! Flow: read the optional `message` argument → echo it back prefixed
//! with `"pong: "`, defaulting to `"pong"` when no message is supplied.
//!
//! Why: gives callers a cheap, dependency-free way to verify the tool
//! harness is reachable and responding before running real work.
use serde_json::{json, Value};
use anyhow::Result;
use super::super::Tool;
use super::super::ToolCtx;
/// Tool that echoes back a message; used for connectivity/latency checks.
pub struct Pong;
impl Tool for Pong {
+22
View File
@@ -1,3 +1,13 @@
//! Tool for appending timestamped tasks to the session's todo list.
//!
//! Flow: extract the `task` argument → format a Markdown checkbox line
//! with a UTC timestamp → open `todo.md` in the session directory
//! (creating it if needed) in append mode → write the line.
//!
//! Why: the file lives under `ctx.session_dir` so it persists per
//! session and is picked up by the TUI's Todo panel; appending (rather
//! than rewriting) keeps prior tasks intact.
use std::fs;
use std::path::PathBuf;
use serde_json::{json, Value};
@@ -5,6 +15,7 @@ use anyhow::{Result, anyhow};
use super::super::Tool;
use super::super::ToolCtx;
/// Tool that appends a timestamped task line to the session's todo.md.
pub struct Todowrite;
impl Tool for Todowrite {
@@ -29,6 +40,17 @@ impl Tool for Todowrite {
})
}
/// Append a timestamped, unchecked task line to the session's `todo.md`.
///
/// Flow: extract `task` argument → build `- [ ] <task> (<timestamp>)`
/// line with a UTC `%Y-%m-%d %H:%M:%S` timestamp → open (create if
/// missing) `<session_dir>/todo.md` in append mode → write the line.
///
/// Why: append-only so the file acts as a running log rather than
/// requiring the agent to track and rewrite existing content.
///
/// Return: confirmation string echoing the added task, or an error
/// if the `task` argument is missing or the file can't be opened/written.
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let task = args.get("task")
.and_then(|v| v.as_str())
+39
View File
@@ -1,8 +1,23 @@
//! Tools for orchestrating multi-agent workflow runs.
//!
//! Flow: the LLM emits a `workflow_run` tool call with a JSON-encoded
//! `WorkflowScript` (Agent/Parallel/Pipeline/Phase primitives) which is
//! deserialized and handed to `app::workflow::engine::run_workflow` for
//! execution. Sibling agents spawned within the same run can share
//! ephemeral text via the `note_finding` tool, which forwards to
//! `app::workflow::engine::note_finding`.
//!
//! Why: decomposing a task into a workflow script lets the harness fan
//! out independent subtasks (parallel/pipeline/phased) instead of the
//! agent handling everything inline; simple tasks should skip this tool
//! entirely per its own description string.
use serde_json::{json, Value};
use anyhow::{Result, anyhow};
use super::Tool;
use super::ToolCtx;
/// Tool that parses and executes a JSON-encoded workflow script (Agent/Parallel/Pipeline/Phase).
pub struct WorkflowRun;
impl Tool for WorkflowRun {
@@ -31,6 +46,18 @@ impl Tool for WorkflowRun {
})
}
/// Parse the `script`/`args` tool arguments and execute the workflow.
///
/// Flow: extract `script` string → deserialize into `WorkflowScript` →
/// collect optional `args` object into a `HashMap<String, String>` for
/// `{{key}}` template substitution → delegate to
/// `app::workflow::engine::run_workflow`.
///
/// Why: template args are silently filtered to string values only
/// (non-string values are dropped rather than erroring).
///
/// Return: the workflow engine's output string, or an error if the
/// script argument is missing or fails to parse as JSON.
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let script_str = args.get("script")
.and_then(|v| v.as_str())
@@ -53,6 +80,7 @@ impl Tool for WorkflowRun {
}
}
/// Tool that shares a text finding with sibling agents in the current workflow run.
pub struct NoteFinding;
impl Tool for NoteFinding {
@@ -77,6 +105,17 @@ impl Tool for NoteFinding {
})
}
/// Record `text` as a finding visible to sibling agents in the run.
///
/// Flow: extract `text` argument → forward to
/// `app::workflow::engine::note_finding` → return a truncated
/// confirmation echo.
///
/// Why: findings are ephemeral (not persisted to memory) and are
/// meant to be prepended to sibling agents' next tool-round context.
///
/// Return: confirmation string containing up to the first 80 chars
/// of the recorded text.
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
let text = args.get("text")
.and_then(|v| v.as_str())