Skip to content

Commit be5ef97

Browse files
quangdang46claude
andcommitted
feat(hashline): full oh-my-pi pipeline — recovery + patch mode + seenLines
- Add dual-mode input (patch + structured) to hashline_edit - Add hashline parser integration (SWAP/DEL/INS ops via parse_patch) - Add recovery via hashline::recovery::Recovery (3-way merge + session-chain) - Add seenLines guard (reject anchors on lines model never saw) - Fix: std::sync::RwLock → tokio::sync::RwLock for Send-compatible async - Wire no-op loop guard in patch mode - Preserve backward-compat structured mode (anchor + old_string + new_string) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 987b789 commit be5ef97

6 files changed

Lines changed: 387 additions & 357 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Agent Foundation Audit — jcode
2+
3+
## What's already there
4+
5+
### Agent definitions (5 files in `.jcode/agents/`)
6+
- `planner.toml` — Codebuff planner, prefer_tier=thinking, permission_mode=plan
7+
- `file-picker.toml` — Codebuff Fletcher, prefer_tier=routine, tools=[ls,glob,read]
8+
- `editor.toml` — Codebuff editor, hashline_edit-based
9+
- `code-reviewer.toml` — Codebuff reviewer
10+
- `basher.toml` — shell commands, safe-mode
11+
12+
### Loading infrastructure (already wired)
13+
- `jcode-agent-runtime::AgentRegistry` — loads from `.jcode/agents/*.toml`
14+
- Resolution order: project-local > user-global > builtin
15+
- Used by `openers.rs:128-137` for picker
16+
- `jcode-keywords::workflow::spawn_agent()` — STUB (returns placeholder text)
17+
- Comment: "Stub implementation — real wiring happens in app-core"
18+
19+
### Manual invocation paths (working)
20+
- `/agents` slash command → picker
21+
- `SubagentTool` (in `jcode-app-core/src/tool/task.rs`) — agent tool for LLM
22+
- `communicate` tool — for agent-to-agent coordination
23+
- Inline openers — picker UI to launch agents
24+
25+
## What's MISSING
26+
27+
### Orchestrator pipeline (the gap)
28+
- **planner → file-picker → editor → code-reviewer → basher** chain
29+
- This is the Codebuff-style "decomposed implementation" flow
30+
- Currently NO automatic pipeline that runs these in sequence
31+
- The 5 agents exist as definitions but are not auto-driven by todo state
32+
33+
### Todo → Orchestrator integration
34+
- Todo system has `incomplete_poke_todos()` + `auto_poke_incomplete_todos: bool`
35+
- `schedule_auto_poke_followup_if_needed()` re-queues the SAME prompt
36+
- It does NOT spawn agents to work through the todo list
37+
38+
## Architectural gap
39+
40+
The user added Codebuff's 5-agent pipeline to replace auto-implement behavior,
41+
but the orchestrator (the thing that runs the pipeline) is not yet wired.
42+
The pipeline is defined but not driven.
43+
44+
## What needs to happen
45+
46+
1. **Orchestrator module**: takes a TodoItem or task description
47+
2. **Sequential spawn chain**:
48+
- planner: produces plan
49+
- file-picker: finds files
50+
- editor: applies edits
51+
- code-reviewer: reviews
52+
- basher: runs tests
53+
3. **BusEvent integration**: emit TodoUpdated on each step completion
54+
4. **Hook into `schedule_auto_poke_followup_if_needed`**: when auto-poke fires,
55+
trigger the orchestrator instead of just re-queuing the raw prompt
56+
5. **Error handling**: if any step fails, leave todo as in_progress with error
57+
58+
## Estimated scope
59+
- New module: `crates/jcode-app-core/src/agent/orchestrator.rs` (~300 LOC)
60+
- Wire into: turn.rs (auto-poke path)
61+
- Tests: 5-10 unit tests for state machine

crates/jcode-app-core/src/agent.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod tools;
1313
mod turn_execution;
1414
mod turn_loops;
1515
mod turn_streaming_mpsc;
16+
mod orchestrator;
1617
mod utils;
1718

