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
28 changes: 28 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: check

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
fmt:
name: cargo fmt --check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check

clippy:
name: cargo clippy -D warnings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: cargo clippy --workspace --all-targets --locked -- -D warnings
14 changes: 6 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,9 @@ V0.6.0 走通的 4-wave 流程,V0.6.x patch + V0.7 minor 起点直接复用:

## 七、Rust 代码格式化约定

`rustfmt.toml` pin stable rustfmt(`max_width = 100` / `tab_spaces = 4` / `use_field_init_shorthand`):

- **不用 `cargo fmt`**(含 `cargo fmt -- <files>` 形式)── cargo fmt **silently 忽略文件参数**,会全 workspace 跑,把存量 ~4-5 kLOC fmt drift 拉进你的 PR diff(2026-05-24 V0.6.5 W1-T3 实战踩坑)
- **新文件 / 大改文件:`rustfmt --edition 2021 <files>` 直调**(commit 前;原地写回)
- **dry-run probe:**`rustfmt --check --edition 2021 <files>`
- **小改 drifted 文件:不 fmt-sweep**(让 drift 维持现状,别动)
- **不上 workspace-wide CI fmt gate**(drift 清零后才指望)
- **drift 清理走独立 chore PR**,按模块拆,一次一个 crate
`rustfmt.toml` pin stable rustfmt(`max_width = 100` / `tab_spaces = 4` / `use_field_init_shorthand`)。**Workspace drift 已一次清零**(`cargo fmt --all` chore PR + CI gate 守),drift-zero policy:

