mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 01:48:20 -10:00
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:
1 parent
847fdf3c3a
commit
ecc5deb692
124 files changed
+1032
-739
No files matched your search
+1
-1
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
@@ -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.
@@ -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"] }
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(
|
||||
"ed
|
||||
));
|
||||
assert!(!codewhale_models::turn_classify::is_runtime_owned_user_message(
|
||||
"ed
|
||||
));
|
||||
assert!(!codewhale_models::turn_classify::is_runtime_owned_user_message("ed));
|
||||
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';
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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")?
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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};
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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```"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {}",
|
||||
|
||||
@@ -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}")))
|
||||
};
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)?);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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
@@ -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"];
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
Reference in new issue
Block a user