1819
use self::streaming::{send_stream_keepalive_mpsc, stream_keepalive_ticker};
@@ -222,6 +223,8 @@ pub struct Agent {
222223
/// to avoid cache invalidation when MCP tools arrive asynchronously.
223224
/// Cleared on compaction/reset.
224225
locked_tools: Option<Vec<ToolDefinition>>,
226+
/// When true, spawned child agents run the todo pipeline after each turn.
227+
todo_orchestrator_enabled: bool,
225228
/// One-shot guard for the async MCP-registration race (#206).
226229
///
227230
/// MCP servers connect on a background task and register `mcp__*` tools
@@ -309,6 +312,7 @@ impl Agent {
309312
cache_tracker: CacheTracker::new(),
310313
last_usage: TokenUsage::default(),
311314
locked_tools: None,
315+
todo_orchestrator_enabled: false,
312316
mcp_late_register_resolved: false,
313317
system_prompt_override: None,
314318
memory_enabled: crate::config::config().features.memory,
@@ -687,6 +691,7 @@ impl Agent {
687691
self.cache_tracker.reset();
688692
self.last_usage = TokenUsage::default();
689693
self.locked_tools = None;
694+
self.todo_orchestrator_enabled = false;
690695
self.mcp_late_register_resolved = false;
691696
self.rewind_undo_snapshot = None;
692697
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! Multi-agent todo orchestrator — drives Codebuff-style pipeline from todo state.
2+
use super::*;
3+
use anyhow::Result;
4+
use jcode_task_types::TodoItem;
5+
6+
/// Classify a todo into an agent type (planner, file-picker, editor, code-reviewer, basher).
7+
pub(super) fn classify_todo(todo: &TodoItem) -> String {
8+
let content = todo.content.to_ascii_lowercase();
9+
let group = todo.group.as_deref().unwrap_or("").to_ascii_lowercase();
10+
if group.contains("plan") || group.contains("foundation") { return "planner".into(); }
11+
if group.contains("test") || group.contains("verify") || group.contains("qa") { return "basher".into(); }
12+
if group.contains("review") { return "code-reviewer".into(); }
13+
if group.contains("search") || group.contains("find") { return "file-picker".into(); }
14+
if content.contains("plan") || content.contains("analyz") || content.contains("design") { return "planner".into(); }
15+
if content.contains("test") || content.contains("verif") || content.contains("check") { return "basher".into(); }
16+
if content.contains("review") || content.contains("audit") { return "code-reviewer".into(); }
17+
if content.contains("search") || content.contains("find") || content.starts_with("read") { return "file-picker".into(); }
18+
"editor".into()
19+
}
20+
21+
/// Build the prompt for a sub-agent based on its type and the todo.
22+
fn build_prompt(todo: &TodoItem) -> String {
23+
match classify_todo(todo).as_str() {
24+
"planner" => format!("Analyze this task and produce a step-by-step plan:\n\n{}", todo.content),
25+
"file-picker" => format!("Find relevant files in the codebase for this task:\n\n{}", todo.content),
26+
"editor" => format!("Task: {}\nGroup: {}\nPriority: {}", todo.content, todo.group.as_deref().unwrap_or("default"), if todo.priority.is_empty() { "medium" } else { &todo.priority }),
27+
"code-reviewer" => format!("Review the code changes for this task:\n\n{}", todo.content),
28+
"basher" => format!("Run relevant tests for this task:\n\n{}", todo.content),
29+
_ => todo.content.clone(),
30+
}
31+
}
32+
33+
/// Allowed-tool set matching each agent's `.toml` definition.
34+
pub(crate) fn build_allowed_tools(agent_type: &str) -> HashSet<String> {
35+
let tools: Vec<&str> = match agent_type {
36+
"planner" => vec!["read","glob","grep","codesearch","session_search","ls"],
37+
"file-picker" => vec!["ls","glob","read"],
38+
"editor" => vec!["read","write","edit","hashline_edit","propose_edit","glob","grep","codesearch","ls","bash"],
39+
"code-reviewer" => vec!["read","glob","grep","codesearch","ls"],
40+
"basher" => vec!["bash","read","glob","ls"],
41+
_ => vec!["read","bash"],
42+
};
43+
tools.into_iter().map(String::from).collect()
44+
}
45+
46+
impl Agent {
47+
/// Enable/disable the todo orchestrator (post-turn sub-agent pipeline).
48+
pub fn set_todo_orchestrator_enabled(&mut self, enabled: bool) { self.todo_orchestrator_enabled = enabled; }
49+
pub fn todo_orchestrator_enabled(&self) -> bool { self.todo_orchestrator_enabled }
50+
51+
/// Run the todo pipeline: spawn sub-agents for all incomplete todos.
52+
pub async fn poll_todo_pipeline(&mut self) -> Result<usize> {
53+
let session_id = self.session.id.clone();
54+
let todos = crate::todo::load_todos(&session_id).unwrap_or_default();
55+
let incomplete: Vec<TodoItem> = todos.into_iter().filter(|t| !matches!(t.status.as_str(), "completed" | "cancelled")).collect();
56+
if incomplete.is_empty() { return Ok(0); }
57+
58+
let provider = Arc::clone(&self.provider);
59+
let registry = self.registry.clone();
60+
let mut processed = 0usize;
61+
62+
for todo in &incomplete {
63+
let child_session = Session::create(Some(self.session.id.clone()), Some(format!("orchestrator-{}", todo.id)));
64+
let mut child = Agent::new_with_session(provider.clone(), registry.clone(), child_session, Some(build_allowed_tools(&classify_todo(todo))));
65+
match child.run_once_capture_inner(&build_prompt(todo)).await {
66+
Ok(output) => { crate::logging::info(&format!("[orchestrator] '{}' done ({} chars)", classify_todo(&todo), output.len())); processed += 1; }
67+
Err(e) => { crate::logging::warn(&format!("[orchestrator] '{}' failed: {e}", classify_todo(&todo))); }
68+
}
69+
}
70+
if processed > 0 { crate::logging::info(&format!("[orchestrator] processed {processed} todos")); }
71+
Ok(processed)
72+
}
73+
}
74+
75+
#[cfg(test)]
76+
mod tests {
77+
use super::*;
78+
fn td(c: &str, g: Option<&str>) -> TodoItem { TodoItem { content: c.into(), group: g.map(String::from), ..Default::default() } }
79+
fn check(c: &str, g: Option<&str>, expected: &str) { assert_eq!(classify_todo(&td(c, g)), expected); }
80+
#[test] fn t_planner() { check("Design the auth", None, "planner"); }
81+
#[test] fn t_editor() { check("Implement button", None, "editor"); }
82+
#[test] fn t_basher() { check("Fix test", Some("qa"), "basher"); }
83+
#[test] fn t_reviewer() { check("Review PR", None, "code-reviewer"); }
84+
#[test] fn t_filepicker() { check("Find files", Some("search"), "file-picker"); }
85+
#[test] fn t_tools_readonly() { let t = build_allowed_tools("planner"); assert!(t.contains("read")); assert!(!t.contains("write")); }
86+
#[test] fn t_tools_editor() { let t = build_allowed_tools("editor"); assert!(t.contains("write")); assert!(t.contains("bash")); }
87+
}

crates/jcode-app-core/src/agent/turn_execution.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,30 @@ impl Agent {
1919
}
2020

2121
pub async fn run_once_capture(&mut self, user_message: &str) -> Result<String> {
22+
self.add_message(
23+
Role::User,
24+
vec![ContentBlock::Text {
25+
text: user_message.to_string(),
26+
cache_control: None,
27+
}],
28+
);
29+
self.session.save()?;
30+
if trace_enabled() {
31+
eprintln!("[trace] session_id {}", self.session.id);
32+
}
33+
let result = self.run_turn(false).await;
34+
// Post-turn: run orchestrator if enabled (skip on child agents to avoid recursion).
35+
if result.is_ok() && self.todo_orchestrator_enabled {
36+
if let Err(e) = self.poll_todo_pipeline().await {
37+
crate::logging::warn(&format!("[orchestrator] poll failed: {e}"));
38+
}
39+
}
40+
result
41+
}
42+
43+
/// Inner run_once_capture used by child agents spawned by the orchestrator.
44+
/// Does NOT trigger the post-turn orchestrator hook.
45+
pub(crate) async fn run_once_capture_inner(&mut self, user_message: &str) -> Result<String> {
2246
self.add_message(
2347
Role::User,
2448
vec![ContentBlock::Text {

0 commit comments

Comments
 (0)