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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/reborn-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ jobs:
run: scripts/ci/check-reborn-qa-fixtures.sh

- name: Run Reborn QA fixture contracts and replay
run: cargo test --test reborn_qa_recorded_behavior -- --nocapture
run: cargo test -p ironclaw_reborn_integration_tests --test reborn_qa_recorded_behavior -- --nocapture

reborn-tests:
name: Tests (Reborn)
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [".", "crates/ironclaw_common", "crates/ironclaw_observability", "crates/ironclaw_host_api", "crates/ironclaw_host_ingress", "crates/ironclaw_filesystem", "crates/ironclaw_attachments", "crates/ironclaw_extractors", "crates/ironclaw_memory", "crates/ironclaw_memory_native", "crates/ironclaw_memory_mem0", "crates/ironclaw_events", "crates/ironclaw_event_projections", "crates/ironclaw_event_streams", "crates/ironclaw_reborn_event_store", "crates/ironclaw_extensions", "crates/ironclaw_extension_host", "crates/ironclaw_processes", "crates/ironclaw_dispatcher", "crates/ironclaw_scripts", "crates/ironclaw_process_sandbox", "crates/ironclaw_mcp", "crates/ironclaw_wasm", "crates/ironclaw_wasm_limiter", "crates/ironclaw_capabilities", "crates/ironclaw_secrets", "crates/ironclaw_network", "crates/ironclaw_host_runtime", "crates/ironclaw_runtime_policy", "crates/ironclaw_authorization", "crates/ironclaw_run_state", "crates/ironclaw_approvals", "crates/ironclaw_resources", "crates/ironclaw_auth", "crates/ironclaw_trust", "crates/ironclaw_turns", "crates/ironclaw_agent_loop", "crates/ironclaw_threads", "crates/ironclaw_prompt_envelope", "crates/ironclaw_hooks", "crates/ironclaw_loop_host", "crates/ironclaw_runner", "crates/ironclaw_reborn_config", "crates/ironclaw_operator", "crates/ironclaw_reborn_composition", "crates/ironclaw_reborn_identity", "crates/ironclaw_first_party_extensions", "crates/ironclaw_reborn_cli", "crates/ironclaw_reborn_traces", "crates/ironclaw_webui", "crates/ironclaw_reborn_openai_compat", "crates/ironclaw_conversations", "crates/ironclaw_product", "crates/ironclaw_telegram_extension", "crates/ironclaw_telegram_v2_adapter", "crates/ironclaw_slack_extension", "crates/ironclaw_outbound", "crates/ironclaw_triggers", "crates/ironclaw_projects", "crates/ironclaw_architecture", "crates/ironclaw_safety", "crates/ironclaw_skills", "crates/ironclaw_llm", "crates/ironclaw_embeddings", "tools/ironclaw_stress"]
default-members = ["crates/ironclaw_reborn_cli"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

default-members applies to every package-unspecified Cargo command, not just cargo run. This switches cargo test --locked --lib to the bin-only CLI and makes root reborn_* test commands in CI select a package that does not own those targets. Please preserve or explicitly set the root integration-test package for those callers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^|/)(Cargo\.toml|CLAUDE\.md|AGENTS\.md|\.claude/.*rules.*|README\.md)$|\.cargo/config.toml$' | sed -n '1,200p'

echo
echo "== Cargo.toml relevant =="
if [ -f Cargo.toml ]; then
  cat -n Cargo.toml | sed -n '1,80p'
fi

echo
echo "== rule mentions =="
for f in CLAUDE.md AGENTS.md; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    rg -n "workspace|workspace-wide|CLIP|Clippy|cargo clippy|default-members|validation|zero-warning|Reborn" "$f" || true
  fi
