refactor(split): extract dependencies, snapshot, lsp and repl crates (S15, CW-6129) [skip ci]

New crates `codewhale-dependencies` (external-tool resolution and bounded
subprocess probes), `codewhale-snapshot` (pre/post-turn side-repo
snapshots), `codewhale-lsp` (language-server client) and `codewhale-repl`
(Python REPL runtime and RLM context files; `write_context_file` lives
here). Callers rewired, no facades; FD-04 forks gated like S14.
Implemented on Boat VM vm3 by the split session; integrated by the
coordinator. Verification in the S16 commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-27 17:25:41 +00:00
1 parent 847fdf3c3a
commit ecc5deb692
124 files changed
+1032 -739

No files matched your search

+1 -1
View File
@@ -555,7 +555,7 @@ A well-structured PR follows a consistent pattern. Recent exemplars include:
- **#386** — `/init` command: new `crates/tui/src/commands/groups/project/init.rs` module, project-type detection,
AGENTS.md generation, command registration in `commands/mod.rs`, localization strings.
- **#389** — Inline LSP diagnostics: LSP subsystem in `crates/tui/src/lsp/`, engine hooks in
- **#389** — Inline LSP diagnostics: LSP subsystem in `crates/lsp/src/`, engine hooks in
`crates/tui/src/core/engine/lsp_hooks.rs`, config toggle, test coverage.
- **#387** — Self-update: new `crates/cli/src/update.rs` module, CLI subcommand registration,
HTTP download + SHA256 verification + atomic binary replacement.
Generated
+71
View File
@@ -1008,6 +1008,24 @@ dependencies = [
"uuid",
]
[[package]]
name = "codewhale-dependencies"
version = "0.10.2"
dependencies = [
"anyhow",
"codewhale-config",
"codewhale-paths",
"codewhale-runtime",
"codewhale-util",
"serde",
"serde_json",
"tempfile",
"tokio",
"tracing",
"uuid",
"wait-timeout",
]
[[package]]
name = "codewhale-execpolicy"
version = "0.10.2"
@@ -1068,6 +1086,24 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "codewhale-lsp"
version = "0.10.2"
dependencies = [
"anyhow",
"async-trait",
"codewhale-config",
"codewhale-runtime",
"codewhale-util",
"libc",
"reqwest",
"serde",
"serde_json",
"tempfile",
"tokio",
"tracing",
]
[[package]]
name = "codewhale-mcp"
version = "0.10.2"
@@ -1226,6 +1262,18 @@ dependencies = [
"webpki-roots",
]
[[package]]
name = "codewhale-repl"
version = "0.10.2"
dependencies = [
"codewhale-dependencies",
"codewhale-runtime",
"serde",
"serde_json",
"tokio",
"uuid",
]
[[package]]
name = "codewhale-runtime"
version = "0.10.2"
@@ -1333,6 +1381,25 @@ dependencies = [
"tracing",
]
[[package]]
name = "codewhale-snapshot"
version = "0.10.2"
dependencies = [
"codewhale-config",
"codewhale-dependencies",
"codewhale-paths",
"codewhale-runtime",
"codewhale-util",
"fd-lock",
"ignore",
"libc",
"serde",
"sha2 0.11.1",
"tempfile",
"tracing",
"wait-timeout",
]
[[package]]
name = "codewhale-state"
version = "0.10.2"
@@ -1405,10 +1472,12 @@ dependencies = [
"codewhale-commands",
"codewhale-config",
"codewhale-core",
"codewhale-dependencies",
"codewhale-execpolicy",
"codewhale-hooks",
"codewhale-lane",
"codewhale-localization",
"codewhale-lsp",
"codewhale-mcp",
"codewhale-memory",
"codewhale-models",
@@ -1419,10 +1488,12 @@ dependencies = [
"codewhale-protocol",
"codewhale-ratatui",
"codewhale-release",
"codewhale-repl",
"codewhale-runtime",
"codewhale-sandbox",
"codewhale-sanitize",
"codewhale-secrets",
"codewhale-snapshot",
"codewhale-telemetry",
"codewhale-tools",
"codewhale-util",
+4
View File
@@ -32,6 +32,10 @@ members = [
"crates/tools",
"crates/tui",
"crates/util",
"crates/dependencies",
"crates/snapshot",
"crates/lsp",
"crates/repl",
"crates/workflow",
"crates/workflow-js",
"tests/portable-session",
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "codewhale-dependencies"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Codewhale external tool resolution and bounded subprocess probes"
[lints]
workspace = true
[features]
test-support = []
[dependencies]
anyhow.workspace = true
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-paths = { path = "../paths", version = "0.10.2" }
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
codewhale-util = { path = "../util", version = "0.10.2" }
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true
wait-timeout = "0.2"
[dev-dependencies]
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
@@ -25,9 +25,12 @@
//! — probing a binary involves a `Command::output` per candidate and
//! we'd rather not pay that on every model turn.
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Command, Output};
use std::sync::OnceLock;
use std::time::Duration;
use wait_timeout::ChildExt;
/// Candidate executable names for the Python interpreter, in the
/// order we try them. On Windows the launcher convention is `py -3`,
@@ -128,7 +131,7 @@ fn probe_output(
probe_output_bounded(command, capture, timeout, VERSION_PROBE_MAX_OUTPUT)
}
pub(crate) fn probe_output_bounded(
pub fn probe_output_bounded(
command: &mut Command,
capture: bool,
timeout: std::time::Duration,
@@ -136,7 +139,6 @@ pub(crate) fn probe_output_bounded(
) -> std::io::Result<std::process::Output> {
use std::io::Read;
use std::process::Stdio;
use wait_timeout::ChildExt;
command
.stdin(Stdio::null())
.stderr(Stdio::null())
@@ -370,8 +372,8 @@ pub fn resolve_pandoc() -> Option<String> {
/// a converter, an OCR engine) is available: `probe` decides, except that a
/// test fixture answers with the recorded host's set so results do not
/// depend on what the machine running them has installed (test builds only).
pub(crate) fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> bool {
#[cfg(test)]
pub fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> bool {
#[cfg(any(test, feature = "test-support"))]
if let Some(available) = RECORDED_HOST_TOOLS.with(|cell| {
cell.borrow()
.as_ref()
@@ -380,12 +382,12 @@ pub(crate) fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> b
return available;
}
// Only test fixtures read the name.
#[cfg(not(test))]
#[cfg(not(any(test, feature = "test-support")))]
let _ = tool;
probe()
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static RECORDED_HOST_TOOLS: std::cell::RefCell<Option<Vec<String>>> =
const { std::cell::RefCell::new(None) };
@@ -393,16 +395,16 @@ thread_local! {
/// Pin [`host_tool_available`] on this thread to a recorded host's tools until
/// the guard drops.
#[cfg(test)]
pub(crate) fn pin_recorded_host_tools(tools: Vec<String>) -> RecordedHostToolsGuard {
#[cfg(any(test, feature = "test-support"))]
pub fn pin_recorded_host_tools(tools: Vec<String>) -> RecordedHostToolsGuard {
RECORDED_HOST_TOOLS.with(|cell| *cell.borrow_mut() = Some(tools));
RecordedHostToolsGuard
}
#[cfg(test)]
pub(crate) struct RecordedHostToolsGuard;
#[cfg(any(test, feature = "test-support"))]
pub struct RecordedHostToolsGuard;
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
impl Drop for RecordedHostToolsGuard {
fn drop(&mut self) {
RECORDED_HOST_TOOLS.with(|cell| *cell.borrow_mut() = None);
@@ -695,7 +697,11 @@ fn probe_command(path: &Path) -> Command {
cmd
}
fn probe_runtime_version(kind: HostRuntimeKind, path: &Path) -> Result<(u32, u32, u32), String> {
fn probe_runtime_version(
kind: HostRuntimeKind,
path: &Path,
bundle_sha256: &str,
) -> Result<(u32, u32, u32), String> {
// Only absolute candidates are run: a relative `PATH` entry resolves
// against the current (workspace) directory, where a repository could
// plant a `node`.
@@ -715,7 +721,7 @@ fn probe_runtime_version(kind: HostRuntimeKind, path: &Path) -> Result<(u32, u32
return Err(format!("does not run (exit {})", output.status));
}
if compiled {
return parse_compiled_host_info(&output.stdout, crate::extension_host::bundle_sha256());
return parse_compiled_host_info(&output.stdout, bundle_sha256);
}
let banner = String::from_utf8_lossy(&output.stdout);
kind.parse(&banner)
@@ -858,9 +864,13 @@ fn untrusted_location(
/// runtime with its reason, rather than falling through to a search.
/// Otherwise the search candidates, minus those in an [`untrusted_location`],
/// which are recorded as rejected without being run. Blocking.
fn resolve_runtime(kind: HostRuntimeKind, override_path: Option<&Path>) -> NodeResolution {
fn resolve_runtime(
kind: HostRuntimeKind,
override_path: Option<&Path>,
bundle_sha256: &str,
) -> NodeResolution {
if let Some(path) = override_path {
return select_runtime(kind, vec![path.to_path_buf()]);
return select_runtime(kind, vec![path.to_path_buf()], bundle_sha256);
}
let cwd = std::env::current_dir().ok();
let home = codewhale_paths::user_home();
@@ -887,7 +897,7 @@ fn resolve_runtime(kind: HostRuntimeKind, override_path: Option<&Path>) -> NodeR
}
})
.collect();
let mut resolution = select_runtime(kind, candidates);
let mut resolution = select_runtime(kind, candidates, bundle_sha256);
skipped.append(&mut resolution.rejected);
resolution.rejected = skipped;
resolution
@@ -902,11 +912,12 @@ pub fn resolve_extension_host_runtime(
choice: codewhale_config::extension_host::ExtensionHostRuntime,
node_override: Option<&Path>,
bun_override: Option<&Path>,
bundle_sha256: &str,
) -> HostRuntimeResolution {
select_host_runtime(
choice,
|| resolve_runtime(HostRuntimeKind::Bun, bun_override),
|| resolve_runtime(HostRuntimeKind::Node, node_override),
|| resolve_runtime(HostRuntimeKind::Bun, bun_override, bundle_sha256),
|| resolve_runtime(HostRuntimeKind::Node, node_override, bundle_sha256),
)
}
@@ -962,7 +973,11 @@ fn accepted_flags(path: &Path, flags: &[&'static str]) -> Vec<&'static str> {
.collect()
}
fn select_runtime(kind: HostRuntimeKind, candidates: Vec<PathBuf>) -> NodeResolution {
fn select_runtime(
kind: HostRuntimeKind,
candidates: Vec<PathBuf>,
bundle_sha256: &str,
) -> NodeResolution {
let mut seen = std::collections::HashSet::new();
let mut resolution = NodeResolution::default();
for candidate in candidates {
@@ -971,7 +986,7 @@ fn select_runtime(kind: HostRuntimeKind, candidates: Vec<PathBuf>) -> NodeResolu
if !seen.insert(candidate.clone()) {
continue;
}
match probe_runtime_version(kind, &candidate) {
match probe_runtime_version(kind, &candidate, bundle_sha256) {
Ok(version) if kind.supported(version) => {
resolution.selected = Some((candidate, version));
break;
@@ -1053,7 +1068,6 @@ pub trait ExternalTool {
/// Convenience: run the tool with arguments and return only the
/// exit status (discards stdout/stderr).
#[cfg_attr(not(test), expect(dead_code))]
fn status(args: &[&str], cwd: &std::path::Path) -> std::io::Result<std::process::ExitStatus> {
let mut cmd = Self::command().ok_or_else(Self::not_found_error)?;
cmd.args(args).current_dir(cwd).status()
@@ -1118,7 +1132,7 @@ pub struct Git;
/// This is the single definition site; [`Git::command`] and
/// [`Git::tokio_command`] apply it to every product git spawn. Call it
/// directly only for a non-git program that may shell out to git (`gh`).
pub(crate) fn apply_git_noninteractive_env(cmd: &mut Command) {
pub fn apply_git_noninteractive_env(cmd: &mut Command) {
cmd.env("GIT_TERMINAL_PROMPT", "0").env("GIT_PAGER", "");
if std::env::var_os("GIT_SSH_COMMAND").is_none() && std::env::var_os("GIT_SSH").is_none() {
cmd.env("GIT_SSH_COMMAND", "ssh -o BatchMode=yes");
@@ -1133,7 +1147,7 @@ impl Git {
/// only. A read that touches the working tree also runs the superproject's
/// clean filters, which no flag disables: build it from
/// [`Self::review_command`] too.
pub(crate) const REVIEW_DIFF_ARGS: [&'static str; 4] = [
pub const REVIEW_DIFF_ARGS: [&'static str; 4] = [
"--no-ext-diff",
"--no-textconv",
"--submodule=short",
@@ -1145,7 +1159,7 @@ impl Git {
/// Construct a read-only review command with content conversion disabled.
/// Review callers also pass [`Self::REVIEW_DIFF_ARGS`] for diffs.
/// Configured filters otherwise execute even when those flags are present.
pub(crate) fn review_command(workspace: &Path) -> anyhow::Result<Command> {
pub fn review_command(workspace: &Path) -> anyhow::Result<Command> {
let overrides = Self::review_filter_overrides(workspace)?;
let mut command = Self::review_base(workspace)?;
let mut count = 2;
@@ -1162,7 +1176,7 @@ impl Git {
/// Git with fsmonitor, hooks, lazy fetch and replace objects disabled,
/// running in `workspace`. [`Self::review_command`] adds filter overrides.
pub(crate) fn review_base(workspace: &Path) -> anyhow::Result<Command> {
pub fn review_base(workspace: &Path) -> anyhow::Result<Command> {
use anyhow::Context;
let mut command = Self::command().context("git not found on PATH")?;
@@ -1187,7 +1201,7 @@ impl Git {
.env("GIT_CONFIG_KEY_0", "codewhale.reviewConfigCapability")
.env("GIT_CONFIG_VALUE_0", &value)
.args(["config", "--get", "codewhale.reviewConfigCapability"]);
match crate::snapshot::repo::run_bounded_git_with_timeout(
match run_bounded_git_with_timeout(
&mut probe,
"config",
Self::REVIEW_CONFIG_TIMEOUT,
@@ -1238,7 +1252,7 @@ impl Git {
/// Config overrides (`key`, `value`) that neutralize every clean/process
/// filter driver configured for the repository at `workspace`. Callers
/// apply them through `GIT_CONFIG_KEY_n`/`GIT_CONFIG_VALUE_n`.
pub(crate) fn review_filter_overrides(
pub fn review_filter_overrides(
workspace: &Path,
) -> anyhow::Result<Vec<(String, &'static str)>> {
use anyhow::{Context, bail};
@@ -1251,12 +1265,9 @@ impl Git {
"--get-regexp",
r"^filter\..*\.(clean|process|required)$",
]);
let output = crate::snapshot::repo::run_bounded_git_with_timeout(
&mut command,
"config",
Self::REVIEW_CONFIG_TIMEOUT,
)
.context("Failed to inspect Git review filters")?;
let output =
run_bounded_git_with_timeout(&mut command, "config", Self::REVIEW_CONFIG_TIMEOUT)
.context("Failed to inspect Git review filters")?;
let no_filters =
output.status.code() == Some(1) && output.stdout.is_empty() && output.stderr.is_empty();
if (!output.status.success() && !no_filters)
@@ -1517,6 +1528,126 @@ pub fn split_interpreter_spec(spec: &str) -> (String, Vec<String>) {
(program, args)
}
/// Grace granted to the pipe readers after git has exited. A clean git
/// closes its own write ends, so EOF is already waiting; only a grandchild
/// that inherited the pipes (a post-checkout hook, a `git gc` pack worker)
/// can hold them past exit, and it must not hold the turn pipeline either.
const GIT_PIPE_DRAIN_GRACE: Duration = Duration::from_secs(2);
/// Run a pre-configured git command under `timeout` with both pipes drained
/// while the child runs (the same concurrent drain `Command::output`
/// performs). A wedged git degrades to an error instead of hanging the caller.
///
/// The timeout path kills the child and reaps it on a detached thread: on a
/// hard-wedged mount git can sit in uninterruptible kernel I/O where even
/// SIGKILL is deferred.
pub fn run_bounded_git_with_timeout(
cmd: &mut std::process::Command,
subcommand: &str,
timeout: Duration,
) -> io::Result<Output> {
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn()?;
// Drain both pipes while waiting: the restore path's `ls-tree -r` and
// the diff commands emit output that grows with workspace size, and a
// child blocked on a full pipe buffer never exits — it would turn every
// such call into a guaranteed timeout. Readers stream into shared
// buffers, and the completion channel lets the collect below bound how
// long a grandchild-held pipe may hold the call after git has exited.
let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
let stdout_pipe = child.stdout.take();
let stderr_pipe = child.stderr.take();
let stdout_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let stderr_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
{
let buf = std::sync::Arc::clone(&stdout_buf);
let done_tx = done_tx.clone();
std::thread::spawn(move || {
if let Some(mut reader) = stdout_pipe {
read_pipe_to_buffer(&mut reader, &buf);
}
let _ = done_tx.send(());
});
}
{
let buf = std::sync::Arc::clone(&stderr_buf);
let done_tx = done_tx.clone();
std::thread::spawn(move || {
if let Some(mut reader) = stderr_pipe {
read_pipe_to_buffer(&mut reader, &buf);
}
let _ = done_tx.send(());
});
}
drop(done_tx);
let Some(status) = child.wait_timeout(timeout)? else {
let _ = child.kill();
// Reap off the pipeline thread (see the doc comment): the kernel may
// not deliver the kill until an uninterruptible syscall returns.
std::thread::spawn(move || {
let _ = child.wait();
});
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!("git {subcommand} timed out after {}s", timeout.as_secs()),
));
};
// Wait for the readers up to the grace, then return what was captured. A
// clean git already closed its write ends, so both readers deliver
// immediately; the grace only covers a pipe still held open by an
// inherited copy (a daemonizing hook). On expiry the captured output is
// returned with a note on stderr instead of silently truncating; the
// reader itself ends whenever whatever holds the pipe exits, holding
// only a pipe read — never the turn pipeline.
let mut partial = false;
let mut drained = 0;
while drained < 2 {
match done_rx.recv_timeout(GIT_PIPE_DRAIN_GRACE) {
Ok(()) => drained += 1,
Err(_) => {
partial = true;
break;
}
}
}
let stdout = stdout_buf.lock().map(|buf| buf.clone()).unwrap_or_default();
let mut stderr = stderr_buf.lock().map(|buf| buf.clone()).unwrap_or_default();
if partial {
if !stderr.is_empty() && stderr.last() != Some(&b'\n') {
stderr.push(b'\n');
}
stderr.extend_from_slice(
b"[codewhale] git output pipes did not close after git exited \
(kept open by a hook or subprocess?); captured output may be partial\n",
);
}
Ok(Output {
status,
stdout,
stderr,
})
}
/// Read a child's pipe to end into a shared buffer, tolerating interruption.
fn read_pipe_to_buffer(reader: &mut impl io::Read, buf: &std::sync::Mutex<Vec<u8>>) {
let mut chunk = [0_u8; 8192];
loop {
match reader.read(&mut chunk) {
Ok(0) => return,
Ok(read) => {
if let Ok(mut buf) = buf.lock() {
buf.extend_from_slice(&chunk[..read]);
}
}
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
Err(_) => return,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1564,10 +1695,13 @@ mod tests {
#[cfg(unix)]
#[test]
fn version_probes_scrub_credentials_preloads_and_close_stdin() {
let _home = crate::test_support::SealedHome::new();
let _secret =
crate::test_support::EnvVarGuard::set("DEEPSEEK_API_KEY", "synthetic-probe-secret");
let _preload = crate::test_support::EnvVarGuard::set("NODE_OPTIONS", "synthetic-preload");
let _home = codewhale_runtime::test_support::SealedHome::new();
let _secret = codewhale_runtime::test_support::EnvVarGuard::set(
"DEEPSEEK_API_KEY",
"synthetic-probe-secret",
);
let _preload =
codewhale_runtime::test_support::EnvVarGuard::set("NODE_OPTIONS", "synthetic-preload");
let mut command = version_probe_command("/bin/sh");
command.args(["-c", "[ -z \"${DEEPSEEK_API_KEY+x}\" ] && [ -z \"${NODE_OPTIONS+x}\" ] && ! read ignored && printf clean"]);
let output = probe_output(&mut command, true, VERSION_PROBE_TIMEOUT).unwrap();
@@ -1674,6 +1808,7 @@ mod tests {
good.clone(),
later,
],
"",
);
assert_eq!(resolution.selected, Some((good, (22, 20, 0))));
assert_eq!(resolution.rejected.len(), 3);
@@ -1720,6 +1855,7 @@ mod tests {
select_runtime(
kind,
candidates.iter().map(|path| (*path).clone()).collect(),
"",
)
};
@@ -1869,7 +2005,8 @@ mod tests {
// A failing override never falls through to a search.
let broken = script("broken-bun", "exit 3");
let bun_only = resolve_extension_host_runtime(Choice::Bun, None, Some(broken.as_path()));
let bun_only =
resolve_extension_host_runtime(Choice::Bun, None, Some(broken.as_path()), "");
assert!(bun_only.selected.is_none());
let failure = bun_only.failure();
assert!(
@@ -1883,6 +2020,7 @@ mod tests {
Choice::Auto,
Some(node.as_path()),
Some(broken.as_path()),
"",
);
let summary = auto.summary();
assert!(summary.starts_with("node 24.1.0 at "), "{summary}");
@@ -2227,8 +2365,8 @@ mod tests {
#[cfg(unix)]
#[test]
fn runtime_commands_do_not_inherit_parent_secret_env() {
let _env_lock = crate::test_support::lock_test_env();
let _secret = crate::test_support::EnvVarGuard::set(
let _env_lock = codewhale_runtime::test_support::lock_test_env();
let _secret = codewhale_runtime::test_support::EnvVarGuard::set(
"CODEWHALE_TEST_RUNTIME_SECRET",
"runtime-secret-value",
);
@@ -2264,9 +2402,11 @@ mod tests {
if !Git::available() {
return;
}
let _env_lock = crate::test_support::lock_test_env();
let _secret =
crate::test_support::EnvVarGuard::set("CODEWHALE_TEST_GIT_SECRET", "git-secret-value");
let _env_lock = codewhale_runtime::test_support::lock_test_env();
let _secret = codewhale_runtime::test_support::EnvVarGuard::set(
"CODEWHALE_TEST_GIT_SECRET",
"git-secret-value",
);
let repo = tempfile::tempdir().expect("repo");
let hooks = tempfile::tempdir().expect("hooks");
let marker = hooks.path().join("seen");
@@ -2366,7 +2506,7 @@ mod tests {
assert!(!repo.join("helper-seen").exists());
return;
}
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let real_git = resolve_executable_path("git", "--version").expect("absolute native Git");
let repo = tempfile::tempdir().unwrap();
let wrapper_dir = tempfile::tempdir().unwrap();
@@ -2441,7 +2581,7 @@ mod tests {
use std::os::unix::fs::PermissionsExt;
const CHILD: &str = "CODEWHALE_TEST_GIT_REVIEW_PATH_CHANGE";
if let Some(repo) = std::env::var_os(CHILD) {
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let repo = PathBuf::from(repo);
let initial = Git::review_command(&repo)
.unwrap()
@@ -2451,7 +2591,7 @@ mod tests {
assert!(initial.status.success());
assert_eq!(initial.stdout, b"false\n");
let changed = std::env::var_os("CODEWHALE_TEST_CHANGED_GIT_PATH").unwrap();
let _path = crate::test_support::EnvVarGuard::set("PATH", changed);
let _path = codewhale_runtime::test_support::EnvVarGuard::set("PATH", changed);
let output = Git::review_command(&repo)
.unwrap()
.args(["status", "--porcelain"])
@@ -2466,7 +2606,7 @@ mod tests {
);
return;
}
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let real_git = resolve_executable_path("git", "--version").expect("absolute native Git");
let quoted_git = real_git.replace('\'', "'\\''");
let repo = tempfile::tempdir().unwrap();
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "codewhale-lsp"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Codewhale language server client and diagnostics"
[lints]
workspace = true
[features]
test-support = ["codewhale-runtime/test-support"]
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
codewhale-util = { path = "../util", version = "0.10.2" }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
tokio.workspace = true
tracing.workspace = true
[dev-dependencies]
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
[target.'cfg(unix)'.dependencies]
libc.workspace = true
@@ -692,7 +692,7 @@ fn parse_publish_diagnostics(value: &Value) -> Option<DiagnosticMessage> {
}
/// Encode an absolute filesystem path without following its links.
pub(crate) fn uri_from_path(path: &Path) -> String {
pub fn uri_from_path(path: &Path) -> String {
reqwest::Url::from_file_path(path)
.map(|url| url.to_string())
.unwrap_or_default()
@@ -715,7 +715,8 @@ pub(super) fn path_from_uri(uri: &str) -> Option<PathBuf> {
url.to_file_path().ok()
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
#[cfg_attr(not(test), allow(dead_code, unused_imports))]
pub(super) mod tests {
use super::*;
@@ -968,7 +969,7 @@ pub(super) mod tests {
assert!(parse_publish_diagnostics(&payload).is_none());
}
#[cfg(unix)]
pub(crate) const STDIO_FIXTURE: &str = r#"
pub const STDIO_FIXTURE: &str = r#"
import json, os, select, sys, time
mode, pid_path = sys.argv[1:]
input_stream = os.fdopen(0, 'rb', buffering=0)
@@ -1128,7 +1129,7 @@ while True:
#[cfg(unix)]
#[tokio::test]
async fn language_server_does_not_inherit_parent_secret_env() {
use crate::test_support::{EnvVarGuard, lock_test_env};
use codewhale_runtime::test_support::{EnvVarGuard, lock_test_env};
let _env_lock = lock_test_env();
let _secret = EnvVarGuard::set("CODEWHALE_TEST_LSP_SECRET", "lsp-secret-value");
let root = tempfile::tempdir().unwrap();
File renamed without changes.
@@ -112,7 +112,7 @@ impl Default for LspConfig {
impl LspConfig {
/// Resolve `(command, args)` for `lang`. User-supplied overrides take
/// precedence over the built-in registry.
pub(crate) fn resolve_command(&self, lang: Language) -> Option<(String, Vec<String>)> {
pub fn resolve_command(&self, lang: Language) -> Option<(String, Vec<String>)> {
if let Some(parts) = self.servers.get(lang.as_key())
&& let Some((first, rest)) = parts.split_first()
{
@@ -153,32 +153,32 @@ pub struct LspManager {
/// best-effort post-edit hook, this preserves the distinction between an
/// honest empty result, a server/read error, and a timeout.
#[derive(Debug)]
pub(crate) struct LintReadResult {
pub(crate) file: PathBuf,
pub(crate) status: LintReadStatus,
pub(crate) items: Vec<Diagnostic>,
pub(crate) freshness: DiagnosticFreshness,
pub struct LintReadResult {
pub file: PathBuf,
pub status: LintReadStatus,
pub items: Vec<Diagnostic>,
pub freshness: DiagnosticFreshness,
/// Number of diagnostics after severity selection but before the
/// configured per-file cap was applied. Unknown when the request did not
/// complete successfully.
pub(crate) total_diagnostic_count: Option<usize>,
pub(crate) truncated: bool,
pub total_diagnostic_count: Option<usize>,
pub truncated: bool,
}
#[derive(Debug)]
pub(crate) enum LintReadStatus {
pub enum LintReadStatus {
Success,
Error(String),
Timeout { wait_ms: u64 },
}
#[derive(Debug, Default, serde::Serialize)]
pub(crate) struct DiagnosticFreshness {
pub(crate) source_revision: Option<String>,
pub(crate) document_version: Option<i64>,
pub(crate) diagnostic_version: Option<i64>,
pub struct DiagnosticFreshness {
pub source_revision: Option<String>,
pub document_version: Option<i64>,
pub diagnostic_version: Option<i64>,
/// Only an exact publication version match proves the text was checked.
pub(crate) freshness: Option<&'static str>,
pub freshness: Option<&'static str>,
}
struct DiagnosticPollSuccess {
@@ -259,7 +259,7 @@ impl LspManager {
/// Inject a fake transport for a language. Used by tests so we never
/// fork a real LSP server in CI.
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub async fn install_test_transport(&self, lang: Language, transport: Arc<dyn LspTransport>) {
self.test_transports.lock().await.insert(lang, transport);
}
@@ -800,7 +800,7 @@ impl LspManager {
/// transport pool. Every attempted file gets an explicit outcome, and the
/// same configured severity policy as post-edit diagnostics is applied:
/// errors are always retained and warnings depend on `include_warnings`.
pub(crate) async fn diagnostics_for_paths(
pub async fn diagnostics_for_paths(
&self,
files: &[PathBuf],
) -> Result<Vec<LintReadResult>, String> {
@@ -1024,28 +1024,29 @@ impl LspManager {
}
}
#[cfg(test)]
pub(crate) mod tests {
#[cfg(any(test, feature = "test-support"))]
#[cfg_attr(not(test), allow(dead_code, unused_imports))]
pub mod tests {
use super::*;
use async_trait::async_trait;
use std::sync::atomic::{AtomicUsize, Ordering};
/// Fake transport: returns a fixed list of diagnostics. Used by
/// integration tests so we never spawn a real LSP server in CI.
pub(crate) struct FakeTransport {
pub struct FakeTransport {
items: Vec<Diagnostic>,
calls: AtomicUsize,
}
impl FakeTransport {
pub(crate) fn new(items: Vec<Diagnostic>) -> Self {
pub fn new(items: Vec<Diagnostic>) -> Self {
Self {
items,
calls: AtomicUsize::new(0),
}
}
pub(crate) fn call_count(&self) -> usize {
pub fn call_count(&self) -> usize {
self.calls.load(Ordering::Relaxed)
}
}
@@ -1057,7 +1058,7 @@ pub(crate) mod tests {
_path: &Path,
_text: &str,
_wait: Duration,
) -> anyhow::Result<crate::lsp::client::DiagnosticPublication> {
) -> anyhow::Result<crate::client::DiagnosticPublication> {
self.calls.fetch_add(1, Ordering::Relaxed);
Ok(self.items.clone().into())
}
File renamed without changes.
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "codewhale-repl"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Codewhale sandboxed Python REPL runtime"
[lints]
workspace = true
[features]
test-support = []
[dependencies]
codewhale-dependencies = { path = "../dependencies", version = "0.10.2" }
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
uuid.workspace = true
[dev-dependencies]
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
+25
View File
@@ -0,0 +1,25 @@
//! Long-lived Python REPL runtime used by the RLM loop and by inline
//! `` ```repl `` block execution in the agent loop.
pub mod runtime;
pub mod sandbox;
pub use runtime::{
BatchResp, PythonRuntime, ReplRound, RpcDispatcher, RpcRequest, RpcResponse, SingleResp,
};
pub use sandbox::{ReplBlock, extract_repl_blocks, has_repl_block};
use std::path::PathBuf;
use uuid::Uuid;
pub fn write_context_file(body: &str) -> std::io::Result<PathBuf> {
let dir = std::env::temp_dir().join("deepseek_rlm_ctx");
std::fs::create_dir_all(&dir)?;
let path = dir.join(format!(
"session_{}_{}.txt",
std::process::id(),
Uuid::new_v4().simple()
));
std::fs::write(&path, body)?;
Ok(path)
}
@@ -28,7 +28,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, ChildStdin, ChildStdout};
use uuid::Uuid;
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
use codewhale_runtime::child_env;
// ---------------------------------------------------------------------------
@@ -218,7 +218,7 @@ impl PythonRuntime {
let session_id = Uuid::new_v4().simple().to_string();
let bootstrap = render_bootstrap(&session_id);
let mut cmd = crate::dependencies::Python::tokio_command().ok_or_else(|| {
let mut cmd = codewhale_dependencies::Python::tokio_command().ok_or_else(|| {
"no Python interpreter found on PATH (tried python3, python, py -3). \
Install Python 3 and restart codewhale."
.to_string()
@@ -360,7 +360,7 @@ impl PythonRuntime {
/// acquire quoting semantics. The old owned file is released only after
/// the kernel has successfully loaded the replacement.
pub async fn replace_context(&mut self, body: &str) -> Result<(), String> {
let path = crate::rlm::session::write_context_file(body)
let path = crate::write_context_file(body)
.map_err(|e| format!("write refreshed REPL context: {e}"))?;
let path_literal = serde_json::to_string(&path.to_string_lossy())
.map_err(|e| format!("encode refreshed REPL context path: {e}"))?;
@@ -1,7 +1,7 @@
//! REPL fence-extraction utilities.
//!
//! The agent's main loop scans assistant text for ` ```repl ` fenced blocks
//! and feeds them to a [`crate::repl::runtime::PythonRuntime`]. Capturing
//! and feeds them to a [`crate::runtime::PythonRuntime`]. Capturing
//! `FINAL(...)` and routing sub-LLM RPCs are handled inside the runtime via
//! a stdin/stdout protocol — no scraping required here.
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "codewhale-snapshot"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Codewhale pre/post-turn workspace snapshots in a side git repo"
[lints]
workspace = true
[features]
test-support = ["codewhale-runtime/test-support"]
[dependencies]
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-dependencies = { path = "../dependencies", version = "0.10.2" }
codewhale-paths = { path = "../paths", version = "0.10.2" }
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
codewhale-util = { path = "../util", version = "0.10.2" }
fd-lock = "4.0.4"
ignore = "0.4"
serde.workspace = true
sha2.workspace = true
tempfile.workspace = true
tracing.workspace = true
wait-timeout = "0.2"
[dev-dependencies]
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
[target.'cfg(unix)'.dependencies]
libc.workspace = true
@@ -13,7 +13,7 @@ use std::process::{Command, Stdio};
use sha2::{Digest, Sha256};
use super::{SnapshotId, SnapshotRepo};
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
/// Blobs above this size are reported by size only: hashing them for a
/// Preview list is not worth the I/O, and the read route refuses them anyway.
@@ -61,7 +61,7 @@ struct RawChange {
}
fn git(repo: &SnapshotRepo) -> io::Result<Command> {
let mut command = crate::dependencies::Git::command()
let mut command = codewhale_dependencies::Git::command()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "git not found on PATH"))?;
command
.arg("--git-dir")
@@ -380,7 +380,7 @@ impl SnapshotRepo {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{EnvVarGuard, TestEnvLock, lock_test_env};
use codewhale_runtime::test_support::{EnvVarGuard, TestEnvLock, lock_test_env};
use std::fs;
struct Home {
File renamed without changes.
@@ -30,15 +30,15 @@ pub fn snapshot_dir_for(workspace: &Path) -> io::Result<PathBuf> {
/// The shared snapshot store. An unsealed test gets a private one: a snapshot
/// repo opened by a fixture must not land in the developer's real store.
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
fn snapshot_state_base() -> io::Result<PathBuf> {
match crate::test_support::unsealed_state_dir("snapshots") {
match codewhale_runtime::test_support::unsealed_state_dir("snapshots") {
Some(base) => Ok(base),
None => codewhale_config::resolve_state_dir("snapshots").map_err(io::Error::other),
}
}
#[cfg(not(test))]
#[cfg(not(any(test, feature = "test-support")))]
fn snapshot_state_base() -> io::Result<PathBuf> {
codewhale_config::resolve_state_dir("snapshots").map_err(io::Error::other)
}
@@ -68,7 +68,7 @@ pub fn ensure_snapshot_dir(workspace: &Path) -> io::Result<PathBuf> {
/// Strip a trailing `.worktrees/<name>` segment so all worktrees of the
/// same checkout share a `project_hash`. If the path doesn't look like a
/// worktree it's returned unchanged.
pub(crate) fn strip_worktree_suffix(path: &Path) -> PathBuf {
pub fn strip_worktree_suffix(path: &Path) -> PathBuf {
let mut components: Vec<_> = path.components().collect();
if components.len() >= 2
&& let Some(parent) = components.get(components.len() - 2)
@@ -87,10 +87,10 @@ pub(crate) fn strip_worktree_suffix(path: &Path) -> PathBuf {
/// Hex-encoded deterministic FNV-1a digest. This is only a directory tag, not
/// a security boundary, but it must remain stable across process launches.
///
/// `pub(crate)` because the workspace drive lease derives its own file layout
/// `pub` because the workspace drive lease derives its own file layout
/// from the same project/worktree split (`workspace_lease::WorkspaceDriveLease::lease_path`),
/// and two copies of this hash would be two things to keep in step.
pub(crate) fn stable_hex(path: &Path) -> String {
pub fn stable_hex(path: &Path) -> String {
let mut hash = 0xcbf2_9ce4_8422_2325u64;
for byte in path.to_string_lossy().as_bytes() {
hash ^= u64::from(*byte);
@@ -141,10 +141,11 @@ mod tests {
#[test]
fn explicit_profile_owns_snapshot_creation_and_lookup() {
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
let profile = tmp.path().join("selected-profile");
let _profile = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &profile);
let _profile =
codewhale_runtime::test_support::EnvVarGuard::set("CODEWHALE_HOME", &profile);
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace");
let expected = snapshot_dir_with_base(&workspace, &profile.join("snapshots"));
@@ -163,9 +164,10 @@ mod tests {
#[test]
fn invalid_profile_is_an_error_without_ambient_fallback() {
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let tmp = tempdir().expect("tempdir");
let _profile = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", "relative-profile");
let _profile =
codewhale_runtime::test_support::EnvVarGuard::set("CODEWHALE_HOME", "relative-profile");
assert!(snapshot_dir_for(tmp.path()).is_err());
assert!(snapshot_git_dir(tmp.path()).is_err());
assert!(ensure_snapshot_dir(tmp.path()).is_err());
@@ -34,7 +34,7 @@ pub fn prune_older_than(workspace: &Path, max_age: Duration) -> io::Result<usize
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{EnvVarGuard, lock_test_env};
use codewhale_runtime::test_support::{EnvVarGuard, lock_test_env};
use tempfile::tempdir;
#[test]
@@ -19,9 +19,7 @@ use std::path::{Component, Path, PathBuf};
use std::process::Output;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use wait_timeout::ChildExt as _;
use crate::dependencies::ExternalTool;
use codewhale_dependencies::{ExternalTool, run_bounded_git_with_timeout};
use super::paths::{ensure_snapshot_dir, snapshot_git_dir};
@@ -161,7 +159,7 @@ pub struct PathRestoreOutcome {
/// A snapshot as it was just written: its commit and the root tree it holds.
///
/// The commit id can be rewritten by the prune that follows every snapshot
/// once the repo holds more than [`crate::snapshot::DEFAULT_MAX_SNAPSHOTS`]
/// once the repo holds more than [`crate::DEFAULT_MAX_SNAPSHOTS`]
/// (see `rebuild_survivor_chain`); the tree id cannot, because a tree is
/// content-addressed and the rebuild re-commits the same tree.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -488,8 +486,8 @@ impl SnapshotRepo {
// and stores metadata in `.git`. We then continue to use
// explicit `--git-dir` / `--work-tree` flags for every other
// command so behaviour is invariant of cwd.
let mut git =
crate::dependencies::Git::command().ok_or_else(|| io_other("git not found on PATH"))?;
let mut git = codewhale_dependencies::Git::command()
.ok_or_else(|| io_other("git not found on PATH"))?;
let init = git.arg("init").arg("--quiet").arg(parent);
let init = run_bounded_git(init, "init")
.map_err(|e| io_other(format!("failed to run git init: {e}")))?;
@@ -782,8 +780,8 @@ impl SnapshotRepo {
}
/// Point the side repo's HEAD branch at a commit id that does not exist.
#[cfg(test)]
pub(crate) fn point_head_at_missing_commit_for_test(&self) {
#[cfg(any(test, feature = "test-support"))]
pub fn point_head_at_missing_commit_for_test(&self) {
let branch = run_git(&self.git_dir, &self.work_tree, &["symbolic-ref", "HEAD"])
.expect("symbolic-ref");
let branch = String::from_utf8_lossy(&branch.stdout).trim().to_string();
@@ -1452,7 +1450,7 @@ impl SnapshotRepo {
let index = scratch.path().join("index");
let run = |args: &[&str]| -> io::Result<String> {
let subcommand = args.first().copied().unwrap_or("git");
let mut cmd = crate::dependencies::Git::command()
let mut cmd = codewhale_dependencies::Git::command()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "git not found on PATH"))?;
cmd.env("GIT_INDEX_FILE", &index)
.arg("--git-dir")
@@ -2045,7 +2043,7 @@ impl SnapshotRepo {
/// age instead of stamping "now".
fn commit_tree_preserving_date(&self, args: &[&str], timestamp: i64) -> io::Result<String> {
let date = format!("{timestamp} +0000");
let mut command = crate::dependencies::Git::command()
let mut command = codewhale_dependencies::Git::command()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "git not found on PATH"))?;
command
.arg("--git-dir")
@@ -2080,7 +2078,7 @@ impl SnapshotRepo {
/// The survivors are rebuilt as a fresh orphan chain; each keeps its
/// tree, label, session id and timestamp, and the dropped ones become
/// unreachable for gc to reclaim.
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub fn prune_keep_last_n(&self, max_count: usize) -> io::Result<usize> {
self.with_write_lock(|| self.prune_keep_last_n_locked(max_count, 1))
}
@@ -2377,17 +2375,11 @@ fn cleanup_stale_pack_temps_in(
// snapshot-disabled-with-warning and proceeds without the git data. Tests
// use a tighter budget so a regression that deadlocks a child on its own
// output fails in seconds instead of hanging for the full window.
#[cfg(not(test))]
#[cfg(not(any(test, feature = "test-support")))]
const GIT_COMMAND_TIMEOUT: Duration = Duration::from_secs(300);
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
const GIT_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
/// Grace granted to the pipe readers after git has exited. A clean git
/// closes its own write ends, so EOF is already waiting; only a grandchild
/// that inherited the pipes (a post-checkout hook, a `git gc` pack worker)
/// can hold them past exit, and it must not hold the turn pipeline either.
const GIT_PIPE_DRAIN_GRACE: Duration = Duration::from_secs(2);
/// Run a pre-configured git command under [`GIT_COMMAND_TIMEOUT`] with both
/// pipes drained while the child runs (the same concurrent drain
/// `Command::output` performs). Every git invocation in this module goes
@@ -2406,115 +2398,8 @@ fn run_bounded_git(cmd: &mut std::process::Command, subcommand: &str) -> io::Res
run_bounded_git_with_timeout(cmd, subcommand, GIT_COMMAND_TIMEOUT)
}
pub(crate) fn run_bounded_git_with_timeout(
cmd: &mut std::process::Command,
subcommand: &str,
timeout: Duration,
) -> io::Result<Output> {
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn()?;
// Drain both pipes while waiting: the restore path's `ls-tree -r` and
// the diff commands emit output that grows with workspace size, and a
// child blocked on a full pipe buffer never exits — it would turn every
// such call into a guaranteed timeout. Readers stream into shared
// buffers, and the completion channel lets the collect below bound how
// long a grandchild-held pipe may hold the call after git has exited.
let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
let stdout_pipe = child.stdout.take();
let stderr_pipe = child.stderr.take();
let stdout_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let stderr_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
{
let buf = std::sync::Arc::clone(&stdout_buf);
let done_tx = done_tx.clone();
std::thread::spawn(move || {
if let Some(mut reader) = stdout_pipe {
read_pipe_to_buffer(&mut reader, &buf);
}
let _ = done_tx.send(());
});
}
{
let buf = std::sync::Arc::clone(&stderr_buf);
let done_tx = done_tx.clone();
std::thread::spawn(move || {
if let Some(mut reader) = stderr_pipe {
read_pipe_to_buffer(&mut reader, &buf);
}
let _ = done_tx.send(());
});
}
drop(done_tx);
let Some(status) = child.wait_timeout(timeout)? else {
let _ = child.kill();
// Reap off the pipeline thread (see the doc comment): the kernel may
// not deliver the kill until an uninterruptible syscall returns.
std::thread::spawn(move || {
let _ = child.wait();
});
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!("git {subcommand} timed out after {}s", timeout.as_secs()),
));
};
// Wait for the readers up to the grace, then return what was captured. A
// clean git already closed its write ends, so both readers deliver
// immediately; the grace only covers a pipe still held open by an
// inherited copy (a daemonizing hook). On expiry the captured output is
// returned with a note on stderr instead of silently truncating; the
// reader itself ends whenever whatever holds the pipe exits, holding
// only a pipe read — never the turn pipeline.
let mut partial = false;
let mut drained = 0;
while drained < 2 {
match done_rx.recv_timeout(GIT_PIPE_DRAIN_GRACE) {
Ok(()) => drained += 1,
Err(_) => {
partial = true;
break;
}
}
}
let stdout = stdout_buf.lock().map(|buf| buf.clone()).unwrap_or_default();
let mut stderr = stderr_buf.lock().map(|buf| buf.clone()).unwrap_or_default();
if partial {
if !stderr.is_empty() && stderr.last() != Some(&b'\n') {
stderr.push(b'\n');
}
stderr.extend_from_slice(
b"[codewhale] git output pipes did not close after git exited \
(kept open by a hook or subprocess?); captured output may be partial\n",
);
}
Ok(Output {
status,
stdout,
stderr,
})
}
/// Read a child's pipe to end into a shared buffer, tolerating interruption.
fn read_pipe_to_buffer(reader: &mut impl io::Read, buf: &std::sync::Mutex<Vec<u8>>) {
let mut chunk = [0_u8; 8192];
loop {
match reader.read(&mut chunk) {
Ok(0) => return,
Ok(read) => {
if let Ok(mut buf) = buf.lock() {
buf.extend_from_slice(&chunk[..read]);
}
}
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
Err(_) => return,
}
}
}
fn run_git(git_dir: &Path, work_tree: &Path, args: &[&str]) -> io::Result<Output> {
let mut cmd = crate::dependencies::Git::command()
let mut cmd = codewhale_dependencies::Git::command()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "git not found on PATH"))?;
cmd.arg("--git-dir")
.arg(git_dir)
@@ -2541,7 +2426,7 @@ fn io_other(msg: impl Into<String>) -> io::Error {
/// `text` whole, or the longest prefix that fits in `max_bytes` and ends on a
/// char boundary; the flag says which of the two the caller got.
pub(crate) fn truncate_at_char_boundary(text: &str, max_bytes: usize) -> (String, bool) {
pub fn truncate_at_char_boundary(text: &str, max_bytes: usize) -> (String, bool) {
if text.len() <= max_bytes {
return (text.to_string(), false);
}
@@ -2617,7 +2502,7 @@ pub fn estimate_workspace_size_bounded(
Ok(total)
}
pub(crate) fn unsafe_workspace_snapshot_reason(
pub fn unsafe_workspace_snapshot_reason(
workspace: &Path,
home: Option<&Path>,
) -> Option<&'static str> {
@@ -2716,7 +2601,7 @@ fn ancestor_is_not_a_directory(root: &Path, rel: &Path) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::lock_test_env;
use codewhale_runtime::test_support::lock_test_env;
use std::fs::{File, FileTimes};
use tempfile::tempdir;
@@ -2792,11 +2677,11 @@ mod tests {
/// owns the process-wide env-var mutex so tests across modules
/// don't trample each other's home env vars.
pub(super) struct ScopedHome {
_vars: Vec<crate::test_support::EnvVarGuard>,
_guard: crate::test_support::TestEnvLock,
_vars: Vec<codewhale_runtime::test_support::EnvVarGuard>,
_guard: codewhale_runtime::test_support::TestEnvLock,
}
pub(super) fn scoped_home(home: &Path) -> ScopedHome {
use crate::test_support::EnvVarGuard;
use codewhale_runtime::test_support::EnvVarGuard;
let guard = lock_test_env();
ScopedHome {
_vars: vec![
@@ -3274,9 +3159,9 @@ mod tests {
let held = peer.write().expect("peer holds the write lock");
let (done_tx, done_rx) = std::sync::mpsc::channel();
let env_ticket = crate::test_support::env_scope_ticket();
let env_ticket = codewhale_runtime::test_support::env_scope_ticket();
let opener = std::thread::spawn(move || {
let _membership = crate::test_support::join_env_scope(env_ticket);
let _membership = codewhale_runtime::test_support::join_env_scope(env_ticket);
let opened = SnapshotRepo::open_or_init(&workspace);
let _ = done_tx.send(());
opened
@@ -3919,7 +3804,7 @@ mod tests {
let tmp = tempdir().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
crate::dependencies::Git::command()
codewhale_dependencies::Git::command()
.expect("git not found")
.arg("-C")
.arg(&workspace)
@@ -3928,7 +3813,7 @@ mod tests {
.status()
.unwrap();
std::fs::write(workspace.join("tracked.txt"), b"committed").unwrap();
crate::dependencies::Git::command()
codewhale_dependencies::Git::command()
.expect("git not found")
.arg("-C")
.arg(&workspace)
@@ -3936,7 +3821,7 @@ mod tests {
.arg("tracked.txt")
.status()
.unwrap();
crate::dependencies::Git::command()
codewhale_dependencies::Git::command()
.expect("git not found")
.arg("-C")
.arg(&workspace)
@@ -3950,7 +3835,7 @@ mod tests {
.arg("init")
.status()
.unwrap();
let user_head_before = crate::dependencies::Git::command()
let user_head_before = codewhale_dependencies::Git::command()
.expect("git not found")
.arg("-C")
.arg(&workspace)
@@ -3967,7 +3852,7 @@ mod tests {
repo.snapshot("post-turn:1").unwrap();
repo.restore(&id).unwrap();
let user_head_after = crate::dependencies::Git::command()
let user_head_after = codewhale_dependencies::Git::command()
.expect("git not found")
.arg("-C")
.arg(&workspace)
+7
View File
@@ -111,6 +111,10 @@ codewhale-runtime = { path = "../runtime", version = "0.10.2" }
codewhale-sandbox = { path = "../sandbox", version = "0.10.2" }
codewhale-secrets = { path = "../secrets", version = "0.10.2" }
codewhale-telemetry = { path = "../telemetry", version = "0.10.2" }
codewhale-dependencies = { path = "../dependencies", version = "0.10.2" }
codewhale-lsp = { path = "../lsp", version = "0.10.2" }
codewhale-repl = { path = "../repl", version = "0.10.2" }
codewhale-snapshot = { path = "../snapshot", version = "0.10.2" }
codewhale-util = { path = "../util", version = "0.10.2" }
codewhale-tools = { path = "../tools", version = "0.10.2" }
codewhale-workflow = { path = "../workflow", version = "0.10.2" }
@@ -206,6 +210,9 @@ shell-words = "1.1.1"
codewhale-build-support = { path = "../build-support", version = "0.10.2" }
[dev-dependencies]
codewhale-dependencies = { path = "../dependencies", version = "0.10.2", features = ["test-support"] }
codewhale-lsp = { path = "../lsp", version = "0.10.2", features = ["test-support"] }
codewhale-snapshot = { path = "../snapshot", version = "0.10.2", features = ["test-support"] }
codewhale-notify = { path = "../notify", version = "0.10.2", features = ["test-thread-scoped-state"] }
codewhale-config = { path = "../config", version = "0.10.2", features = ["schema", "test-support"] }
codewhale-agent-contract = { path = "../agent-contract", version = "0.10.2", features = [
+9 -9
View File
@@ -91,10 +91,10 @@ use codewhale_execpolicy::ApprovalMode;
use crate::commands::groups::plugins::plugin_network_policy;
use crate::dependencies::ExternalTool as _;
use crate::tui::app::App;
use crate::tui::history::HistoryCell;
use codewhale_config::pricing::CostCurrency;
use codewhale_dependencies::ExternalTool as _;
use codewhale_localization::{MessageId, tr};
use codewhale_net::network_policy::NetworkPolicy;
@@ -920,7 +920,7 @@ fn hosted_work_url(repo: &str, branch: &str) -> String {
}
fn read_git_value(workspace: &Path, args: &[&str]) -> Option<String> {
let mut command = crate::dependencies::Git::command()?;
let mut command = codewhale_dependencies::Git::command()?;
let output = command.arg("-C").arg(workspace).args(args).output().ok()?;
if !output.status.success()
|| output.stdout.is_empty()
@@ -941,7 +941,7 @@ fn valid_branch_name(branch: &str) -> bool {
if branch.is_empty() || branch.len() > MAX_GIT_VALUE_BYTES {
return false;
}
let Some(mut command) = crate::dependencies::Git::command() else {
let Some(mut command) = codewhale_dependencies::Git::command() else {
return false;
};
command
@@ -1869,7 +1869,7 @@ fn flatten_history_lines(lines: Vec<ratatui::text::Line<'static>>) -> String {
/// Read the workspace snapshot repository read-only and project its state
/// (D8): only an existing repo is opened, never created.
fn project_restore_points(workspace: &Path) -> RestorePointProjection {
match crate::snapshot::SnapshotRepo::open_existing(workspace) {
match codewhale_snapshot::SnapshotRepo::open_existing(workspace) {
Ok(None) => RestorePointProjection::None,
Err(err) => RestorePointProjection::Unreadable {
reason: err.to_string(),
@@ -1885,7 +1885,7 @@ fn project_restore_points(workspace: &Path) -> RestorePointProjection {
}
}
fn project_restore_snapshot(snapshot: &crate::snapshot::Snapshot) -> RestoreSnapshot {
fn project_restore_snapshot(snapshot: &codewhale_snapshot::Snapshot) -> RestoreSnapshot {
let parsed = crate::core::turn::parse_snapshot_label(&snapshot.label);
RestoreSnapshot {
id: snapshot.id.as_str().to_string(),
@@ -3359,7 +3359,7 @@ impl CommandSkillGroupContext for SkillGroupAdapter<'_> {
fn snapshot_list(&mut self, limit: usize) -> Result<Vec<SnapshotEntry>, String> {
let workspace = self.host.app.borrow().workspace.clone();
let repo = match crate::snapshot::SnapshotRepo::open_or_init(&workspace) {
let repo = match codewhale_snapshot::SnapshotRepo::open_or_init(&workspace) {
Ok(repo) => repo,
Err(err) => {
return Err(format!(
@@ -3391,7 +3391,7 @@ impl CommandSkillGroupContext for SkillGroupAdapter<'_> {
let workspace = self.host.app.borrow().workspace.clone();
let id = id.to_owned();
let restore = move || {
let repo = match crate::snapshot::SnapshotRepo::open_or_init(&workspace) {
let repo = match codewhale_snapshot::SnapshotRepo::open_or_init(&workspace) {
Ok(repo) => repo,
Err(err) => {
return Err(format!(
@@ -3400,7 +3400,7 @@ impl CommandSkillGroupContext for SkillGroupAdapter<'_> {
));
}
};
let id = crate::snapshot::SnapshotId::parse(&id)
let id = codewhale_snapshot::SnapshotId::parse(&id)
.map_err(|err| format!("Restore failed: {err}"))?;
repo.restore(&id)
.map_err(|err| format!("Restore failed: {err}"))
@@ -6540,7 +6540,7 @@ mod tests {
let _home = scoped_home(&tmp);
let skills_dir = tmp.path().join("skills");
let file = tmp.path().join("a.txt");
let repo = crate::snapshot::SnapshotRepo::open_or_init(tmp.path()).unwrap();
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(tmp.path()).unwrap();
std::fs::write(&file, b"v1").unwrap();
repo.snapshot("pre-turn:1").unwrap();
std::fs::write(&file, b"v2").unwrap();
@@ -5,10 +5,10 @@
//! an asynchronous I/O execution model.
use super::SharedCommandHost;
use crate::dependencies::{ExternalTool, Git};
use crate::tui::app::App;
use crate::tui::history::HistoryCell;
use codewhale_command_contract::facets::*;
use codewhale_dependencies::{ExternalTool, Git};
use std::path::PathBuf;
pub(super) struct DebugOperationsAdapter<'a> {
@@ -125,7 +125,7 @@ fn session_diff(app: &App) -> std::io::Result<Option<DebugDiffObservation>> {
let Some(session_id) = app.current_session_id.as_deref() else {
return Ok(None);
};
let Some(repo) = crate::snapshot::SnapshotRepo::open_existing(&app.workspace)? else {
let Some(repo) = codewhale_snapshot::SnapshotRepo::open_existing(&app.workspace)? else {
return Ok(None);
};
// Newest first, so the session's first restore point is the last match.
@@ -164,7 +164,7 @@ fn workspace_git_diff(workspace: &std::path::Path) -> DebugDiffObservation {
let patch = git(&["diff", "--no-color", "--no-ext-diff", "--no-textconv"]);
match (names, stat, patch) {
(Ok(names), Ok(stat), Ok(patch)) => {
let (patch, patch_truncated) = crate::snapshot::repo::truncate_at_char_boundary(
let (patch, patch_truncated) = codewhale_snapshot::repo::truncate_at_char_boundary(
&String::from_utf8_lossy(&patch.stdout),
MAX_DIFF_PATCH_BYTES,
);
@@ -331,7 +331,7 @@ pub(in crate::commands) struct SnapshotOwner {
}
impl SnapshotOwner {
fn owns(&self, snapshot: &crate::snapshot::Snapshot) -> bool {
fn owns(&self, snapshot: &codewhale_snapshot::Snapshot) -> bool {
snapshot.session_id.as_deref() == Some(self.session_id.as_str())
&& self.until.is_none_or(|until| snapshot.timestamp <= until)
}
@@ -403,11 +403,11 @@ fn is_restore_point_label(label: &str) -> bool {
/// One `/undo` step, planned but not applied.
pub(in crate::commands) struct UndoStep {
/// Restore point the step started at.
pub(in crate::commands) target: crate::snapshot::Snapshot,
pub(in crate::commands) target: codewhale_snapshot::Snapshot,
/// Tree the step ended at: the next restore point this conversation
/// owns, or, for the newest step, a snapshot of the workspace as it is
/// now. Trees, not commit ids, because a prune rewrites commit ids.
pub(in crate::commands) end: crate::snapshot::SnapshotId,
pub(in crate::commands) end: codewhale_snapshot::SnapshotId,
/// The paths the step changed that are still as it left them.
pub(in crate::commands) restore: Vec<PathBuf>,
/// Changed paths `/undo` leaves in place because they are not regular
@@ -416,7 +416,7 @@ pub(in crate::commands) struct UndoStep {
pub(in crate::commands) skipped: Vec<PathBuf>,
/// The `pre-restore:` snapshot planning took of the workspace, when the
/// step ends now; the restore reuses it as its safety backup.
pub(in crate::commands) backup: Option<crate::snapshot::SnapshotId>,
pub(in crate::commands) backup: Option<codewhale_snapshot::SnapshotId>,
/// The step has no recorded end and runs only because the user typed
/// `force`: it also puts back edits made since the request started.
pub(in crate::commands) forced: bool,
@@ -467,13 +467,13 @@ pub(in crate::commands) struct UndoStep {
// The planner is host-internal. Box only its uncommon early outcome to keep
// Result small; the public facet still returns an owned data-only value.
fn plan_undo_step(
repo: &crate::snapshot::SnapshotRepo,
snapshots: Vec<crate::snapshot::Snapshot>,
repo: &codewhale_snapshot::SnapshotRepo,
snapshots: Vec<codewhale_snapshot::Snapshot>,
owners: &[SnapshotOwner],
force: bool,
request_position: &dyn Fn(&str) -> RequestPositionResult,
) -> Result<UndoStep, Box<DebugUndoOutcome>> {
let owned: Vec<crate::snapshot::Snapshot> = snapshots
let owned: Vec<codewhale_snapshot::Snapshot> = snapshots
.into_iter()
.filter(|snapshot| is_restore_point_label(&snapshot.label))
.filter(|snapshot| owners.iter().any(|owner| owner.owns(snapshot)))
@@ -650,7 +650,7 @@ pub(in crate::commands) fn undo_files(app: &mut App, force: bool) -> DebugUndoOu
}
let workspace = app.workspace.clone();
let repo = match crate::snapshot::SnapshotRepo::open_or_init(&workspace) {
let repo = match codewhale_snapshot::SnapshotRepo::open_or_init(&workspace) {
Ok(r) => r,
Err(e) => {
return DebugUndoOutcome::RepoUnavailable {
@@ -663,7 +663,7 @@ pub(in crate::commands) fn undo_files(app: &mut App, force: bool) -> DebugUndoOu
// A post-turn snapshot this process is still taking is the newest step's
// end: without it, every edit since the step's restore point would count
// as the step's.
if !crate::snapshot::wait_for_pending_post_turn_snapshots(POST_TURN_SNAPSHOT_WAIT) {
if !codewhale_snapshot::wait_for_pending_post_turn_snapshots(POST_TURN_SNAPSHOT_WAIT) {
return DebugUndoOutcome::SnapshotPending;
}
@@ -704,7 +704,7 @@ pub(in crate::commands) fn undo_files(app: &mut App, force: bool) -> DebugUndoOu
let target = &step.target;
let is_last_request = step.request_is_last;
let plan: Vec<(PathBuf, crate::snapshot::SnapshotId)> = step
let plan: Vec<(PathBuf, codewhale_snapshot::SnapshotId)> = step
.restore
.iter()
.map(|path| (path.clone(), target.tree.clone()))
@@ -770,9 +770,11 @@ pub(in crate::commands) fn undo_files(app: &mut App, force: bool) -> DebugUndoOu
.map(|outcome| DebugRestoredFile {
path: outcome.path,
action: match outcome.action {
crate::snapshot::PathRestoreAction::Modified => DebugRestoreAction::Modified,
crate::snapshot::PathRestoreAction::Recreated => DebugRestoreAction::Recreated,
crate::snapshot::PathRestoreAction::Removed => DebugRestoreAction::Removed,
codewhale_snapshot::PathRestoreAction::Modified => DebugRestoreAction::Modified,
codewhale_snapshot::PathRestoreAction::Recreated => {
DebugRestoreAction::Recreated
}
codewhale_snapshot::PathRestoreAction::Removed => DebugRestoreAction::Removed,
},
})
.collect(),
@@ -316,7 +316,7 @@ fn test_retry_truncates_long_input() {
#[test]
fn test_patch_undo_requests_session_resync_after_restore() {
use crate::snapshot::SnapshotRepo;
use codewhale_snapshot::SnapshotRepo;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
@@ -379,7 +379,7 @@ fn test_patch_undo_requests_session_resync_after_restore() {
#[test]
fn test_undo_legacy_chain_falls_back_to_conversation_only() {
use crate::snapshot::SnapshotRepo;
use codewhale_snapshot::SnapshotRepo;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
@@ -421,7 +421,7 @@ fn test_undo_legacy_chain_falls_back_to_conversation_only() {
#[test]
fn test_patch_undo_prunes_pre_turn_context() {
use crate::snapshot::SnapshotRepo;
use codewhale_snapshot::SnapshotRepo;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
@@ -485,7 +485,7 @@ fn test_patch_undo_prunes_pre_turn_context() {
#[test]
fn test_patch_undo_never_crosses_session_boundary() {
use crate::snapshot::SnapshotRepo;
use codewhale_snapshot::SnapshotRepo;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
@@ -581,7 +581,7 @@ fn test_undo_reports_that_files_were_not_reverted_when_the_repo_is_unavailable()
// lock before the directory it pointed at is deleted.
struct UndoFixture {
workspace: PathBuf,
repo: crate::snapshot::SnapshotRepo,
repo: codewhale_snapshot::SnapshotRepo,
_home: crate::test_support::SealedHome,
_tmp: tempfile::TempDir,
}
@@ -592,7 +592,7 @@ impl UndoFixture {
let home = crate::test_support::SealedHome::at(tmp.path());
let workspace = tmp.path().join("ws");
std::fs::create_dir_all(&workspace).unwrap();
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace).unwrap();
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace).unwrap();
Self {
workspace,
repo,
@@ -1407,9 +1407,9 @@ fn patch_undo_waits_for_a_pending_post_turn_snapshot() {
fx.write("a.txt", "a1");
// The turn has completed; its post-turn snapshot is still in flight.
let pending = crate::snapshot::PendingPostTurnSnapshot::reserve();
let pending = codewhale_snapshot::PendingPostTurnSnapshot::reserve();
let writer = {
let repo = crate::snapshot::SnapshotRepo::open_or_init(&fx.workspace).unwrap();
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&fx.workspace).unwrap();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(500));
let taken = repo.take_snapshot("post-turn:1", Some("s1")).unwrap();
@@ -23,8 +23,8 @@ use codewhale_command_contract::metadata::{CommandInfo, RegisterCommand};
use crate::commands::CommandResult;
use crate::commands::groups::session::export::render_conversation;
use crate::dependencies::ExternalTool;
use crate::tui::app::AppAction;
use codewhale_dependencies::ExternalTool;
const VISIBILITY_NOTE: &str =
"a secret GitHub gist (unlisted, not private: anyone with the link can read it)";
@@ -253,7 +253,7 @@ fn write_temp_html(html: &str) -> Result<tempfile::NamedTempFile, String> {
/// Upload a file as a secret GitHub gist using the `gh` CLI. `gh gist
/// create` makes secret gists unless `--public` is passed; it never is here.
fn upload_gist(path: &Path) -> Result<String, String> {
let mut cmd = crate::dependencies::Gh::command()
let mut cmd = codewhale_dependencies::Gh::command()
.ok_or_else(|| "the `gh` CLI was not found".to_string())?;
let output = cmd
.args(gist_create_args(path))
@@ -21,13 +21,13 @@ use codewhale_command_contract::facets::{
use codewhale_command_contract::handler::CommandCapabilities;
use crate::error_taxonomy::ErrorSeverity;
use crate::snapshot::SnapshotRepo;
use crate::test_support::{EnvVarGuard, TestEnvLock};
use crate::tui::app::{App, TuiOptions};
use crate::tui::clipboard::ClipboardHandler;
use crate::tui::history::HistoryCell;
use codewhale_models::{ContentBlock, ImageUrlContent, Message, Role, ToolCaller};
use codewhale_runtime::config::Config;
use codewhale_snapshot::SnapshotRepo;
use crate::commands::session_export_test_support::{
assert_only_export_facet_exposed, normalize_export_time, normalize_recorded_export,
@@ -312,7 +312,7 @@ fn adapter_projects_absent_restore_points_without_creating_a_repo() {
let workspace = harness.temp.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace");
harness.app.workspace = workspace.clone();
let before = crate::snapshot::snapshot_git_dir(&workspace).expect("snapshot path");
let before = codewhale_snapshot::snapshot_git_dir(&workspace).expect("snapshot path");
assert!(!before.exists(), "precondition: no side repo yet");
let projection = conversation_projection(&mut harness.app);
@@ -322,7 +322,7 @@ fn adapter_projects_absent_restore_points_without_creating_a_repo() {
RestorePointProjection::None
));
assert!(
!crate::snapshot::snapshot_git_dir(&workspace)
!codewhale_snapshot::snapshot_git_dir(&workspace)
.expect("snapshot path")
.exists(),
"projection must never create the snapshot repo"
@@ -1291,14 +1291,14 @@ fn command_export_does_not_create_a_snapshot_repo_for_a_fresh_workspace() {
std::fs::create_dir_all(&workspace).expect("workspace");
let app = &mut harness.app;
app.workspace = workspace.clone();
let before = crate::snapshot::snapshot_git_dir(&workspace).expect("snapshot path");
let before = codewhale_snapshot::snapshot_git_dir(&workspace).expect("snapshot path");
assert!(!before.exists(), "precondition: no side repo yet");
let result = dispatch(app, Some("clipboard"));
assert!(!result.is_error, "{:?}", result.message);
assert!(
!crate::snapshot::snapshot_git_dir(&workspace)
!codewhale_snapshot::snapshot_git_dir(&workspace)
.expect("snapshot path")
.exists(),
"export must never create the side repo"
@@ -538,7 +538,7 @@ fn portable_export_source_has_no_host_dependency() {
"crate::tui",
"crate::client",
"codewhale_runtime::config",
"crate::snapshot",
"codewhale_snapshot",
"crate::session_manager",
"std::fs",
"std::net",
+1 -1
View File
@@ -297,7 +297,7 @@ fn run_scripted_turn_with_deadline(
// Held across the whole turn: the engine builds and refreshes its system
// prompt on this thread's current-thread runtime.
let _environment = crate::prompts::pin_recorded_environment(&os, &shell);
let _host_tools = crate::dependencies::pin_recorded_host_tools(recorded_host_tools(case));
let _host_tools = codewhale_dependencies::pin_recorded_host_tools(recorded_host_tools(case));
let record = runtime.block_on(async {
let config = Config::default();
let engine_config = EngineConfig {
+2 -2
View File
@@ -988,7 +988,7 @@ fn file_write_delete_paths(
/// silently allowed.
fn paths_git_cannot_restore(workspace: &std::path::Path, paths: &[String]) -> Vec<String> {
let run = |args: &[&str]| -> Option<bool> {
let mut command = crate::dependencies::Git::review_command(workspace).ok()?;
let mut command = codewhale_dependencies::Git::review_command(workspace).ok()?;
// Paths are file names, never patterns: `notes[1].txt` must not match
// a tracked `notes1.txt`, nor `:(glob)*` every tracked file.
command
@@ -2022,7 +2022,7 @@ fn strictly_inside(workspace: Option<&std::path::Path>, target: &str) -> bool {
return false;
};
if target.contains(['*', '?', '[', ']', '{', '}', '~', '$'])
|| crate::snapshot::repo::unsafe_workspace_snapshot_reason(
|| codewhale_snapshot::repo::unsafe_workspace_snapshot_reason(
&workspace
.canonicalize()
.unwrap_or_else(|_| workspace.to_path_buf()),
+11 -11
View File
@@ -37,7 +37,6 @@ use crate::route_runtime::resolve_runtime_route;
use crate::route_runtime::{
ResolvedRuntimeRoute, ValidatedRuntimeRoute, resolve_runtime_route_for_identity,
};
use crate::snapshot::{WorkspaceSnapshotKind, WorkspaceSnapshotRef};
use crate::tools::goal::{
GoalPauseReason, GoalSnapshot, GoalStatus, SharedGoalState, new_shared_goal_state,
};
@@ -74,6 +73,7 @@ use codewhale_models::{
};
use codewhale_runtime::config::{Config, DEFAULT_MAX_SUBAGENTS};
use codewhale_runtime::features::{Feature, Features};
use codewhale_snapshot::{WorkspaceSnapshotKind, WorkspaceSnapshotRef};
use codewhale_util::utils::spawn_supervised;
#[cfg(test)]
@@ -405,7 +405,7 @@ pub struct EngineConfig {
pub record_restore_points: bool,
/// Post-edit LSP diagnostics injection (#136). When `None`, the engine
/// constructs a disabled manager so the field is always present.
pub lsp_config: Option<crate::lsp::LspConfig>,
pub lsp_config: Option<codewhale_lsp::LspConfig>,
/// Durable runtime services exposed to model-visible tools.
pub runtime_services: RuntimeToolServices,
/// Per-role/type sub-agent model overrides already resolved from config.
@@ -1029,7 +1029,7 @@ pub struct Engine {
/// One lazy, session-scoped working kernel for inline `repl` blocks.
/// Its context is refreshed before each run, while user-created Python
/// state stays alive across model turns.
repl_kernel: Option<crate::repl::PythonRuntime>,
repl_kernel: Option<codewhale_repl::PythonRuntime>,
subagent_manager: SharedSubAgentManager,
subagent_execution_limits: Option<SubAgentExecutionLimits>,
/// The deterministic Auto-Review policy shared with every child runtime
@@ -1154,11 +1154,11 @@ pub struct Engine {
/// next receipt's `changed_paths` is computed against. `None` before a
/// turn's first snapshot and after one failed, so a span that cannot be
/// accounted for is reported as unknown rather than folded into the next.
restore_point_since: Option<crate::snapshot::SnapshotId>,
restore_point_since: Option<codewhale_snapshot::SnapshotId>,
/// Post-edit LSP diagnostics injection (#136). Populated unconditionally
/// — when LSP is disabled in config, this is an inert manager that
/// always returns `None` from `diagnostics_for`.
lsp_manager: Arc<crate::lsp::LspManager>,
lsp_manager: Arc<codewhale_lsp::LspManager>,
/// External sandbox backend (#516). When `Some`, exec_shell routes commands
/// through this instead of spawning a local process.
sandbox_backend: Option<std::sync::Arc<dyn codewhale_sandbox::backend::SandboxBackend>>,
@@ -1171,7 +1171,7 @@ pub struct Engine {
no_new_privs_active: Option<bool>,
/// Diagnostics collected during the current step's tool calls. Drained
/// and forwarded as a synthetic user message before the next API call.
pending_lsp_blocks: Vec<crate::lsp::DiagnosticBlock>,
pending_lsp_blocks: Vec<codewhale_lsp::DiagnosticBlock>,
/// Current operating mode. Updated on `ChangeMode` and `SendMessage`.
current_mode: AppMode,
/// R1: cumulative wall-clock budget for the turn currently running.
@@ -2462,8 +2462,8 @@ impl Engine {
host.context().file_read_tracker.clone()
});
let lsp_manager = Arc::new(match config.lsp_config.clone() {
Some(cfg) => crate::lsp::LspManager::new(cfg, config.workspace.clone()),
None => crate::lsp::LspManager::disabled(),
Some(cfg) => codewhale_lsp::LspManager::new(cfg, config.workspace.clone()),
None => codewhale_lsp::LspManager::disabled(),
});
// External sandbox backend (#516). Logged but non-fatal: if the
@@ -2945,9 +2945,9 @@ impl Engine {
/// snapshot [`Self::post_turn_snapshot_after_complete`] takes. Called
/// before `TurnComplete`, so a `/undo` the user types as soon as the
/// turn ends waits for that snapshot instead of racing it (#6644).
fn reserve_post_turn_snapshot(&self) -> Option<crate::snapshot::PendingPostTurnSnapshot> {
fn reserve_post_turn_snapshot(&self) -> Option<codewhale_snapshot::PendingPostTurnSnapshot> {
(self.config.snapshots_enabled && !self.config.record_restore_points)
.then(crate::snapshot::PendingPostTurnSnapshot::reserve)
.then(codewhale_snapshot::PendingPostTurnSnapshot::reserve)
}
/// Take the post-turn snapshot reserved by
@@ -2959,7 +2959,7 @@ impl Engine {
&self,
task: &'static str,
prompt: String,
pending: Option<crate::snapshot::PendingPostTurnSnapshot>,
pending: Option<codewhale_snapshot::PendingPostTurnSnapshot>,
) {
let Some(pending) = pending else {
return;
+1 -1
View File
@@ -98,7 +98,7 @@ impl Engine {
let blocks = std::mem::take(&mut self.pending_lsp_blocks);
let found: usize = blocks.iter().map(|b| b.items.len()).sum();
let files = blocks.len();
let rendered = crate::lsp::render_blocks(&blocks);
let rendered = codewhale_lsp::render_blocks(&blocks);
if rendered.is_empty() {
return;
}
+6 -4
View File
@@ -2440,10 +2440,12 @@ async fn pending_runtime_injections_make_the_body_unavailable_without_consuming_
let identity = deepseek_identity();
let (mut engine, _handle, _tmp) = preview_engine(&config);
engine.config.features.disable(Feature::Mcp);
engine.pending_lsp_blocks.push(crate::lsp::DiagnosticBlock {
file: std::path::PathBuf::from("src/lib.rs"),
items: Vec::new(),
});
engine
.pending_lsp_blocks
.push(codewhale_lsp::DiagnosticBlock {
file: std::path::PathBuf::from("src/lib.rs"),
items: Vec::new(),
});
let manifest = engine
.build_request_manifest(inputs(
+2 -2
View File
@@ -409,7 +409,7 @@ impl Engine {
)?;
let mut kernel = None;
if invocation.mode != RlmMode::Completion {
let path = tempfile::TempPath::try_from_path(crate::rlm::session::write_context_file(
let path = tempfile::TempPath::try_from_path(codewhale_repl::write_context_file(
&invocation.prompt,
)?)?;
let cancel = caller
@@ -421,7 +421,7 @@ impl Engine {
biased;
() = cancel.cancelled() => Err("RLM Python startup cancelled by its originating turn".to_string()),
() = wait_for_deadline(invocation.deadline) => Err("RLM Python startup exhausted its original wall-clock deadline".to_string()),
result = crate::repl::PythonRuntime::spawn_with_context(&path) => result,
result = codewhale_repl::PythonRuntime::spawn_with_context(&path) => result,
};
match startup {
Ok(runtime) => {
+5 -5
View File
@@ -300,17 +300,17 @@ impl BridgeFixture {
&self,
prompt: String,
model: Option<String>,
) -> crate::repl::runtime::SingleResp {
) -> codewhale_repl::runtime::SingleResp {
self.bridge().dispatch_rlm(prompt, model).await
}
}
impl crate::repl::runtime::RpcDispatcher for BridgeFixture {
impl codewhale_repl::runtime::RpcDispatcher for BridgeFixture {
fn dispatch<'a>(
&'a self,
request: crate::repl::runtime::RpcRequest,
) -> Pin<Box<dyn Future<Output = crate::repl::runtime::RpcResponse> + Send + 'a>> {
request: codewhale_repl::runtime::RpcRequest,
) -> Pin<Box<dyn Future<Output = codewhale_repl::runtime::RpcResponse> + Send + 'a>> {
Box::pin(async move {
crate::repl::runtime::RpcDispatcher::dispatch(&self.bridge(), request).await
codewhale_repl::runtime::RpcDispatcher::dispatch(&self.bridge(), request).await
})
}
}
@@ -961,7 +961,7 @@ fn snapshot_notice_precedes_first_provider_call_and_is_owned_by_session() {
#[test]
fn recorded_snapshot_receipts_bracket_the_turn_and_its_file_writes() {
use crate::llm_client::mock::{MockLlmClient, canned};
use crate::snapshot::WorkspaceSnapshotKind;
use codewhale_snapshot::WorkspaceSnapshotKind;
let _env = lock_test_env();
let root = tempdir().unwrap();
let _home = EnvVarGuard::set("CODEWHALE_HOME", root.path());
@@ -1086,7 +1086,7 @@ fn recorded_snapshot_receipts_bracket_the_turn_and_its_file_writes() {
Some(&["out.md".to_string()][..])
);
let repo = crate::snapshot::SnapshotRepo::open_existing(&workspace)
let repo = codewhale_snapshot::SnapshotRepo::open_existing(&workspace)
.unwrap()
.expect("snapshot repo");
let listed = repo.list(usize::MAX).unwrap();
@@ -1097,8 +1097,8 @@ fn recorded_snapshot_receipts_bracket_the_turn_and_its_file_writes() {
);
let delta = repo
.diff_snapshots(
&crate::snapshot::SnapshotId::parse(&receipts[0].tree_id).unwrap(),
&crate::snapshot::SnapshotId::parse(&receipts[3].tree_id).unwrap(),
&codewhale_snapshot::SnapshotId::parse(&receipts[0].tree_id).unwrap(),
&codewhale_snapshot::SnapshotId::parse(&receipts[3].tree_id).unwrap(),
100,
)
.unwrap();
@@ -498,8 +498,7 @@ fn env_only_auth_error_gets_recovery_hint() {
fn config_auth_error_does_not_blame_env() {
let _guard = lock_test_env();
let _env = ScopedDeepSeekApiKey::set("stale-env-key");
let cfg = Config::default()
.with_legacy_root(Some("fresh-config-key".to_string()), None);
let cfg = Config::default().with_legacy_root(Some("fresh-config-key".to_string()), None);
let (engine, _handle) = Engine::new(EngineConfig::default(), &cfg);
let message =
@@ -720,7 +719,10 @@ fn external_user_message_op(content: &str, mode: AppMode, config: &Config) -> Op
content: content.to_string(),
images: Vec::new(),
mode,
route: resolved_route_for_test(config, codewhale_config::descriptors::defaults::DEFAULT_TEXT_MODEL),
route: resolved_route_for_test(
config,
codewhale_config::descriptors::defaults::DEFAULT_TEXT_MODEL,
),
compaction: Box::new(CompactionConfig::default()),
initial_routed_usage: Box::default(),
goal_objective: None,
@@ -750,7 +752,11 @@ fn external_user_message_op_with_submission(
submission_id: Option<String>,
) -> Op {
let mut op = external_user_message_op(content, mode, config);
if let Op::SendMessage(TurnSpec { submission_id: token, .. }) = &mut op {
if let Op::SendMessage(TurnSpec {
submission_id: token,
..
}) = &mut op
{
*token = submission_id;
}
op
@@ -763,7 +769,10 @@ fn auto_review_message_op(content: &str, config: &Config) -> Op {
content: content.to_string(),
images: Vec::new(),
mode: AppMode::Agent,
route: resolved_route_for_test(config, codewhale_config::descriptors::defaults::DEFAULT_TEXT_MODEL),
route: resolved_route_for_test(
config,
codewhale_config::descriptors::defaults::DEFAULT_TEXT_MODEL,
),
compaction: Box::new(CompactionConfig::default()),
initial_routed_usage: Box::default(),
goal_objective: None,
@@ -1291,7 +1300,7 @@ async fn narrowing_the_posture_discards_python_kernels() {
client,
);
// Without a Python interpreter there is no kernel to discard.
let Ok(kernel) = crate::repl::runtime::PythonRuntime::new().await else {
let Ok(kernel) = codewhale_repl::runtime::PythonRuntime::new().await else {
return;
};
engine.repl_kernel = Some(kernel);
@@ -1354,7 +1363,7 @@ async fn a_broken_repl_kernel_is_replaced_by_the_next_turn() {
engine.session.auto_approve = true;
// What a turn dropped mid-round leaves behind.
let mut stale = crate::repl::PythonRuntime::new().await.expect("spawn");
let mut stale = codewhale_repl::PythonRuntime::new().await.expect("spawn");
let dropped = tokio::time::timeout(
Duration::from_millis(50),
stale.execute("import time\ntime.sleep(30)"),
@@ -455,7 +455,7 @@ async fn measure_production_mode_tool_catalogs() -> serde_json::Value {
// cross-platform built-in profile. Pin host capabilities because Windows
// system-directory lookup can find interpreters outside PATH. Keep the
// nonexistent PATH root for the canonical shell spelling below.
let _host_tools = crate::dependencies::pin_recorded_host_tools(Vec::new());
let _host_tools = codewhale_dependencies::pin_recorded_host_tools(Vec::new());
let _path = EnvVarGuard::set("PATH", tmp.path().join("no-host-interpreters"));
// Shell syntax is part of the tool schema. A bare name plus the empty
// PATH resolves to the same `bash` spelling on Unix and Windows without
@@ -1287,8 +1287,8 @@ async fn deferred_tool_first_use_does_not_emit_a_retry_status() {
.mount(&server)
.await;
let api_config = Config::default()
.with_legacy_root(Some("test-key".to_string()), Some(server.uri()));
let api_config =
Config::default().with_legacy_root(Some("test-key".to_string()), Some(server.uri()));
let (engine, handle) = Engine::new(
EngineConfig {
model: codewhale_config::descriptors::defaults::DEFAULT_TEXT_MODEL.to_string(),
@@ -268,14 +268,14 @@ fn working_set_reaches_model_as_turn_metadata() {
#[test]
fn turn_metadata_includes_git_workspace_snapshot_in_repo() {
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
if !crate::dependencies::Git::available() {
if !codewhale_dependencies::Git::available() {
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let init = crate::dependencies::Git::output(&["init", "-q"], root);
let init = codewhale_dependencies::Git::output(&["init", "-q"], root);
if init.is_err() || !init.unwrap().status.success() {
return;
}
@@ -304,14 +304,14 @@ fn turn_metadata_includes_git_workspace_snapshot_in_repo() {
/// block's bytes and priming caution), a changed one must re-emit it once.
#[test]
fn turn_metadata_git_snapshot_emitted_only_on_change() {
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
if !crate::dependencies::Git::available() {
if !codewhale_dependencies::Git::available() {
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let init = crate::dependencies::Git::output(&["init", "-q"], root);
let init = codewhale_dependencies::Git::output(&["init", "-q"], root);
if init.is_err() || !init.unwrap().status.success() {
return;
}
@@ -25,9 +25,7 @@ async fn trust_warning_is_internal_and_tracks_current_state() {
assert!(crate::runtime_handoff::is_internal_runtime_handoff(
&warning
));
assert!(codewhale_models::turn_classify::is_runtime_owned_user_message(
&warning
));
assert!(codewhale_models::turn_classify::is_runtime_owned_user_message(&warning));
// The transcript side (no history cell for it) is pinned in
// `tui::history::tests`, keeping this runtime test off the UI crate path.
assert!(
@@ -52,9 +50,7 @@ async fn trust_warning_is_internal_and_tracks_current_state() {
assert!(!crate::runtime_handoff::is_internal_runtime_handoff(
&quoted
));
assert!(!codewhale_models::turn_classify::is_runtime_owned_user_message(
&quoted
));
assert!(!codewhale_models::turn_classify::is_runtime_owned_user_message(&quoted));
engine.session.add_message(prompt.clone());
let initial = engine.session.messages.len();
engine.refresh_pinned_header_for_turn(&context).unwrap();
@@ -846,8 +842,8 @@ async fn dropped_operation_span_completes_as_cancelled() {
#[tokio::test]
async fn code_execution_does_not_inherit_parent_secret_env() {
use crate::dependencies::ExternalTool as _;
if !crate::dependencies::Python::available() {
use codewhale_dependencies::ExternalTool as _;
if !codewhale_dependencies::Python::available() {
// `dependencies::tests::runtime_commands_do_not_inherit_parent_secret_env`
// still covers the scrubbed Python constructor without Python.
return;
@@ -914,8 +910,8 @@ async fn code_execution_scenario() {
#[tokio::test]
async fn interpreter_execution_policy_preserves_readonly_and_exact_call_elevation() {
use codewhale_sandbox::SandboxPolicy;
use crate::tools::spec::ToolContext;
use codewhale_sandbox::SandboxPolicy;
for tool in [CODE_EXECUTION_TOOL_NAME, JS_EXECUTION_TOOL_NAME] {
let workspace = tempdir().expect("workspace");
let outside = tempdir().expect("outside workspace");
@@ -700,7 +700,7 @@ fn parse_patch_paths_skips_dev_null() {
#[tokio::test]
async fn post_edit_hook_injects_diagnostics_message_before_next_request() {
use crate::lsp::{Diagnostic, Language, Severity};
use codewhale_lsp::{Diagnostic, Language, Severity};
use std::sync::Arc;
let tmp = tempdir().expect("tempdir");
@@ -709,7 +709,7 @@ async fn post_edit_hook_injects_diagnostics_message_before_next_request() {
fs::create_dir_all(workspace.join("src")).unwrap();
fs::write(&target, "let x: i32 = \"not a number\";").unwrap();
let lsp_config = crate::lsp::LspConfig::default();
let lsp_config = codewhale_lsp::LspConfig::default();
let engine_config = EngineConfig {
workspace: workspace.clone(),
lsp_config: Some(lsp_config),
@@ -718,7 +718,7 @@ async fn post_edit_hook_injects_diagnostics_message_before_next_request() {
let (mut engine, _handle) = Engine::new(engine_config, &Config::default());
// Install a fake transport that always reports a type error.
let fake = Arc::new(crate::lsp::tests::FakeTransport::new(vec![Diagnostic {
let fake = Arc::new(codewhale_lsp::tests::FakeTransport::new(vec![Diagnostic {
line: 1,
column: 14,
severity: Severity::Error,
@@ -770,7 +770,7 @@ async fn post_edit_hook_is_silent_when_lsp_disabled() {
fs::create_dir_all(workspace.join("src")).unwrap();
fs::write(&target, "fn main() {}").unwrap();
let lsp_config = crate::lsp::LspConfig {
let lsp_config = codewhale_lsp::LspConfig {
enabled: false,
..Default::default()
};
@@ -792,17 +792,17 @@ async fn post_edit_hook_is_silent_when_lsp_disabled() {
#[tokio::test]
async fn post_edit_hook_skips_unknown_tool_names() {
use crate::lsp::{Diagnostic, Language, Severity};
use codewhale_lsp::{Diagnostic, Language, Severity};
use std::sync::Arc;
let tmp = tempdir().expect("tempdir");
let engine_config = EngineConfig {
workspace: tmp.path().to_path_buf(),
lsp_config: Some(crate::lsp::LspConfig::default()),
lsp_config: Some(codewhale_lsp::LspConfig::default()),
..Default::default()
};
let (mut engine, _handle) = Engine::new(engine_config, &Config::default());
let fake = Arc::new(crate::lsp::tests::FakeTransport::new(vec![Diagnostic {
let fake = Arc::new(codewhale_lsp::tests::FakeTransport::new(vec![Diagnostic {
line: 1,
column: 1,
severity: Severity::Error,
@@ -1074,7 +1074,7 @@ async fn mcp_boot_reports_ready_server_before_stalled_server_finishes() {
#[tokio::test]
async fn first_turn_waits_for_explicit_mcp_schema_without_waiting_for_unrelated_server() {
let Some(node) = crate::dependencies::resolve_node() else {
let Some(node) = codewhale_dependencies::resolve_node() else {
return;
};
let tmp = tempdir().expect("tempdir");
@@ -1450,7 +1450,7 @@ lines.on('line', async (line) => {
/// timeout on "it would have started by now".
#[tokio::test]
async fn lazy_boot_leaves_unselected_servers_unspawned() {
let Some(node) = crate::dependencies::resolve_node() else {
let Some(node) = codewhale_dependencies::resolve_node() else {
return;
};
let tmp = tempdir().expect("tempdir");
@@ -1628,7 +1628,7 @@ async fn mcp_search_fixture(
Arc<crate::llm_client::mock::MockLlmClient>,
) {
let node =
crate::dependencies::resolve_node().expect("MCP discovery qualification requires Node");
codewhale_dependencies::resolve_node().expect("MCP discovery qualification requires Node");
let tmp = tempdir().expect("tempdir");
let server = tmp.path().join("discovery.mjs");
fs::write(&server, r#"import fs from 'node:fs';
+8 -8
View File
@@ -27,7 +27,7 @@ use codewhale_execpolicy::tool_rules::{tool_denied, tool_matches_any_rule};
use codewhale_models::Tool;
use crate::core::session::ToolActivationCache;
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
use codewhale_runtime::features::{Feature, Features};
use codewhale_runtime::regex_cache::compile_user_regex;
pub(crate) const CODE_EXECUTION_TOOL_NAME: &str = "code_execution";
@@ -302,11 +302,11 @@ pub(crate) fn ensure_advanced_tooling(
// `python3` isn't on PATH — the model treated the tool as reliable
// once it appeared in the catalog. We now probe at catalog-build
// time and only advertise when an interpreter resolves. See
// `crate::dependencies::resolve_python_interpreter` for the probe.
// `codewhale_dependencies::resolve_python_interpreter` for the probe.
if mode != AppMode::Plan
&& !catalog.iter().any(|t| t.name == CODE_EXECUTION_TOOL_NAME)
&& crate::dependencies::host_tool_available(CODE_EXECUTION_TOOL_NAME, || {
crate::dependencies::resolve_python_interpreter().is_some()
&& codewhale_dependencies::host_tool_available(CODE_EXECUTION_TOOL_NAME, || {
codewhale_dependencies::resolve_python_interpreter().is_some()
})
{
catalog.push(Tool {
@@ -347,8 +347,8 @@ pub(crate) fn ensure_advanced_tooling(
// the tool only when `resolve_node()` succeeds.
if mode != AppMode::Plan
&& !catalog.iter().any(|t| t.name == JS_EXECUTION_TOOL_NAME)
&& crate::dependencies::host_tool_available(JS_EXECUTION_TOOL_NAME, || {
crate::dependencies::resolve_node().is_some()
&& codewhale_dependencies::host_tool_available(JS_EXECUTION_TOOL_NAME, || {
codewhale_dependencies::resolve_node().is_some()
})
{
let mut tool = crate::tools::js_execution::js_execution_tool_definition();
@@ -1652,10 +1652,10 @@ pub(super) async fn execute_code_execution_tool(
context: &ToolContext,
) -> Result<ToolResult, ToolError> {
let code = required_str(input, "code")?;
let interpreter = crate::dependencies::Python::resolve().ok_or_else(|| {
let interpreter = codewhale_dependencies::Python::resolve().ok_or_else(|| {
ToolError::execution_failed("code_execution: Python interpreter became unavailable")
})?;
let (program, mut args) = crate::dependencies::split_interpreter_spec(&interpreter);
let (program, mut args) = codewhale_dependencies::split_interpreter_spec(&interpreter);
args.push("-".to_string());
let budget = Duration::from_secs(120);
let mut cmd =
@@ -38,11 +38,11 @@ fn code_execution_description_does_not_claim_process_sandboxing() {
/// Python output must match our UTF-8 decoder even with non-UTF-8 parent stdio.
#[tokio::test]
async fn code_execution_returns_utf8_stdout_and_stderr() {
use crate::dependencies::ExternalTool as _;
use crate::test_support::{EnvVarGuard, lock_test_env};
use codewhale_dependencies::ExternalTool as _;
let _env_lock = lock_test_env();
if !crate::dependencies::Python::available() {
if !codewhale_dependencies::Python::available() {
return;
}
let _encoding = EnvVarGuard::set("PYTHONIOENCODING", "gbk");
@@ -458,7 +458,7 @@ fn execute_tools_is_eager_in_code_mode_and_deferred_in_direct() {
#[cfg(unix)]
#[tokio::test]
async fn dropped_code_execution_kills_the_interpreter_tree() {
if crate::dependencies::resolve_python_interpreter().is_none() {
if codewhale_dependencies::resolve_python_interpreter().is_none() {
return;
}
let tmp = tempfile::tempdir().expect("tempdir");
+5 -5
View File
@@ -836,7 +836,7 @@ impl Engine {
#[allow(clippy::too_many_arguments)] // mirrors `gate_nested_call`
async fn repl_fence_blocked_reason(
&mut self,
blocks: &[crate::repl::ReplBlock],
blocks: &[codewhale_repl::ReplBlock],
// What the approval card says would run, e.g. "the reply's ```repl
// block(s) in the session REPL kernel".
what_runs: &str,
@@ -3509,7 +3509,7 @@ impl Engine {
{
tool_restore_point = self
.take_restore_point(
crate::snapshot::WorkspaceSnapshotKind::Tool,
codewhale_snapshot::WorkspaceSnapshotKind::Tool,
format!("tool:{tool_id}"),
Some(tool_id.as_str()),
super::file_write_tool_target_paths(&tool_name, &tool_input),
@@ -3614,7 +3614,7 @@ impl Engine {
// Close the span the call ran in (recording hosts only).
if tool_restore_point && self.config.record_restore_points {
self.take_restore_point(
crate::snapshot::WorkspaceSnapshotKind::PostTool,
codewhale_snapshot::WorkspaceSnapshotKind::PostTool,
format!("post-tool:{tool_id}"),
Some(tool_id.as_str()),
None,
@@ -3916,7 +3916,7 @@ impl Engine {
if !code_execution_offered(mode, tool_catalog, nested_gate_env.tool_policy) {
return refused("code execution is not available on this turn".to_string());
}
let block = crate::repl::ReplBlock {
let block = codewhale_repl::ReplBlock {
code,
start_offset: 0,
end_offset: 0,
@@ -4254,7 +4254,7 @@ impl Engine {
&plan.input,
) {
self.take_restore_point(
crate::snapshot::WorkspaceSnapshotKind::Tool,
codewhale_snapshot::WorkspaceSnapshotKind::Tool,
format!("tool:{nested_id}"),
Some(nested_id.as_str()),
super::file_write_tool_target_paths(&plan.name, &plan.input),
@@ -29,13 +29,13 @@ impl Engine {
// fence is not a way around that: it runs only when the tool is
// on this turn's surface, and only after the same approval.
let repl_fence_present = has_sendable_assistant_content
&& crate::repl::sandbox::has_repl_block(current_text_visible);
&& codewhale_repl::sandbox::has_repl_block(current_text_visible);
let repl_fence_offered =
code_execution_offered(progress.mode, &progress.tool_catalog, tool_policy);
let mut repl_fence_skip_reason = (repl_fence_present && !repl_fence_offered)
.then(|| "code execution is not available on this turn".to_string());
let repl_blocks = if repl_fence_present && repl_fence_offered {
crate::repl::sandbox::extract_repl_blocks(current_text_visible)
codewhale_repl::sandbox::extract_repl_blocks(current_text_visible)
} else {
Vec::new()
};
@@ -91,7 +91,7 @@ impl Engine {
Err("REPL startup cancelled".into())
}
() = wait_for_deadline(repl_deadline) => Err("parent turn deadline reached during REPL startup".into()),
result = crate::repl::runtime::PythonRuntime::new() => result,
result = codewhale_repl::runtime::PythonRuntime::new() => result,
};
self.repl_kernel = match startup {
Ok(runtime) => Some(runtime),
+2 -2
View File
@@ -507,11 +507,11 @@ pub enum Event {
/// (`pre_turn`), before one file-modifying tool call (`tool`), after that
/// call (`post_tool`, recording hosts only), or after the turn
/// (`post_turn`). A host that records these on its turn records owns
/// exactly those restore points (see `crate::snapshot::WorkspaceSnapshotRef`).
/// exactly those restore points (see `codewhale_snapshot::WorkspaceSnapshotRef`).
/// With `EngineConfig::record_restore_points` every receipt of a turn
/// arrives before its `TurnComplete`.
WorkspaceSnapshotTaken {
snapshot: crate::snapshot::WorkspaceSnapshotRef,
snapshot: codewhale_snapshot::WorkspaceSnapshotRef,
},
/// Immutable billing route captured at CodeWhale's pre-permit application
+2 -2
View File
@@ -1567,8 +1567,8 @@ mod tests {
base_url: Some("https://example.invalid".into()),
},
Event::WorkspaceSnapshotTaken {
snapshot: crate::snapshot::WorkspaceSnapshotRef {
kind: crate::snapshot::WorkspaceSnapshotKind::Tool,
snapshot: codewhale_snapshot::WorkspaceSnapshotRef {
kind: codewhale_snapshot::WorkspaceSnapshotKind::Tool,
snapshot_id: "a".repeat(40),
tree_id: "b".repeat(40),
session_id: "thr_1".into(),
+16 -14
View File
@@ -8,15 +8,15 @@
//! [`restore_point_snapshot`] (`pre-turn:`, `tool:`, `post-tool:`) and
//! [`post_turn_snapshot`] book-end a turn and its tool calls by
//! taking a workspace-level snapshot into a side git repo (see
//! `crate::snapshot`). They are intentionally non-blocking and
//! `codewhale_snapshot`). They are intentionally non-blocking and
//! non-fatal: any IO error is logged at WARN and swallowed so a busted
//! filesystem or missing `git` binary never derails the agent loop.
//! `/restore N` and the `revert_turn` tool both consume these
//! snapshots.
use crate::core::events::TurnRoute;
use crate::snapshot::{SnapshotRepo, TakenSnapshot};
use codewhale_models::Usage;
use codewhale_snapshot::{SnapshotRepo, TakenSnapshot};
use std::path::Path;
use std::time::Instant;
@@ -708,7 +708,7 @@ pub fn restore_point_snapshot(
label: &str,
cap_bytes: u64,
session_id: Option<&str>,
since: Option<&crate::snapshot::SnapshotId>,
since: Option<&codewhale_snapshot::SnapshotId>,
) -> Option<(TakenSnapshot, Option<Vec<std::path::PathBuf>>)> {
snapshot_with_label_since(workspace, label, cap_bytes, session_id, since)
}
@@ -744,7 +744,7 @@ fn snapshot_with_label_since(
label: &str,
cap_bytes: u64,
session_id: Option<&str>,
since: Option<&crate::snapshot::SnapshotId>,
since: Option<&codewhale_snapshot::SnapshotId>,
) -> Option<(TakenSnapshot, Option<Vec<std::path::PathBuf>>)> {
match SnapshotRepo::open_or_init_with_cap(workspace, cap_bytes) {
Ok(repo) => {
@@ -817,7 +817,7 @@ fn snapshot_with_label_since(
}
/// The count cap [`SnapshotRepo::prune_keep_last_n`] applies after each
/// snapshot: [`crate::snapshot::DEFAULT_MAX_SNAPSHOTS`], which tests can
/// snapshot: [`codewhale_snapshot::DEFAULT_MAX_SNAPSHOTS`], which tests can
/// lower per workspace to exercise pruning without taking fifty snapshots.
fn max_snapshots_for(workspace: &Path) -> usize {
#[cfg(test)]
@@ -825,7 +825,7 @@ fn max_snapshots_for(workspace: &Path) -> usize {
return max;
}
let _ = workspace;
crate::snapshot::DEFAULT_MAX_SNAPSHOTS
codewhale_snapshot::DEFAULT_MAX_SNAPSHOTS
}
#[cfg(test)]
@@ -976,11 +976,11 @@ fn maybe_notify_snapshots_disabled_once(
let message = error.to_string();
// The gate markers are declared by the snapshot policy that produces them,
// so this stays one classifier rather than a second copy of the rules.
let scope = if message.contains(crate::snapshot::GATE_TOO_LARGE_MARKER) {
let scope = if message.contains(codewhale_snapshot::GATE_TOO_LARGE_MARKER) {
SnapshotsDisabledScope::WorkspaceTooLarge
} else if message.contains(crate::snapshot::GATE_TOO_MANY_ENTRIES_MARKER) {
} else if message.contains(codewhale_snapshot::GATE_TOO_MANY_ENTRIES_MARKER) {
SnapshotsDisabledScope::TooManyFiles
} else if message.contains(crate::snapshot::GATE_UNSAFE_LOCATION_MARKER) {
} else if message.contains(codewhale_snapshot::GATE_UNSAFE_LOCATION_MARKER) {
SnapshotsDisabledScope::UnsafeLocation
} else {
// A real snapshot/data-loss error, not a gate: say snapshots are
@@ -994,7 +994,9 @@ fn maybe_notify_snapshots_disabled_once(
};
let limit = match scope {
SnapshotsDisabledScope::WorkspaceTooLarge => format_cap_bytes(cap_bytes),
SnapshotsDisabledScope::TooManyFiles => crate::snapshot::SIZE_WALK_MAX_ENTRIES.to_string(),
SnapshotsDisabledScope::TooManyFiles => {
codewhale_snapshot::SIZE_WALK_MAX_ENTRIES.to_string()
}
_ => String::new(),
};
record_snapshot_notice(workspace, session_id, scope, limit)
@@ -1242,7 +1244,7 @@ mod snapshot_notice_tests {
(
format!(
"{}: over 2 bytes in x",
crate::snapshot::GATE_TOO_MANY_ENTRIES_MARKER
codewhale_snapshot::GATE_TOO_MANY_ENTRIES_MARKER
),
SnapshotsDisabledScope::TooManyFiles,
0,
@@ -1250,7 +1252,7 @@ mod snapshot_notice_tests {
(
format!(
"{} for home directory: x",
crate::snapshot::GATE_UNSAFE_LOCATION_MARKER
codewhale_snapshot::GATE_UNSAFE_LOCATION_MARKER
),
SnapshotsDisabledScope::UnsafeLocation,
2 * 1024 * 1024 * 1024,
@@ -1279,7 +1281,7 @@ mod snapshot_notice_tests {
// Nothing else asserts it reaches the user, so a dropped
// placeholder would render "more than files" silently.
assert!(
line.contains(&crate::snapshot::SIZE_WALK_MAX_ENTRIES.to_string()),
line.contains(&codewhale_snapshot::SIZE_WALK_MAX_ENTRIES.to_string()),
"the entry ceiling must be stated, not left as a blank limit: {line}"
);
}
@@ -1293,7 +1295,7 @@ mod snapshot_notice_tests {
std::io::ErrorKind::InvalidInput,
format!(
"{}: over x bytes in y",
crate::snapshot::GATE_TOO_LARGE_MARKER
codewhale_snapshot::GATE_TOO_LARGE_MARKER
),
);
assert!(maybe_notify_snapshots_disabled_once(
+1 -1
View File
@@ -632,7 +632,7 @@ pub(crate) async fn run_exec_agent(
execution_config
.lsp
.clone()
.map(crate::lsp::LspConfig::from)
.map(codewhale_lsp::LspConfig::from)
})
.flatten();
let mut engine_features = execution_config.features();
@@ -40,10 +40,11 @@ pub(crate) fn review(input: &Value) -> Result<Value, String> {
} else {
options.runtime
};
crate::dependencies::resolve_extension_host_runtime(
codewhale_dependencies::resolve_extension_host_runtime(
choice,
options.node_override.as_deref(),
options.bun_override.as_deref(),
crate::extension_host::bundle_sha256(),
)
.selected
.ok_or("composition reviewer runtime is unavailable")?
+6 -5
View File
@@ -605,7 +605,7 @@ struct RuntimePin {
/// or removal mid-session changes nothing until the next process; a
/// binary replaced at the pinned path is refused at the handshake). With
/// the one-line summary of why it was chosen, for `/plugin`.
pinned: Option<(crate::dependencies::HostRuntime, String)>,
pinned: Option<(codewhale_dependencies::HostRuntime, String)>,
/// `runtime = "auto"` only: a Bun host failed to launch or handshake
/// before anything was pinned, so every later launch this session
/// resolves Node.
@@ -619,7 +619,7 @@ struct RuntimePin {
fn prepare_launch(
options: &ExtensionHostOptions,
tier: HostTier,
pinned: Option<(crate::dependencies::HostRuntime, String)>,
pinned: Option<(codewhale_dependencies::HostRuntime, String)>,
bun_failed: bool,
snapshot_state_paths: &[PathBuf],
) -> Result<(supervisor::HostLaunch, Option<String>), String> {
@@ -631,10 +631,11 @@ fn prepare_launch(
} else {
options.runtime
};
let mut resolution = crate::dependencies::resolve_extension_host_runtime(
let mut resolution = codewhale_dependencies::resolve_extension_host_runtime(
choice,
options.node_override.as_deref(),
options.bun_override.as_deref(),
crate::extension_host::bundle_sha256(),
);
// Report the configured choice, not the Node-only fallback.
resolution.choice = options.runtime;
@@ -675,7 +676,7 @@ fn host_root(options: &ExtensionHostOptions) -> Result<PathBuf, String> {
/// Blocking; creates the host's data dir and probes bubblewrap on Linux.
pub(crate) fn planned_sandbox(
options: &ExtensionHostOptions,
runtime: &crate::dependencies::HostRuntime,
runtime: &codewhale_dependencies::HostRuntime,
tier: HostTier,
) -> Result<supervisor::HostSandbox, String> {
supervisor::planned_sandbox(tier, runtime, &host_root(options)?)
@@ -2730,7 +2731,7 @@ impl ExtensionHostManager {
};
if shared.options.runtime
!= codewhale_config::extension_host::ExtensionHostRuntime::Auto
|| launch.runtime.kind != crate::dependencies::HostRuntimeKind::Bun
|| launch.runtime.kind != codewhale_dependencies::HostRuntimeKind::Bun
|| summary.is_none()
{
break Err(reason);
+2 -2
View File
@@ -91,8 +91,8 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::{mpsc, oneshot};
use tokio_util::sync::CancellationToken;
use crate::dependencies::{HostRuntime, HostRuntimeKind};
use crate::tools::codemode::PauseClock;
use codewhale_dependencies::{HostRuntime, HostRuntimeKind};
use super::protocol::{
self, CoreRequest, Direction, HelloParams, HostLimits, HostMessage, HostNotification,
@@ -291,7 +291,7 @@ pub const HOST_MEMORY_CAP: u64 = 1 << 30;
/// `extension-host/test/harness.mjs` (`HOST_ARGS`).
///
/// - Node: a 256 MB old-space cap, `__proto__` throws, no native addons,
/// and the [`crate::dependencies::NODE_NATIVE_CODE_FLAGS`] this Node
/// and the [`codewhale_dependencies::NODE_NATIVE_CODE_FLAGS`] this Node
/// accepts (`node:sqlite`, and `node:ffi` where it exists). On Windows it
/// also keeps symlinked paths as given, so module resolution never lstats
/// the drive root the LPAC cannot read.
+49 -23
View File
@@ -673,10 +673,11 @@ fn every_package_in_the_bundle_has_a_licence_notice_and_a_third_party_entry() {
/// A Node for the integration tests, or `None` (skip) when there is none and
/// the tests were not explicitly required.
pub(crate) fn node_for_tests(test: &str) -> Option<PathBuf> {
let resolution = crate::dependencies::resolve_extension_host_runtime(
let resolution = codewhale_dependencies::resolve_extension_host_runtime(
codewhale_config::extension_host::ExtensionHostRuntime::Node,
None,
None,
crate::extension_host::bundle_sha256(),
);
match resolution.selected.as_ref() {
Some(runtime) => Some(runtime.path.clone()),
@@ -1664,10 +1665,11 @@ fn compiled_image_for_tests() -> Option<PathBuf> {
return None;
};
let binary = PathBuf::from(binary);
let resolution = crate::dependencies::resolve_extension_host_runtime(
let resolution = codewhale_dependencies::resolve_extension_host_runtime(
codewhale_config::extension_host::ExtensionHostRuntime::Bun,
None,
Some(&binary),
crate::extension_host::bundle_sha256(),
);
let runtime = resolution
.selected
@@ -3503,10 +3505,11 @@ async fn explicit_retry_refreshes_same_byte_authority_without_inheriting_old_han
/// A Bun >= 1.4.0 for the Bun integration tests, or `None` (skip) unless
/// `CODEWHALE_EXT_HOST_BUN_TESTS` requires one.
fn bun_for_tests(test: &str) -> Option<PathBuf> {
let resolution = crate::dependencies::resolve_extension_host_runtime(
let resolution = codewhale_dependencies::resolve_extension_host_runtime(
codewhale_config::extension_host::ExtensionHostRuntime::Bun,
None,
None,
crate::extension_host::bundle_sha256(),
);
match resolution.selected {
Some(runtime) => Some(runtime.path),
@@ -3572,7 +3575,7 @@ fn node_is_the_default_runtime_and_bun_is_an_opt_in() {
#[test]
fn launch_plan_gives_each_runtime_its_own_flags() {
use crate::dependencies::{HostRuntime, HostRuntimeKind};
use codewhale_dependencies::{HostRuntime, HostRuntimeKind};
let home = tempfile::tempdir().unwrap();
let bundle = home.path().join("codewhale-extension-host.mjs");
for (kind, expected) in [
@@ -3605,7 +3608,7 @@ fn launch_plan_gives_each_runtime_its_own_flags() {
compiled: false,
native_code_flags: match kind {
HostRuntimeKind::Bun => Vec::new(),
HostRuntimeKind::Node => crate::dependencies::NODE_NATIVE_CODE_FLAGS.to_vec(),
HostRuntimeKind::Node => codewhale_dependencies::NODE_NATIVE_CODE_FLAGS.to_vec(),
},
};
let launch = super::supervisor::plan_launch(
@@ -3683,14 +3686,18 @@ async fn handshake_refuses_a_runtime_or_version_mismatch_and_an_unapplied_kernel
let Some(node) = node_for_tests("handshake_refuses_a_runtime_or_version_mismatch") else {
return;
};
use crate::dependencies::HostRuntimeKind;
use codewhale_dependencies::HostRuntimeKind;
let home = tempfile::tempdir().unwrap();
let bundle = super::materialize_bundle(home.path()).unwrap();
// Resolved, so the Node gets the native-code flags it accepts.
let runtime =
crate::dependencies::resolve_extension_host_runtime(NODE, Some(node.as_path()), None)
.selected
.expect("the test Node resolves");
let runtime = codewhale_dependencies::resolve_extension_host_runtime(
NODE,
Some(node.as_path()),
None,
crate::extension_host::bundle_sha256(),
)
.selected
.expect("the test Node resolves");
struct NoEvents;
impl super::supervisor::HostEvents for NoEvents {
fn register(&self, _: &protocol::RegisterParams) -> protocol::RegisterResult {
@@ -3772,10 +3779,14 @@ async fn handshake_refuses_a_tier_or_built_in_module_digest_mismatch() {
}
let home = tempfile::tempdir().unwrap();
let bundle = super::materialize_bundle(home.path()).unwrap();
let runtime =
crate::dependencies::resolve_extension_host_runtime(NODE, Some(node.as_path()), None)
.selected
.expect("the test Node resolves");
let runtime = codewhale_dependencies::resolve_extension_host_runtime(
NODE,
Some(node.as_path()),
None,
crate::extension_host::bundle_sha256(),
)
.selected
.expect("the test Node resolves");
let launch_for = |tier: HostTier| {
super::supervisor::plan_launch(tier, &runtime, &bundle, home.path(), 1 << 30, &[]).unwrap()
};
@@ -4010,7 +4021,7 @@ async fn auto_uses_node_for_the_session_when_the_bun_host_fails_to_start() {
async fn memory_hog_is_stopped(
manager: Arc<ExtensionHostManager>,
fixture: &FixturePlugins,
kind: crate::dependencies::HostRuntimeKind,
kind: codewhale_dependencies::HostRuntimeKind,
) {
use super::supervisor::MemoryEnforcement;
let engine = manager.attach(fixture.registry());
@@ -4048,7 +4059,7 @@ async fn memory_hog_is_stopped(
{
assert!(
message.contains("allocation failed")
|| (kind == crate::dependencies::HostRuntimeKind::Bun
|| (kind == codewhale_dependencies::HostRuntimeKind::Bun
&& message.ends_with("RangeError: Out of memory")),
"{message}"
);
@@ -4114,7 +4125,7 @@ async fn memory_cap_stops_a_node_host() {
memory_hog_is_stopped(
manager,
&fixture,
crate::dependencies::HostRuntimeKind::Node,
codewhale_dependencies::HostRuntimeKind::Node,
)
.await;
}
@@ -4128,7 +4139,12 @@ async fn memory_cap_stops_a_bun_host() {
let _policy = TestPolicyGuard::extension_host(true);
let fixture = FixturePlugins::new(&["memory-hog"]).await;
let manager = bun_manager(&fixture, bun, memory_cap_supervision());
memory_hog_is_stopped(manager, &fixture, crate::dependencies::HostRuntimeKind::Bun).await;
memory_hog_is_stopped(
manager,
&fixture,
codewhale_dependencies::HostRuntimeKind::Bun,
)
.await;
}
/// Actual Rust containment/memory authority, using the same exact compiled
@@ -4142,7 +4158,12 @@ async fn compiled_native_host_memory_cap_is_enforced() {
let _policy = TestPolicyGuard::extension_host(true);
let fixture = FixturePlugins::new(&["memory-hog"]).await;
let manager = bun_manager(&fixture, binary, memory_cap_supervision());
memory_hog_is_stopped(manager, &fixture, crate::dependencies::HostRuntimeKind::Bun).await;
memory_hog_is_stopped(
manager,
&fixture,
codewhale_dependencies::HostRuntimeKind::Bun,
)
.await;
eprintln!(
"compiled-native-memory=passed platform={} arch={}",
std::env::consts::OS,
@@ -4995,7 +5016,7 @@ fn a_plugin_named_like_a_tier_zero_owner_fails_validation_and_never_reaches_the_
#[test]
fn launch_plans_carry_their_tier_and_use_a_data_directory_each() {
#[cfg(not(windows))]
use crate::dependencies::{HostRuntime, HostRuntimeKind};
use codewhale_dependencies::{HostRuntime, HostRuntimeKind};
let home = tempfile::tempdir().unwrap();
#[cfg(windows)]
let bundle = super::materialize_bundle(home.path()).unwrap();
@@ -5003,9 +5024,14 @@ fn launch_plans_carry_their_tier_and_use_a_data_directory_each() {
let bundle = home.path().join("codewhale-extension-host.mjs");
// Windows admission verifies the real copied runtime and exact bundle.
#[cfg(windows)]
let runtime = crate::dependencies::resolve_extension_host_runtime(NODE, None, None)
.selected
.expect("tier planning requires the test Node runtime on Windows");
let runtime = codewhale_dependencies::resolve_extension_host_runtime(
NODE,
None,
None,
crate::extension_host::bundle_sha256(),
)
.selected
.expect("tier planning requires the test Node runtime on Windows");
#[cfg(not(windows))]
let runtime = HostRuntime {
compiled: false,
+1 -1
View File
@@ -51,7 +51,7 @@ use windows_sys::Win32::System::Threading::{
};
use windows_sys::Win32::System::WindowsProgramming::PROCESS_CREATION_CHILD_PROCESS_OVERRIDE;
use crate::dependencies::{HostRuntime, HostRuntimeKind};
use codewhale_dependencies::{HostRuntime, HostRuntimeKind};
use codewhale_runtime::process_tree::ProcessTree;
use codewhale_util::files::WindowsDirectory;
@@ -98,7 +98,7 @@ fn windows_acl_edit_guard_serializes_processes() {
}
fn runtime(choice: ExtensionHostRuntime, override_path: Option<&Path>) -> Option<HostRuntime> {
let resolution = crate::dependencies::resolve_extension_host_runtime(
let resolution = codewhale_dependencies::resolve_extension_host_runtime(
choice,
(choice == ExtensionHostRuntime::Node)
.then_some(override_path)
@@ -106,6 +106,7 @@ fn runtime(choice: ExtensionHostRuntime, override_path: Option<&Path>) -> Option
(choice == ExtensionHostRuntime::Bun)
.then_some(override_path)
.flatten(),
crate::extension_host::bundle_sha256(),
);
match resolution.selected {
Some(runtime) => Some(runtime),
@@ -129,8 +130,8 @@ async fn owner_boundary(runtime: HostRuntime) {
let _policy = TestPolicyGuard::extension_host(true);
let fixture = FixturePlugins::new(&["secret-probe"]).await;
let choice = match runtime.kind {
crate::dependencies::HostRuntimeKind::Node => ExtensionHostRuntime::Node,
crate::dependencies::HostRuntimeKind::Bun => ExtensionHostRuntime::Bun,
codewhale_dependencies::HostRuntimeKind::Node => ExtensionHostRuntime::Node,
codewhale_dependencies::HostRuntimeKind::Bun => ExtensionHostRuntime::Bun,
};
let registry = fixture.registry();
let plugin = registry.get("secret-probe").unwrap();
+1 -1
View File
@@ -71,7 +71,7 @@ pub fn inventory() -> Result<Vec<TailnetNode>> {
&mut command,
std::iter::empty::<(&str, &str)>(),
);
let output = crate::dependencies::probe_output_bounded(
let output = codewhale_dependencies::probe_output_bounded(
&mut command,
true,
Duration::from_secs(5),
+1 -1
View File
@@ -25,7 +25,7 @@
//! This module owns capability and state outside the renderer so
//! `widgets/mod.rs` / `ui.rs` stay projection-only.
use crate::dependencies::{ExternalTool, Git};
use codewhale_dependencies::{ExternalTool, Git};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
+19 -22
View File
@@ -15,7 +15,7 @@ use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use tempfile::NamedTempFile;
use wait_timeout::ChildExt;
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
mod acp_server;
pub mod agent_roster;
@@ -37,7 +37,6 @@ mod core;
mod cost_status;
/// Guarded installation I/O reusing Engine's confined file primitives.
pub mod delivery_files;
mod dependencies;
mod doctor;
mod doctor_fix;
mod error_taxonomy;
@@ -52,7 +51,6 @@ mod image_attach;
mod integrations;
mod llm_client;
mod local_ollama;
mod lsp;
mod mcp;
mod mcp_server;
mod model_inventory;
@@ -76,7 +74,6 @@ pub mod reasoning_preference;
mod receipts;
mod remote_control;
mod remote_setup;
pub mod repl;
mod repo_law;
mod request_manifest;
mod request_tuning;
@@ -111,7 +108,6 @@ mod session_reconcile;
mod session_resume;
mod session_secret_scrub;
mod skills;
mod snapshot;
mod superfast;
mod task_manager;
mod telemetry_notice;
@@ -5835,7 +5831,7 @@ async fn run_doctor(
println!();
println!("{}", "Tool Dependencies:".bold());
match crate::dependencies::resolve_python_interpreter() {
match codewhale_dependencies::resolve_python_interpreter() {
Some(name) => println!(
" {} Python: {} → code_execution tool registered",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
@@ -5845,7 +5841,7 @@ async fn run_doctor(
println!(
" {} Python: not found (tried {:?})",
"✗".truecolor(red_r, red_g, red_b),
crate::dependencies::PYTHON_CANDIDATES,
codewhale_dependencies::PYTHON_CANDIDATES,
);
println!(" code_execution tool is NOT advertised to the model on this install.");
println!(" Install Python 3 and ensure one of those names is on PATH:");
@@ -5864,7 +5860,7 @@ async fn run_doctor(
}
}
match crate::dependencies::resolve_node() {
match codewhale_dependencies::resolve_node() {
Some(_) => println!(
" {} Node.js: present → js_execution tool registered",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
@@ -5911,10 +5907,11 @@ async fn run_doctor(
crate::extension_host::tier::HostTier::Plugin
};
let resolution = tokio::task::spawn_blocking(move || {
let resolution = crate::dependencies::resolve_extension_host_runtime(
let resolution = codewhale_dependencies::resolve_extension_host_runtime(
options.runtime,
options.node_override.as_deref(),
options.bun_override.as_deref(),
crate::extension_host::bundle_sha256(),
);
let sandbox = resolution
.selected
@@ -5974,7 +5971,7 @@ async fn run_doctor(
}
}
match crate::dependencies::resolve_pandoc() {
match codewhale_dependencies::resolve_pandoc() {
Some(_) => println!(
" {} pandoc: present → pandoc_convert tool registered",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
@@ -5997,7 +5994,7 @@ async fn run_doctor(
}
}
match crate::dependencies::resolve_tesseract() {
match codewhale_dependencies::resolve_tesseract() {
Some(_) => {
if cfg!(target_os = "macos") {
println!(
@@ -6042,7 +6039,7 @@ async fn run_doctor(
// PDF text extraction is an optional integration. Codewhale itself stays
// a single required executable; file and web tools report a typed
// failed `binary_unavailable` result when Poppler is not installed.
match crate::dependencies::resolve_pdftotext() {
match codewhale_dependencies::resolve_pdftotext() {
Some(_) => println!(
" {} pdftotext: available → PDF text extraction enabled",
"✓".truecolor(aqua_r, aqua_g, aqua_b),
@@ -9211,10 +9208,10 @@ fn rustc_version() -> String {
// `RustC::available()` resolves the tool once, capturing the `--version`
// banner as a side effect of the probe; reuse it instead of launching a
// second rustc process (each launch loads libLLVM).
if !crate::dependencies::RustC::available() {
if !codewhale_dependencies::RustC::available() {
return "not installed (only needed to build from source)".to_string();
}
crate::dependencies::rustc_version_banner().unwrap_or_else(|| "unknown".to_string())
codewhale_dependencies::rustc_version_banner().unwrap_or_else(|| "unknown".to_string())
}
/// List saved sessions
@@ -11218,7 +11215,7 @@ fn run_gh_pr_diff(number: u32, repo: Option<&str>, view: &GhPullRequest) -> Resu
}
fn run_gh_pr_checkout(number: u32, repo: Option<&str>) -> Result<()> {
let mut cmd = crate::dependencies::Gh::command()
let mut cmd = codewhale_dependencies::Gh::command()
.ok_or_else(|| anyhow::anyhow!("gh not found on PATH"))?;
cmd.arg("pr").arg("checkout").arg(number.to_string());
if let Some(r) = repo {
@@ -11238,7 +11235,7 @@ fn run_gh_pr_checkout(number: u32, repo: Option<&str>) -> Result<()> {
/// belongs to. `gh api` needs an explicit repository path, unlike `gh pr`
/// which infers it from the working directory.
fn run_gh_repo_name() -> Result<String> {
let mut cmd = crate::dependencies::Gh::command()
let mut cmd = codewhale_dependencies::Gh::command()
.ok_or_else(|| anyhow::anyhow!("gh not found on PATH"))?;
cmd.arg("repo")
.arg("view")
@@ -11636,7 +11633,7 @@ fn run_gh_post_pr_review(
if !comments.is_empty() {
payload["comments"] = serde_json::json!(comments);
}
let mut cmd = crate::dependencies::Gh::command()
let mut cmd = codewhale_dependencies::Gh::command()
.ok_or_else(|| anyhow::anyhow!("gh not found on PATH"))?;
cmd.arg("api")
.arg("--method")
@@ -11776,11 +11773,11 @@ fn collect_diff(
pr_view.context("PR snapshot is required")?,
)?
} else {
let mut cmd = crate::dependencies::Git::review_command(workspace)?;
let mut cmd = codewhale_dependencies::Git::review_command(workspace)?;
// Review repository content without executing its diff drivers.
cmd.current_dir(workspace)
.arg("diff")
.args(crate::dependencies::Git::REVIEW_DIFF_ARGS);
.args(codewhale_dependencies::Git::REVIEW_DIFF_ARGS);
if args.staged {
cmd.arg("--cached");
}
@@ -11814,7 +11811,7 @@ fn collect_base_commit_messages(
path: Option<&std::path::Path>,
workspace: &std::path::Path,
) -> Result<String> {
let mut cmd = crate::dependencies::Git::review_command(workspace)?;
let mut cmd = codewhale_dependencies::Git::review_command(workspace)?;
cmd.current_dir(workspace)
.args([
"log",
@@ -11854,7 +11851,7 @@ fn collect_base_commit_messages(
/// becomes that one line; any other failure (dubious ownership, permissions,
/// a corrupt repository) keeps git's stderr, which names the fix.
fn ensure_review_workspace_is_git_repo(workspace: &std::path::Path) -> Result<()> {
let output = crate::dependencies::Git::review_command(workspace)?
let output = codewhale_dependencies::Git::review_command(workspace)?
.current_dir(workspace)
.args(["rev-parse", "--is-inside-work-tree"])
.output()
@@ -11935,7 +11932,7 @@ fn run_apply(args: ApplyArgs) -> Result<()> {
tmp.write_all(patch.as_bytes())?;
let tmp_path = tmp.path().to_path_buf();
let output = crate::dependencies::Git::command()
let output = codewhale_dependencies::Git::command()
.ok_or_else(|| anyhow::anyhow!("git not found on PATH"))?
.arg("apply")
.arg("--whitespace=nowarn")
@@ -272,7 +272,7 @@
let workspace = tempfile::tempdir().unwrap();
let path = workspace.path();
let git = |args: &[&str]| {
let output = crate::dependencies::Git::command()
let output = codewhale_dependencies::Git::command()
.expect("git")
.current_dir(path)
.args([
@@ -346,7 +346,7 @@
// Positive control: the same repository really can invoke its helper.
let mut baseline = vec!["diff", "--ext-diff"];
baseline.extend_from_slice(diff_flags);
let baseline = crate::dependencies::Git::command()
let baseline = codewhale_dependencies::Git::command()
.unwrap()
.current_dir(path)
.args(&baseline)
+5 -5
View File
@@ -10,7 +10,7 @@
//! records plus the `approval.*` events in its append-only event log.
//!
//! A terminal session also reads the workspace snapshots the engine already
//! takes before and after each turn (`crate::snapshot`, when snapshots are
//! takes before and after each turn (`codewhale_snapshot`, when snapshots are
//! on): their difference is every file the turn changed, including files a
//! shell command changed, which no tool record names.
//!
@@ -30,7 +30,7 @@
//! the thread, so a thread receipt cannot read them and says that shell file
//! changes are not itemized. A snapshot difference covers everything that
//! wrote to the workspace during the turn, not only this agent. Snapshots are
//! pruned to the newest [`crate::snapshot::DEFAULT_MAX_SNAPSHOTS`], so older
//! pruned to the newest [`codewhale_snapshot::DEFAULT_MAX_SNAPSHOTS`], so older
//! turns have none.
use std::collections::{BTreeSet, HashMap};
@@ -215,7 +215,7 @@ pub(crate) fn session_receipt(
notes.insert(format!(
"Shell file changes: {} without a before/after snapshot (snapshots off, or pruned: the newest {} are kept; or a repeated prompt whose snapshots could not be told apart), so files a command changed there are not itemized.",
plural(unmatched, "turn", "turns"),
crate::snapshot::DEFAULT_MAX_SNAPSHOTS
codewhale_snapshot::DEFAULT_MAX_SNAPSHOTS
));
}
changes
@@ -583,7 +583,7 @@ fn snapshot_turn_changes(
wanted: Option<&str>,
) -> Result<Option<TurnChanges>, String> {
use crate::core::turn::{parse_snapshot_label, snapshot_label_prompt_snippet};
let Some(repo) = crate::snapshot::SnapshotRepo::open_existing(workspace)
let Some(repo) = codewhale_snapshot::SnapshotRepo::open_existing(workspace)
.map_err(|error| error.to_string())?
else {
return Ok(None);
@@ -593,7 +593,7 @@ fn snapshot_turn_changes(
// Pair each post-turn:N with the open pre-turn:N of this session, oldest
// first. The sequence restarts when the session is resumed, so a pair
// closes on the first matching post-turn.
let mut open: HashMap<u64, (crate::snapshot::SnapshotId, Option<String>)> = HashMap::new();
let mut open: HashMap<u64, (codewhale_snapshot::SnapshotId, Option<String>)> = HashMap::new();
let mut pairs = Vec::new();
for snapshot in snapshots
.into_iter()
-10
View File
@@ -1,10 +0,0 @@
//! Long-lived Python REPL runtime used by the RLM loop and by inline
//! `` ```repl `` block execution in the agent loop.
pub mod runtime;
pub mod sandbox;
pub use runtime::{
BatchResp, PythonRuntime, ReplRound, RpcDispatcher, RpcRequest, RpcResponse, SingleResp,
};
pub use sandbox::{ReplBlock, extract_repl_blocks, has_repl_block};
+1 -1
View File
@@ -10,8 +10,8 @@
//! `Rlm` / `RlmBatch` requests it submits an admitted Core turn at depth-1.
//! Python and its persistent session never retain this borrowed dispatcher.
use crate::repl::runtime::{BatchResp, RpcDispatcher, RpcRequest, RpcResponse, SingleResp};
use codewhale_models::Usage;
use codewhale_repl::runtime::{BatchResp, RpcDispatcher, RpcRequest, RpcResponse, SingleResp};
use futures_util::future::join_all;
use std::sync::Arc;
use std::sync::Mutex;
+1 -13
View File
@@ -10,8 +10,8 @@ use serde_json::{Value, json};
use tokio::sync::Mutex;
use uuid::Uuid;
use crate::repl::PythonRuntime;
use codewhale_models::{ContentBlock, Message, SystemPrompt};
use codewhale_repl::PythonRuntime;
pub type SharedRlmSessionStore = Arc<Mutex<HashMap<String, Arc<Mutex<RlmSession>>>>>;
@@ -107,18 +107,6 @@ impl Default for RlmSessionConfig {
}
}
pub fn write_context_file(body: &str) -> std::io::Result<PathBuf> {
let dir = std::env::temp_dir().join("deepseek_rlm_ctx");
std::fs::create_dir_all(&dir)?;
let path = dir.join(format!(
"session_{}_{}.txt",
std::process::id(),
Uuid::new_v4().simple()
));
std::fs::write(&path, body)?;
Ok(path)
}
#[derive(Debug, Clone)]
pub struct SessionObjectSnapshot {
pub session_id: String,
+2 -2
View File
@@ -643,8 +643,8 @@ mod tests {
#[tokio::test]
async fn wall_clock_deadline_interrupts_pending_work_and_keeps_partial_result() {
let _home = crate::test_support::SealedHome::new();
use crate::dependencies::ExternalTool as _;
assert!(crate::dependencies::Python::resolve().is_some());
use codewhale_dependencies::ExternalTool as _;
assert!(codewhale_dependencies::Python::resolve().is_some());
for pending_model in [true, false] {
let partial = if pending_model {
"```repl\nprint(_os.environ['RLM_CONTEXT_FILE'])\n```"
+34 -30
View File
@@ -47,7 +47,7 @@ mod constitution;
mod notification_delivery;
#[cfg(test)]
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
use crate::automation_manager::{
AutomationManager, AutomationRecord, AutomationRunRecord, AutomationSchedulerConfig,
@@ -247,7 +247,7 @@ struct RuntimeWorkspaceScope {
directory: Arc<std::fs::File>,
mcp: Mutex<Option<(u64, Arc<Mutex<McpPool>>)>>,
mcp_policy: parking_lot::Mutex<Value>,
lsp: std::sync::OnceLock<Arc<crate::lsp::LspManager>>,
lsp: std::sync::OnceLock<Arc<codewhale_lsp::LspManager>>,
owner: SharedRuntimeThreadManager,
cleanup_runtime: tokio::runtime::Handle,
/// The workspace drive lease held for this scope's lifetime: at most one
@@ -8370,8 +8370,8 @@ const PATCH_UNDO_WORKSPACE_UNAVAILABLE: &str = "workspace_unavailable";
/// the snapshot store still holds.
struct UndoSegment {
turn_id: String,
pre: crate::snapshot::SnapshotId,
post: crate::snapshot::SnapshotId,
pre: codewhale_snapshot::SnapshotId,
post: codewhale_snapshot::SnapshotId,
pre_label: String,
/// Paths that changed in the window only while one of the turn's own
/// tool calls was running: the turn's changes.
@@ -8389,9 +8389,9 @@ struct UndoSegment {
/// turn and is skipped. `None` means a window the engine never closed (the
/// turn died before its post-turn snapshot) or no window at all.
fn turn_snapshot_windows(
snapshots: &[crate::snapshot::WorkspaceSnapshotRef],
snapshots: &[codewhale_snapshot::WorkspaceSnapshotRef],
) -> Option<Vec<std::ops::RangeInclusive<usize>>> {
use crate::snapshot::WorkspaceSnapshotKind;
use codewhale_snapshot::WorkspaceSnapshotKind;
let mut windows = Vec::new();
let mut open = None;
for (index, snapshot) in snapshots.iter().enumerate() {
@@ -8442,7 +8442,7 @@ fn declared_write_path(workspace: &FsPath, raw: &str) -> Option<PathBuf> {
.components()
.filter(|component| !matches!(component, Component::CurDir))
.collect();
crate::snapshot::workspace_relative_path(workspace, rel.to_str()?)
codewhale_snapshot::workspace_relative_path(workspace, rel.to_str()?)
}
/// Who could have changed the workspace in the span after one receipt.
@@ -8463,7 +8463,7 @@ enum SpanWriter {
fn attribute_window(
workspace: &FsPath,
turn_id: &str,
receipts: &[crate::snapshot::WorkspaceSnapshotRef],
receipts: &[codewhale_snapshot::WorkspaceSnapshotRef],
) -> Result<
(
std::collections::BTreeSet<PathBuf>,
@@ -8471,7 +8471,7 @@ fn attribute_window(
),
ApiError,
> {
use crate::snapshot::WorkspaceSnapshotKind;
use codewhale_snapshot::WorkspaceSnapshotKind;
let mut owned = std::collections::BTreeSet::new();
let mut foreign = std::collections::BTreeSet::new();
// Undeclared calls whose `post_tool` receipt is still ahead: everything
@@ -8651,7 +8651,7 @@ fn patch_undo_workspace_files(
// Every repository failure is operational and aborts: "nothing to
// restore" cannot be proven while Git is unavailable.
let repo = crate::snapshot::SnapshotRepo::open_or_init(workspace).map_err(|e| {
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(workspace).map_err(|e| {
ApiError::internal(format!(
"Snapshot repo unavailable; conversation preserved: {e}"
))
@@ -8662,8 +8662,8 @@ fn patch_undo_workspace_files(
.list(usize::MAX)
.map_err(|e| ApiError::internal(format!("Failed to list snapshots: {e}")))?;
let resolve = |turn_id: &str,
receipt: &crate::snapshot::WorkspaceSnapshotRef|
-> Result<(crate::snapshot::SnapshotId, String), ApiError> {
receipt: &codewhale_snapshot::WorkspaceSnapshotRef|
-> Result<(codewhale_snapshot::SnapshotId, String), ApiError> {
listed
.iter()
.find(|snapshot| receipt.matches(snapshot))
@@ -8745,7 +8745,10 @@ fn patch_undo_workspace_files(
// path -> (content to restore, content the dropped turns left)
let mut plan: std::collections::BTreeMap<
PathBuf,
(crate::snapshot::SnapshotId, crate::snapshot::SnapshotId),
(
codewhale_snapshot::SnapshotId,
codewhale_snapshot::SnapshotId,
),
> = std::collections::BTreeMap::new();
for segment in &segments {
let changed = repo
@@ -8848,7 +8851,7 @@ fn patch_undo_workspace_files(
.with_code(PATCH_UNDO_UNTRUSTED));
}
let restore_plan: Vec<(PathBuf, crate::snapshot::SnapshotId)> = to_restore
let restore_plan: Vec<(PathBuf, codewhale_snapshot::SnapshotId)> = to_restore
.iter()
.map(|(path, before, _)| (path.clone(), before.clone()))
.collect();
@@ -8986,7 +8989,7 @@ async fn revert_thread_file(
}
fn snapshot_id_is_well_formed(id: &str) -> bool {
crate::snapshot::SnapshotId::is_well_formed(id)
codewhale_snapshot::SnapshotId::is_well_formed(id)
}
fn expected_hash_is_well_formed(hash: &str) -> bool {
@@ -9001,7 +9004,7 @@ fn expected_hash_is_well_formed(hash: &str) -> bool {
fn revert_file_from_snapshot(
workspace: &FsPath,
owned: &[crate::snapshot::WorkspaceSnapshotRef],
owned: &[codewhale_snapshot::WorkspaceSnapshotRef],
req: &RevertThreadFileRequest,
) -> Result<RevertThreadFileResponse, ApiError> {
// Every caller-supplied path passes through this one gate. It accepts a
@@ -9009,20 +9012,21 @@ fn revert_file_from_snapshot(
// rejects everything else (`..`, empty, or outside the work tree). The
// name is used literally: brackets, spaces and glob characters are part
// of the filename, never a pattern.
let rel = crate::snapshot::workspace_relative_path(workspace, &req.path).ok_or_else(|| {
ApiError::bad_request(format!(
"path must name a regular file inside the thread workspace {}; got '{}'",
workspace.display(),
req.path
))
})?;
let rel =
codewhale_snapshot::workspace_relative_path(workspace, &req.path).ok_or_else(|| {
ApiError::bad_request(format!(
"path must name a regular file inside the thread workspace {}; got '{}'",
workspace.display(),
req.path
))
})?;
if !workspace.is_dir() {
return Err(ApiError::conflict(format!(
"Workspace directory {} is not available; mount or restore it before restoring files.",
workspace.display()
)));
}
let repo = crate::snapshot::SnapshotRepo::open_or_init(workspace)
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(workspace)
.map_err(|e| ApiError::internal(format!("Snapshot repo unavailable: {e}")))?;
repo.validate_restore_file(&rel)
.map_err(map_file_restore_err)?;
@@ -9036,13 +9040,13 @@ fn revert_file_from_snapshot(
// `tree_id` its turn record holds (a prune rewrites commit ids but keeps
// trees). A foreign, unrecorded or pruned id is a conflict the client
// resolves by refreshing its change record.
let restore_points: Vec<&crate::snapshot::WorkspaceSnapshotRef> = owned
let restore_points: Vec<&codewhale_snapshot::WorkspaceSnapshotRef> = owned
.iter()
.filter(|receipt| {
matches!(
receipt.kind,
crate::snapshot::WorkspaceSnapshotKind::Tool
| crate::snapshot::WorkspaceSnapshotKind::PreTurn
codewhale_snapshot::WorkspaceSnapshotKind::Tool
| codewhale_snapshot::WorkspaceSnapshotKind::PreTurn
)
})
.collect();
@@ -10945,9 +10949,9 @@ async fn restore_snapshot(
}
fn restore_snapshot_for_workspace(workspace: &FsPath, id: &str) -> Result<(), ApiError> {
let repo = crate::snapshot::SnapshotRepo::open_or_init(workspace)
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(workspace)
.map_err(|e| ApiError::internal(format!("Snapshot repo init failed: {e}")))?;
let snapshot_id = crate::snapshot::SnapshotId::parse(id)
let snapshot_id = codewhale_snapshot::SnapshotId::parse(id)
.map_err(|e| ApiError::bad_request(format!("Invalid snapshot id: {e}")))?;
repo.restore(&snapshot_id)
.map_err(|e| ApiError::internal(format!("Snapshot restore failed: {e}")))
@@ -10968,7 +10972,7 @@ fn snapshot_entries_for_workspace(
)));
}
};
let repo = crate::snapshot::SnapshotRepo::open_or_init(workspace)
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(workspace)
.map_err(|e| ApiError::internal(format!("Snapshot repo unavailable: {e}")))?;
let snapshots = repo
.list(limit)
+2 -2
View File
@@ -52,7 +52,7 @@ use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use crate::dependencies::{ExternalTool as _, Git};
use codewhale_dependencies::{ExternalTool as _, Git};
use super::workspace::{
FILE_SERVE_MAX_BYTES, canonical_workspace, collect_workspace_status, content_revision,
@@ -121,7 +121,7 @@ async fn git_read(workspace: &FsPath, args: &[&str]) -> Result<GitRun, ApiError>
/// Write path for operator-driven mutations. Non-interactive by contract:
/// [`Git::tokio_command`] carries the shared no-prompt environment
/// ([`crate::dependencies::apply_git_noninteractive_env`]) so a credential or
/// ([`codewhale_dependencies::apply_git_noninteractive_env`]) so a credential or
/// key prompt can never hang the request. Hooks and filters run exactly as they do for the user's own
/// `git` — a Review-sheet commit is the user's commit.
async fn git_write(workspace: &FsPath, args: Vec<String>) -> Result<GitRun, ApiError> {
+3 -3
View File
@@ -28,8 +28,8 @@ use serde_json::{Value, json};
use super::workspace::{canonical_workspace, precheck_file_target, relative_request_path};
use super::{ApiError, RuntimeApiState};
use crate::lsp::LspManager;
use crate::lsp::registry::Language;
use codewhale_lsp::LspManager;
use codewhale_lsp::registry::Language;
const LSP_OPERATIONS: &[&str] = &["diagnostics", "symbols", "definition", "references"];
const LANGUAGES: &[Language] = &[
@@ -58,7 +58,7 @@ async fn lsp_manager(state: &RuntimeApiState) -> Result<Arc<LspManager>, ApiErro
.read()
.lsp
.clone()
.map(crate::lsp::LspConfig::from)
.map(codewhale_lsp::LspConfig::from)
.unwrap_or_default();
Ok(state
.workspace_scope
+51 -51
View File
@@ -344,7 +344,7 @@ fn saved_session_with_blocks(blocks: Vec<codewhale_models::ContentBlock>) -> Sav
}
fn run_test_git(workspace: &std::path::Path, args: &[&str]) -> Result<()> {
let output = crate::dependencies::Git::output(args, workspace)
let output = codewhale_dependencies::Git::output(args, workspace)
.with_context(|| format!("git {args:?} failed to spawn"))?;
if !output.status.success() {
bail!(
@@ -1052,7 +1052,7 @@ struct TestServerOverrides {
compat_stream_test_hook: Option<mpsc::UnboundedSender<CompatStreamTestPoint>>,
plugin_discovery: Option<Arc<crate::plugins::PluginDiscoveryContext>>,
/// Pre-seeded workspace LSP manager (test transports, disabled configs).
lsp_manager: Option<Arc<crate::lsp::LspManager>>,
lsp_manager: Option<Arc<codewhale_lsp::LspManager>>,
/// Engine LRU capacity; `None` keeps the fixture default of 8.
max_active_threads: Option<usize>,
/// Stops the server through the product's own graceful-shutdown path.
@@ -7558,22 +7558,22 @@ async fn patch_undo_endpoint_refuses_turns_it_cannot_prove() -> Result<()> {
}
fn snapshot_receipt(
kind: crate::snapshot::WorkspaceSnapshotKind,
taken: &crate::snapshot::TakenSnapshot,
kind: codewhale_snapshot::WorkspaceSnapshotKind,
taken: &codewhale_snapshot::TakenSnapshot,
session_id: &str,
) -> crate::snapshot::WorkspaceSnapshotRef {
crate::snapshot::WorkspaceSnapshotRef::new(kind, taken, session_id, None)
) -> codewhale_snapshot::WorkspaceSnapshotRef {
codewhale_snapshot::WorkspaceSnapshotRef::new(kind, taken, session_id, None)
}
/// The receipts of a shell turn: its command runs from the pre-turn snapshot
/// to the post-turn one, so the whole window is the turn's own span.
fn shell_turn_receipts(
repo: &crate::snapshot::SnapshotRepo,
pre: &crate::snapshot::TakenSnapshot,
post: &crate::snapshot::TakenSnapshot,
repo: &codewhale_snapshot::SnapshotRepo,
pre: &codewhale_snapshot::TakenSnapshot,
post: &codewhale_snapshot::TakenSnapshot,
session_id: &str,
) -> Vec<crate::snapshot::WorkspaceSnapshotRef> {
use crate::snapshot::WorkspaceSnapshotKind::{PostTurn, PreTurn};
) -> Vec<codewhale_snapshot::WorkspaceSnapshotRef> {
use codewhale_snapshot::WorkspaceSnapshotKind::{PostTurn, PreTurn};
let mut pre_receipt = snapshot_receipt(PreTurn, pre, session_id);
pre_receipt.tool_call_id = Some("user_shell_1".to_string());
let mut post_receipt = snapshot_receipt(PostTurn, post, session_id);
@@ -7582,9 +7582,9 @@ fn shell_turn_receipts(
}
fn changed_between(
repo: &crate::snapshot::SnapshotRepo,
from: &crate::snapshot::TakenSnapshot,
to: &crate::snapshot::TakenSnapshot,
repo: &codewhale_snapshot::SnapshotRepo,
from: &codewhale_snapshot::TakenSnapshot,
to: &codewhale_snapshot::TakenSnapshot,
) -> Vec<String> {
repo.changed_paths_between(&from.tree, &to.tree)
.expect("diff snapshots")
@@ -7596,7 +7596,7 @@ fn changed_between(
fn dropped_turn(
turn_id: &str,
may_change_files: bool,
snapshots: Vec<crate::snapshot::WorkspaceSnapshotRef>,
snapshots: Vec<codewhale_snapshot::WorkspaceSnapshotRef>,
) -> crate::runtime_threads::DroppedTurnSnapshots {
crate::runtime_threads::DroppedTurnSnapshots {
turn_id: turn_id.to_string(),
@@ -7608,7 +7608,7 @@ fn dropped_turn(
}
fn git_missing() -> bool {
crate::dependencies::Git::command().is_none()
codewhale_dependencies::Git::command().is_none()
}
/// A patch-undo restores exactly what the dropped turn changed — every write
@@ -7627,7 +7627,7 @@ fn patch_undo_helper_restores_only_the_dropped_turns_changes() -> Result<()> {
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let a = workspace.join("a.txt");
let other = workspace.join("other.txt");
fs::write(&a, "before")?;
@@ -7690,7 +7690,7 @@ fn patch_undo_helper_refuses_to_overwrite_later_edits() -> Result<()> {
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let a = workspace.join("a.txt");
fs::write(&a, "before")?;
let pre = repo.take_snapshot("pre-turn:1", Some("thr_a"))?;
@@ -7717,7 +7717,7 @@ fn patch_undo_helper_refuses_to_overwrite_later_edits() -> Result<()> {
/// forking over changed files. A turn that ran no tools needs none.
#[test]
fn patch_undo_helper_fails_honestly_without_an_owned_restore_point() -> Result<()> {
use crate::snapshot::WorkspaceSnapshotKind::PreTurn;
use codewhale_snapshot::WorkspaceSnapshotKind::PreTurn;
if git_missing() {
return Ok(());
}
@@ -7729,7 +7729,7 @@ fn patch_undo_helper_fails_honestly_without_an_owned_restore_point() -> Result<(
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let a = workspace.join("a.txt");
fs::write(&a, "before")?;
let pre = repo.take_snapshot("pre-turn:1", Some("thr_a"))?;
@@ -7801,7 +7801,7 @@ fn patch_undo_helper_survives_a_prune_that_rewrites_commit_ids() -> Result<()> {
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let a = workspace.join("a.txt");
fs::write(&a, "ancient")?;
repo.take_snapshot("pre-turn:0", Some("thr_old"))?;
@@ -7834,12 +7834,12 @@ fn patch_undo_helper_survives_a_prune_that_rewrites_commit_ids() -> Result<()> {
/// One recorded receipt with the span data a recording engine reports.
fn span_receipt(
kind: crate::snapshot::WorkspaceSnapshotKind,
taken: &crate::snapshot::TakenSnapshot,
kind: codewhale_snapshot::WorkspaceSnapshotKind,
taken: &codewhale_snapshot::TakenSnapshot,
call: Option<&str>,
write_paths: Option<&[&str]>,
changed: Option<Vec<String>>,
) -> crate::snapshot::WorkspaceSnapshotRef {
) -> codewhale_snapshot::WorkspaceSnapshotRef {
let mut receipt = snapshot_receipt(kind, taken, "thr_a");
receipt.tool_call_id = call.map(str::to_string);
receipt.write_paths = write_paths.map(|paths| paths.iter().map(|p| p.to_string()).collect());
@@ -7855,7 +7855,7 @@ fn span_receipt(
/// and a span with no record of its changes fails closed.
#[test]
fn patch_undo_helper_attributes_changes_to_the_turns_own_tool_spans() -> Result<()> {
use crate::snapshot::WorkspaceSnapshotKind::{PostTool, PostTurn, PreTurn, Tool};
use codewhale_snapshot::WorkspaceSnapshotKind::{PostTool, PostTurn, PreTurn, Tool};
if git_missing() {
return Ok(());
}
@@ -7867,7 +7867,7 @@ fn patch_undo_helper_attributes_changes_to_the_turns_own_tool_spans() -> Result<
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let own = workspace.join("own.txt");
let sibling = workspace.join("sibling.txt");
fs::write(&own, "before")?;
@@ -7986,7 +7986,7 @@ fn patch_undo_helper_attributes_changes_to_the_turns_own_tool_spans() -> Result<
/// when the snapshots show nothing changed.
#[test]
fn patch_undo_helper_refuses_declared_writes_snapshots_cannot_hold() -> Result<()> {
use crate::snapshot::WorkspaceSnapshotKind::{PostTool, PostTurn, PreTurn, Tool};
use codewhale_snapshot::WorkspaceSnapshotKind::{PostTool, PostTurn, PreTurn, Tool};
if git_missing() {
return Ok(());
}
@@ -7999,7 +7999,7 @@ fn patch_undo_helper_refuses_declared_writes_snapshots_cannot_hold() -> Result<(
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
fs::write(workspace.join(".gitignore"), "*.local\n")?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let pre = repo.take_snapshot("pre-turn:1", Some("thr_a"))?;
let tool = repo.take_snapshot("tool:call-1", Some("thr_a"))?;
fs::write(workspace.join(".env.local"), "SECRET=changed")?;
@@ -8075,7 +8075,7 @@ fn patch_undo_helper_refuses_an_untrusted_rollback_and_touches_nothing() -> Resu
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let file = workspace.join("a.txt");
fs::write(&file, "before")?;
@@ -8127,7 +8127,7 @@ fn patch_undo_helper_allows_an_untrusted_undo_with_nothing_to_roll_back() -> Res
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let file = workspace.join("a.txt");
fs::write(&file, "only-state")?;
@@ -8178,7 +8178,7 @@ fn revert_file_helper_restores_only_the_named_file() -> Result<()> {
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let wanted = workspace.join("wanted.txt");
let other = workspace.join("other.txt");
@@ -8186,7 +8186,7 @@ fn revert_file_helper_restores_only_the_named_file() -> Result<()> {
fs::write(&other, "other-before")?;
let taken = repo.take_snapshot("pre-turn:1", Some("session-a"))?;
let owned = [snapshot_receipt(
crate::snapshot::WorkspaceSnapshotKind::PreTurn,
codewhale_snapshot::WorkspaceSnapshotKind::PreTurn,
&taken,
"session-a",
)];
@@ -8224,13 +8224,13 @@ fn revert_file_helper_accepts_an_absolute_path_inside_the_workspace() -> Result<
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let file = workspace.join("a.txt");
fs::write(&file, "v1")?;
let taken = repo.take_snapshot("pre-turn:1", Some("session-a"))?;
let owned = [snapshot_receipt(
crate::snapshot::WorkspaceSnapshotKind::PreTurn,
codewhale_snapshot::WorkspaceSnapshotKind::PreTurn,
&taken,
"session-a",
)];
@@ -8260,7 +8260,7 @@ fn revert_file_helper_accepts_an_absolute_path_inside_the_workspace() -> Result<
/// the workspace untouched.
#[test]
fn revert_file_helper_requires_exact_snapshot_identity_and_reviewed_hash() -> Result<()> {
use crate::snapshot::WorkspaceSnapshotKind::{PostTurn, Tool};
use codewhale_snapshot::WorkspaceSnapshotKind::{PostTurn, Tool};
let _lock = lock_test_env();
let root = tempfile::tempdir()?;
let home = root.path().join("home");
@@ -8269,7 +8269,7 @@ fn revert_file_helper_requires_exact_snapshot_identity_and_reviewed_hash() -> Re
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let a = workspace.join("a.txt");
let b = workspace.join("b.txt");
@@ -8382,7 +8382,7 @@ fn revert_file_helper_refuses_foreign_snapshots_and_paths_outside_the_workspace(
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
let file = workspace.join("a.txt");
// Only another thread recorded a restore point, so this thread owns
@@ -8652,7 +8652,7 @@ fn restore_snapshot_endpoint_helper_restores_workspace_files() -> Result<()> {
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
fs::write(workspace.join("a.txt"), "v1")?;
let snapshot_id = repo.snapshot("pre-turn:1")?;
fs::write(workspace.join("a.txt"), "v2")?;
@@ -8789,7 +8789,7 @@ fn snapshots_endpoint_lists_workspace_snapshots() -> Result<()> {
let workspace = root.path().join("workspace");
fs::create_dir_all(&workspace)?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
fs::write(workspace.join("a.txt"), "v1")?;
repo.snapshot("pre-turn:1")?;
fs::write(workspace.join("a.txt"), "v2")?;
@@ -21248,7 +21248,7 @@ async fn call_change_route_reads_one_calls_workspace_span() -> Result<()> {
// `post-tool` partner after. `out.md` is the command's own write;
// `script.py` is a path it modified. Afterwards the work tree moves on
// again — the route must report the span, not what is on disk now.
let repo = crate::snapshot::SnapshotRepo::open_or_init(&workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
fs::write(workspace.join("script.py"), "print('v1')\n")?;
let tool = repo.take_snapshot(&format!("tool:{SHELL_CALL}"), Some(&thread.id))?;
fs::write(workspace.join("out.md"), "written by the command\n")?;
@@ -22461,9 +22461,9 @@ async fn targets_report_self_and_probe_remote_endpoints() -> Result<()> {
#[tokio::test]
async fn lsp_routes_serve_workspace_files_through_a_transport() -> Result<()> {
use crate::lsp::diagnostics::{Diagnostic, Severity};
use crate::lsp::registry::Language;
use crate::lsp::tests::FakeTransport;
use codewhale_lsp::diagnostics::{Diagnostic, Severity};
use codewhale_lsp::registry::Language;
use codewhale_lsp::tests::FakeTransport;
let tmp = tempfile::tempdir()?;
let workspace = tmp.path().join("workspace");
@@ -22472,8 +22472,8 @@ async fn lsp_routes_serve_workspace_files_through_a_transport() -> Result<()> {
fs::write(workspace.join("notes.unknownext"), "hi\n")?;
// Seed the shared manager with a fake Rust transport — no real LSP spawn.
let manager = Arc::new(crate::lsp::LspManager::new(
crate::lsp::LspConfig::default(),
let manager = Arc::new(codewhale_lsp::LspManager::new(
codewhale_lsp::LspConfig::default(),
workspace.canonicalize()?,
));
manager
@@ -22677,7 +22677,7 @@ async fn voice_routes_report_capability_and_fail_closed_without_a_recorder() ->
#[tokio::test]
async fn git_routes_drive_a_real_workspace_repo() -> Result<()> {
use crate::dependencies::{ExternalTool as _, Git};
use codewhale_dependencies::{ExternalTool as _, Git};
let tmp = tempfile::tempdir()?;
let workspace = tmp.path().join("workspace");
@@ -22980,7 +22980,7 @@ async fn git_routes_drive_a_real_workspace_repo() -> Result<()> {
/// current state and writes nothing; no `expect` keeps the old behaviour.
#[tokio::test]
async fn git_write_preconditions_reject_stale_state() -> Result<()> {
use crate::dependencies::{ExternalTool as _, Git};
use codewhale_dependencies::{ExternalTool as _, Git};
let tmp = tempfile::tempdir()?;
let workspace = tmp.path().join("workspace");
@@ -24713,7 +24713,7 @@ mod thread_snapshot_ownership {
/// `None` when git or a loopback listener is unavailable.
async fn fixture(root: &FsPath, overrides: TestServerOverrides) -> Result<Option<Fixture>> {
if crate::dependencies::Git::command().is_none() {
if codewhale_dependencies::Git::command().is_none() {
return Ok(None);
}
let workspace = root.join("workspace");
@@ -25251,7 +25251,7 @@ mod thread_snapshot_ownership {
.find(|receipt| receipt["kind"] == "tool")
.context("a tool receipt")?;
let repo = crate::snapshot::SnapshotRepo::open_or_init(&fx.workspace)?;
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&fx.workspace)?;
let tui = repo.take_snapshot("tool:tui-call", Some("tui-session-6621"))?;
let listed_a = repo
.list(usize::MAX)?
@@ -25452,7 +25452,7 @@ mod thread_snapshot_ownership {
};
let pruned = fx.create_thread().await?;
fx.write_turn(&pruned, &[("pruned.txt", "p")]).await?;
crate::snapshot::SnapshotRepo::open_or_init(&fx.workspace)?
codewhale_snapshot::SnapshotRepo::open_or_init(&fx.workspace)?
.prune_older_than(Duration::ZERO)?;
refused(&fx, pruned, "restore_point_pruned").await?;
assert_eq!(fx.read("pruned.txt").as_deref(), Some("p"));
@@ -25657,7 +25657,7 @@ mod thread_snapshot_ownership {
.collect();
fx.write_turn(&thread_id, &writes).await?;
let listed =
crate::snapshot::SnapshotRepo::open_or_init(&fx.workspace)?.list(usize::MAX)?;
codewhale_snapshot::SnapshotRepo::open_or_init(&fx.workspace)?.list(usize::MAX)?;
assert!(
listed.len() < 12,
"the cap pruned the tool snapshots: {}",
+6 -6
View File
@@ -234,10 +234,10 @@ fn read_snapshot_blob(
snapshot_id: &str,
path: &str,
) -> Result<Option<Vec<u8>>, ApiError> {
let Ok(id) = crate::snapshot::SnapshotId::parse(snapshot_id) else {
let Ok(id) = codewhale_snapshot::SnapshotId::parse(snapshot_id) else {
return Ok(None);
};
let Some(repo) = crate::snapshot::SnapshotRepo::open_existing(workspace)
let Some(repo) = codewhale_snapshot::SnapshotRepo::open_existing(workspace)
.map_err(|error| ApiError::internal(format!("snapshot repo unavailable: {error}")))?
else {
return Ok(None);
@@ -501,14 +501,14 @@ fn call_span_files(
limit: usize,
) -> Result<CallSpanOutcome, ApiError> {
let (Ok(pre), Ok(post)) = (
crate::snapshot::SnapshotId::parse(pre_tree),
crate::snapshot::SnapshotId::parse(post_tree),
codewhale_snapshot::SnapshotId::parse(pre_tree),
codewhale_snapshot::SnapshotId::parse(post_tree),
) else {
return Err(ApiError::internal(
"a recorded restore point is not a valid snapshot id",
));
};
let Some(repo) = crate::snapshot::SnapshotRepo::open_existing(workspace)
let Some(repo) = codewhale_snapshot::SnapshotRepo::open_existing(workspace)
.map_err(|error| ApiError::internal(format!("snapshot repo unavailable: {error}")))?
else {
// No store at all. Its receipts cannot be resolved any more than a
@@ -520,7 +520,7 @@ fn call_span_files(
// Asked before the diff: git's answer to a pruned object is a failure of
// the whole command, and a caller has to tell "these were pruned" apart
// from "this repo is broken".
let has_tree = |id: &crate::snapshot::SnapshotId| {
let has_tree = |id: &codewhale_snapshot::SnapshotId| {
repo.has_tree(id)
.map_err(|error| ApiError::internal(format!("snapshot tree lookup failed: {error}")))
};
+2 -2
View File
@@ -8,7 +8,7 @@ use base64::Engine as _;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::dependencies::Git;
use codewhale_dependencies::Git;
use codewhale_util::git_metadata::is_git_metadata_name;
use super::{ApiError, RuntimeApiState};
@@ -1025,7 +1025,7 @@ pub(super) async fn workspace_instructions(
mod tests {
use super::*;
#[cfg(unix)]
use crate::dependencies::ExternalTool as _;
use codewhale_dependencies::ExternalTool as _;
#[cfg(unix)]
#[test]
+18 -18
View File
@@ -1731,7 +1731,7 @@ pub struct TurnRecord {
/// imported from a saved session, and turns that ran with snapshots off
/// carry none, and so have no restore point.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workspace_snapshots: Vec<crate::snapshot::WorkspaceSnapshotRef>,
pub workspace_snapshots: Vec<codewhale_snapshot::WorkspaceSnapshotRef>,
}
impl TurnRecord {
@@ -6972,7 +6972,7 @@ pub(crate) struct DroppedTurnSnapshots {
/// not keep tool calls.
pub may_change_files: bool,
/// The restore points the engine reported for the turn, in order.
pub snapshots: Vec<crate::snapshot::WorkspaceSnapshotRef>,
pub snapshots: Vec<codewhale_snapshot::WorkspaceSnapshotRef>,
/// Paths the turn's file-tool calls (`write_file`, `edit_file`,
/// `apply_patch`) declared they write, as the calls named them, for every
/// call that ran (completed, or stopped mid-run). A turn-scoped undo can
@@ -12207,14 +12207,14 @@ impl RuntimeThreadManager {
if turn.thread_id != thread_id {
return Ok(None);
}
let recorded = |kind: crate::snapshot::WorkspaceSnapshotKind| {
let recorded = |kind: codewhale_snapshot::WorkspaceSnapshotKind| {
turn.workspace_snapshots.iter().rfind(|receipt| {
receipt.kind == kind
&& receipt.tool_call_id.as_deref() == Some(tool_call_id.as_str())
})
};
let pre = recorded(crate::snapshot::WorkspaceSnapshotKind::Tool);
let post = recorded(crate::snapshot::WorkspaceSnapshotKind::PostTool);
let pre = recorded(codewhale_snapshot::WorkspaceSnapshotKind::Tool);
let post = recorded(codewhale_snapshot::WorkspaceSnapshotKind::PostTool);
let item = manager.item_for_call(&turn, &tool_call_id)?;
// A call the turn recorded no item for is a call this turn never
// ran: the caller asked about the wrong turn. A call with an item
@@ -13191,7 +13191,7 @@ impl RuntimeThreadManager {
pub(crate) fn thread_workspace_snapshots(
&self,
thread_id: &str,
) -> Result<Vec<crate::snapshot::WorkspaceSnapshotRef>> {
) -> Result<Vec<codewhale_snapshot::WorkspaceSnapshotRef>> {
Ok(self
.store
.list_turns_for_thread(thread_id)?
@@ -17668,7 +17668,7 @@ impl RuntimeThreadManager {
let lsp_config = (!isolated_chat)
.then(|| cfg.lsp.clone())
.flatten()
.map(crate::lsp::LspConfig::from);
.map(codewhale_lsp::LspConfig::from);
let max_subagents = cfg
.max_subagents_for_provider(&route_identity)
.clamp(1, MAX_SUBAGENTS);
@@ -20973,7 +20973,7 @@ impl RuntimeThreadManager {
"workspace snapshot receipt was not recorded; this turn has no such restore point: {err:#}"
);
} else {
if snapshot.kind == crate::snapshot::WorkspaceSnapshotKind::Tool
if snapshot.kind == codewhale_snapshot::WorkspaceSnapshotKind::Tool
&& let Some(call_id) = snapshot.tool_call_id.as_ref()
{
tool_restore_points.insert(call_id.clone(), snapshot.tree_id.clone());
@@ -21363,14 +21363,14 @@ impl RuntimeThreadManager {
fn initial_turn_workspace(
&self,
thread_id: &str,
snapshots: &[crate::snapshot::WorkspaceSnapshotRef],
snapshots: &[codewhale_snapshot::WorkspaceSnapshotRef],
) -> TurnWorkspaceArtifacts {
if let Some(pair) = turn_snapshot_pair(snapshots) {
return TurnWorkspaceArtifacts::pending(pair.pre.tree_id);
}
let pre = snapshots
.iter()
.find(|snapshot| snapshot.kind == crate::snapshot::WorkspaceSnapshotKind::PreTurn);
.find(|snapshot| snapshot.kind == codewhale_snapshot::WorkspaceSnapshotKind::PreTurn);
let gated = || -> Option<TurnWorkspaceReason> {
if !self.config.read().snapshots_config().enabled {
return Some(TurnWorkspaceReason::SnapshotsDisabled);
@@ -22618,16 +22618,16 @@ pub struct TurnArtifactsView {
/// The pre-turn and post-turn restore points recorded on one turn: the
/// pair its workspace delta is diffed from.
struct TurnSnapshotPair {
pre: crate::snapshot::WorkspaceSnapshotRef,
post: crate::snapshot::WorkspaceSnapshotRef,
pre: codewhale_snapshot::WorkspaceSnapshotRef,
post: codewhale_snapshot::WorkspaceSnapshotRef,
}
/// The turn's first `pre_turn` and last `post_turn` receipts, when both were
/// recorded.
fn turn_snapshot_pair(
snapshots: &[crate::snapshot::WorkspaceSnapshotRef],
snapshots: &[codewhale_snapshot::WorkspaceSnapshotRef],
) -> Option<TurnSnapshotPair> {
use crate::snapshot::WorkspaceSnapshotKind;
use codewhale_snapshot::WorkspaceSnapshotKind;
let pre = snapshots
.iter()
.find(|snapshot| snapshot.kind == WorkspaceSnapshotKind::PreTurn)?;
@@ -22648,11 +22648,11 @@ fn workspace_delta(
pre: &str,
post: &str,
item_paths: &[String],
) -> Result<(crate::snapshot::SnapshotDelta, HashSet<String>)> {
let repo = crate::snapshot::SnapshotRepo::open_existing(workspace)?
) -> Result<(codewhale_snapshot::SnapshotDelta, HashSet<String>)> {
let repo = codewhale_snapshot::SnapshotRepo::open_existing(workspace)?
.context("workspace snapshot repo is missing")?;
let pre = crate::snapshot::SnapshotId::parse(pre)?;
let post = crate::snapshot::SnapshotId::parse(post)?;
let pre = codewhale_snapshot::SnapshotId::parse(pre)?;
let post = codewhale_snapshot::SnapshotId::parse(post)?;
let delta = repo.diff_snapshots(&pre, &post, turn_artifacts::MAX_TURN_ARTIFACTS)?;
let mut tracked = repo.tracked_paths(&pre, item_paths)?;
tracked.extend(repo.tracked_paths(&post, item_paths)?);
+5 -5
View File
@@ -23385,10 +23385,10 @@ async fn workspace_turn_fixture() -> Result<WorkspaceTurnFixture> {
/// Runtime engine tags it (the thread's own id, #6621).
fn fixture_restore_point(
fixture: &WorkspaceTurnFixture,
kind: crate::snapshot::WorkspaceSnapshotKind,
kind: codewhale_snapshot::WorkspaceSnapshotKind,
label: &str,
tool_call_id: Option<&str>,
) -> Result<crate::snapshot::WorkspaceSnapshotRef> {
) -> Result<codewhale_snapshot::WorkspaceSnapshotRef> {
let (taken, _) = crate::core::turn::restore_point_snapshot(
&fixture.workspace,
label,
@@ -23397,7 +23397,7 @@ fn fixture_restore_point(
None,
)
.ok_or_else(|| anyhow!("snapshot {label} failed"))?;
Ok(crate::snapshot::WorkspaceSnapshotRef::new(
Ok(codewhale_snapshot::WorkspaceSnapshotRef::new(
kind,
&taken,
&fixture.thread.id,
@@ -23423,8 +23423,8 @@ enum ScriptedSnapshots {
async fn run_workspace_turn(
fixture: &WorkspaceTurnFixture,
scripted: ScriptedSnapshots,
) -> Result<(TurnRecord, Vec<crate::snapshot::WorkspaceSnapshotRef>)> {
use crate::snapshot::WorkspaceSnapshotKind;
) -> Result<(TurnRecord, Vec<codewhale_snapshot::WorkspaceSnapshotRef>)> {
use codewhale_snapshot::WorkspaceSnapshotKind;
let manager = &fixture.manager;
let mut harness = install_mock_engine(manager, &fixture.thread.id).await;
let turn = manager
@@ -146,7 +146,7 @@ pub(crate) fn is_sha256_hex(value: &str) -> bool {
pub(crate) fn confined_workspace_path(workspace_roots: &[PathBuf], raw: &str) -> Option<String> {
let rel = workspace_roots
.iter()
.find_map(|root| crate::snapshot::workspace_relative_path(root, raw))?;
.find_map(|root| codewhale_snapshot::workspace_relative_path(root, raw))?;
relative_display(&rel)
}
@@ -244,7 +244,7 @@ pub(crate) fn artifact_refs_from_tool_metadata(
// turn — never from the tool's result metadata, which a tool controls.
let restore_snapshot_id = context
.restore_snapshot_id
.filter(|id| crate::snapshot::SnapshotId::is_well_formed(id))
.filter(|id| codewhale_snapshot::SnapshotId::is_well_formed(id))
.map(str::to_owned);
if let Some(mutation) = metadata.get("mutation") {
@@ -497,11 +497,11 @@ pub(crate) struct WorkspaceDelta {
/// pre-turn restore point recorded on the turn, which file-revert accepts
/// for this thread.
pub(crate) fn delta_refs(
delta: &crate::snapshot::SnapshotDelta,
delta: &codewhale_snapshot::SnapshotDelta,
restore_snapshot_id: Option<&str>,
recorded_at: DateTime<Utc>,
) -> Vec<TurnArtifactRef> {
use crate::snapshot::DeltaChange;
use codewhale_snapshot::DeltaChange;
delta
.entries
.iter()
+1 -1
View File
@@ -4713,7 +4713,7 @@ fn copy_file_create_new(src: &Path, dst: &Path) -> io::Result<bool> {
/// Always non-fatal. Returns silently — callers don't need the count
/// (the underlying repo logs at WARN if anything blew up).
pub fn prune_workspace_snapshots(workspace: &Path, max_age: std::time::Duration) {
match crate::snapshot::prune_older_than(workspace, max_age) {
match codewhale_snapshot::prune_older_than(workspace, max_age) {
Ok(0) => {}
Ok(n) => {
tracing::debug!(target: "snapshot", "boot prune removed {n} snapshot(s)");
+1 -1
View File
@@ -152,7 +152,7 @@ mod tests {
);
inside(
"snapshots",
&crate::snapshot::snapshot_dir_for(Path::new("/nonexistent/codewhale-fence-probe"))
&codewhale_snapshot::snapshot_dir_for(Path::new("/nonexistent/codewhale-fence-probe"))
.expect("snapshot dir"),
);
let audit = codewhale_runtime::audit::audit_log_path().expect("audit log path");
+3 -3
View File
@@ -234,18 +234,18 @@ fn run_command(program: &str, args: &[&str], cwd: &Path) -> CommandProbe {
#[cfg(test)]
mod tests {
use super::*;
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
use std::fs;
use std::path::Path;
use tempfile::tempdir;
fn git_available() -> bool {
crate::dependencies::Git::available()
codewhale_dependencies::Git::available()
}
fn init_git_repo(root: &Path) {
let run = |args: &[&str]| {
let status = crate::dependencies::Git::status(args, root).expect("git should spawn");
let status = codewhale_dependencies::Git::status(args, root).expect("git should spawn");
assert!(status.success(), "git {args:?} failed");
};
run(&["init", "-q"]);
+1 -1
View File
@@ -639,7 +639,7 @@ fn clean_pdf_text_preserves_leading_indentation() {
#[tokio::test]
async fn read_file_pdf_path_uses_optional_pdftotext_adapter() {
if !sample_pdf_present() || crate::dependencies::resolve_pdftotext().is_none() {
if !sample_pdf_present() || codewhale_dependencies::resolve_pdftotext().is_none() {
return;
}
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../");
+14 -14
View File
@@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
use async_trait::async_trait;
use serde_json::{Value, json};
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
use super::spec::{
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
@@ -218,7 +218,7 @@ fn git_diff_blocking(input: Value, context: &ToolContext) -> Result<ToolResult,
"diff".to_string(),
"--no-color".to_string(),
];
args.extend(crate::dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
args.extend(codewhale_dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
args.push(format!("--unified={unified}"));
if cached {
args.push("--cached".to_string());
@@ -331,7 +331,7 @@ fn git_commit_plan_blocking(input: Value, context: &ToolContext) -> Result<ToolR
"--no-color".to_string(),
"-U3".to_string(),
];
diff_args.extend(crate::dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
diff_args.extend(codewhale_dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
if let Some(pathspec) = &git_ctx.pathspec {
diff_args.push("--".to_string());
diff_args.push(pathspec.display().to_string());
@@ -391,7 +391,7 @@ fn git_commit_plan_blocking(input: Value, context: &ToolContext) -> Result<ToolR
"--cached".to_string(),
"--quiet".to_string(),
];
staged_args.extend(crate::dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
staged_args.extend(codewhale_dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
let index_has_staged_changes =
run_git_command(working_dir, &staged_args)?.status.code() == Some(1);
@@ -507,17 +507,17 @@ fn pathspec_from(working_dir: &Path, resolved: &Path) -> PathBuf {
/// Build the git command for the read-only workspace tools (`git_status`,
/// `git_diff`, `git_log`, `git_show`, `git_blame`, `verify`). It goes through
/// [`crate::dependencies::Git::review_command`], so fsmonitor, hooks and
/// [`codewhale_dependencies::Git::review_command`], so fsmonitor, hooks and
/// clean/process filters from the workspace's own config do not run.
pub(super) fn read_only_git_command(
working_dir: &Path,
) -> Result<std::process::Command, ToolError> {
if !crate::dependencies::Git::available() {
if !codewhale_dependencies::Git::available() {
return Err(ToolError::not_available(
"git is not installed or not in PATH",
));
}
crate::dependencies::Git::review_command(working_dir)
codewhale_dependencies::Git::review_command(working_dir)
.map_err(|e| ToolError::execution_failed(format!("Failed to prepare git: {e:#}")))
}
@@ -1224,12 +1224,12 @@ mod tests {
use tempfile::tempdir;
fn git_available() -> bool {
crate::dependencies::Git::available()
codewhale_dependencies::Git::available()
}
fn init_git_repo(root: &Path) {
let run = |args: &[&str]| {
let status = crate::dependencies::Git::status(args, root).expect("git should spawn");
let status = codewhale_dependencies::Git::status(args, root).expect("git should spawn");
assert!(status.success(), "git {args:?} failed");
};
@@ -1241,7 +1241,7 @@ mod tests {
fn commit_all(root: &Path, message: &str) {
let run = |args: &[&str]| {
let status = crate::dependencies::Git::status(args, root).expect("git should spawn");
let status = codewhale_dependencies::Git::status(args, root).expect("git should spawn");
assert!(status.success(), "git {args:?} failed");
};
run(&["add", "."]);
@@ -1273,7 +1273,7 @@ mod tests {
)
.expect("write hook");
fs::set_permissions(&hook, fs::Permissions::from_mode(0o755)).expect("chmod");
let status = crate::dependencies::Git::status(
let status = codewhale_dependencies::Git::status(
&["config", "core.fsmonitor", &hook.to_string_lossy()],
tmp.path(),
)
@@ -1374,8 +1374,8 @@ mod tests {
assert!(uncached.content.contains("diff --git"));
assert!(uncached.content.contains("lib.rs"));
let _ =
crate::dependencies::Git::status(&["add", "src/lib.rs"], tmp.path()).expect("git add");
let _ = codewhale_dependencies::Git::status(&["add", "src/lib.rs"], tmp.path())
.expect("git add");
let cached = tool
.execute(json!({ "path": "src", "cached": true }), &ctx)
@@ -1840,7 +1840,7 @@ Binary files a/image.png and b/image.png differ
let external = script("external.sh", "");
let textconv = script("textconv.sh", "cat \"$1\"\n");
let git = |dir: &Path, args: &[&str]| {
let status = crate::dependencies::Git::status(args, dir).expect("git should spawn");
let status = codewhale_dependencies::Git::status(args, dir).expect("git should spawn");
assert!(status.success(), "git {args:?} failed");
};
let identity = ["-c", "[email protected]", "-c", "user.name=Test"];
+8 -8
View File
@@ -14,7 +14,7 @@ use super::spec::{
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
optional_bool, optional_str, optional_u64, required_str,
};
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
const DEFAULT_LOG_MAX_COUNT: u64 = 20;
const MAX_LOG_MAX_COUNT: u64 = 200;
@@ -216,7 +216,7 @@ impl ToolSpec for GitShowTool {
let unified = optional_u64(&input, "unified", DEFAULT_UNIFIED)?.min(MAX_UNIFIED);
let mut args = vec!["show".to_string(), "--no-color".to_string()];
args.extend(crate::dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
args.extend(codewhale_dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
if patch {
args.push(format!("--unified={unified}"));
} else {
@@ -909,7 +909,7 @@ async fn run_git_command_bounded(
args: &[String],
timeout: std::time::Duration,
) -> Result<Option<Output>, ToolError> {
let Some(mut cmd) = crate::dependencies::Git::tokio_command() else {
let Some(mut cmd) = codewhale_dependencies::Git::tokio_command() else {
return Err(ToolError::not_available(
"git is not installed or not in PATH",
));
@@ -953,11 +953,11 @@ mod tests {
use tempfile::tempdir;
fn git_available() -> bool {
crate::dependencies::Git::available()
codewhale_dependencies::Git::available()
}
fn run_git(root: &Path, args: &[&str]) {
let status = crate::dependencies::Git::status(args, root).expect("git should spawn");
let status = codewhale_dependencies::Git::status(args, root).expect("git should spawn");
assert!(status.success(), "git {args:?} failed");
}
@@ -1289,14 +1289,14 @@ mod tests {
assert!(result.success, "{}", result.content);
// #6561 D03-01: remote-tracking refs only, so no local tag appeared.
let tags = crate::dependencies::Git::output(&["tag", "--list"], work.path())
let tags = codewhale_dependencies::Git::output(&["tag", "--list"], work.path())
.expect("git should spawn");
assert!(tags.status.success());
assert_eq!(String::from_utf8_lossy(&tags.stdout).trim(), "");
// The refs arrived, but nothing was checked out: the work tree has no
// file.txt and no local branch moved.
let refs = crate::dependencies::Git::output(&["branch", "-r"], work.path())
let refs = codewhale_dependencies::Git::output(&["branch", "-r"], work.path())
.expect("git should spawn");
assert!(refs.status.success());
let refs = String::from_utf8_lossy(&refs.stdout);
@@ -1472,7 +1472,7 @@ mod tests {
}
fn current_branch(root: &Path) -> String {
let output = crate::dependencies::Git::output(&["branch", "--show-current"], root)
let output = codewhale_dependencies::Git::output(&["branch", "--show-current"], root)
.expect("git should spawn");
assert!(output.status.success());
String::from_utf8_lossy(&output.stdout).trim().to_string()
+5 -5
View File
@@ -7,8 +7,8 @@ use std::process::Command;
use serde_json::Value;
use crate::dependencies::ExternalTool;
use crate::tools::spec::{ToolContext, ToolError};
use codewhale_dependencies::ExternalTool;
const DEFAULT_GH: &str = "/opt/homebrew/bin/gh";
const FALLBACK_GH_PATHS: &[&str] = &[
@@ -59,7 +59,7 @@ pub(super) fn run_gh_json(context: &ToolContext, args: &[&str]) -> Result<Value,
}
pub(super) fn ensure_github_repo(context: &ToolContext) -> Result<(), ToolError> {
let out = crate::dependencies::Git::output(
let out = codewhale_dependencies::Git::output(
&["rev-parse", "--is-inside-work-tree"],
&context.workspace,
)
@@ -74,7 +74,7 @@ pub(super) fn ensure_github_repo(context: &ToolContext) -> Result<(), ToolError>
}
pub(super) fn git_status_porcelain(context: &ToolContext) -> Result<String, ToolError> {
let out = crate::dependencies::Git::output(&["status", "--porcelain"], &context.workspace)
let out = codewhale_dependencies::Git::output(&["status", "--porcelain"], &context.workspace)
.map_err(|e| ToolError::execution_failed(format!("failed to run git status: {e}")))?;
if !out.status.success() {
return Err(ToolError::execution_failed(format!(
@@ -196,7 +196,7 @@ pub(super) async fn host_gh(
{
tokio::process::Command::new(binary)
} else {
crate::dependencies::Gh::tokio_command()
codewhale_dependencies::Gh::tokio_command()
.ok_or_else(|| ToolError::not_available("gh CLI is unavailable"))?
};
codewhale_util::utils::suppress_tokio_console_window(&mut command);
@@ -216,7 +216,7 @@ pub(super) async fn host_git(
args: &[&str],
cancel: &tokio_util::sync::CancellationToken,
) -> Result<String, ToolError> {
let mut command = crate::dependencies::Git::tokio_command()
let mut command = codewhale_dependencies::Git::tokio_command()
.ok_or_else(|| ToolError::not_available("git is unavailable"))?;
command.args(args).current_dir(&context.workspace);
host_output(&mut command, &[], cancel, "Git repository inspection").await
+1 -1
View File
@@ -261,7 +261,7 @@ mod tests {
#[test]
fn close_refuses_when_git_cannot_determine_worktree_status() {
use crate::dependencies::{ExternalTool, Git};
use codewhale_dependencies::{ExternalTool, Git};
let tmp = tempfile::tempdir().expect("tempdir");
assert!(
+2 -1
View File
@@ -756,10 +756,11 @@ mod tests {
#[tokio::test(flavor = "current_thread")]
async fn actual_github_host_draft_read_and_operator_use_one_owned_artifact() {
let _policy = crate::plugins::activation::TestPolicyGuard::extension_host(false);
let runtime = crate::dependencies::resolve_extension_host_runtime(
let runtime = codewhale_dependencies::resolve_extension_host_runtime(
codewhale_config::extension_host::ExtensionHostRuntime::Node,
None,
None,
crate::extension_host::bundle_sha256(),
);
let Some(runtime) = runtime.selected else {
assert_ne!(
+5 -4
View File
@@ -74,7 +74,7 @@ impl ToolSpec for ImageOcrTool {
pub(crate) fn ocr_available() -> bool {
std::env::var_os("CODEWHALE_LOCAL_OCR_UNAVAILABLE").is_none()
&& (crate::dependencies::resolve_tesseract().is_some() || native_ocr_available())
&& (codewhale_dependencies::resolve_tesseract().is_some() || native_ocr_available())
}
const OCR_TIMEOUT: Duration = Duration::from_secs(120);
@@ -202,12 +202,13 @@ impl CapturedOcr {
let supplied = self.overrides.as_ref().map(|value| value.tesseract.clone());
#[cfg(all(test, unix))]
{
supplied
.unwrap_or_else(|| crate::dependencies::resolve_tesseract().map(OsString::from))
supplied.unwrap_or_else(|| {
codewhale_dependencies::resolve_tesseract().map(OsString::from)
})
}
#[cfg(not(all(test, unix)))]
{
crate::dependencies::resolve_tesseract().map(OsString::from)
codewhale_dependencies::resolve_tesseract().map(OsString::from)
}
};
OcrNativeStep {
+4 -4
View File
@@ -13,7 +13,7 @@
//! and local execution with an external backend are refused. This is not a
//! persistent REPL; source is read from stdin rather than a host temporary file.
//!
//! Registration is gated by [`crate::dependencies::resolve_node`]:
//! Registration is gated by [`codewhale_dependencies::resolve_node`]:
//! when Node is missing the tool is simply not advertised, so the
//! model never sees a runtime it can't actually use. See
//! `core::engine::tool_catalog::ensure_advanced_tooling` for the
@@ -23,7 +23,7 @@ use std::ffi::OsString;
use std::path::Path;
use std::time::Duration;
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
use serde_json::{Value, json};
use crate::tools::spec::{ToolContext, ToolError, ToolResult, required_str};
@@ -150,7 +150,7 @@ pub async fn execute_js_execution_tool(
context: &ToolContext,
) -> Result<ToolResult, ToolError> {
let code = required_str(input, "code")?;
let node = crate::dependencies::Node::resolve().ok_or_else(|| {
let node = codewhale_dependencies::Node::resolve().ok_or_else(|| {
ToolError::execution_failed("js_execution: Node.js runtime became unavailable")
})?;
let budget = js_execution_timeout();
@@ -208,7 +208,7 @@ mod tests {
/// The tool simply isn't advertised in that case, so happy-path
/// tests don't fail; they just don't exercise the spawn path.
fn node_present() -> bool {
crate::dependencies::resolve_node().is_some()
codewhale_dependencies::resolve_node().is_some()
}
fn proxy_env<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<OsString> + 'a {
+14 -14
View File
@@ -1,6 +1,6 @@
//! Model-facing LSP code-intelligence tool.
//!
//! Extends the existing [`crate::lsp::LspManager`] lifecycle — never spawns a
//! Extends the existing [`codewhale_lsp::LspManager`] lifecycle — never spawns a
//! competing server pool. Operations: diagnostics, read_lints, symbols,
//! definition, references.
@@ -9,7 +9,7 @@ use serde::Serialize;
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use crate::lsp::LintReadStatus;
use codewhale_lsp::LintReadStatus;
use super::spec::{
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
@@ -128,7 +128,7 @@ struct ReadLintsDiagnostic {
#[derive(Serialize)]
struct ReadLintsFile {
#[serde(flatten)]
freshness: crate::lsp::DiagnosticFreshness,
freshness: codewhale_lsp::DiagnosticFreshness,
file: String,
status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -352,9 +352,9 @@ fn resolve_lint_path(workspace: &Path, raw: &str) -> Result<PathBuf, ToolError>
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp::{Diagnostic, Language, LspConfig, LspManager, Severity};
use crate::tools::spec::ToolContext;
use async_trait::async_trait;
use codewhale_lsp::{Diagnostic, Language, LspConfig, LspManager, Severity};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -367,13 +367,13 @@ mod tests {
}
#[async_trait]
impl crate::lsp::LspTransport for CountingTransport {
impl codewhale_lsp::LspTransport for CountingTransport {
async fn diagnostics_for(
&self,
_path: &Path,
_text: &str,
_wait: Duration,
) -> anyhow::Result<crate::lsp::client::DiagnosticPublication> {
) -> anyhow::Result<codewhale_lsp::client::DiagnosticPublication> {
self.calls.fetch_add(1, Ordering::Relaxed);
Ok(vec![Diagnostic {
line: 1,
@@ -400,13 +400,13 @@ mod tests {
struct EmptyTransport;
#[async_trait]
impl crate::lsp::LspTransport for EmptyTransport {
impl codewhale_lsp::LspTransport for EmptyTransport {
async fn diagnostics_for(
&self,
_path: &Path,
_text: &str,
_wait: Duration,
) -> anyhow::Result<crate::lsp::client::DiagnosticPublication> {
) -> anyhow::Result<codewhale_lsp::client::DiagnosticPublication> {
Ok(Vec::new().into())
}
@@ -427,13 +427,13 @@ mod tests {
}
#[async_trait]
impl crate::lsp::LspTransport for FixedTransport {
impl codewhale_lsp::LspTransport for FixedTransport {
async fn diagnostics_for(
&self,
_path: &Path,
_text: &str,
_wait: Duration,
) -> anyhow::Result<crate::lsp::client::DiagnosticPublication> {
) -> anyhow::Result<codewhale_lsp::client::DiagnosticPublication> {
Ok(self.items.clone().into())
}
@@ -443,13 +443,13 @@ mod tests {
struct ErrorTransport;
#[async_trait]
impl crate::lsp::LspTransport for ErrorTransport {
impl codewhale_lsp::LspTransport for ErrorTransport {
async fn diagnostics_for(
&self,
_path: &Path,
_text: &str,
_wait: Duration,
) -> anyhow::Result<crate::lsp::client::DiagnosticPublication> {
) -> anyhow::Result<codewhale_lsp::client::DiagnosticPublication> {
anyhow::bail!("server exploded")
}
@@ -459,13 +459,13 @@ mod tests {
struct TimeoutTransport;
#[async_trait]
impl crate::lsp::LspTransport for TimeoutTransport {
impl codewhale_lsp::LspTransport for TimeoutTransport {
async fn diagnostics_for(
&self,
_path: &Path,
_text: &str,
_wait: Duration,
) -> anyhow::Result<crate::lsp::client::DiagnosticPublication> {
) -> anyhow::Result<codewhale_lsp::client::DiagnosticPublication> {
std::future::pending().await
}
+3 -3
View File
@@ -9,7 +9,7 @@
//! changelog as ..." workflows that previously required the user
//! to drop into a terminal between turns.
//!
//! Registration is gated by [`crate::dependencies::resolve_pandoc`]
//! Registration is gated by [`codewhale_dependencies::resolve_pandoc`]
//! (see [`crate::tools::registry::ToolRegistryBuilder::with_pandoc_tools`]).
//! When pandoc isn't installed the tool simply doesn't appear in the
//! catalog, so the model never sees a binary it can't actually use.
@@ -166,7 +166,7 @@ impl ToolSpec for PandocConvertTool {
// catalog build and the model's call should produce a clear
// error rather than the cryptic "program not found" from raw
// Command::spawn.
let pandoc = crate::dependencies::resolve_pandoc().ok_or_else(|| {
let pandoc = codewhale_dependencies::resolve_pandoc().ok_or_else(|| {
ToolError::execution_failed(
"pandoc_convert: pandoc binary not found on PATH. \
Install pandoc 2.15 or newer (macOS: `brew install pandoc`; \
@@ -301,7 +301,7 @@ mod tests {
use tempfile::tempdir;
fn pandoc_present() -> bool {
crate::dependencies::resolve_pandoc().is_some()
codewhale_dependencies::resolve_pandoc().is_some()
}
fn pandoc_environment_unavailable(err: &ToolError) -> bool {
+6 -5
View File
@@ -1123,8 +1123,8 @@ impl ToolRegistryBuilder {
/// sees a binary it can't actually use.
#[must_use]
pub fn with_pandoc_tools(self) -> Self {
if crate::dependencies::host_tool_available("pandoc_convert", || {
crate::dependencies::resolve_pandoc().is_some()
if codewhale_dependencies::host_tool_available("pandoc_convert", || {
codewhale_dependencies::resolve_pandoc().is_some()
}) {
use super::pandoc::PandocConvertTool;
self.with_tool(Arc::new(PandocConvertTool))
@@ -1138,7 +1138,8 @@ impl ToolRegistryBuilder {
/// Tesseract when installed.
#[must_use]
pub fn with_image_ocr_tools(self) -> Self {
if crate::dependencies::host_tool_available("image_ocr", super::image_ocr::ocr_available) {
if codewhale_dependencies::host_tool_available("image_ocr", super::image_ocr::ocr_available)
{
use super::image_ocr::ImageOcrTool;
self.with_tool(Arc::new(ImageOcrTool))
} else {
@@ -1303,7 +1304,7 @@ impl ToolRegistryBuilder {
/// Include the `revert_turn` tool. Approval-gated since it mutates
/// the workspace; the model uses it when the user asks to "undo my
/// last edit". Backed by the per-workspace snapshot side-repo
/// (`crate::snapshot`).
/// (`codewhale_snapshot`).
#[must_use]
pub fn with_revert_turn_tool(self) -> Self {
use super::revert_turn::RevertTurnTool;
@@ -1410,7 +1411,7 @@ impl ToolRegistryBuilder {
}
/// Include the model-facing LSP intelligence tools. They reuse the
/// session [`crate::lsp::LspManager`] attached to `ToolContext` and never
/// session [`codewhale_lsp::LspManager`] attached to `ToolContext` and never
/// spawn a second server lifecycle.
#[must_use]
pub fn with_lsp_tool(self) -> Self {
+1 -1
View File
@@ -20,7 +20,7 @@ use serde_json::{Value, json};
use super::spec::{
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec, optional_u64,
};
use crate::snapshot::SnapshotRepo;
use codewhale_snapshot::SnapshotRepo;
/// Default offset: revert the most-recent turn (i.e. the last `pre-turn:*`
/// snapshot in history).
+4 -4
View File
@@ -11,7 +11,7 @@ use serde_json::{Value, json};
use crate::client::CodewhaleClient;
#[cfg(test)]
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
use codewhale_models::Usage;
use codewhale_util::utils::truncate_with_ellipsis;
@@ -1932,7 +1932,7 @@ async fn resolve_diff_target(
};
let mut args = vec!["diff".to_string()];
args.extend(crate::dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
args.extend(codewhale_dependencies::Git::REVIEW_DIFF_ARGS.map(String::from));
if staged {
args.push("--cached".to_string());
if let Some(base_commit) = base_commit {
@@ -1988,7 +1988,7 @@ async fn run_review_git(
) -> Result<std::process::Output, ToolError> {
let workspace = workspace.to_path_buf();
tokio::task::spawn_blocking(move || {
let mut cmd = crate::dependencies::Git::review_command(&workspace)
let mut cmd = codewhale_dependencies::Git::review_command(&workspace)
.map_err(|e| ToolError::execution_failed(e.to_string()))?;
cmd.args(args).output().map_err(|e| {
ToolError::execution_failed(format!("Failed to {operation} with git: {e}"))
@@ -2965,7 +2965,7 @@ mod tests {
}
fn fixture_git(workspace: &Path, args: &[&str]) -> std::process::Output {
let mut command = crate::dependencies::Git::command().expect("git test dependency");
let mut command = codewhale_dependencies::Git::command().expect("git test dependency");
let output = command
.args(args)
.current_dir(workspace)
+2 -2
View File
@@ -1218,7 +1218,7 @@ pub fn repository_tool_workspace(
required_head: Option<&str>,
) -> Option<PathBuf> {
let required = required_head.map(str::trim).filter(|head| !head.is_empty());
let mut command = crate::dependencies::Git::review_base(workspace).ok()?;
let mut command = codewhale_dependencies::Git::review_base(workspace).ok()?;
command.current_dir(workspace).args(["rev-parse", "HEAD"]);
let output = command.output().ok()?;
if !output.status.success() {
@@ -1230,7 +1230,7 @@ pub fn repository_tool_workspace(
{
return None;
}
let mut status = crate::dependencies::Git::review_base(workspace).ok()?;
let mut status = codewhale_dependencies::Git::review_base(workspace).ok()?;
status
.current_dir(workspace)
.args(["status", "--porcelain"]);
+2 -2
View File
@@ -128,7 +128,7 @@ pub(crate) async fn report(
#[cfg(test)]
mod tests {
use super::*;
use crate::dependencies::ExternalTool;
use codewhale_dependencies::ExternalTool;
#[tokio::test]
async fn disabled_review_host_refuses_before_demand_or_provider() {
let temp = tempfile::tempdir().unwrap();
@@ -172,7 +172,7 @@ mod tests {
let _manager =
crate::extension_host::TestManagerGuard::install(std::sync::Arc::clone(&manager));
let git = |args: &[&str]| {
let output = crate::dependencies::Git::command()
let output = codewhale_dependencies::Git::command()
.unwrap()
.args(args)
.current_dir(root.path())
+2 -2
View File
@@ -12,7 +12,7 @@ use anyhow::{Context, Result, bail};
use serde::Deserialize;
use wait_timeout::ChildExt;
use crate::dependencies::{ExternalTool, Gh, Git};
use codewhale_dependencies::{ExternalTool, Gh, Git};
const MAX_OUTPUT_BYTES: usize = 8 * 1024 * 1024;
const VIEW_FIELDS: &str =
@@ -465,7 +465,7 @@ fn run_command(workspace: &Path, program: Program, args: &[String]) -> Result<St
Program::Git => Git::review_command(workspace)?,
};
// `gh` shells out to git; give it the same no-prompt environment.
crate::dependencies::apply_git_noninteractive_env(&mut command);
codewhale_dependencies::apply_git_noninteractive_env(&mut command);
command
.args(args)
.current_dir(workspace)
+3 -5
View File
@@ -17,17 +17,15 @@ use std::time::{Duration, Instant};
use async_trait::async_trait;
use serde_json::{Value, json};
use crate::repl::PythonRuntime;
use crate::rlm::RlmBridge;
use crate::rlm::bridge::wait_for_deadline;
use crate::rlm::session::{
ContextMeta, OutputFeedback, RlmSession, derive_session_name, write_context_file,
};
use crate::rlm::session::{ContextMeta, OutputFeedback, RlmSession, derive_session_name};
use crate::tools::fetch_url::FetchUrlTool;
use crate::tools::handle::VarHandle;
use crate::tools::spec::{
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
};
use codewhale_repl::PythonRuntime;
/// Registered name of the persistent RLM session tool.
pub(crate) const RLM_TOOL_NAME: &str = "rlm";
@@ -420,7 +418,7 @@ impl RlmTool {
}
}
let context_path = write_context_file(&body).map_err(|e| {
let context_path = codewhale_repl::write_context_file(&body).map_err(|e| {
ToolError::execution_failed(format!("rlm_open: failed to stage context: {e}"))
})?;
let context_path = tempfile::TempPath::try_from_path(context_path).map_err(|e| {
+3 -3
View File
@@ -4600,14 +4600,14 @@ fn hardened_readonly_argv(command: &str) -> Result<(String, Vec<String>)> {
let at = subcommand_index + 1;
argv.splice(
at..at,
crate::dependencies::Git::REVIEW_DIFF_ARGS.map(String::from),
codewhale_dependencies::Git::REVIEW_DIFF_ARGS.map(String::from),
);
}
"log" | "show" => {
let at = subcommand_index + 1;
argv.splice(
at..at,
crate::dependencies::Git::REVIEW_DIFF_ARGS
codewhale_dependencies::Git::REVIEW_DIFF_ARGS
.into_iter()
.chain(["--no-show-signature"])
.map(String::from),
@@ -6248,7 +6248,7 @@ impl ToolSpec for BashTool {
let overrides = tokio::task::spawn_blocking(move || {
let mut overrides = std::collections::BTreeSet::new();
for dir in git_dirs {
overrides.extend(crate::dependencies::Git::review_filter_overrides(&dir)?);
overrides.extend(codewhale_dependencies::Git::review_filter_overrides(&dir)?);
}
anyhow::Ok(overrides)
})
+2 -2
View File
@@ -1228,7 +1228,7 @@ fn readonly_argv_is_shell_free_and_disables_git_helpers() {
#[cfg(unix)]
#[tokio::test]
async fn readonly_git_reads_run_no_clean_filter_or_textconv() {
use crate::dependencies::ExternalTool as _;
use codewhale_dependencies::ExternalTool as _;
use std::os::unix::fs::PermissionsExt as _;
let workspace = tempdir().expect("workspace");
let outside = tempdir().expect("outside");
@@ -1238,7 +1238,7 @@ async fn readonly_git_reads_run_no_clean_filter_or_textconv() {
let helper = helper.display().to_string();
let repo = workspace.path();
let git = |args: &[&str]| {
let status = crate::dependencies::Git::status(args, repo).expect("git should spawn");
let status = codewhale_dependencies::Git::status(args, repo).expect("git should spawn");
assert!(status.success(), "git {args:?} failed");
};
git(&["init", "-q"]);
+2 -2
View File
@@ -20,12 +20,12 @@ use unicode_normalization::UnicodeNormalization;
use codewhale_execpolicy::ApprovalMode;
use crate::lsp::LspManager;
use crate::rlm::session::SessionObjectSnapshot;
use crate::rlm::session::{SharedRlmSessionStore, new_shared_rlm_session_store};
use crate::tools::handle::{SharedHandleStore, new_shared_handle_store};
use crate::tools::shell::{SharedShellManager, new_shared_shell_manager};
use codewhale_agent_contract::worker_profile::ShellPolicy;
use codewhale_lsp::LspManager;
use codewhale_net::network_policy::NetworkPolicyDecider;
use codewhale_runtime::features::Features;
use codewhale_sandbox::backend::SandboxBackend;
@@ -1405,7 +1405,7 @@ impl ToolContext {
/// binary, timeout, unknown language) degrades to an empty string rather than
/// propagating an error to the caller.
pub async fn lsp_diagnostics_for_paths(context: &ToolContext, paths: &[PathBuf]) -> String {
use crate::lsp::render_blocks;
use codewhale_lsp::render_blocks;
let manager = match context.lsp_manager.as_ref() {
Some(m) if m.config().enabled => m,
+1 -1
View File
@@ -240,7 +240,7 @@ impl DeliveryEvidence {
candidates.extend(baseline.dirty.keys().cloned());
if let Some(head) = baseline.head.as_deref() {
let mut args = vec!["diff"];
args.extend(crate::dependencies::Git::REVIEW_DIFF_ARGS);
args.extend(codewhale_dependencies::Git::REVIEW_DIFF_ARGS);
args.extend(["--name-only", "-z", head, "HEAD", "--"]);
let output = git(&baseline.root, &args)?;
for path in output
+1 -1
View File
@@ -42,7 +42,6 @@ use crate::core::engine::tool_catalog::{
use crate::core::events::{AgentProgressEventMeta, Event};
#[cfg(test)]
use crate::core::session::ToolActivationCache;
use crate::dependencies::{ExternalTool, Git};
use crate::llm_client::LlmError;
use crate::reasoning_preference::ReasoningEffort;
use crate::request_tuning::RequestTuning;
@@ -72,6 +71,7 @@ use codewhale_agent_contract::worker_profile::{
narrow_model_steps, narrow_optional_limit,
};
use codewhale_config::AppMode;
use codewhale_dependencies::{ExternalTool, Git};
#[cfg(test)]
use codewhale_execpolicy::ApprovalMode;
use codewhale_execpolicy::canonical_action::{
+1 -1
View File
@@ -8243,7 +8243,7 @@ fn every_named_role_has_one_complete_capability_based_surface() {
}
// The converter registers only when the `pandoc` binary exists.
if matches!(role, FleetRole::Builder | FleetRole::Worker)
&& crate::dependencies::resolve_pandoc().is_some()
&& codewhale_dependencies::resolve_pandoc().is_some()
{
expected.insert("pandoc_convert".to_string());
}
+1 -1
View File
@@ -5,8 +5,8 @@ use std::path::{Path, PathBuf};
use uuid::Uuid;
use crate::dependencies::{ExternalTool, Git};
use crate::tools::spec::ToolError;
use codewhale_dependencies::{ExternalTool, Git};
use codewhale_agent_contract::role::FleetRole;
+5 -5
View File
@@ -9,7 +9,6 @@ use serde_json::{Value, json};
use tokio::process::Command;
use uuid::Uuid;
use crate::dependencies::ExternalTool;
use crate::task_manager::{
NewTaskRequest, TaskArtifactRef, TaskAttemptRecord, TaskCancelDisposition, TaskGateRecord,
TaskRecord,
@@ -23,6 +22,7 @@ use crate::work_graph::{
CancelOutcome, OperationIntent, OperationObservation, OperationOwnerSnapshot, OwnerState,
task_owner_snapshot,
};
use codewhale_dependencies::ExternalTool;
use codewhale_execpolicy::command_safety::{SafetyAnalysis, SafetyLevel, analyze_command};
const MAX_SUMMARY_CHARS: usize = 900;
@@ -918,7 +918,7 @@ impl TasksTool {
let patch_path = manager.artifact_absolute_path(patch_ref);
let workspace = context.workspace.clone();
let out = tokio::task::spawn_blocking(move || {
crate::dependencies::Git::command()
codewhale_dependencies::Git::command()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "git not found"))?
.args(["apply", "--check"])
.arg(&patch_path)
@@ -1271,7 +1271,7 @@ fn task_id_schema() -> Value {
/// The working-tree patch and changed paths an attempt records, read without
/// running repository-configured diff drivers or filters.
async fn attempt_diff(workspace: &Path) -> Result<(String, Vec<String>), ToolError> {
let review = crate::dependencies::Git::REVIEW_DIFF_ARGS;
let review = codewhale_dependencies::Git::REVIEW_DIFF_ARGS;
let diff = git_output(
workspace,
&[&["diff", "--binary", "--no-color"][..], &review[..]].concat(),
@@ -1295,7 +1295,7 @@ async fn git_output(workspace: &Path, args: &[&str]) -> Result<String, ToolError
// Reads repository content, so repository-configured filters, fsmonitor
// and hooks stay off.
let out = tokio::task::spawn_blocking(move || {
crate::dependencies::Git::review_command(&cwd)
codewhale_dependencies::Git::review_command(&cwd)
.map_err(|e| std::io::Error::other(format!("{e:#}")))?
.args(&args_owned)
.output()
@@ -1411,7 +1411,7 @@ mod tests {
let textconv = script("textconv.sh", "cat \"$1\"\n");
let repo = tmp.path();
let git = |args: &[&str]| {
let status = crate::dependencies::Git::status(args, repo).expect("git should spawn");
let status = codewhale_dependencies::Git::status(args, repo).expect("git should spawn");
assert!(status.success(), "git {args:?} failed");
};
git(&["init", "-q"]);
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff. Show more