- **commit 前必跑** `cargo fmt --all`(或 `cargo fmt -p <crate>` 局部目标 crate);CI gate (`.github/workflows/check.yml::fmt`) `cargo fmt --all -- --check` 不过 PR 不能 merge
- **`rustfmt --edition 2021 <files>` 直调仍 OK** ── 单文件场景照样能用,与 `cargo fmt` 等价
- **0 maintenance overhead** ── 不再有"drift 维持现状"或"小改 drifted 文件不 fmt-sweep"的特例;**一律 fmt 干净**
- 旧 drift 历史(V0.5 - V0.6.5)git log 可查(任何 commit 之前 fmt drift ~4-5 kLOC,已 chore PR `cargo fmt --all` 清零)
6 changes: 2 additions & 4 deletions crates/ccteam-cli/tests/doctor_codex_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ fn run_doctor_with_path(extra_path: &std::path::Path, args: &[&str]) -> (String,
#[test]
fn check_codex_version_reports_ok_for_supported_release() {
let dir = fake_codex_dir("codex 0.131.0", "Not logged in");
let (stdout, _stderr, code) =
run_doctor_with_path(dir.path(), &["--check-codex-version"]);
let (stdout, _stderr, code) = run_doctor_with_path(dir.path(), &["--check-codex-version"]);
assert_eq!(code, 0, "stdout: {stdout}");
assert!(stdout.contains("[OK]"), "stdout: {stdout}");
assert!(stdout.contains("0.131"), "stdout: {stdout}");
Expand All @@ -65,8 +64,7 @@ fn check_codex_version_reports_ok_for_supported_release() {
#[test]
fn check_codex_version_warns_on_old_release() {
let dir = fake_codex_dir("codex 0.120.0", "Not logged in");
let (stdout, _stderr, code) =
run_doctor_with_path(dir.path(), &["--check-codex-version"]);
let (stdout, _stderr, code) = run_doctor_with_path(dir.path(), &["--check-codex-version"]);
assert_eq!(code, 0);
assert!(stdout.contains("[WARN]"), "stdout: {stdout}");
}
Expand Down
13 changes: 2 additions & 11 deletions crates/ccteam-cli/tests/start_with_imd_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,7 @@ fn start_spawns_imd_supervisor_unless_no_imd_set() {
let _ = std::fs::remove_file(&heartbeat);
let no_imd_started = SystemTime::now();
let mut child2 = Command::new(ccteam_bin())
.args([
"start",
"--no-web",
"--no-imd",
"--tick-seconds",
"1",
])
.args(["start", "--no-web", "--no-imd", "--tick-seconds", "1"])
.env("HOME", fake_home)
.env("CCTEAM_HOME", &ccteam_home)
.env("CCTEAM_PROJECTS_ROOT", fake_home.join("projects"))
Expand All @@ -167,10 +161,7 @@ fn start_spawns_imd_supervisor_unless_no_imd_set() {
// seen the heartbeat if it were going to appear.
std::thread::sleep(Duration::from_secs(6));
let stale = match std::fs::metadata(&heartbeat) {
Ok(meta) => meta
.modified()
.map(|m| m < no_imd_started)
.unwrap_or(true),
Ok(meta) => meta.modified().map(|m| m < no_imd_started).unwrap_or(true),
Err(_) => true,
};

Expand Down
6 changes: 2 additions & 4 deletions crates/ccteam-core/src/agent_naming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,8 @@ pub const SCIENTIST_NAMES: &[&str] = &[
/// Comparison is case-insensitive but the returned name preserves the
/// canonical PascalCase form from [`SCIENTIST_NAMES`].
pub fn pick_unused_bot_name(existing: &[String]) -> String {
let taken: std::collections::HashSet<String> = existing
.iter()
.map(|s| s.to_ascii_lowercase())
.collect();
let taken: std::collections::HashSet<String> =
existing.iter().map(|s| s.to_ascii_lowercase()).collect();
for &name in SCIENTIST_NAMES {
if !taken.contains(&name.to_ascii_lowercase()) {
return name.to_string();
Expand Down
9 changes: 4 additions & 5 deletions crates/ccteam-core/src/execution/claude_bg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ use futures::stream::{self, BoxStream};

use crate::harness::{
parse_backgrounded_short_id, parse_pid_from_state, sigterm_pid, state_json_path,
AgentSpecBrief, AgentVendor, ExecutionMode, HarnessAdapter, HarnessError, SpawnCtx, ThreadEvent,
ThreadHandle, TurnId, TurnInput, CLAUDE_BIN_ENV,
AgentSpecBrief, AgentVendor, ExecutionMode, HarnessAdapter, HarnessError, SpawnCtx,
ThreadEvent, ThreadHandle, TurnId, TurnInput, CLAUDE_BIN_ENV,
};
use crate::tmux::session_name_for_slug;

Expand Down Expand Up @@ -189,8 +189,7 @@ impl HarnessAdapter for ClaudeBgAdapter {
}
};

sigterm_pid(pid).map_err(|err| {
HarnessError::ShutdownFailed(format!("SIGTERM pid {pid}: {err}"))
})
sigterm_pid(pid)
.map_err(|err| HarnessError::ShutdownFailed(format!("SIGTERM pid {pid}: {err}")))
}
}
5 changes: 1 addition & 4 deletions crates/ccteam-core/src/execution/codex_jsonrpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,7 @@ impl CodexJsonRpcClient {
/// writer background tasks are spawned immediately.
pub async fn connect_uds(socket_path: &Path) -> Result<Self> {
let stream = UnixStream::connect(socket_path).await.with_context(|| {
format!(
"connect codex app-server UDS at {}",
socket_path.display()
)
format!("connect codex app-server UDS at {}", socket_path.display())
})?;
let (read_half, write_half) = stream.into_split();
Ok(Self::spawn(read_half, write_half))
Expand Down
12 changes: 10 additions & 2 deletions crates/ccteam-core/src/execution/transcript_tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -633,12 +633,20 @@ mod tests {

// Write the main session FIRST (so its mtime is older).
let main_path = dir.join("main-sid.jsonl");
std::fs::write(&main_path, r#"{"type":"last-prompt","sessionId":"main-sid"}"#).unwrap();
std::fs::write(
&main_path,
r#"{"type":"last-prompt","sessionId":"main-sid"}"#,
)
.unwrap();

// Then write a subagent jsonl — newer mtime.
std::thread::sleep(std::time::Duration::from_millis(10));
let sa_path = dir.join("subagent-sid.jsonl");
std::fs::write(&sa_path, r#"{"type":"agent-setting","sessionId":"subagent-sid"}"#).unwrap();
std::fs::write(
&sa_path,
r#"{"type":"agent-setting","sessionId":"subagent-sid"}"#,
)
.unwrap();

let (picked_sid, picked_path) =
discover_active_session(cwd).expect("should pick the main session, not the subagent");
Expand Down
13 changes: 3 additions & 10 deletions crates/ccteam-core/src/handoff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,7 @@ pub fn list_handoffs(project_dir: &Path, workflow_slug: &str) -> Result<Vec<Path
return Ok(Vec::new());
}
let mut entries: Vec<(u32, String, PathBuf)> = Vec::new();
for entry in std::fs::read_dir(&dir)
.with_context(|| format!("read_dir {}", dir.display()))?
{
for entry in std::fs::read_dir(&dir).with_context(|| format!("read_dir {}", dir.display()))? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
Expand Down Expand Up @@ -194,11 +192,7 @@ pub fn list_handoffs(project_dir: &Path, workflow_slug: &str) -> Result<Vec<Path
/// <!-- ccteam handoff: stage-2-fixer.md -->
/// ...body...
/// ```
pub fn read_concat(
project_dir: &Path,
workflow_slug: &str,
last_n: usize,
) -> Result<String> {
pub fn read_concat(project_dir: &Path, workflow_slug: &str, last_n: usize) -> Result<String> {
if last_n == 0 {
return Ok(String::new());
}
Expand Down Expand Up @@ -272,8 +266,7 @@ pub fn write_handoff(opts: &WriteHandoffOptions) -> Result<PathBuf> {
std::process::id()
);
tmp.set_file_name(tmp_name);
std::fs::write(&tmp, &opts.content)
.with_context(|| format!("write {}", tmp.display()))?;
std::fs::write(&tmp, &opts.content).with_context(|| format!("write {}", tmp.display()))?;
std::fs::rename(&tmp, &final_path)
.with_context(|| format!("rename {} → {}", tmp.display(), final_path.display()))?;
Ok(final_path)
Expand Down
67 changes: 46 additions & 21 deletions crates/ccteam-core/src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,29 @@ pub enum TurnInput {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ThreadEvent {
ThreadStarted { thread_id: String },
TurnStarted { turn_id: String },
TurnCompleted { turn_id: String, usage: UnifiedTokenUsage },
TurnFailed { turn_id: String, err: ThreadErrorEvent },
ItemStarted { item: ThreadItem },
ItemUpdated { item: ThreadItem },
ItemCompleted { item: ThreadItem },
ThreadStarted {
thread_id: String,
},
TurnStarted {
turn_id: String,
},
TurnCompleted {
turn_id: String,
usage: UnifiedTokenUsage,
},
TurnFailed {
turn_id: String,
err: ThreadErrorEvent,
},
ItemStarted {
item: ThreadItem,
},
ItemUpdated {
item: ThreadItem,
},
ItemCompleted {
item: ThreadItem,
},
Error(ThreadErrorEvent),
}

Expand All @@ -209,10 +225,21 @@ pub struct ThreadItem {
pub enum ThreadItemDetails {
AgentMessage(String),
Reasoning(String),
CommandExecution { cmd: String, status: String },
FileChange { path: PathBuf, kind: String },
ToolCall { name: String, args: serde_json::Value },
WebSearch { query: String },
CommandExecution {
cmd: String,
status: String,
},
FileChange {
path: PathBuf,
kind: String,
},
ToolCall {
name: String,
args: serde_json::Value,
},
WebSearch {
query: String,
},
Error(String),
}

Expand Down Expand Up @@ -296,11 +323,8 @@ pub trait HarnessAdapter: Send + Sync {

/// Submit one user-input turn to an existing thread. Bg adapters
/// (single-turn) return a synthetic turn id from the spawn line.
async fn submit_turn(
&self,
h: &ThreadHandle,
input: TurnInput,
) -> Result<TurnId, HarnessError>;
async fn submit_turn(&self, h: &ThreadHandle, input: TurnInput)
-> Result<TurnId, HarnessError>;

/// Stream of thread events. Adapters that don't yet feed structured
/// events return an empty stream (the orchestrator's legacy
Expand All @@ -313,10 +337,7 @@ pub trait HarnessAdapter: Send + Sync {
/// session-id, Codex thread id). Bg adapters return
/// [`HarnessError::NotImplemented`] because every spawn is a fresh
/// 1M context (red line R3, Claude vendor).
async fn resume_thread(
&self,
persistent_id: &str,
) -> Result<ThreadHandle, HarnessError>;
async fn resume_thread(&self, persistent_id: &str) -> Result<ThreadHandle, HarnessError>;

/// Graceful close. Idempotent on missing PID / missing tmux
/// session (matches V0.5.x `shutdown_session` semantics).
Expand Down Expand Up @@ -709,7 +730,11 @@ mod tests {

#[test]
fn execution_mode_serde_round_trip() {
for m in [ExecutionMode::InProc, ExecutionMode::Bg, ExecutionMode::Chat] {
for m in [
ExecutionMode::InProc,
ExecutionMode::Bg,
ExecutionMode::Chat,
] {
let json = serde_json::to_string(&m).unwrap();
let back: ExecutionMode = serde_json::from_str(&json).unwrap();
assert_eq!(m, back);
Expand Down
4 changes: 2 additions & 2 deletions crates/ccteam-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ pub use advise::{
budget_ledger_path as advise_budget_ledger_path, load_budget_ledger as load_advise_budget,
sum_advise_today, sum_advise_today_by_vendor, AdviseBudgetLedger, AdviseError, Agreement,
AnswerStatus, BudgetSample, BudgetSnapshot, CodexStatus, ParallelResult, VendorAnswer,
VoteResult, APPROX_COST_PER_CALL_USD as APPROX_ADVISE_COST_USD,
DEFAULT_ADVISE_BUDGET_USD_24H, DEFAULT_CODEX_TIMEOUT_SECS,
VoteResult, APPROX_COST_PER_CALL_USD as APPROX_ADVISE_COST_USD, DEFAULT_ADVISE_BUDGET_USD_24H,
DEFAULT_CODEX_TIMEOUT_SECS,
};
pub use auto_loop::{AutoLoopDecision, AutoLoopFrontMatter, AutoLoopState};
pub use claude_job::{
Expand Down
5 changes: 1 addition & 4 deletions crates/ccteam-core/src/mode_inferrer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,7 @@ pub fn infer_mode(intent: &Intent) -> InferenceResult {
(Presence::FULL_ATTENDED, Timeline::LONG_RUNNING) => {
// Long-running + user staying full-attended is unusual —
// surface ambiguity rather than silently picking one.
let mut scores = vec![
(CreatorMode::Bg, 0.6),
(CreatorMode::InProc, 0.4),
];
let mut scores = vec![(CreatorMode::Bg, 0.6), (CreatorMode::InProc, 0.4)];
if task_type == "qa-loop" {
scores[0].1 += 0.1;
}
Expand Down
6 changes: 2 additions & 4 deletions crates/ccteam-core/src/preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,11 @@ pub fn load_or_default(root: &Path) -> Preferences {

/// Atomically write preferences to `<root>/preferences.toml`.
pub fn save(root: &Path, prefs: &Preferences) -> Result<()> {
std::fs::create_dir_all(root)
.with_context(|| format!("mkdir {}", root.display()))?;
std::fs::create_dir_all(root).with_context(|| format!("mkdir {}", root.display()))?;
let raw = toml::to_string_pretty(prefs).context("serialize preferences")?;
let path = preferences_path(root);
let tmp = path.with_extension("toml.tmp");
std::fs::write(&tmp, raw.as_bytes())
.with_context(|| format!("write {}", tmp.display()))?;
std::fs::write(&tmp, raw.as_bytes()).with_context(|| format!("write {}", tmp.display()))?;
std::fs::rename(&tmp, &path)
.with_context(|| format!("rename {} -> {}", tmp.display(), path.display()))?;
Ok(())
Expand Down
5 changes: 1 addition & 4 deletions crates/ccteam-core/src/spawn_brief.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,7 @@ pub fn render_spawn_brief(template: &str, ctx: &SpawnContext) -> Result<String>
}

if out.contains("{{stage_num}}") {
let val = ctx
.stage_num
.map(|n| n.to_string())
.unwrap_or_default();
let val = ctx.stage_num.map(|n| n.to_string()).unwrap_or_default();
out = out.replace("{{stage_num}}", &val);
}

Expand Down
Loading
Loading