done
if [ -d .claude/rules ]; then
  for f in .claude/rules/*; do
    echo "-- $f --"
    rg -n "workspace|workspace-wide|CLIP|Clippy|cargo clippy|default-members|validation|zero-warning|Reborn" "$f" || true
  done
fi

echo
echo "== workspace member count and crate features summary =="
python3 - <<'PY'
import tomllib
from pathlib import Path
cargo = Path('Cargo.toml')
if not cargo.exists():
    raise SystemExit(1)
data = tomllib.loads(cargo.read_text())
ws=data.get('workspace',{})
print('members=', ws.get('members'))
print('default-members=', ws.get('default-members'))
print('package=', data.get('package',{}).get('name'))
PY

echo
echo "== deterministic cargo default-members selector probe if cargo/config available =="
if command -v cargo >/dev/null 2>&1; then
  tmp="$(mktemp -d)"
  mkdir -p "$tmp/proj" "$tmp/proj/a" "$tmp/proj/b"
  cat > "$tmp/proj/Cargo.toml" <<'EOF'
[workspace]
members = ["a", "b"]
default-members = ["a"]
EOF
  mkdir -p "$tmp/proj/a/src" "$tmp/proj/b/src"
  cat > "$tmp/proj/a/src/lib.rs" <<'EOF'
pub fn add(a: i32, b: i32) -> i32 { a + b }
EOF
  cat > "$tmp/proj/b/src/lib.rs" <<'EOF'
pub fn sub(x: i32, y: i32) -> i32 { x - y }
EOF
  mkdir -p "$tmp/proj/.cargo"
  echo 'cargo-default = true' > "$tmp/proj/.cargo/config.toml"
  (cd "$tmp/proj" && cargo metadata --format-version 1 --offline >/tmp/cargo_metadata_default.json)
  python3 - <<'PY'
import json
from pathlib import Path
data=json.loads(Path('/tmp/cargo_metadata_default.json').read_text())
print("workspace=.", data["workspace_root"])
print("packages=", sorted(p.get("name") for p in data["packages"]))
PY
else
  echo "cargo not available"
fi

Repository: nearai/ironclaw

Length of output: 24141


Disable default-members unless it is needed for a scoped run command.

default-members = ["crates/ironclaw_reborn_cli"] changes root commands such as cargo build, cargo test, cargo check, and cargo clippy to select only the CLI crate. CLAUDE.md/AGENTS.md and .claude/rules/review-discipline.md require the workspace-wide clippy command for Reborn work; keeping this setting creates a confusing root command that violates that workspace-wide validation invariant. Use a scope-limited run alias/metadata instead of making the whole workspace default to the CLI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` at line 3, Remove the workspace-level default-members setting
from Cargo.toml so root cargo build, test, check, and clippy commands continue
targeting the full workspace. If CLI-only execution is required, replace it with
a scope-limited run alias or equivalent metadata rather than changing workspace
defaults.

Source: Coding guidelines

exclude = [
"crates/ironclaw_silk_decoder",
"fuzz",
Expand Down
61 changes: 59 additions & 2 deletions crates/ironclaw_reborn_cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::ffi::OsString;

use clap::{CommandFactory, Parser};

use crate::commands::Command;
Expand All @@ -6,13 +8,68 @@ use crate::commands::Command;
#[command(name = "ironclaw", about = "IronClaw agent runtime", version)]
pub(crate) struct Cli {
#[command(subcommand)]
pub(crate) command: Command,
pub(crate) command: Option<Command>,
}

pub(crate) fn command() -> clap::Command {
Cli::command()
}

pub(crate) fn run() -> anyhow::Result<()> {
Cli::parse().command.execute()
let cli = Cli::parse_from(args_with_default_serve(std::env::args_os()));
cli.command
.unwrap_or(Command::Serve(
crate::commands::serve::ServeCommand::default(),
))
.execute()
}

fn args_with_default_serve(mut args: impl Iterator<Item = OsString>) -> Vec<OsString> {
let program = args.next().unwrap_or_else(|| OsString::from("ironclaw"));
let mut rest: Vec<_> = args.collect();

let has_explicit_command = rest
.first()
.and_then(|arg| arg.to_str())
.is_some_and(|arg| Cli::command().find_subcommand(arg).is_some());
let is_help_or_version = rest
.iter()
.any(|arg| matches!(arg.to_str(), Some("--help" | "-h" | "--version" | "-V")));

if !has_explicit_command && !is_help_or_version {
rest.insert(0, OsString::from("serve"));
}

std::iter::once(program).chain(rest).collect()
}

#[cfg(test)]
mod tests {
use super::{Cli, args_with_default_serve};
use crate::commands::Command;
use clap::Parser;
use std::ffi::OsString;

#[test]
fn missing_subcommand_inserts_serve() {
let args = [OsString::from("ironclaw")];
let cli = Cli::try_parse_from(args_with_default_serve(args.into_iter()))
.expect("default serve command should parse");

assert!(matches!(cli.command, Some(Command::Serve(_))));
}

#[test]
fn serve_options_before_another_subcommand_are_rejected() {
let args = [
OsString::from("ironclaw"),
OsString::from("--host"),
OsString::from("127.0.0.1"),
OsString::from("status"),
];
let error = Cli::try_parse_from(args_with_default_serve(args.into_iter()))
.expect_err("serve options must not be silently ignored");

assert!(error.to_string().contains("unexpected argument 'status'"));
}
}
2 changes: 1 addition & 1 deletion crates/ironclaw_reborn_cli/src/commands/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl ironclaw_reborn_composition::AdminApiTokenMinter for SignedSessionTokenMint
}
}

#[derive(Debug, Args)]
#[derive(Debug, Default, Args)]
pub(crate) struct ServeCommand {
/// Host interface for the Reborn WebChat v2 HTTP listener.
/// Overrides `[webui].listen_host` from the boot config file.
Expand Down
2 changes: 1 addition & 1 deletion crates/ironclaw_reborn_cli/src/commands/traces/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use ironclaw_reborn_traces::contribution::{
};

fn unwrap_traces_command(cli: Cli) -> TracesSubcommand {
let Command::Traces(command) = cli.command else {
let Some(Command::Traces(command)) = cli.command else {
panic!("expected traces command");
};
command.command
Expand Down
27 changes: 27 additions & 0 deletions crates/ironclaw_reborn_cli/tests/smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3055,6 +3055,33 @@ fn run_reports_runtime_readiness_snapshot_without_touching_v1_state() {
);
}

#[test]
fn no_subcommand_defaults_to_serve_command() {
let temp = tempfile::tempdir().expect("tempdir");
let reborn_home = temp.path().join("reborn-home");
let home_dir = temp.path().join("home");

let output = reborn_command()
.env("IRONCLAW_REBORN_HOME", &reborn_home)
.env("HOME", &home_dir)
.env_remove("IRONCLAW_REBORN_PROFILE")
.env_remove("IRONCLAW_REBORN_WEBUI_TOKEN")
.env_remove("IRONCLAW_REBORN_WEBUI_USER_ID")
.output()
.expect("ironclaw-reborn should default to serve");

assert!(
!output.status.success(),
"default serve must fail closed without a WebUI token; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("IRONCLAW_REBORN_WEBUI_TOKEN must be set"),
"stderr should match the `serve` auth gate: {stderr}"
);
}

#[test]
fn doctor_uses_reborn_home_override_without_touching_v1_state() {
let temp = tempfile::tempdir().expect("tempdir");
Expand Down
68 changes: 60 additions & 8 deletions crates/ironclaw_webui/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@

use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;

const PINNED_PNPM_PACKAGE: &str = "pnpm@11.7.0";

fn main() -> Result<(), Box<dyn std::error::Error>> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); // safety: build script — Cargo always sets this
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); // safety: build script — Cargo always sets this
Expand Down Expand Up @@ -130,18 +133,22 @@ fn required_frontend_dist_files(dist_dir: &Path) -> [PathBuf; 3] {
]
}

fn run_command(command: &str, args: &[&str], cwd: &Path) -> Result<(), Box<dyn std::error::Error>> {
let status = Command::new(command).args(args).current_dir(cwd).status()?;
fn run_command<S: AsRef<str>>(
command: &str,
args: &[S],
cwd: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let rendered = render_command(command, args);
let status = Command::new(command)
.args(args.iter().map(AsRef::as_ref))
.current_dir(cwd)
.status()?;
if status.success() {
return Ok(());
}

Err(std::io::Error::other(format!(
"`{}` failed in {} with status {status}",
std::iter::once(command)
.chain(args.iter().copied())
.collect::<Vec<_>>()
.join(" "),
"`{rendered}` failed in {} with status {status}",
cwd.display(),
))
.into())
Expand All @@ -151,7 +158,35 @@ fn run_pnpm(args: &[&str], cwd: &Path) -> Result<(), Box<dyn std::error::Error>>
let mut corepack_args = Vec::with_capacity(args.len() + 1);
corepack_args.push("pnpm");
corepack_args.extend_from_slice(args);
run_command(corepack_command(), &corepack_args, cwd)
match run_command(corepack_command(), &corepack_args, cwd) {
Ok(()) => Ok(()),
Err(error) if is_not_found_error(error.as_ref()) => {
eprintln!(
"`{}` was not found; falling back to `npm exec --yes --package={}`",
corepack_command(),
PINNED_PNPM_PACKAGE,
);
let mut npm_args = Vec::with_capacity(args.len() + 5);
npm_args.push("exec".to_string());
npm_args.push("--yes".to_string());
npm_args.push(format!("--package={PINNED_PNPM_PACKAGE}"));
npm_args.push("--".to_string());
npm_args.push("pnpm".to_string());
npm_args.extend(args.iter().map(|arg| (*arg).to_string()));
run_command(npm_command(), &npm_args, cwd).map_err(|fallback_error| {
io::Error::other(format!(
"failed to run pnpm through `{}` or `{}`. Install Corepack/pnpm \
(`corepack enable pnpm`) or set SKIP_FRONTEND_BUILD=1 for a \
backend-only Rust build. corepack error: {error}; npm fallback \
error: {fallback_error}",
corepack_command(),
npm_command(),
))
.into()
})
}
Err(error) => Err(error),
}
}

fn corepack_command() -> &'static str {
Expand All @@ -162,6 +197,23 @@ fn corepack_command() -> &'static str {
}
}

fn npm_command() -> &'static str {
if cfg!(windows) { "npm.cmd" } else { "npm" }
}

fn is_not_found_error(error: &(dyn std::error::Error + 'static)) -> bool {
error
.downcast_ref::<io::Error>()
.is_some_and(|error| error.kind() == io::ErrorKind::NotFound)
}

fn render_command<S: AsRef<str>>(command: &str, args: &[S]) -> String {
std::iter::once(command)
.chain(args.iter().map(AsRef::as_ref))
.collect::<Vec<_>>()
.join(" ")
}

fn collect(
root: &Path,
dir: &Path,
Expand Down
4 changes: 3 additions & 1 deletion crates/ironclaw_webui/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ and generated JavaScript asset names are outside this module-import rule.
`frontend/dist/`. Cargo does not embed that local preview directory.
`crates/ironclaw_webui/build.rs` runs
`corepack pnpm install --frozen-lockfile` and a Vite production build into
Cargo's `OUT_DIR`, then embeds that generated output into the Rust binary.
Cargo's `OUT_DIR`, then embeds that generated output into the Rust binary. If
`corepack` is not on `PATH`, the build script falls back to
`npm exec --yes --package=pnpm@11.7.0 -- pnpm ...`.

`./build.sh` is the one-shot local refresh helper. It vendors pinned browser
assets, installs dependencies with Corepack, and runs the Vite production build.
Expand Down
4 changes: 2 additions & 2 deletions scripts/ci/quality_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ echo "==> fmt check"
cargo fmt --all -- --check

echo "==> clippy (correctness)"
cargo clippy --locked --all-targets -- -D clippy::correctness
cargo clippy --locked --workspace --all-targets -- -D clippy::correctness

if [ "${IRONCLAW_PREPUSH_TEST:-1}" = "1" ]; then
echo "==> tests (skip with IRONCLAW_PREPUSH_TEST=0)"
cargo test --locked --lib
cargo test --locked --workspace --lib
fi
2 changes: 1 addition & 1 deletion scripts/ci/quality_gate_strict.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ require_command cargo-deny "install with: cargo install cargo-deny"
cargo deny check

echo "==> tests"
cargo test --locked
cargo test --locked --workspace
2 changes: 1 addition & 1 deletion scripts/ci/run-reborn-group-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ fi
for test_name in "${test_names[@]}"; do
echo "::group::cargo test --test ${test_name}"
timeout --signal=INT --kill-after=30s "${test_timeout}" \
cargo test --test "${test_name}" -- --nocapture
cargo test -p ironclaw_reborn_integration_tests --test "${test_name}" -- --nocapture
echo "::endgroup::"
done
2 changes: 1 addition & 1 deletion scripts/ci/run-reborn-root-partition.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ for index in "${!test_names[@]}"; do
test_name="${test_names[$index]}"
echo "::group::cargo test --test ${test_name}"
timeout --signal=INT --kill-after=30s "${test_timeout}" \
cargo test --test "${test_name}" -- --nocapture
cargo test -p ironclaw_reborn_integration_tests --test "${test_name}" -- --nocapture
echo "::endgroup::"
done

Expand Down
Loading